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

Commit b9be40e

Browse files
authored
prepare 4.5.0 release (#147)
1 parent 3c84810 commit b9be40e

33 files changed

+1290
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ Be aware of two considerations when enabling the DEBUG log level:
5353
1. Debug-level logs can be very verbose. It is not recommended that you turn on debug logging in high-volume environments.
5454
1. Potentially sensitive information is logged including LaunchDarkly users created by you in your usage of this SDK.
5555

56+
Using flag data from a file
57+
---------------------------
58+
For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See <a href="http://javadoc.io/page/com.launchdarkly/launchdarkly-client/latest/com/launchdarkly/client/files/FileComponents.html">FileComponents</a> for more details.
59+
5660
Learn more
5761
----------
5862

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ libraries.internal = [
3232
"com.google.guava:guava:19.0",
3333
"joda-time:joda-time:2.9.3",
3434
"com.launchdarkly:okhttp-eventsource:1.7.1",
35+
"org.yaml:snakeyaml:1.19",
3536
"redis.clients:jedis:2.9.0"
3637
]
3738

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.launchdarkly.client.files;
2+
3+
import com.launchdarkly.client.VersionedData;
4+
import com.launchdarkly.client.VersionedDataKind;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
/**
10+
* Internal data structure that organizes flag/segment data into the format that the feature store
11+
* expects. Will throw an exception if we try to add the same flag or segment key more than once.
12+
*/
13+
class DataBuilder {
14+
private final Map<VersionedDataKind<?>, Map<String, ? extends VersionedData>> allData = new HashMap<>();
15+
16+
public Map<VersionedDataKind<?>, Map<String, ? extends VersionedData>> build() {
17+
return allData;
18+
}
19+
20+
public void add(VersionedDataKind<?> kind, VersionedData item) throws DataLoaderException {
21+
@SuppressWarnings("unchecked")
22+
Map<String, VersionedData> items = (Map<String, VersionedData>)allData.get(kind);
23+
if (items == null) {
24+
items = new HashMap<String, VersionedData>();
25+
allData.put(kind, items);
26+
}
27+
if (items.containsKey(item.getKey())) {
28+
throw new DataLoaderException("in " + kind.getNamespace() + ", key \"" + item.getKey() + "\" was already defined", null, null);
29+
}
30+
items.put(item.getKey(), item);
31+
}
32+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.launchdarkly.client.files;
2+
3+
import com.google.gson.JsonElement;
4+
import com.launchdarkly.client.VersionedDataKind;
5+
6+
import java.io.ByteArrayInputStream;
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
/**
15+
* Implements the loading of flag data from one or more files. Will throw an exception if any file can't
16+
* be read or parsed, or if any flag or segment keys are duplicates.
17+
*/
18+
final class DataLoader {
19+
private final List<Path> files;
20+
21+
public DataLoader(List<Path> files) {
22+
this.files = new ArrayList<Path>(files);
23+
}
24+
25+
public Iterable<Path> getFiles() {
26+
return files;
27+
}
28+
29+
public void load(DataBuilder builder) throws DataLoaderException
30+
{
31+
for (Path p: files) {
32+
try {
33+
byte[] data = Files.readAllBytes(p);
34+
FlagFileParser parser = FlagFileParser.selectForContent(data);
35+
FlagFileRep fileContents = parser.parse(new ByteArrayInputStream(data));
36+
if (fileContents.flags != null) {
37+
for (Map.Entry<String, JsonElement> e: fileContents.flags.entrySet()) {
38+
builder.add(VersionedDataKind.FEATURES, FlagFactory.flagFromJson(e.getValue()));
39+
}
40+
}
41+
if (fileContents.flagValues != null) {
42+
for (Map.Entry<String, JsonElement> e: fileContents.flagValues.entrySet()) {
43+
builder.add(VersionedDataKind.FEATURES, FlagFactory.flagWithValue(e.getKey(), e.getValue()));
44+
}
45+
}
46+
if (fileContents.segments != null) {
47+
for (Map.Entry<String, JsonElement> e: fileContents.segments.entrySet()) {
48+
builder.add(VersionedDataKind.SEGMENTS, FlagFactory.segmentFromJson(e.getValue()));
49+
}
50+
}
51+
} catch (DataLoaderException e) {
52+
throw new DataLoaderException(e.getMessage(), e.getCause(), p);
53+
} catch (IOException e) {
54+
throw new DataLoaderException(null, e, p);
55+
}
56+
}
57+
}
58+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.launchdarkly.client.files;
2+
3+
import java.nio.file.Path;
4+
5+
/**
6+
* Indicates that the file processor encountered an error in one of the input files. This exception is
7+
* not surfaced to the host application, it is only logged, and we don't do anything different programmatically
8+
* with different kinds of exceptions, therefore it has no subclasses.
9+
*/
10+
@SuppressWarnings("serial")
11+
class DataLoaderException extends Exception {
12+
private final Path filePath;
13+
14+
public DataLoaderException(String message, Throwable cause, Path filePath) {
15+
super(message, cause);
16+
this.filePath = filePath;
17+
}
18+
19+
public DataLoaderException(String message, Throwable cause) {
20+
this(message, cause, null);
21+
}
22+
23+
public Path getFilePath() {
24+
return filePath;
25+
}
26+
27+
public String getDescription() {
28+
StringBuilder s = new StringBuilder();
29+
if (getMessage() != null) {
30+
s.append(getMessage());
31+
if (getCause() != null) {
32+
s.append(" ");
33+
}
34+
}
35+
if (getCause() != null) {
36+
s.append(" [").append(getCause().toString()).append("]");
37+
}
38+
if (filePath != null) {
39+
s.append(": ").append(filePath);
40+
}
41+
return s.toString();
42+
}
43+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.launchdarkly.client.files;
2+
3+
/**
4+
* The entry point for the file data source, which allows you to use local files as a source of
5+
* feature flag state. This would typically be used in a test environment, to operate using a
6+
* predetermined feature flag state without an actual LaunchDarkly connection.
7+
* <p>
8+
* To use this component, call {@link #fileDataSource()} to obtain a factory object, call one or
9+
* methods to configure it, and then add it to your LaunchDarkly client configuration. At a
10+
* minimum, you will want to call {@link FileDataSourceFactory#filePaths(String...)} to specify
11+
* your data file(s); you can also use {@link FileDataSourceFactory#autoUpdate(boolean)} to
12+
* specify that flags should be reloaded when a file is modified. See {@link FileDataSourceFactory}
13+
* for all configuration options.
14+
* <pre>
15+
* FileDataSourceFactory f = FileComponents.fileDataSource()
16+
* .filePaths("./testData/flags.json")
17+
* .autoUpdate(true);
18+
* LDConfig config = new LDConfig.Builder()
19+
* .updateProcessorFactory(f)
20+
* .build();
21+
* </pre>
22+
* <p>
23+
* This will cause the client <i>not</i> to connect to LaunchDarkly to get feature flags. The
24+
* client may still make network connections to send analytics events, unless you have disabled
25+
* this with {@link com.launchdarkly.client.LDConfig.Builder#sendEvents(boolean)} or
26+
* {@link com.launchdarkly.client.LDConfig.Builder#offline(boolean)}.
27+
* <p>
28+
* Flag data files can be either JSON or YAML. They contain an object with three possible
29+
* properties:
30+
* <ul>
31+
* <li> {@code flags}: Feature flag definitions.
32+
* <li> {@code flagVersions}: Simplified feature flags that contain only a value.
33+
* <li> {@code segments}: User segment definitions.
34+
* </ul>
35+
* <p>
36+
* The format of the data in {@code flags} and {@code segments} is defined by the LaunchDarkly application
37+
* and is subject to change. Rather than trying to construct these objects yourself, it is simpler
38+
* to request existing flags directly from the LaunchDarkly server in JSON format, and use this
39+
* output as the starting point for your file. In Linux you would do this:
40+
* <pre>
41+
* curl -H "Authorization: {your sdk key}" https://app.launchdarkly.com/sdk/latest-all
42+
* </pre>
43+
* <p>
44+
* The output will look something like this (but with many more properties):
45+
* <pre>
46+
* {
47+
* "flags": {
48+
* "flag-key-1": {
49+
* "key": "flag-key-1",
50+
* "on": true,
51+
* "variations": [ "a", "b" ]
52+
* },
53+
* "flag-key-2": {
54+
* "key": "flag-key-2",
55+
* "on": true,
56+
* "variations": [ "c", "d" ]
57+
* }
58+
* },
59+
* "segments": {
60+
* "segment-key-1": {
61+
* "key": "segment-key-1",
62+
* "includes": [ "user-key-1" ]
63+
* }
64+
* }
65+
* }
66+
* </pre>
67+
* <p>
68+
* Data in this format allows the SDK to exactly duplicate all the kinds of flag behavior supported
69+
* by LaunchDarkly. However, in many cases you will not need this complexity, but will just want to
70+
* set specific flag keys to specific values. For that, you can use a much simpler format:
71+
* <pre>
72+
* {
73+
* "flagValues": {
74+
* "my-string-flag-key": "value-1",
75+
* "my-boolean-flag-key": true,
76+
* "my-integer-flag-key": 3
77+
* }
78+
* }
79+
* </pre>
80+
* <p>
81+
* Or, in YAML:
82+
* <pre>
83+
* flagValues:
84+
* my-string-flag-key: "value-1"
85+
* my-boolean-flag-key: true
86+
* my-integer-flag-key: 3
87+
* </pre>
88+
* <p>
89+
* It is also possible to specify both {@code flags} and {@code flagValues}, if you want some flags
90+
* to have simple values and others to have complex behavior. However, it is an error to use the
91+
* same flag key or segment key more than once, either in a single file or across multiple files.
92+
* <p>
93+
* If the data source encounters any error in any file-- malformed content, a missing file, or a
94+
* duplicate key-- it will not load flags from any of the files.
95+
*
96+
* @since 4.5.0
97+
*/
98+
public abstract class FileComponents {
99+
/**
100+
* Creates a {@link FileDataSourceFactory} which you can use to configure the file data
101+
* source.
102+
* @return a {@link FileDataSourceFactory}
103+
*/
104+
public static FileDataSourceFactory fileDataSource() {
105+
return new FileDataSourceFactory();
106+
}
107+
}

0 commit comments

Comments
 (0)