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

Commit 3f9f2b6

Browse files
prepare 5.6.0 release (#241)
1 parent 62fa2ce commit 3f9f2b6

File tree

6 files changed

+236
-95
lines changed

6 files changed

+236
-95
lines changed

src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java

Lines changed: 104 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.launchdarkly.sdk.server;
22

3+
import com.google.common.collect.ImmutableMap;
4+
import com.google.common.collect.Maps;
35
import com.google.gson.TypeAdapter;
46
import com.google.gson.annotations.JsonAdapter;
57
import com.google.gson.stream.JsonReader;
@@ -10,7 +12,6 @@
1012
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
1113

1214
import java.io.IOException;
13-
import java.util.Collections;
1415
import java.util.HashMap;
1516
import java.util.Map;
1617
import java.util.Objects;
@@ -36,19 +37,20 @@
3637
*/
3738
@JsonAdapter(FeatureFlagsState.JsonSerialization.class)
3839
public final class FeatureFlagsState implements JsonSerializable {
39-
private final Map<String, LDValue> flagValues;
40-
private final Map<String, FlagMetadata> flagMetadata;
40+
private final ImmutableMap<String, FlagMetadata> flagMetadata;
4141
private final boolean valid;
4242

4343
static class FlagMetadata {
44+
final LDValue value;
4445
final Integer variation;
4546
final EvaluationReason reason;
4647
final Integer version;
4748
final Boolean trackEvents;
4849
final Long debugEventsUntilDate;
4950

50-
FlagMetadata(Integer variation, EvaluationReason reason, Integer version, boolean trackEvents,
51-
Long debugEventsUntilDate) {
51+
FlagMetadata(LDValue value, Integer variation, EvaluationReason reason, Integer version,
52+
boolean trackEvents, Long debugEventsUntilDate) {
53+
this.value = LDValue.normalize(value);
5254
this.variation = variation;
5355
this.reason = reason;
5456
this.version = version;
@@ -60,7 +62,8 @@ static class FlagMetadata {
6062
public boolean equals(Object other) {
6163
if (other instanceof FlagMetadata) {
6264
FlagMetadata o = (FlagMetadata)other;
63-
return Objects.equals(variation, o.variation) &&
65+
return value.equals(o.value) &&
66+
Objects.equals(variation, o.variation) &&
6467
Objects.equals(reason, o.reason) &&
6568
Objects.equals(version, o.version) &&
6669
Objects.equals(trackEvents, o.trackEvents) &&
@@ -75,13 +78,27 @@ public int hashCode() {
7578
}
7679
}
7780

78-
private FeatureFlagsState(Map<String, LDValue> flagValues,
79-
Map<String, FlagMetadata> flagMetadata, boolean valid) {
80-
this.flagValues = Collections.unmodifiableMap(flagValues);
81-
this.flagMetadata = Collections.unmodifiableMap(flagMetadata);
81+
private FeatureFlagsState(ImmutableMap<String, FlagMetadata> flagMetadata, boolean valid) {
82+
this.flagMetadata = flagMetadata;
8283
this.valid = valid;
8384
}
8485

86+
/**
87+
* Returns a {@link Builder} for creating instances.
88+
* <p>
89+
* Application code will not normally use this builder, since the SDK creates its own instances.
90+
* However, it may be useful in testing, to simulate values that might be returned by
91+
* {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}.
92+
*
93+
* @param options the same {@link FlagsStateOption}s, if any, that would be passed to
94+
* {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}
95+
* @return a builder object
96+
* @since 5.6.0
97+
*/
98+
public static Builder builder(FlagsStateOption... options) {
99+
return new Builder(options);
100+
}
101+
85102
/**
86103
* Returns true if this object contains a valid snapshot of feature flag state, or false if the
87104
* state could not be computed (for instance, because the client was offline or there was no user).
@@ -98,7 +115,8 @@ public boolean isValid() {
98115
* {@code null} if there was no such flag
99116
*/
100117
public LDValue getFlagValue(String key) {
101-
return flagValues.get(key);
118+
FlagMetadata data = flagMetadata.get(key);
119+
return data == null ? null : data.value;
102120
}
103121

104122
/**
@@ -115,64 +133,100 @@ public EvaluationReason getFlagReason(String key) {
115133
* Returns a map of flag keys to flag values. If a flag would have evaluated to the default value,
116134
* its value will be null.
117135
* <p>
136+
* The returned map is unmodifiable.
137+
* <p>
118138
* Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
119139
* Instead, serialize the FeatureFlagsState object to JSON using {@code Gson.toJson()} or {@code Gson.toJsonTree()}.
120140
* @return an immutable map of flag keys to JSON values
121141
*/
122142
public Map<String, LDValue> toValuesMap() {
123-
return flagValues;
143+
return Maps.transformValues(flagMetadata, v -> v.value);
124144
}
125145

126146
@Override
127147
public boolean equals(Object other) {
128148
if (other instanceof FeatureFlagsState) {
129149
FeatureFlagsState o = (FeatureFlagsState)other;
130-
return flagValues.equals(o.flagValues) &&
131-
flagMetadata.equals(o.flagMetadata) &&
150+
return flagMetadata.equals(o.flagMetadata) &&
132151
valid == o.valid;
133152
}
134153
return false;
135154
}
136155

137156
@Override
138157
public int hashCode() {
139-
return Objects.hash(flagValues, flagMetadata, valid);
158+
return Objects.hash(flagMetadata, valid);
140159
}
141160

142-
static class Builder {
143-
private Map<String, LDValue> flagValues = new HashMap<>();
144-
private Map<String, FlagMetadata> flagMetadata = new HashMap<>();
161+
/**
162+
* A builder for a {@link FeatureFlagsState} instance.
163+
* <p>
164+
* Application code will not normally use this builder, since the SDK creates its own instances.
165+
* However, it may be useful in testing, to simulate values that might be returned by
166+
* {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}.
167+
*
168+
* @since 5.6.0
169+
*/
170+
public static class Builder {
171+
private ImmutableMap.Builder<String, FlagMetadata> flagMetadata = ImmutableMap.builder();
145172
private final boolean saveReasons;
146173
private final boolean detailsOnlyForTrackedFlags;
147174
private boolean valid = true;
148175

149-
Builder(FlagsStateOption... options) {
176+
private Builder(FlagsStateOption... options) {
150177
saveReasons = FlagsStateOption.hasOption(options, FlagsStateOption.WITH_REASONS);
151178
detailsOnlyForTrackedFlags = FlagsStateOption.hasOption(options, FlagsStateOption.DETAILS_ONLY_FOR_TRACKED_FLAGS);
152179
}
153180

154-
Builder valid(boolean valid) {
181+
/**
182+
* Sets the {@link FeatureFlagsState#isValid()} property. This is true by default.
183+
*
184+
* @param valid the new property value
185+
* @return the builder
186+
*/
187+
public Builder valid(boolean valid) {
155188
this.valid = valid;
156189
return this;
157190
}
158191

159-
Builder addFlag(DataModel.FeatureFlag flag, Evaluator.EvalResult eval) {
160-
flagValues.put(flag.getKey(), eval.getValue());
161-
final boolean flagIsTracked = flag.isTrackEvents() ||
162-
(flag.getDebugEventsUntilDate() != null && flag.getDebugEventsUntilDate() > System.currentTimeMillis());
192+
public Builder add(
193+
String flagKey,
194+
LDValue value,
195+
Integer variationIndex,
196+
EvaluationReason reason,
197+
int flagVersion,
198+
boolean trackEvents,
199+
Long debugEventsUntilDate
200+
) {
201+
final boolean flagIsTracked = trackEvents ||
202+
(debugEventsUntilDate != null && debugEventsUntilDate > System.currentTimeMillis());
163203
final boolean wantDetails = !detailsOnlyForTrackedFlags || flagIsTracked;
164204
FlagMetadata data = new FlagMetadata(
205+
value,
206+
variationIndex,
207+
(saveReasons && wantDetails) ? reason : null,
208+
wantDetails ? Integer.valueOf(flagVersion) : null,
209+
trackEvents,
210+
debugEventsUntilDate
211+
);
212+
flagMetadata.put(flagKey, data);
213+
return this;
214+
}
215+
216+
Builder addFlag(DataModel.FeatureFlag flag, Evaluator.EvalResult eval) {
217+
return add(
218+
flag.getKey(),
219+
eval.getValue(),
165220
eval.isDefault() ? null : eval.getVariationIndex(),
166-
(saveReasons && wantDetails) ? eval.getReason() : null,
167-
wantDetails ? flag.getVersion() : null,
221+
eval.getReason(),
222+
flag.getVersion(),
168223
flag.isTrackEvents(),
169-
flag.getDebugEventsUntilDate());
170-
flagMetadata.put(flag.getKey(), data);
171-
return this;
224+
flag.getDebugEventsUntilDate()
225+
);
172226
}
173227

174228
FeatureFlagsState build() {
175-
return new FeatureFlagsState(flagValues, flagMetadata, valid);
229+
return new FeatureFlagsState(flagMetadata.build(), valid);
176230
}
177231
}
178232

@@ -181,9 +235,9 @@ static class JsonSerialization extends TypeAdapter<FeatureFlagsState> {
181235
public void write(JsonWriter out, FeatureFlagsState state) throws IOException {
182236
out.beginObject();
183237

184-
for (Map.Entry<String, LDValue> entry: state.flagValues.entrySet()) {
238+
for (Map.Entry<String, FlagMetadata> entry: state.flagMetadata.entrySet()) {
185239
out.name(entry.getKey());
186-
gsonInstance().toJson(entry.getValue(), LDValue.class, out);
240+
gsonInstance().toJson(entry.getValue().value, LDValue.class, out);
187241
}
188242

189243
out.name("$flagsState");
@@ -229,7 +283,7 @@ public void write(JsonWriter out, FeatureFlagsState state) throws IOException {
229283
@Override
230284
public FeatureFlagsState read(JsonReader in) throws IOException {
231285
Map<String, LDValue> flagValues = new HashMap<>();
232-
Map<String, FlagMetadata> flagMetadata = new HashMap<>();
286+
Map<String, FlagMetadata> flagMetadataWithoutValues = new HashMap<>();
233287
boolean valid = true;
234288
in.beginObject();
235289
while (in.hasNext()) {
@@ -239,7 +293,7 @@ public FeatureFlagsState read(JsonReader in) throws IOException {
239293
while (in.hasNext()) {
240294
String metaName = in.nextName();
241295
FlagMetadata meta = gsonInstance().fromJson(in, FlagMetadata.class);
242-
flagMetadata.put(metaName, meta);
296+
flagMetadataWithoutValues.put(metaName, meta);
243297
}
244298
in.endObject();
245299
} else if (name.equals("$valid")) {
@@ -250,7 +304,22 @@ public FeatureFlagsState read(JsonReader in) throws IOException {
250304
}
251305
}
252306
in.endObject();
253-
return new FeatureFlagsState(flagValues, flagMetadata, valid);
307+
ImmutableMap.Builder<String, FlagMetadata> allFlagMetadata = ImmutableMap.builder();
308+
for (Map.Entry<String, LDValue> e: flagValues.entrySet()) {
309+
FlagMetadata m0 = flagMetadataWithoutValues.get(e.getKey());
310+
if (m0 != null) {
311+
FlagMetadata m1 = new FlagMetadata(
312+
e.getValue(),
313+
m0.variation,
314+
m0.reason,
315+
m0.version,
316+
m0.trackEvents != null && m0.trackEvents.booleanValue(),
317+
m0.debugEventsUntilDate
318+
);
319+
allFlagMetadata.put(e.getKey(), m1);
320+
}
321+
}
322+
return new FeatureFlagsState(allFlagMetadata.build(), valid);
254323
}
255324
}
256325
}

src/main/java/com/launchdarkly/sdk/server/LDClient.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION;
4242
import static com.launchdarkly.sdk.server.DataModel.FEATURES;
4343
import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
44+
import static com.launchdarkly.sdk.server.Util.isAsciiHeaderValue;
4445

4546
/**
4647
* A client for the LaunchDarkly API. Client instances are thread-safe. Applications should instantiate
@@ -82,8 +83,15 @@ public final class LDClient implements LDClientInterface {
8283
* values; it will still continue trying to connect in the background. You can detect whether
8384
* initialization has succeeded by calling {@link #isInitialized()}. If you prefer to customize
8485
* this behavior, use {@link LDClient#LDClient(String, LDConfig)} instead.
86+
* <p>
87+
* For rules regarding the throwing of unchecked exceptions for error conditions, see
88+
* {@link LDClient#LDClient(String, LDConfig)}.
8589
*
8690
* @param sdkKey the SDK key for your LaunchDarkly environment
91+
* @throws IllegalArgumentException if a parameter contained a grossly malformed value;
92+
* for security reasons, in case of an illegal SDK key, the exception message does
93+
* not include the key
94+
* @throws NullPointerException if a non-nullable parameter was null
8795
* @see LDClient#LDClient(String, LDConfig)
8896
*/
8997
public LDClient(String sdkKey) {
@@ -136,14 +144,32 @@ private static final DataModel.Segment getSegment(DataStore store, String key) {
136144
* // do whatever is appropriate if initialization has timed out
137145
* }
138146
* </code></pre>
147+
* <p>
148+
* This constructor can throw unchecked exceptions if it is immediately apparent that
149+
* the SDK cannot work with these parameters. For instance, if the SDK key contains a
150+
* non-printable character that cannot be used in an HTTP header, it will throw an
151+
* {@link IllegalArgumentException} since the SDK key is normally sent to LaunchDarkly
152+
* in an HTTP header and no such value could possibly be valid. Similarly, a null
153+
* value for a non-nullable parameter may throw a {@link NullPointerException}. The
154+
* constructor will not throw an exception for any error condition that could only be
155+
* detected after making a request to LaunchDarkly (such as an SDK key that is simply
156+
* wrong despite being valid ASCII, so it is invalid but not illegal); those are logged
157+
* and treated as an unsuccessful initialization, as described above.
139158
*
140159
* @param sdkKey the SDK key for your LaunchDarkly environment
141160
* @param config a client configuration object
161+
* @throws IllegalArgumentException if a parameter contained a grossly malformed value;
162+
* for security reasons, in case of an illegal SDK key, the exception message does
163+
* not include the key
164+
* @throws NullPointerException if a non-nullable parameter was null
142165
* @see LDClient#LDClient(String, LDConfig)
143166
*/
144167
public LDClient(String sdkKey, LDConfig config) {
145168
checkNotNull(config, "config must not be null");
146169
this.sdkKey = checkNotNull(sdkKey, "sdkKey must not be null");
170+
if (!isAsciiHeaderValue(sdkKey) ) {
171+
throw new IllegalArgumentException("SDK key contained an invalid character");
172+
}
147173
this.offline = config.offline;
148174

149175
this.sharedExecutor = createSharedExecutor(config);
@@ -268,7 +294,7 @@ private void sendFlagRequestEvent(Event.FeatureRequest event) {
268294

269295
@Override
270296
public FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) {
271-
FeatureFlagsState.Builder builder = new FeatureFlagsState.Builder(options);
297+
FeatureFlagsState.Builder builder = FeatureFlagsState.builder(options);
272298

273299
if (isOffline()) {
274300
Loggers.EVALUATION.debug("allFlagsState() was called when client is in offline mode.");

src/main/java/com/launchdarkly/sdk/server/Util.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ static Headers.Builder getHeadersBuilderFor(HttpConfiguration config) {
3737
return builder;
3838
}
3939

40+
// This is specifically testing whether the string would be considered a valid HTTP header value
41+
// *by the OkHttp client*. The actual HTTP spec does not prohibit characters >= 127; OkHttp's
42+
// check is overly strict, as was pointed out in https://github.com/square/okhttp/issues/2016.
43+
// But all OkHttp 3.x and 4.x versions so far have continued to enforce that check. Control
44+
// characters other than a tab are always illegal.
45+
//
46+
// The value we're mainly concerned with is the SDK key (Authorization header). If an SDK key
47+
// accidentally has (for instance) a newline added to it, we don't want to end up having OkHttp
48+
// throw an exception mentioning the value, which might get logged (https://github.com/square/okhttp/issues/6738).
49+
static boolean isAsciiHeaderValue(String value) {
50+
for (int i = 0; i < value.length(); i++) {
51+
char ch = value.charAt(i);
52+
if ((ch < 0x20 || ch > 0x7e) && ch != '\t') {
53+
return false;
54+
}
55+
}
56+
return true;
57+
}
58+
4059
static void configureHttpClientBuilder(HttpConfiguration config, OkHttpClient.Builder builder) {
4160
builder.connectionPool(new ConnectionPool(5, 5, TimeUnit.SECONDS))
4261
.connectTimeout(config.getConnectTimeout())

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

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

3+
import com.launchdarkly.sdk.server.LDConfig;
4+
35
/**
46
* Integration between the LaunchDarkly SDK and file data.
57
* <p>
@@ -56,8 +58,10 @@ public enum DuplicateKeysHandling {
5658
* <p>
5759
* This will cause the client <i>not</i> to connect to LaunchDarkly to get feature flags. The
5860
* client may still make network connections to send analytics events, unless you have disabled
59-
* this with {@link com.launchdarkly.sdk.server.Components#noEvents()} or
60-
* {@link com.launchdarkly.sdk.server.LDConfig.Builder#offline(boolean)}.
61+
* this with {@link com.launchdarkly.sdk.server.Components#noEvents()}. IMPORTANT: Do <i>not</i>
62+
* set {@link LDConfig.Builder#offline(boolean)} to {@code true}; doing so would not just put the
63+
* SDK "offline" with regard to LaunchDarkly, but will completely turn off all flag data sources
64+
* to the SDK <i>including the file data source</i>.
6165
* <p>
6266
* Flag data files can be either JSON or YAML. They contain an object with three possible
6367
* properties:

0 commit comments

Comments
 (0)