31

as a tcl developer starting with groovy, I am a little bit surprised about the list and map support in groovy. Maybe I am missing something here.

I am used to convert between strings, lists and arrays/maps in tcl on the fly. In tcl, something like

"['a':2,'b':4]".each {key, value -> println key + " " + value}

would be possible, where as in groovy, the each command steps through each character of the string.

This would be much of a problem is I could easily use something like the split or tokenize command, but because a serialized list or map isn't just "a:2,b:4", it is a little bit harder to parse.

It seems that griffon developers use a stringToMap library (http://code.google.com/p/stringtomap/) but the example can't cope with the serialized maps either.

So my question is now: what's the best way to parse a map or a list in groovy?

Cheers, Ralf

PS: it's a groovy question, but I've tagged it with grails, because I need this functionality for grails where I would like to pass maps through the URL

Update: This is still an open question for me... so here are some updates for those who have the same problem:

  • when you turn a Map into a String, a .toString() will result in something which can't be turned back into a map in all cases, but an .inspect() will give you a String which can be evaluated back to a map!
  • in Grails, there is a .encodeAsJSON() and JSON.parse(String) - both work great, but I haven't checked out yet what the parser will do with JSON functions (possible security problem)
rdmueller
  • 10,742
  • 10
  • 69
  • 126
  • 3
    If you're using grails and want a map, I'd look at POSTing a JSON message. Probably easier to generate on the client side and there are things built into grails to evaluate JSON. – Ted Naleid Feb 06 '10 at 19:25
  • thanx. json could indeed be a very good alternative! – rdmueller Feb 07 '10 at 13:41
  • 1
    Passing maps through the URL in Grails sounds like a job for URL mappings. Check out the Embedded Variables section of the grails user guide (section 6.4.2). You can define a custom URL structure to pass whatever map you'd like, ie http://myapp.com/controller/action/key1/value1/key2/value2 It won't work too well for multi-dimensional maps, or huge data structures, but I would contend those shouldn't be passed around via URLs anyway. – Steve Goodman Feb 10 '10 at 16:39
  • good idea - when you have small maps. In my case, I would like to submit the data to be plotted on a chart through the URL. Something like the google chart API. So my maps can be quite big... – rdmueller Feb 11 '10 at 08:10

5 Answers5

33

You might want to try a few of your scenarios using evaluate, it might do what you are looking for.

def stringMap = "['a':2,'b':4]"
def map = evaluate(stringMap)

assert map.a == 2
assert map.b == 4

def stringMapNested = "['foo':'bar', baz:['alpha':'beta']]"
def map2 = evaluate(stringMapNested)

assert map2.foo == "bar"
assert map2.baz.alpha == "beta"
John Wagenleitner
  • 10,967
  • 1
  • 40
  • 39
21

Not exactly native groovy, but useful for serializing to JSON:

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

def map = ['a':2,'b':4 ]
def s = new JsonBuilder(map).toString()
println s

assert map == new JsonSlurper().parseText(s)

with meta-programming:

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

Map.metaClass.toJson   = { new JsonBuilder(delegate).toString() }
String.metaClass.toMap = { new JsonSlurper().parseText(delegate) }

def map = ['a':2,'b':4 ]
assert map.toJson() == '{"a":2,"b":4}'
assert map.toJson().toMap() == map

unfortunately, it's not possible to override the toString() method...

rdmueller
  • 10,742
  • 10
  • 69
  • 126
Jonas Eicher
  • 1,413
  • 12
  • 18
  • 1
    I guess this is a clean solution... with a little bit of meta programming, this functionality can be easily added to the String class... – rdmueller Jul 08 '14 at 15:31
3

I think you are looking for a combination of ConfigObject and ConfigSlurper. Something like this would do the trick.

def foo = new ConfigObject()
foo.bar = [ 'a' : 2, 'b' : 4 ]

// we need to serialize it
new File( 'serialized.groovy' ).withWriter{ writer ->
  foo.writeTo( writer )
}

def config = new ConfigSlurper().parse(new File('serialized.groovy').toURL())    

// highest level structure is a map ["bar":...], that's why we need one loop more
config.each { _,v ->
    v.each {key, value -> println key + " " + value}
}
Grega Kešpret
  • 11,827
  • 6
  • 39
  • 44
2

I hope this help:

    foo= "['a':2,'b':4]"
    Map mapResult=[:]
    mapResult += foo.replaceAll('\\[|\\]', '').split(',').collectEntries { entry ->
        def pair = entry.split(':')
        [(pair.first().trim()): pair.last().trim()]
       }
Ali
  • 430
  • 3
  • 16
1

If you don't want to use evaluate(), do instead:

def stringMap = "['a':2,'b':4]"
stringMap = stringMap.replaceAll('\\[|\\]','')
def newMap = [:]
stringMap.tokenize(',').each {
kvTuple = it.tokenize(':')
newMap[kvTuple[0]] = kvTuple[1]
}
println newMap
Noam Manos
  • 15,216
  • 3
  • 86
  • 85
  • That is a pretty limited as it will only work with Strings. For example, in your example the values that are put in newMap will be the Strings "2" and "4", not the numbers 2 and 4. – Jeff Scott Brown Jul 08 '14 at 16:00