Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 21ea92a

Browse files
prepare 5.3.0 release (#228)
1 parent 9bc200b commit 21ea92a

File tree

12 files changed

+105
-29
lines changed

12 files changed

+105
-29
lines changed

src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@
1414
* @see TestData
1515
*/
1616
public abstract class FileData {
17+
/**
18+
* Determines how duplicate feature flag or segment keys are handled.
19+
*
20+
* @see FileDataSourceBuilder#duplicateKeysHandling
21+
* @since 5.3.0
22+
*/
23+
public enum DuplicateKeysHandling {
24+
/**
25+
* Data loading will fail if keys are duplicated across files.
26+
*/
27+
FAIL,
28+
29+
/**
30+
* Keys that are duplicated across files will be ignored, and the first occurrence will be used.
31+
*/
32+
IGNORE
33+
}
34+
1735
/**
1836
* Creates a {@link FileDataSourceBuilder} which you can use to configure the file data source.
1937
* This allows you to use local files (or classpath resources containing file data) as a source of

src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
public final class FileDataSourceBuilder implements DataSourceFactory {
2929
final List<SourceInfo> sources = new ArrayList<>(); // visible for tests
3030
private boolean autoUpdate = false;
31+
private FileData.DuplicateKeysHandling duplicateKeysHandling = FileData.DuplicateKeysHandling.FAIL;
3132

3233
/**
3334
* Adds any number of source files for loading flag data, specifying each file path as a string. The files will
@@ -101,13 +102,27 @@ public FileDataSourceBuilder autoUpdate(boolean autoUpdate) {
101102
this.autoUpdate = autoUpdate;
102103
return this;
103104
}
105+
106+
/**
107+
* Specifies how to handle keys that are duplicated across files.
108+
* <p>
109+
* By default, data loading will fail if keys are duplicated across files ({@link FileData.DuplicateKeysHandling#FAIL}).
110+
*
111+
* @param duplicateKeysHandling specifies how to handle duplicate keys
112+
* @return the same factory object
113+
* @since 5.3.0
114+
*/
115+
public FileDataSourceBuilder duplicateKeysHandling(FileData.DuplicateKeysHandling duplicateKeysHandling) {
116+
this.duplicateKeysHandling = duplicateKeysHandling;
117+
return this;
118+
}
104119

105120
/**
106121
* Used internally by the LaunchDarkly client.
107122
*/
108123
@Override
109124
public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates) {
110-
return new FileDataSourceImpl(dataSourceUpdates, sources, autoUpdate);
125+
return new FileDataSourceImpl(dataSourceUpdates, sources, autoUpdate, duplicateKeysHandling);
111126
}
112127

113128
static abstract class SourceInfo {

src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,19 @@ final class FileDataSourceImpl implements DataSource {
5858

5959
private final DataSourceUpdates dataSourceUpdates;
6060
private final DataLoader dataLoader;
61+
private final FileData.DuplicateKeysHandling duplicateKeysHandling;
6162
private final AtomicBoolean inited = new AtomicBoolean(false);
6263
private final FileWatcher fileWatcher;
6364

64-
FileDataSourceImpl(DataSourceUpdates dataSourceUpdates, List<SourceInfo> sources, boolean autoUpdate) {
65+
FileDataSourceImpl(
66+
DataSourceUpdates dataSourceUpdates,
67+
List<SourceInfo> sources,
68+
boolean autoUpdate,
69+
FileData.DuplicateKeysHandling duplicateKeysHandling
70+
) {
6571
this.dataSourceUpdates = dataSourceUpdates;
6672
this.dataLoader = new DataLoader(sources);
73+
this.duplicateKeysHandling = duplicateKeysHandling;
6774

6875
FileWatcher fw = null;
6976
if (autoUpdate) {
@@ -97,7 +104,7 @@ public Future<Void> start() {
97104
}
98105

99106
private boolean reload() {
100-
DataBuilder builder = new DataBuilder();
107+
DataBuilder builder = new DataBuilder(duplicateKeysHandling);
101108
try {
102109
dataLoader.load(builder);
103110
} catch (FileDataException e) {
@@ -262,6 +269,11 @@ public void load(DataBuilder builder) throws FileDataException
262269
*/
263270
static final class DataBuilder {
264271
private final Map<DataKind, Map<String, ItemDescriptor>> allData = new HashMap<>();
272+
private final FileData.DuplicateKeysHandling duplicateKeysHandling;
273+
274+
public DataBuilder(FileData.DuplicateKeysHandling duplicateKeysHandling) {
275+
this.duplicateKeysHandling = duplicateKeysHandling;
276+
}
265277

266278
public FullDataSet<ItemDescriptor> build() {
267279
ImmutableList.Builder<Map.Entry<DataKind, KeyedItems<ItemDescriptor>>> allBuilder = ImmutableList.builder();
@@ -278,6 +290,9 @@ public void add(DataKind kind, String key, ItemDescriptor item) throws FileDataE
278290
allData.put(kind, items);
279291
}
280292
if (items.containsKey(key)) {
293+
if (duplicateKeysHandling == FileData.DuplicateKeysHandling.IGNORE) {
294+
return;
295+
}
281296
throw new FileDataException("in " + kind.getName() + ", key \"" + key + "\" was already defined", null, null);
282297
}
283298
items.put(key, item);

src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package com.launchdarkly.sdk.server.integrations;
22

3-
import com.google.gson.Gson;
4-
import com.google.gson.JsonElement;
5-
import com.google.gson.JsonObject;
63
import com.launchdarkly.sdk.LDValue;
74
import com.launchdarkly.sdk.server.integrations.FileDataSourceImpl.DataBuilder;
85
import com.launchdarkly.sdk.server.integrations.FileDataSourceImpl.DataLoader;
@@ -29,8 +26,7 @@
2926

3027
@SuppressWarnings("javadoc")
3128
public class DataLoaderTest {
32-
private static final Gson gson = new Gson();
33-
private DataBuilder builder = new DataBuilder();
29+
private DataBuilder builder = new DataBuilder(FileData.DuplicateKeysHandling.FAIL);
3430

3531
@Test
3632
public void canLoadFromFilePath() throws Exception {
@@ -75,22 +71,20 @@ public void canLoadMultipleFiles() throws Exception {
7571
@Test
7672
public void flagValueIsConvertedToFlag() throws Exception {
7773
DataLoader ds = new DataLoader(FileData.dataSource().filePaths(resourceFilePath("value-only.json")).sources);
78-
JsonObject expected = gson.fromJson(
74+
LDValue expected = LDValue.parse(
7975
"{\"key\":\"flag2\",\"on\":true,\"fallthrough\":{\"variation\":0},\"variations\":[\"value2\"]," +
80-
"\"trackEvents\":false,\"deleted\":false,\"version\":1}",
81-
JsonObject.class);
76+
"\"trackEvents\":false,\"deleted\":false,\"version\":1}");
8277
ds.load(builder);
83-
ItemDescriptor flag = toDataMap(builder.build()).get(FEATURES).get(FLAG_VALUE_1_KEY);
84-
JsonObject actual = gson.toJsonTree(flag.getItem()).getAsJsonObject();
78+
LDValue actual = getItemAsJson(builder, FEATURES, FLAG_VALUE_1_KEY);
8579
// Note, we're comparing one property at a time here because the version of the Java SDK we're
8680
// building against may have more properties than it did when the test was written.
87-
for (Map.Entry<String, JsonElement> e: expected.entrySet()) {
88-
assertThat(actual.get(e.getKey()), equalTo(e.getValue()));
81+
for (String key: expected.keys()) {
82+
assertThat(actual.get(key), equalTo(expected.get(key)));
8983
}
9084
}
9185

9286
@Test
93-
public void duplicateFlagKeyInFlagsThrowsException() throws Exception {
87+
public void duplicateFlagKeyInFlagsThrowsExceptionByDefault() throws Exception {
9488
try {
9589
DataLoader ds = new DataLoader(FileData.dataSource().filePaths(
9690
resourceFilePath("flag-only.json"),
@@ -103,7 +97,7 @@ public void duplicateFlagKeyInFlagsThrowsException() throws Exception {
10397
}
10498

10599
@Test
106-
public void duplicateFlagKeyInFlagsAndFlagValuesThrowsException() throws Exception {
100+
public void duplicateFlagKeyInFlagsAndFlagValuesThrowsExceptionByDefault() throws Exception {
107101
try {
108102
DataLoader ds = new DataLoader(FileData.dataSource().filePaths(
109103
resourceFilePath("flag-only.json"),
@@ -116,7 +110,7 @@ public void duplicateFlagKeyInFlagsAndFlagValuesThrowsException() throws Excepti
116110
}
117111

118112
@Test
119-
public void duplicateSegmentKeyThrowsException() throws Exception {
113+
public void duplicateSegmentKeyThrowsExceptionByDefault() throws Exception {
120114
try {
121115
DataLoader ds = new DataLoader(FileData.dataSource().filePaths(
122116
resourceFilePath("segment-only.json"),
@@ -128,6 +122,35 @@ public void duplicateSegmentKeyThrowsException() throws Exception {
128122
}
129123
}
130124

125+
@Test
126+
public void duplicateKeysCanBeAllowed() throws Exception {
127+
DataBuilder data1 = new DataBuilder(FileData.DuplicateKeysHandling.IGNORE);
128+
DataLoader loader1 = new DataLoader(FileData.dataSource().filePaths(
129+
resourceFilePath("flag-only.json"),
130+
resourceFilePath("flag-with-duplicate-key.json")
131+
).sources);
132+
loader1.load(data1);
133+
assertThat(getItemAsJson(data1, FEATURES, "flag1").get("on"), equalTo(LDValue.of(true))); // value from first file
134+
135+
DataBuilder data2 = new DataBuilder(FileData.DuplicateKeysHandling.IGNORE);
136+
DataLoader loader2 = new DataLoader(FileData.dataSource().filePaths(
137+
resourceFilePath("value-with-duplicate-key.json"),
138+
resourceFilePath("flag-only.json")
139+
).sources);
140+
loader2.load(data2);
141+
assertThat(getItemAsJson(data2, FEATURES, "flag2").get("variations"),
142+
equalTo(LDValue.buildArray().add(LDValue.of("value2a")).build())); // value from first file
143+
144+
DataBuilder data3 = new DataBuilder(FileData.DuplicateKeysHandling.IGNORE);
145+
DataLoader loader3 = new DataLoader(FileData.dataSource().filePaths(
146+
resourceFilePath("segment-only.json"),
147+
resourceFilePath("segment-with-duplicate-key.json")
148+
).sources);
149+
loader3.load(data3);
150+
assertThat(getItemAsJson(data3, SEGMENTS, "seg1").get("included"),
151+
equalTo(LDValue.buildArray().add(LDValue.of("user1")).build())); // value from first file
152+
}
153+
131154
@Test
132155
public void versionsAreIncrementedForEachLoad() throws Exception {
133156
DataLoader ds = new DataLoader(FileData.dataSource().filePaths(
@@ -136,11 +159,11 @@ public void versionsAreIncrementedForEachLoad() throws Exception {
136159
resourceFilePath("value-only.json")
137160
).sources);
138161

139-
DataBuilder data1 = new DataBuilder();
162+
DataBuilder data1 = new DataBuilder(FileData.DuplicateKeysHandling.FAIL);
140163
ds.load(data1);
141164
assertVersionsMatch(data1.build(), 1);
142165

143-
DataBuilder data2 = new DataBuilder();
166+
DataBuilder data2 = new DataBuilder(FileData.DuplicateKeysHandling.FAIL);
144167
ds.load(data2);
145168
assertVersionsMatch(data2.build(), 2);
146169
}
@@ -164,4 +187,9 @@ private void assertVersionsMatch(FullDataSet<ItemDescriptor> data, int expectedV
164187
}
165188
}
166189
}
190+
191+
private LDValue getItemAsJson(DataBuilder builder, DataKind kind, String key) {
192+
ItemDescriptor flag = toDataMap(builder.build()).get(kind).get(key);
193+
return LDValue.parse(kind.serialize(flag));
194+
}
167195
}

src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceTestData.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class FileDataSourceTestData {
2929
ImmutableMap.<String, LDValue>of(FLAG_VALUE_1_KEY, FLAG_VALUE_1);
3030

3131
public static final String FULL_SEGMENT_1_KEY = "seg1";
32-
public static final LDValue FULL_SEGMENT_1 = LDValue.parse("{\"key\":\"seg1\",\"include\":[\"user1\"]}");
32+
public static final LDValue FULL_SEGMENT_1 = LDValue.parse("{\"key\":\"seg1\",\"included\":[\"user1\"]}");
3333
public static final Map<String, LDValue> FULL_SEGMENTS =
3434
ImmutableMap.<String, LDValue>of(FULL_SEGMENT_1_KEY, FULL_SEGMENT_1);
3535

src/test/resources/filesource/all-properties.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"segments": {
1616
"seg1": {
1717
"key": "seg1",
18-
"include": ["user1"]
18+
"included": ["user1"]
1919
}
2020
}
2121
}

src/test/resources/filesource/all-properties.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ flagValues:
1414
segments:
1515
seg1:
1616
key: seg1
17-
include: ["user1"]
17+
included: ["user1"]

src/test/resources/filesource/flag-with-duplicate-key.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"flag1": {
88
"key": "flag1",
9-
"on": true
9+
"on": false
1010
}
1111
}
1212
}

src/test/resources/filesource/segment-only.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"segments": {
33
"seg1": {
44
"key": "seg1",
5-
"include": ["user1"]
5+
"included": ["user1"]
66
}
77
}
88
}

src/test/resources/filesource/segment-only.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
segments:
33
seg1:
44
key: seg1
5-
include: ["user1"]
5+
included: ["user1"]

0 commit comments

Comments
 (0)