Using GStrings one can access the properties of the object, including nested properties. But how to access the n'th element inside a list property?
class Foo {
List<Bar> elements
}
class Bar {
String version
}
I need to access version property in Foo.elements object for a specific index using GString.
Tried below code without success.
def property = "elements[0].version"
fooObject."$property" fails to identify the property
So there are three ways in which I think this problem can be solved depending upon how much flexibility is allowed
class Foo {
List<Bar> elements
}
class Bar {
String version
}
Let's say fooObject is the object of Foo, e.g.:
def fooObject = new Foo(elements:[new Bar(version:1), new Bar(version:2)])
If this is possible for you:
println fooObject."elements"[1]."version"
Otherwise, put everything in a string and then interpolate:
println "${fooObject.elements[1].version}"
Ultimately, if both of the above don't fly for you:
def property='elements[1].version'
def expr = 'fooObject.' + property
println Eval.me('fooObject', fooObject, expr)
The last one makes the fooObject available as fooObject to the expression being evaluated and evaluates the expression.
Ideally, it could be:
def prop1 = "elements"
def prop2 = "version"
fooObject."$prop1"[0]."$prop2"
Lengthy and generic one would be using inject:
class Foo {
List<Bar> elements
}
class Bar {
String version
}
def fooObject = new Foo(elements: [new Bar(version: '1'),
new Bar(version: '2'),
new Bar(version: '3')])
def fetchVersion(property, fooObject) {
property.tokenize(/./).inject(fooObject) {obj, elem ->
if(elem.contains(/[/)){
def var = elem.tokenize(/[]/)
obj?."${var[0]}".getAt("${var[1]}".toInteger())
} else {
obj?."$elem"
}
}
}
assert fetchVersion("elements[0].version", fooObject) == '1'
assert fetchVersion("elements[1].version", fooObject) == '2'
assert fetchVersion("elements[2].version", fooObject) == '3'
assert fetchVersion("elements[7].version", fooObject) == null
Related
I have a class like this:
in foo.groovy
class Foo {
String thing
Integer other
Foo(String thing) {
this.thing = thing
}
Foo(Integer other) {
this.other = other
}
}
return Foo.class
Now I would like to invoke these constructors. What I am doing is:
Other.groovy
def foo = evaluate(new File(ClassLoader.getSystemResource('foo.groovy').file)).newInstance(10)
def foo2 = evaluate(new File(ClassLoader.getSystemResource('foo.groovy').file)).newInstance("thing")
But this doesn't seem like the correct way of doing it. Ideally I would like to actually name the file Foo.groovy but then I get an error because it automatically declares the class for me. Basically, I want it to work like a classic Java class
Maybe I'm missing something here, but:
class Foo {
String thing
Integer other
Foo(String thing) {
this.thing = thing
}
Foo(Integer other) {
this.other = other
}
}
def x = new Foo(10)
assert x.other == 10 // true
def y = new Foo("foo")
assert y.thing == "foo" // true
What are you trying to accomplish here other than that?
Edit: Try it here.
I get an object via some 3rd party api. I use a wrapper function to get it and then return a map from its properties:
wrapperFunc() {
def myObj = someapi.getblah().getSomeObect()
return [
aaa: myObj.aaa,
bbb: myObj.bbb,
ccc: myObj.ccc
]
}
Now I could manually go through EVERY property in the object like this, but is there an elegant groovy feature to dynamically build a map from the object's properties?
You could do something like this:
class Widget {
int width
int height
static void main(args) {
def obj = new Widget(width: 7, height: 9)
List<MetaProperty> metaProperties = obj.metaClass.properties
def props = [:]
for(MetaProperty mp : metaProperties) {
props[mp.name] = mp.getProperty(obj)
}
// props will look like [width:7, class:class demo.Widget, height:9]
}
}
This is basically a variant of #jeff-scott-brown's answer.
First, create a class that contains the Object-to-Map logic that uses the Groovy MetaClass to access a type's properties. findAll filters out the "class" property, which I assume you don't care about. The collectEntries line transforms each MetaProperty object into a Map entry.
class ElegantGroovyFeature {
static Map asType(Object o, Class m) {
if (m == Map) {
o.metaClass.properties
.findAll { it.getSetter() != null }
.collectEntries { prop -> [prop.name, prop.getProperty(o)] }
} else {
o.asType(m)
}
}
}
The extension class overrides the asType method, which corresponds to the as operator, enabling you to convert arbitrary objects to Maps using obj as Map expressions:
def obj = someapi.getBlah().getSomeObject()
use (ElegantGroovyFeature) {
def mapOfProperties = obj as Map
}
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
console.log(map); // Map { foo: "bar", baenter code herez: 42 }
I have JSON looking like:
{
"days": [
{
"mintemp": "21.8"
}
]
}
With Groovy, I parse it like this:
class WeatherRow {
String mintemp
}
def file = new File("data.json")
def slurper = new JsonSlurper().parse(file)
def days = slurper.days
def firstRow = days[0] as WeatherRow
println firstRow.mintemp
But actually, I would like to name my instance variable something like minTemp (or even something completely random, like numberOfPonies). Is there a way in Groovy to map a member of a map passed to a constructor to something else?
To clarify, I was looking for something along the lines of #XmlElement(name="mintemp"), but could not easily find it:
class WeatherRow {
#Element(name="mintemp")
String minTemp
}
Create a constructor that takes a map.
Runnable example:
import groovy.json.JsonSlurper
def testJsonStr = '''
{"days": [
{ "mintemp": "21.8" }
]}'''
class WeatherRow {
String minTemp
WeatherRow(map) {
println "Got called with constructor that takes a map: $map"
minTemp = map.mintemp
}
}
def slurper = new JsonSlurper().parseText(testJsonStr)
def days = slurper.days
def firstRow = days[0] as WeatherRow
println firstRow.minTemp
Result:
Got called with constructor that takes a map: [mintemp:21.8]
21.8
(of course you'd remove the println line, it's just there for the demo)
You can achieve this using annotation and simple custom annotation processor like this:
1. Create a Custom Annotation Class
#Retention(RetentionPolicy.RUNTIME)
#interface JsonDeserializer {
String[] names() default []
}
2. Annotate your instance fields with the custom annotation
class WeatherRow{
#JsonDeserializer(names = ["mintemp"])
String mintemp;
#JsonDeserializer(names = ["mintemp"])
String minTemp;
#JsonDeserializer(names = ["mintemp"])
String numberOfPonies;
}
3. Add custom json deserializer method using annotation processing:
static WeatherRow fromJson(def jsonObject){
WeatherRow weatherRow = new WeatherRow();
try{
weatherRow = new WeatherRow(jsonObject);
}catch(MissingPropertyException ex){
//swallow missing property exception.
}
WeatherRow.class.getDeclaredFields().each{
def jsonDeserializer = it.getDeclaredAnnotations()?.find{it.annotationType() == JsonDeserializer}
def fieldNames = [];
fieldNames << it.name;
if(jsonDeserializer){
fieldNames.addAll(jsonDeserializer.names());
fieldNames.each{i ->
if(jsonObject."$i")//TODO: if field type is not String type custom parsing here.
weatherRow."${it.name}" = jsonObject."$i";
}
}
};
return weatherRow;
}
Example:
def testJsonStr = '''
{
"days": [
{
"mintemp": "21.8"
}
]
}'''
def parsedWeatherRows = new JsonSlurper().parseText(testJsonStr);
assert WeatherRow.fromJson(parsedWeatherRows.days[0]).mintemp == "21.8"
assert WeatherRow.fromJson(parsedWeatherRows.days[0]).minTemp == "21.8"
assert WeatherRow.fromJson(parsedWeatherRows.days[0]).numberOfPonies == "21.8"
Check the full working code at groovyConsole.
take these objects
class Obj1 {
Obj2 obj2
}
class Obj2 {
Obj3 obj3
}
class Obj3 {
String tryme
}
Now, Crud operations on this model is happening by means of an angularjs app. The angular app sends back the fields that changed. so for example, it may send
[
{
"jsonPath": "/obj2/obj3/tryme",
"newValue": "New Name"
}
]
So with groovy, is there an easy way to access that nested field? i could do it with java reflection, but thats a lot of code. If not with pojo's, this is a mongodb, so I suppose i can do it with json slurp if its easier, i just don't know. any advice is appreciated.
So to show the problems with the solutions i have found so far. Take this
Obj1 a = new Obj1()
with the edit object of this
[
{
"jsonPath": "/obj2/obj3/tryme",
"newValue": "New Name"
}
]
Doing the pojo route, finding a null field of obj2 is not an issue. The issue is i have no way of knowing what type it is in order to initialize the field and keep walking the tree.
Please refrain from Groovy is typeless, we don't use def around here, everything needs to be statically typed.
So I am also trying this from the JsonSlurp aspect too, just eliminate the pojo all together. But even that is problematic because it seems I'm back to iterating a map of maps to get to the field. Same problem, easier to solve.
class MongoRecordEditor {
def getProperty(def object, String propertyPath) {
propertyPath.tokenize('/').inject object, {obj, prop ->
def retObj = obj[prop]
if (retObj == null){
println obj[prop].class
}
}
}
void setProperty(def object, String propertyPath, Object value) {
def pathElements = propertyPath.tokenize('/')
def objectField
if (pathElements.size() == 1){
objectField = pathElements[0]
} else {
objectField = pathElements[0..-2].join('/')
}
Object parent = getProperty(object, objectField)
parent[pathElements[-1]] = value
}
}
is the culmination of many ideas. Now getting def retObj = obj[prop] to run is a piece of cake. the problem is, if the field isn't initialized, then retObj is always null, therefore i can't get the type that its supposed to be to initialize it.
and yes I know, once I figure out how to make it work, I will type it.
Maybe something like this?
class Obj1 {
Obj2 obj2
}
class Obj2 {
Obj3 obj3
}
class Obj3 {
String tryme
}
def a = new Obj1(obj2: new Obj2(obj3: new Obj3(tryme:"test")))
for (value in "obj2/obj3/tryme".split("/")) {
a = a?."${value}"
}
println a
You could create trait and then make Obj1 use it:
trait DynamicPath {
def get(String path) {
def target = this
for (value in path.split("/")) {
target = target?."${value}"
}
target
}
}
class Obj1 implements DynamicPath{
Obj2 obj2
}
println a.get("obj2/obj3/tryme");
Not sure if this is what you want... And it relies on the objects having a default constructor, and there may be better ways of doing it...
Those caveats aside, given you have:
import groovy.transform.*
import groovy.json.*
#ToString
class Obj1 {
Obj2 obj2
}
#ToString
class Obj2 {
Obj3 obj3
}
#ToString
class Obj3 {
String tryme
}
def changeRequest = '''[
{
"jsonPath": "/obj2/obj3/tryme",
"newValue": "New Name"
}
]'''
Then, you can define a manipulator like so:
def change(Object o, String path, String value) {
Object current = o
String[] pathElements = path.split('/').drop(1)
pathElements[0..-2].each { f ->
if(current."$f" == null) {
current."$f" = current.class.declaredFields.find { it -> f == it.name }?.type.getConstructor().newInstance()
}
current = current."$f"
}
current."${pathElements[-1]}" = value
o
}
And call it like
def results = new JsonSlurper().parseText(changeRequest).collect {
change(new Obj1(), it.jsonPath, it.newValue)
}
To give you a list containing your one new Obj1 instance:
[Obj1(Obj2(Obj3(New Name)))]
I understand that we cannot access Map properties the same way we access them in other classes, because of the ability to get map keys with dot notations in groovy.
Now, Is there a way, for a class that implements java.util.Map, to still benefit from the expando metaclass for using propertyMissing ?
Here is what I'm trying :
LinkedHashMap.metaClass.methodMissing = { method, args ->
println "Invoking ${method}"
"Invoking ${method}"
}
LinkedHashMap.metaClass.propertyMissing = { method, args ->
println "Accessing ${method}"
"Accessing ${method}"
}
def foo = [:]
assert "Invoking bar" == foo.bar() // this works fine
assert "Accessing bar" == foo.bar // this doesn't work, for obvious reasons, but I'd like to be able to do that...
I've been trying through custom DelegatingMetaClasses but didn't succeed...
Not sure it fits your use-case, but you could use Guava and the withDefault method on Maps...
#Grab( 'com.google.guava:guava:16.0.1' )
import static com.google.common.base.CaseFormat.*
def map
map = [:].withDefault { key ->
LOWER_UNDERSCORE.to(LOWER_CAMEL, key).with { alternate ->
map.containsKey(alternate) ? map[alternate] : null
}
}
map.possibleSolution = 'maybe'
assert map.possible_solution == 'maybe'
One side-effect of this is that after the assert, the map contains two key:value pairs:
assert map == [possibleSolution:'maybe', possible_solution:'maybe']
If I understood well you can provide a custom map:
class CustomMap extends LinkedHashMap {
def getAt(name) {
println "getAt($name)"
def r = super.getAt(name)
r ? r : this.propertyMissing(name)
}
def get(name) {
println "get($name)"
super.get(name)
def r = super.get(name)
r ? r : this.propertyMissing(name)
}
def methodMissing(method, args) {
println "methodMissing($method, $args)"
"Invoking ${method}"
}
def propertyMissing(method) {
println "propertyMissing($method)"
"Accessing ${method}"
}
}
def foo = [bar:1] as CustomMap
assert foo.bar == 1
assert foo['bar'] == 1
assert foo.lol == 'Accessing lol'
assert foo['lol'] == 'Accessing lol'
assert foo.bar() == 'Invoking bar'
I reread the groovy Maps javadocs, and I noticed there are 2 versions of the get method. One that takes a single argument, and one that takes 2.
The version that takes 2 does almost what I describe here : it returns a default value if it doesn't find your key.
I get the desired effect, but not in dot notation, therefore I just post this as an alternative solution in case anyone comes across this post :
Map.metaClass.customGet = { key ->
def alternate = key.replaceAll(/_\w/){ it[1].toUpperCase() }
return delegate.get(key, delegate.get(alternate, 'Sorry...'))
}
def m = [myKey : 'Found your key']
assert 'Found your key' == m.customGet('myKey')
assert 'Found your key' == m.customGet('my_key')
assert 'Sorry...' == m.customGet('another_key')
println m
-Result-
m = [myKey:Found your key, my_key:Found your key, anotherKey:Sorry..., another_key:Sorry...]
As in Tim's solution, this leads to m containing both keys after the second assert + 2 keys with the default value (Sorry...) everytime we ask for a new value not present in the initial map... which could be solved by removing the keys with default values. e.g. :
Map.metaClass.customGet = { key ->
def alternate = key.replaceAll(/_\w/){ it[1].toUpperCase() }
def ret = delegate.get(key, delegate.get(alternate, 'Sorry...'))
if (ret == 'Sorry...') {
delegate.remove(key)
delegate.remove(alternate)
}
ret
}
Feel free to comment/correct any mistakes this could lead to... just thinking out loud here...