Skip to content

Commit 3ab330a

Browse files
toddbaertrenovate[bot]beeme1mrthisthat
authored
fix: null handling with Structure, Value (#663)
Signed-off-by: Todd Baert <[email protected]> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Beemer <[email protected]> Co-authored-by: Giovanni Liva <[email protected]>
1 parent 75ff31e commit 3ab330a

10 files changed

+140
-81
lines changed

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
<!-- used so that lombok can generate suppressions for spotbugs. It needs to find it on the relevant classpath -->
5454
<groupId>com.github.spotbugs</groupId>
5555
<artifactId>spotbugs</artifactId>
56-
<version>4.7.3</version>
56+
<version>4.8.0</version>
5757
<scope>provided</scope>
5858
</dependency>
5959

@@ -365,7 +365,7 @@
365365
<dependency>
366366
<groupId>com.github.spotbugs</groupId>
367367
<artifactId>spotbugs</artifactId>
368-
<version>4.7.3</version>
368+
<version>4.8.0</version>
369369
</dependency>
370370
</dependencies>
371371
<executions>

spotbugs-exclusions.xml

+20
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@
2626
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
2727
<Bug pattern="EI_EXPOSE_REP2"/>
2828
</And>
29+
<And>
30+
Added in spotbugs 4.8.0 - EventProvider shares a name with something from the standard lib (confusing), but change would be breaking
31+
<Class name="dev.openfeature.sdk.EventProvider"/>
32+
<Bug pattern="PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_CLASS_NAMES"/>
33+
</And>
34+
<And>
35+
Added in spotbugs 4.8.0 - Metadata shares a name with something from the standard lib (confusing), but change would be breaking
36+
<Class name="dev.openfeature.sdk.Metadata"/>
37+
<Bug pattern="PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_CLASS_NAMES"/>
38+
</And>
39+
<And>
40+
Added in spotbugs 4.8.0 - Reason shares a name with something from the standard lib (confusing), but change would be breaking
41+
<Class name="dev.openfeature.sdk.Reason"/>
42+
<Bug pattern="PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_CLASS_NAMES"/>
43+
</And>
44+
<And>
45+
Added in spotbugs 4.8.0 - FlagValueType.STRING shares a name with something from the standard lib (confusing), but change would be breaking
46+
<Class name="dev.openfeature.sdk.FlagValueType"/>
47+
<Bug pattern="PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_FIELD_NAMES"/>
48+
</And>
2949

3050
<!-- Test class that should be excluded -->
3151
<Match>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dev.openfeature.sdk;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
@SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" })
7+
abstract class AbstractStructure implements Structure {
8+
9+
protected final Map<String, Value> attributes;
10+
11+
AbstractStructure() {
12+
this.attributes = new HashMap<>();
13+
}
14+
15+
AbstractStructure(Map<String, Value> attributes) {
16+
this.attributes = attributes;
17+
}
18+
19+
/**
20+
* Get all values as their underlying primitives types.
21+
*
22+
* @return all attributes on the structure into a Map
23+
*/
24+
@Override
25+
public Map<String, Object> asObjectMap() {
26+
return attributes
27+
.entrySet()
28+
.stream()
29+
// custom collector, workaround for Collectors.toMap in JDK8
30+
// https://bugs.openjdk.org/browse/JDK-8148463
31+
.collect(HashMap::new,
32+
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
33+
HashMap::putAll);
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
package dev.openfeature.sdk;
22

3-
import lombok.EqualsAndHashCode;
4-
import lombok.ToString;
5-
63
import java.util.HashMap;
74
import java.util.HashSet;
85
import java.util.Map;
6+
import java.util.Optional;
97
import java.util.Set;
10-
import java.util.stream.Collectors;
8+
9+
import lombok.EqualsAndHashCode;
10+
import lombok.ToString;
1111

1212
/**
13-
* {@link ImmutableStructure} represents a potentially nested object type which is used to represent
13+
* {@link ImmutableStructure} represents a potentially nested object type which
14+
* is used to represent
1415
* structured data.
15-
* The ImmutableStructure is a Structure implementation which is threadsafe, and whose attributes can
16-
* not be modified after instantiation.
16+
* The ImmutableStructure is a Structure implementation which is threadsafe, and
17+
* whose attributes can
18+
* not be modified after instantiation. All references are clones.
1719
*/
1820
@ToString
1921
@EqualsAndHashCode
20-
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
21-
public final class ImmutableStructure implements Structure {
22-
23-
private final Map<String, Value> attributes;
22+
@SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" })
23+
public final class ImmutableStructure extends AbstractStructure {
2424

2525
/**
2626
* create an immutable structure with the empty attributes.
2727
*/
2828
public ImmutableStructure() {
29-
this(new HashMap<>());
29+
super();
3030
}
3131

3232
/**
@@ -35,10 +35,14 @@ public ImmutableStructure() {
3535
* @param attributes attributes.
3636
*/
3737
public ImmutableStructure(Map<String, Value> attributes) {
38-
Map<String, Value> copy = attributes.entrySet()
38+
super(new HashMap<>(attributes.entrySet()
3939
.stream()
40-
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().clone()));
41-
this.attributes = new HashMap<>(copy);
40+
.collect(HashMap::new,
41+
(accumulated, entry) -> accumulated.put(entry.getKey(),
42+
Optional.ofNullable(entry.getValue())
43+
.map(e -> e.clone())
44+
.orElse(null)),
45+
HashMap::putAll)));
4246
}
4347

4448
@Override
@@ -63,25 +67,11 @@ public Map<String, Value> asMap() {
6367
return attributes
6468
.entrySet()
6569
.stream()
66-
.collect(Collectors.toMap(
67-
Map.Entry::getKey,
68-
e -> getValue(e.getKey())
69-
));
70-
}
71-
72-
/**
73-
* Get all values, with primitives types.
74-
*
75-
* @return all attributes on the structure into a Map
76-
*/
77-
@Override
78-
public Map<String, Object> asObjectMap() {
79-
return attributes
80-
.entrySet()
81-
.stream()
82-
.collect(Collectors.toMap(
83-
Map.Entry::getKey,
84-
e -> convertValue(getValue(e.getKey()))
85-
));
70+
.collect(HashMap::new,
71+
(accumulated, entry) -> accumulated.put(entry.getKey(),
72+
Optional.ofNullable(entry.getValue())
73+
.map(e -> e.clone())
74+
.orElse(null)),
75+
HashMap::putAll);
8676
}
8777
}

src/main/java/dev/openfeature/sdk/MutableStructure.java

+6-25
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package dev.openfeature.sdk;
22

3-
import lombok.EqualsAndHashCode;
4-
import lombok.ToString;
5-
63
import java.time.Instant;
74
import java.util.HashMap;
85
import java.util.List;
96
import java.util.Map;
107
import java.util.Set;
11-
import java.util.stream.Collectors;
8+
9+
import lombok.EqualsAndHashCode;
10+
import lombok.ToString;
1211

1312
/**
1413
* {@link MutableStructure} represents a potentially nested object type which is used to represent
@@ -19,16 +18,14 @@
1918
@ToString
2019
@EqualsAndHashCode
2120
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
22-
public class MutableStructure implements Structure {
23-
24-
protected final Map<String, Value> attributes;
21+
public class MutableStructure extends AbstractStructure {
2522

2623
public MutableStructure() {
27-
this.attributes = new HashMap<>();
24+
super();
2825
}
2926

3027
public MutableStructure(Map<String, Value> attributes) {
31-
this.attributes = new HashMap<>(attributes);
28+
super(attributes);
3229
}
3330

3431
@Override
@@ -92,20 +89,4 @@ public <T> MutableStructure add(String key, List<Value> value) {
9289
public Map<String, Value> asMap() {
9390
return new HashMap<>(this.attributes);
9491
}
95-
96-
/**
97-
* Get all values, with primitives types.
98-
*
99-
* @return all attributes on the structure into a Map
100-
*/
101-
@Override
102-
public Map<String, Object> asObjectMap() {
103-
return attributes
104-
.entrySet()
105-
.stream()
106-
.collect(Collectors.toMap(
107-
Map.Entry::getKey,
108-
e -> convertValue(getValue(e.getKey()))
109-
));
110-
}
11192
}

src/main/java/dev/openfeature/sdk/Structure.java

+17-11
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,17 @@ public interface Structure {
4848
Map<String, Object> asObjectMap();
4949

5050
/**
51-
* convertValue is converting the object type Value in a primitive type.
51+
* Converts the Value into its equivalent primitive type.
5252
*
5353
* @param value - Value object to convert
54-
* @return an Object containing the primitive type.
54+
* @return an Object containing the primitive type, or null.
5555
*/
5656
default Object convertValue(Value value) {
57+
58+
if (value == null || value.isNull()) {
59+
return null;
60+
}
61+
5762
if (value.isBoolean()) {
5863
return value.asBoolean();
5964
}
@@ -85,15 +90,14 @@ default Object convertValue(Value value) {
8590
if (value.isStructure()) {
8691
Structure s = value.asStructure();
8792
return s.asMap()
88-
.keySet()
93+
.entrySet()
8994
.stream()
90-
.collect(
91-
Collectors.toMap(
92-
key -> key,
93-
key -> convertValue(s.getValue(key))
94-
)
95-
);
95+
.collect(HashMap::new,
96+
(accumulated, entry) -> accumulated.put(entry.getKey(),
97+
convertValue(entry.getValue())),
98+
HashMap::putAll);
9699
}
100+
97101
throw new ValueNotConvertableError();
98102
}
99103

@@ -134,7 +138,9 @@ default <T extends Structure> Map<String, Value> merge(Function<Map<String, Valu
134138
*/
135139
static Structure mapToStructure(Map<String, Object> map) {
136140
return new MutableStructure(map.entrySet().stream()
137-
.filter(e -> e.getValue() != null)
138-
.collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue()))));
141+
.collect(HashMap::new,
142+
(accumulated, entry) -> accumulated.put(entry.getKey(),
143+
objectToValue(entry.getValue())),
144+
HashMap::putAll));
139145
}
140146
}

