Skip to content

Commit ef551a2

Browse files
authored
Merge pull request #13869 from jdaugherty/feature/7.0.0/13655-dot-notation
2 parents 20862d9 + 682e7db commit ef551a2

File tree

14 files changed

+482
-29
lines changed

14 files changed

+482
-29
lines changed

grails-bootstrap/src/main/groovy/org/grails/config/CodeGenConfig.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class CodeGenConfig implements Cloneable, ConfigMap {
178178
}
179179

180180
protected <T> T convertToType(Object value, Class<T> requiredType) {
181-
if(value == null) {
181+
if(value == null || value instanceof NavigableMap.NullSafeNavigator) {
182182
return null
183183
}
184184
else if(requiredType.isInstance(value)) {

grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy

Lines changed: 183 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.grails.config
1818
import groovy.transform.CompileDynamic
1919
import groovy.transform.CompileStatic
2020
import groovy.transform.EqualsAndHashCode
21+
import org.codehaus.groovy.runtime.DefaultGroovyMethods
2122
import org.slf4j.Logger
2223
import org.slf4j.LoggerFactory
2324

@@ -47,14 +48,14 @@ class NavigableMap implements Map<String, Object>, Cloneable {
4748
final Map<String, Object> delegateMap
4849
final String dottedPath
4950

50-
public NavigableMap() {
51+
NavigableMap() {
5152
rootConfig = this
5253
path = []
5354
dottedPath = ""
5455
delegateMap = new LinkedHashMap<>()
5556
}
5657

57-
public NavigableMap(NavigableMap rootConfig, List<String> path) {
58+
NavigableMap(NavigableMap rootConfig, List<String> path) {
5859
super()
5960
this.rootConfig = rootConfig
6061
this.path = path
@@ -144,7 +145,7 @@ class NavigableMap implements Map<String, Object>, Cloneable {
144145
delegateMap.entrySet()
145146
}
146147

147-
public void merge(Map sourceMap, boolean parseFlatKeys=false) {
148+
void merge(Map sourceMap, boolean parseFlatKeys=false) {
148149
mergeMaps(this, "", this, sourceMap, parseFlatKeys)
149150
}
150151

@@ -341,17 +342,17 @@ class NavigableMap implements Map<String, Object>, Cloneable {
341342
targetMap.put(sourceKey, newValue)
342343
}
343344

344-
public Object getAt(Object key) {
345+
Object getAt(Object key) {
345346
getProperty(String.valueOf(key))
346347
}
347-
348-
public void setAt(Object key, Object value) {
348+
349+
void setAt(Object key, Object value) {
349350
setProperty(String.valueOf(key), value)
350351
}
351352

352-
public Object getProperty(String name) {
353+
Object getProperty(String name) {
353354
if (!containsKey(name)) {
354-
return null
355+
return new NullSafeNavigator(this, [name].asImmutable())
355356
}
356357
Object result = get(name)
357358
if (!(result instanceof NavigableMap)) {
@@ -361,12 +362,12 @@ class NavigableMap implements Map<String, Object>, Cloneable {
361362
}
362363
return result
363364
}
364-
365-
public void setProperty(String name, Object value) {
365+
366+
void setProperty(String name, Object value) {
366367
mergeMapEntry(rootConfig, dottedPath, this, name, value, false, true)
367368
}
368-
369-
public Object navigate(String... path) {
369+
370+
Object navigate(String... path) {
370371
return navigateMap(this, path)
371372
}
372373

@@ -392,8 +393,8 @@ class NavigableMap implements Map<String, Object>, Cloneable {
392393
}
393394
}
394395
}
395-
396-
public NavigableMap navigateSubMap(List<String> path, boolean createMissing) {
396+
397+
NavigableMap navigateSubMap(List<String> path, boolean createMissing) {
397398
NavigableMap rootMap = this
398399
NavigableMap currentMap = this
399400
StringBuilder accumulatedPath = new StringBuilder()
@@ -488,4 +489,172 @@ class NavigableMap implements Map<String, Object>, Cloneable {
488489
boolean equals(Object obj) {
489490
return delegateMap.equals(obj)
490491
}
492+
493+
/**
494+
* @deprecated This class should be avoided due to known performance reasons. Use {@code config.getProperty(String key, Class<T> targetType)} instead of dot based navigation.
495+
*/
496+
@Deprecated
497+
@CompileStatic
498+
static class NullSafeNavigator implements Map<String, Object>{
499+
final NavigableMap parent
500+
final List<String> path
501+
502+
NullSafeNavigator(NavigableMap parent, List<String> path) {
503+
this.parent = parent
504+
this.path = path
505+
if (LOG.isWarnEnabled()) {
506+
LOG.warn("Accessing config key '{}' through dot notation has known performance issues, consider using 'config.getProperty(key, targetClass)' instead.", path)
507+
}
508+
}
509+
510+
Object getAt(Object key) {
511+
getProperty(String.valueOf(key))
512+
}
513+
514+
void setAt(Object key, Object value) {
515+
setProperty(String.valueOf(key), value)
516+
}
517+
518+
@Override
519+
int size() {
520+
NavigableMap parentMap = parent.navigateSubMap(path, false)
521+
if(parentMap != null) {
522+
return parentMap.size()
523+
}
524+
return 0
525+
}
526+
527+
@Override
528+
boolean isEmpty() {
529+
NavigableMap parentMap = parent.navigateSubMap(path, false)
530+
if(parentMap != null) {
531+
return parentMap.isEmpty()
532+
}
533+
return true
534+
}
535+
536+
boolean containsKey(Object key) {
537+
NavigableMap parentMap = parent.navigateSubMap(path, false)
538+
if(parentMap == null) return false
539+
else {
540+
return parentMap.containsKey(key)
541+
}
542+
}
543+
544+
@Override
545+
boolean containsValue(Object value) {
546+
NavigableMap parentMap = parent.navigateSubMap(path, false)
547+
if(parentMap != null) {
548+
return parentMap.containsValue(value)
549+
}
550+
return false
551+
}
552+
553+
@Override
554+
Object get(Object key) {
555+
return getAt(key)
556+
}
557+
558+
@Override
559+
Object put(String key, Object value) {
560+
throw new UnsupportedOperationException("Configuration cannot be modified");
561+
}
562+
563+
@Override
564+
Object remove(Object key) {
565+
throw new UnsupportedOperationException("Configuration cannot be modified");
566+
}
567+
568+
@Override
569+
void putAll(Map<? extends String, ?> m) {
570+
throw new UnsupportedOperationException("Configuration cannot be modified");
571+
}
572+
573+
@Override
574+
void clear() {
575+
throw new UnsupportedOperationException("Configuration cannot be modified");
576+
}
577+
578+
@Override
579+
Set<String> keySet() {
580+
NavigableMap parentMap = parent.navigateSubMap(path, false)
581+
if(parentMap != null) {
582+
return parentMap.keySet()
583+
}
584+
return Collections.emptySet()
585+
}
586+
587+
@Override
588+
Collection<Object> values() {
589+
NavigableMap parentMap = parent.navigateSubMap(path, false)
590+
if(parentMap != null) {
591+
return parentMap.values()
592+
}
593+
return Collections.emptySet()
594+
}
595+
596+
@Override
597+
Set<Entry<String, Object>> entrySet() {
598+
NavigableMap parentMap = parent.navigateSubMap(path, false)
599+
if(parentMap != null) {
600+
return parentMap.entrySet()
601+
}
602+
return Collections.emptySet()
603+
}
604+
605+
Object getProperty(String name) {
606+
NavigableMap parentMap = parent.navigateSubMap(path, false)
607+
if(parentMap == null) {
608+
return new NullSafeNavigator(parent, ((path + [name]) as List<String>).asImmutable())
609+
} else {
610+
return parentMap.get(name)
611+
}
612+
}
613+
614+
void setProperty(String name, Object value) {
615+
NavigableMap parentMap = parent.navigateSubMap(path, true)
616+
parentMap.setProperty(name, value)
617+
}
618+
619+
boolean asBoolean() {
620+
false
621+
}
622+
623+
Object invokeMethod(String name, Object args) {
624+
throw new NullPointerException("Cannot invoke method " + name + "() on NullSafeNavigator");
625+
}
626+
627+
boolean equals(Object to) {
628+
return to == null || DefaultGroovyMethods.is(this, to)
629+
}
630+
631+
Iterator iterator() {
632+
return Collections.EMPTY_LIST.iterator()
633+
}
634+
635+
Object plus(String s) {
636+
return toString() + s
637+
}
638+
639+
Object plus(Object o) {
640+
throw new NullPointerException("Cannot invoke method plus on NullSafeNavigator")
641+
}
642+
643+
boolean is(Object other) {
644+
return other == null || DefaultGroovyMethods.is(this, other)
645+
}
646+
647+
Object asType(Class c) {
648+
if(c==Boolean || c==boolean) return false
649+
return null
650+
}
651+
652+
String toString() {
653+
return null
654+
}
655+
656+
// public int hashCode() {
657+
// throw new NullPointerException("Cannot invoke method hashCode() on NullSafeNavigator");
658+
// }
659+
}
491660
}

grails-bootstrap/src/test/groovy/grails/config/ConfigMapSpec.groovy

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,26 @@ test.another = true
5858
configMap.getProperty('bar.two') == 'good4'
5959

6060
}
61-
def "should support flattening keys"() {
61+
def "should support flattening keys - map syntax"() {
6262
given:
6363
NavigableMap configMap = new NavigableMap()
6464
when:
6565
configMap.a = [b: [c: 1, d: 2]]
6666
then:
6767
configMap.toFlatConfig() == ['a.b.c': 1, 'a.b.d': 2]
6868
}
69+
def "should support flattening keys - dot syntax"() {
70+
given:
71+
NavigableMap configMap = new NavigableMap()
72+
when:
73+
configMap.a.b.c = 1
74+
configMap.a.b.d = 2
75+
then:
76+
configMap.toFlatConfig() == ['a.b.c': 1, 'a.b.d': 2]
77+
}
6978

7079
@Issue('#9146')
71-
def "should support hashCode()"() {
80+
def "should support hashCode() - map syntax"() {
7281
given:
7382
NavigableMap configMap = new NavigableMap()
7483
when:
@@ -77,7 +86,18 @@ test.another = true
7786
configMap.hashCode() == configMap.hashCode()
7887
}
7988
80-
def "should support flattening list values"() {
89+
@Issue('#9146')
90+
def "should support hashCode() - dot syntax"() {
91+
given:
92+
NavigableMap configMap = new NavigableMap()
93+
when:
94+
configMap.a.b.c = 1
95+
configMap.a.b.d = 2
96+
then:"hasCode() doesn't cause a Stack Overflow error"
97+
configMap.hashCode() == configMap.hashCode()
98+
}
99+
100+
def "should support flattening list values - map syntax"() {
81101
given:
82102
NavigableMap configMap = new NavigableMap()
83103
when:
@@ -90,8 +110,23 @@ test.another = true
90110
'a.b.c[2]': 3,
91111
'a.b.d': 2]
92112
}
113+
114+
def "should support flattening list values - dot syntax"() {
115+
given:
116+
NavigableMap configMap = new NavigableMap()
117+
when:
118+
configMap.a.b.c = [1, 2, 3]
119+
configMap.a.b.d = 2
120+
then:
121+
configMap.toFlatConfig() ==
122+
['a.b.c': [1, 2, 3],
123+
'a.b.c[0]': 1,
124+
'a.b.c[1]': 2,
125+
'a.b.c[2]': 3,
126+
'a.b.d': 2]
127+
}
93128
94-
def "should support flattening to properties"() {
129+
def "should support flattening to properties - map syntax"() {
95130
given:
96131
NavigableMap configMap = new NavigableMap()
97132
when:
@@ -104,8 +139,23 @@ test.another = true
104139
'a.b.c[2]': '3',
105140
'a.b.d': '2']
106141
}
142+
143+
def "should support flattening to properties - dot syntax"() {
144+
given:
145+
NavigableMap configMap = new NavigableMap()
146+
when:
147+
configMap.a.b.c = [1, 2, 3]
148+
configMap.a.b.d = 2
149+
then:
150+
configMap.toProperties() ==
151+
['a.b.c': '1,2,3',
152+
'a.b.c[0]': '1',
153+
'a.b.c[1]': '2',
154+
'a.b.c[2]': '3',
155+
'a.b.d': '2']
156+
}
107157
108-
def "should support cloning"() {
158+
def "should support cloning - map syntax"() {
109159
given:
110160
NavigableMap configMap = new NavigableMap()
111161
configMap.a = [b: [c: [1, 2, 3], d: 2]]
@@ -121,4 +171,22 @@ test.another = true
121171
!cloned.is(configMap)
122172
cloned == configMap
123173
}
174+
175+
def "should support cloning - dot syntax"() {
176+
given:
177+
NavigableMap configMap = new NavigableMap()
178+
configMap.a.b.c = [1, 2, 3]
179+
configMap.a.b.d = 2
180+
when:
181+
NavigableMap cloned = configMap.clone()
182+
then:
183+
cloned.toFlatConfig() ==
184+
['a.b.c': [1, 2, 3],
185+
'a.b.c[0]': 1,
186+
'a.b.c[1]': 2,
187+
'a.b.c[2]': 3,
188+
'a.b.d': 2]
189+
!cloned.is(configMap)
190+
cloned == configMap
191+
}
124192
}

0 commit comments

Comments
 (0)