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

Commit a82ac2d

Browse files
LaunchDarklyReleaseBoteli-darklybwoskow-ldgwhelanLDrobertjneal
authored
prepare 2.0.0 release (#10)
* initial implementation of Java common SDK code (#1) * stick with Java 7 for Android compatibility (#2) * add getters to EvaluationReason and hide its subclasses * completely remove EvaluationReason subclasses * add JSON helpers, better serialization logic, and Gson adapter * javadoc fixes * remove @SInCE tags, misc doc fixes, add note about changelogging * fix sample code * rename Gson adapter, use factory method * improve and rigorously test equals() for all immutable types * simplifying * simplifying * make EvaluationDetail non-nullable + use boolean singletons * hide Gson in pom * doc additions * javadoc fixes * add Jackson adapter (#9) * more predictable exception behavior for LDValue.parse() * fix the JSON behavior of EvaluationDetail (#10) * maximize and enforce test coverage (#12) * build and test in Android! (#13) * ensure real nulls can't be stored in an LDValue * clean up some unnecessary coverage warnings * more convenient location for coverage reports * remove rc1 from changelog * Removed the guides link * Use Android machine image for CircleCI Android tests. (#17) And update Android system image for testing to 21 (5.0 Lollipop). * improve Gson integration using reader/writer delegation + add LDValue helpers (#16) * add LDValue.arrayOf() + misc javadoc fixes * Add inExperiment attribute to FALLTHROUGH and RULE_MATCH reasons * fix javadoc * Update src/main/java/com/launchdarkly/sdk/EvaluationReason.java Co-authored-by: Sam Stokes <[email protected]> * Update src/main/java/com/launchdarkly/sdk/EvaluationReason.java Co-authored-by: Sam Stokes <[email protected]> * Update src/main/java/com/launchdarkly/sdk/EvaluationReason.java Co-authored-by: Sam Stokes <[email protected]> * Update src/main/java/com/launchdarkly/sdk/EvaluationReason.java Co-authored-by: Sam Stokes <[email protected]> * respond to review comments * fix test * javadoc fix * add validation of javadoc build in CI * add another Eclipse metadata exclusion to .gitignore * partially revert ch103941 fix that doesn't work in the Java SDK * unrevert some JSON improvements from previous revert * improve Gson integration using reader/writer delegation - take 2 * better Jackson adapter + misc JSON test improvements (#18) * omit redundant method call (this commit was mistakenly left out of the previous merge) * avoid unnecessarily adding ".0" to JSON numbers (#25) * update Gradle to 6.8.3 * Kotlinize main build script * rm obsolete comments * bump Jackson compile-time dependency to 2.10.5.1 due to CVE-2020-25649 (#30) * bump Jackson compile-time dependency to 2.10.5.1 due to CVE-2020-25649 * 2.10.5.1 patch only exists in one of the Jackson modules * exclude Gson & Jackson from published dependencies in a more correct way (#31) * refactor build scripts using buildSrc (#29) * use Releaser v2 config format + newer CircleCI images (#33) * Updates docs URLs * update Gson to 2.8.9 * use Gradle 7 * don't use jcenter * add Java 17 to CI * also test Java 17 in Windows * don't suppress null property values when we serialize with Gson * remove obsolete publish-docs script * Merge feature branch big-segments for 1.3.0 release (#42) * Fix link to CONTRIBUTING.md * attribute reference type * misc cleanup, test coverage * add context type & builders * LDContext JSON marshaling & unmarshaling * javadoc fix * javadoc fix * store multiContexts as an array rather than a List * revert accidental change * add type adapter for ContextKind * add null check/defaulting on getIndividualContext by kind * fix context kind/key validation logic * don't allow deserializing an empty string * remove use of isBlank which requires Java 11 * ContextKind should be Comparable since it is string-like * use percent-encoding for specific characters in fully-qualified key, not URLEncoder * flatten nested multi-kind contexts when building a multi-kind context * fix user JSON validation to require that "custom" is an object or null * attribute ref components are always properties, not array indices * disable Windows Java 11 build * remove secondary meta-attribute * remove LDUser as a concrete type * test coverage * update doc comment * re-add LDUser type, add conversion to LDContext (#56) * re-add LDUser type, add conversion to LDContext * misc fixes * re-add test * allow anonymous user with null key to be converted to a context * doc comment improvements for user/context types Co-authored-by: Eli Bishop <[email protected]> Co-authored-by: Ben Woskow <[email protected]> Co-authored-by: Gavin Whelan <[email protected]> Co-authored-by: Robert J. Neal <[email protected]> Co-authored-by: Robert J. Neal <[email protected]> Co-authored-by: Sam Stokes <[email protected]> Co-authored-by: LaunchDarklyCI <[email protected]> Co-authored-by: Ember Stevens <[email protected]> Co-authored-by: ember-stevens <[email protected]> Co-authored-by: LaunchDarklyReleaseBot <[email protected]> Co-authored-by: Alex Engelberg <[email protected]>
1 parent 77b13b1 commit a82ac2d

35 files changed

+3251
-125
lines changed

.circleci/config.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ workflows:
2424
with-coverage: true
2525
requires:
2626
- build-linux
27-
- build-test-windows:
28-
name: Java 11 - Windows - OpenJDK
29-
openjdk-version: 11.0.2.01
27+
# Windows Java 11 build is temporarily disabled - see story 171428
28+
# - test-windows:
29+
# name: Java 11 - Windows - OpenJDK
30+
# openjdk-version: 11.0.2.01
3031
- build-test-windows:
3132
name: Java 17 - Windows - OpenJDK
3233
openjdk-version: 17.0.1

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This version of the library works with Java 7 and above.
1111

1212
## Contributing
1313

14-
See [Contributing](https://github.com/launchdarkly/dotnet-sdk-common/blob/master/CONTRIBUTING.md).
14+
See [Contributing](CONTRIBUTING.md).
1515

1616
## About LaunchDarkly
1717

buildSrc/src/main/kotlin/TestCoverageOverrides.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ object TestCoverageOverrides {
1515
val methodsWithMissedLineCount = mapOf(
1616
"EvaluationReason.error(com.launchdarkly.sdk.EvaluationReason.ErrorKind)" to 1,
1717
"EvaluationReasonTypeAdapter.parse(com.google.gson.stream.JsonReader)" to 1,
18+
"LDContext.urlEncodeKey(java.lang.String)" to 2,
1819
"LDValue.equals(java.lang.Object)" to 1,
1920
"LDValueTypeAdapter.read(com.google.gson.stream.JsonReader)" to 1,
2021
"json.LDGson.LDTypeAdapter.write(com.google.gson.stream.JsonWriter, java.lang.Object)" to 1,
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
package com.launchdarkly.sdk;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import com.google.gson.annotations.JsonAdapter;
7+
import com.launchdarkly.sdk.json.JsonSerializable;
8+
9+
/**
10+
* An attribute name or path expression identifying a value within an {@link LDContext}.
11+
* <p>
12+
* Applications are unlikely to need to use the AttributeRef type directly, but see below
13+
* for details of the string attribute reference syntax used by methods like
14+
* {@link ContextBuilder#privateAttributes(String...)}.
15+
* <p>
16+
* The reason to use this type directly is to avoid repetitive string parsing in code where
17+
* efficiency is a priority; AttributeRef parses its contents once when it is created, and
18+
* is immutable afterward. If an AttributeRef instance was created from an invalid string,
19+
* it is considered invalid and its {@link #getError()} method will return a non-null error.
20+
* <p>
21+
* The string representation of an attribute reference in LaunchDarkly JSON data uses the
22+
* following syntax:
23+
* <ul>
24+
* <li> If the first character is not a slash, the string is interpreted literally as an
25+
* attribute name. An attribute name can contain any characters, but must not be empty. </li>
26+
* <li> If the first character is a slash, the string is interpreted as a slash-delimited
27+
* path where the first path component is an attribute name, and each subsequent path
28+
* component is the name of a property in a JSON object. Any instances of the characters "/"
29+
* or "~" in a path component are escaped as "~1" or "~0" respectively. This syntax
30+
* deliberately resembles JSON Pointer, but no JSON Pointer behaviors other than those
31+
* mentioned here are supported. </li>
32+
* </ul>
33+
*/
34+
@JsonAdapter(AttributeRefTypeAdapter.class)
35+
public final class AttributeRef implements JsonSerializable, Comparable<AttributeRef> {
36+
private static final Map<String, AttributeRef> COMMON_LITERALS = makeLiteralsMap(
37+
"kind", "key", "name", "anonymous", // built-ins
38+
"email", "firstName", "lastName", "country", "ip", "avatar" // frequently used custom attributes
39+
);
40+
41+
private final String error;
42+
private final String rawPath;
43+
private final String singlePathComponent;
44+
private final String[] components;
45+
46+
private AttributeRef(String rawPath, String singlePathComponent, String[] components) {
47+
this.error = null;
48+
this.rawPath = rawPath == null ? "" : rawPath;
49+
this.singlePathComponent = singlePathComponent;
50+
this.components = components;
51+
}
52+
53+
private AttributeRef(String error, String rawPath) {
54+
this.error = error;
55+
this.rawPath = rawPath == null ? "" : rawPath;
56+
this.singlePathComponent = null;
57+
this.components = null;
58+
}
59+
60+
/**
61+
* Creates an AttributeRef from a string. For the supported syntax and examples, see
62+
* comments on the {@link AttributeRef} type.
63+
* <p>
64+
* This method always returns an AttributeRef that preserves the original string, even if
65+
* validation fails, so that calling {@link #toString()} (or serializing the AttributeRef
66+
* to JSON) will produce the original string. If validation fails, {@link #getError()} will
67+
* return a non-null error and any SDK method that takes this AttributeRef as a parameter
68+
* will consider it invalid.
69+
*
70+
* @param refPath an attribute name or path
71+
* @return an AttributeRef
72+
* @see #fromLiteral(String)
73+
*/
74+
public static AttributeRef fromPath(String refPath) {
75+
if (refPath == null || refPath.isEmpty() || refPath.equals("/")) {
76+
return new AttributeRef(Errors.ATTR_EMPTY, refPath);
77+
}
78+
if (refPath.charAt(0) != '/') {
79+
// When there is no leading slash, this is a simple attribute reference with no character escaping.
80+
return new AttributeRef(refPath, refPath, null);
81+
}
82+
if (refPath.indexOf('/', 1) < 0) {
83+
// There's only one segment, so this is still a simple attribute reference. However, we still may
84+
// need to unescape special characters.
85+
String unescaped = unescapePath(refPath.substring(1));
86+
if (unescaped == null) {
87+
return new AttributeRef(Errors.ATTR_INVALID_ESCAPE, refPath);
88+
}
89+
return new AttributeRef(refPath, unescaped, null);
90+
}
91+
if (refPath.endsWith("/")) {
92+
// String.split won't behave properly in this case
93+
return new AttributeRef(Errors.ATTR_EXTRA_SLASH, refPath);
94+
}
95+
String[] parsed = refPath.substring(1).split("/");
96+
for (int i = 0; i < parsed.length; i++) {
97+
String p = parsed[i];
98+
if (p.isEmpty()) {
99+
return new AttributeRef(Errors.ATTR_EXTRA_SLASH, refPath);
100+
}
101+
String unescaped = unescapePath(p);
102+
if (unescaped == null) {
103+
return new AttributeRef(Errors.ATTR_INVALID_ESCAPE, refPath);
104+
}
105+
parsed[i] = unescaped;
106+
}
107+
return new AttributeRef(refPath, null, parsed);
108+
}
109+
110+
/**
111+
* Similar to {@link #fromPath(String)}, except that it always interprets the string as a literal
112+
* attribute name, never as a slash-delimited path expression.
113+
* <p>
114+
* There is no escaping or unescaping, even if the name contains literal '/' or '~' characters.
115+
* Since an attribute name can contain any characters, this method always returns a valid
116+
* AttributeRef unless the name is empty.
117+
* <p>
118+
* For example: {@code AttributeRef.fromLiteral("name")} is exactly equivalent to
119+
* {@code AttributeRef.fromPath("name")}. {@code AttributeRef.fromLiteral("a/b")} is exactly
120+
* equivalent to {@code AttributeRef.fromPath("a/b")} (since the syntax used by
121+
* {@link #fromPath(String)} treats the whole string as a literal as long as it does not start
122+
* with a slash), or to {@code AttributeRef.fromPath("/a~1b")}.
123+
*
124+
* @param attributeName an attribute name
125+
* @return an AttributeRef
126+
* @see #fromPath(String)
127+
*/
128+
public static AttributeRef fromLiteral(String attributeName) {
129+
if (attributeName == null || attributeName.isEmpty()) {
130+
return new AttributeRef(Errors.ATTR_EMPTY, "");
131+
}
132+
if (attributeName.charAt(0) != '/') {
133+
// When there is no leading slash, this is a simple attribute reference with no character escaping.
134+
AttributeRef internedInstance = COMMON_LITERALS.get(attributeName);
135+
return internedInstance == null ? new AttributeRef(attributeName, attributeName, null) : internedInstance;
136+
}
137+
// If there is a leading slash, then the attribute name actually starts with a slash. To represent it
138+
// as an AttributeRef, it'll need to be escaped.
139+
String escapedPath = "/" + attributeName.replace("~", "~0").replace("/", "~1");
140+
return new AttributeRef(escapedPath, attributeName, null);
141+
}
142+
143+
/**
144+
* True for a valid AttributeRef, false for an invalid AttributeRef.
145+
* <p>
146+
* An AttributeRef can only be invalid for the following reasons:
147+
* <ul>
148+
* <li> The input string was empty, or consisted only of "/". </li>
149+
* <li> A slash-delimited string had a double slash causing one component to be empty, such as "/a//b". </li>
150+
* <li> A slash-delimited string contained a "~" character that was not followed by "0" or "1". </li>
151+
* </ul>
152+
* <p>
153+
* Otherwise, the AttributeRef is valid, but that does not guarantee that such an attribute exists
154+
* in any given {@link LDContext}. For instance, {@code fromLiteral("name")} is a valid AttributeRef,
155+
* but a specific {@link LDContext} might or might not have a name.
156+
* <p>
157+
* See comments on the {@link AttributeRef} type for more details of the attribute reference synax.
158+
*
159+
* @return true if the instance is valid
160+
* @see #getError()
161+
*/
162+
public boolean isValid() {
163+
return error == null;
164+
}
165+
166+
/**
167+
* Null for a valid AttributeRef, or a non-null error message for an invalid AttributeRef.
168+
* <p>
169+
* If this is null, then {@link #isValid()} is true. If it is non-null, then {@link #isValid()} is false.
170+
*
171+
* @return an error string or null
172+
* @see #isValid()
173+
*/
174+
public String getError() {
175+
return error;
176+
}
177+
178+
/**
179+
* The number of path components in the AttributeRef.
180+
* <p>
181+
* For a simple attribute reference such as "name" with no leading slash, this returns 1.
182+
* <p>
183+
* For an attribute reference with a leading slash, it is the number of slash-delimited path
184+
* components after the initial slash. For instance, {@code AttributeRef.fromPath("/a/b").getDepth()}
185+
* returns 2.
186+
* <p>
187+
* For an invalid attribute reference, it returns zero
188+
*
189+
* @return the number of path components
190+
*/
191+
public int getDepth() {
192+
if (error != null) {
193+
return 0;
194+
}
195+
return components == null ? 1 : components.length;
196+
}
197+
198+
/**
199+
* Retrieves a single path component from the attribute reference.
200+
* <p>
201+
* For a simple attribute reference such as "name" with no leading slash, getComponent returns the
202+
* attribute name if index is zero, and null otherwise.
203+
* <p>
204+
* For an attribute reference with a leading slash, if index is non-negative and less than
205+
* {@link #getDepth()}, getComponent returns the path component string at that position.
206+
*
207+
* @param index the zero-based index of the desired path component
208+
* @return the path component, or null if not available
209+
*/
210+
public String getComponent(int index) {
211+
if (components == null) {
212+
return index == 0 ? singlePathComponent : null;
213+
}
214+
return index < 0 || index >= components.length ? null : components[index];
215+
}
216+
217+
/**
218+
* Returns the attribute reference as a string, in the same format used by
219+
* {@link #fromPath(String)}.
220+
* <p>
221+
* If the AttributeRef was created with {@link #fromPath(String)}, this value is identical to
222+
* to the original string. If it was created with {@link #fromLiteral(String)}, the value may
223+
* be different due to unescaping (for instance, an attribute whose name is "/a" would be
224+
* represented as "~1a").
225+
*
226+
* @return the attribute reference string (guaranteed non-null)
227+
*/
228+
@Override
229+
public String toString() {
230+
return rawPath;
231+
}
232+
233+
@Override
234+
public boolean equals(Object other) {
235+
if (other instanceof AttributeRef) {
236+
AttributeRef o = (AttributeRef)other;
237+
return rawPath.equals(o.rawPath);
238+
}
239+
return false;
240+
}
241+
242+
@Override
243+
public int hashCode() {
244+
return rawPath.hashCode();
245+
}
246+
247+
@Override
248+
public int compareTo(AttributeRef o) {
249+
return rawPath.compareTo(o.rawPath);
250+
}
251+
252+
private static String unescapePath(String path) {
253+
// If there are no tildes then there's definitely nothing to do
254+
if (path.indexOf('~') < 0) {
255+
return path;
256+
}
257+
StringBuilder ret = new StringBuilder(100); // arbitrary initial capacity
258+
for (int i = 0; i < path.length(); i++) {
259+
char ch = path.charAt(i);
260+
if (ch != '~')
261+
{
262+
ret.append(ch);
263+
continue;
264+
}
265+
i++;
266+
if (i >= path.length())
267+
{
268+
return null;
269+
}
270+
switch (path.charAt(i)) {
271+
case '0':
272+
ret.append('~');
273+
break;
274+
case '1':
275+
ret.append('/');
276+
break;
277+
default:
278+
return null;
279+
}
280+
}
281+
return ret.toString();
282+
}
283+
284+
private static Map<String, AttributeRef> makeLiteralsMap(String... names) {
285+
Map<String, AttributeRef> ret = new HashMap<>();
286+
for (String name: names) {
287+
ret.put(name, new AttributeRef(name, name, null));
288+
}
289+
return ret;
290+
}
291+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.launchdarkly.sdk;
2+
3+
import com.google.gson.TypeAdapter;
4+
import com.google.gson.stream.JsonReader;
5+
import com.google.gson.stream.JsonWriter;
6+
7+
import java.io.IOException;
8+
9+
final class AttributeRefTypeAdapter extends TypeAdapter<AttributeRef> {
10+
@Override
11+
public AttributeRef read(JsonReader reader) throws IOException {
12+
return AttributeRef.fromPath(Helpers.readNonNullableString(reader));
13+
}
14+
15+
@Override
16+
public void write(JsonWriter writer, AttributeRef a) throws IOException {
17+
writer.value(a.toString());
18+
}
19+
}

0 commit comments

Comments
 (0)