src/main/java/dev/openfeature/sdk/Value.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919
*/
2020
@ToString
2121
@EqualsAndHashCode
22-
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
22+
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType", "checkstyle:NoFinalizer"})
2323
public class Value implements Cloneable {
2424

2525
private final Object innerObject;
2626

27+
protected final void finalize() {
28+
// DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW
29+
}
30+
2731
/**
2832
* Construct a new null Value.
2933
*/
@@ -271,11 +275,7 @@ protected Value clone() {
271275
return new Value(copy);
272276
}
273277
if (this.isStructure()) {
274-
Map<String, Value> copy = this.asStructure().asMap().entrySet().stream().collect(Collectors.toMap(
275-
Map.Entry::getKey,
276-
e -> e.getValue().clone()
277-
));
278-
return new Value(new ImmutableStructure(copy));
278+
return new Value(new ImmutableStructure(this.asStructure().asMap()));
279279
}
280280
if (this.isInstant()) {
281281
Instant copy = Instant.ofEpochMilli(this.asInstant().toEpochMilli());
@@ -294,7 +294,7 @@ public static Value objectToValue(Object object) {
294294
if (object instanceof Value) {
295295
return (Value) object;
296296
} else if (object == null) {
297-
return null;
297+
return new Value();
298298
} else if (object instanceof String) {
299299
return new Value((String) object);
300300
} else if (object instanceof Boolean) {

src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java

+7
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,11 @@ void GettingAMissingValueShouldReturnNull() {
122122

123123
assertEquals(expected, structure.asObjectMap());
124124
}
125+
126+
@Test
127+
void constructorHandlesNullValue() {
128+
HashMap<String, Value> attrs = new HashMap<>();
129+
attrs.put("null", null);
130+
new ImmutableStructure(attrs);
131+
}
125132
}

src/test/java/dev/openfeature/sdk/StructureTest.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ void mapToStructureTest() {
9898
assertEquals(new Value(Instant.ofEpochSecond(0)), res.getValue("Instant"));
9999
assertEquals(new HashMap<>(), res.getValue("Map").asStructure().asMap());
100100
assertEquals(new Value(immutableContext), res.getValue("ImmutableContext"));
101-
assertNull(res.getValue("nullKey"));
101+
assertEquals(new Value(), res.getValue("nullKey"));
102+
}
103+
104+
@Test
105+
void asObjectHandlesNullValue() {
106+
Map<String, Value> map = new HashMap<>();
107+
map.put("null", new Value((String)null));
108+
ImmutableStructure structure = new ImmutableStructure(map);
109+
assertNull(structure.asObjectMap().get("null"));
110+
}
111+
112+
@Test
113+
void convertValueHandlesNullValue() {
114+
ImmutableStructure structure = new ImmutableStructure();
115+
assertNull(structure.convertValue(new Value((String)null)));
102116
}
103117
}

src/test/java/dev/openfeature/sdk/ValueTest.java

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.openfeature.sdk;
22

3+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.junit.jupiter.api.Assertions.assertThrows;
56
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -142,4 +143,9 @@ class Something {}
142143

143144
assertThrows(InstantiationException.class, ()-> new Value(list));
144145
}
146+
147+
@Test public void noOpFinalize() {
148+
Value val = new Value();
149+
assertDoesNotThrow(val::finalize); // does nothing, but we want to defined in and make it final.
150+
}
145151
}

0 commit comments

Comments
 (0)