Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ libraries.shaded = [
"com.google.guava:guava:19.0",
"joda-time:joda-time:2.9.3",
"com.launchdarkly:okhttp-eventsource:1.7.1",
"redis.clients:jedis:2.9.0",
"com.vdurmont:semver4j:2.1.0"
"redis.clients:jedis:2.9.0"
]

libraries.unshaded = [
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version=2.5.1
version=2.6.0
ossrhUsername=
ossrhPassword=
ossrhPassword=
11 changes: 10 additions & 1 deletion src/main/java/com/launchdarkly/client/LDClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public void track(String eventName, LDUser user) {
}

/**
* Register the user
* Registers the user.
*
* @param user the user to register
*/
Expand Down Expand Up @@ -440,6 +440,15 @@ public String secureModeHash(LDUser user) {
return null;
}

/**
* Returns the current version string of the client library.
* @return a version string conforming to Semantic Versioning (http://semver.org)
*/
@Override
public String version() {
return CLIENT_VERSION;
}

private static String getClientVersion() {
Class clazz = LDConfig.class;
String className = clazz.getSimpleName() + ".class";
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/launchdarkly/client/LDClientInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ public interface LDClientInterface extends Closeable {
boolean isOffline();

String secureModeHash(LDUser user);

String version();
}
13 changes: 2 additions & 11 deletions src/main/java/com/launchdarkly/client/OperandType.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.launchdarkly.client;

import com.google.gson.JsonPrimitive;
import com.vdurmont.semver4j.Semver;
import com.vdurmont.semver4j.Semver.SemverType;
import com.vdurmont.semver4j.SemverException;

/**
* Operator value that can be applied to {@link JsonPrimitive} objects. Incompatible types or other errors
Expand Down Expand Up @@ -36,14 +33,8 @@ public Object getValueAsType(JsonPrimitive value) {
return Util.jsonPrimitiveToDateTime(value);
case semVer:
try {
Semver sv = new Semver(value.getAsString(), SemverType.LOOSE);
// LOOSE means only the major version is required. But comparisons between loose and strictly
// compliant versions don't work properly, so we always convert to a strict version (i.e. fill
// in the minor/patch versions with zeroes if they were absent). Note that if we ever switch
// to a different semver library that doesn't have exactly the same "loose" mode, we will need
// to preprocess the string before parsing to get the same behavior.
return sv.toStrict();
} catch (SemverException e) {
return SemanticVersion.parse(value.getAsString(), true);
} catch (SemanticVersion.InvalidVersionException e) {
return null;
}
default:
Expand Down
176 changes: 176 additions & 0 deletions src/main/java/com/launchdarkly/client/SemanticVersion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package com.launchdarkly.client;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Simple implementation of semantic version parsing and comparison according to the Semantic
* Versions 2.0.0 standard (http://semver.org).
*/
class SemanticVersion implements Comparable<SemanticVersion> {

private static Pattern VERSION_REGEX = Pattern.compile(
"^(?<major>0|[1-9]\\d*)(\\.(?<minor>0|[1-9]\\d*))?(\\.(?<patch>0|[1-9]\\d*))?" +
"(\\-(?<prerel>[0-9A-Za-z\\-\\.]+))?(\\+(?<build>[0-9A-Za-z\\-\\.]+))?$");

@SuppressWarnings("serial")
public static class InvalidVersionException extends Exception {
public InvalidVersionException(String message) {
super(message);
}
}

private final int major;
private final int minor;
private final int patch;
private final String prerelease;
private final String build;

public SemanticVersion(int major, int minor, int patch, String prerelease, String build) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.prerelease = prerelease;
this.build = build;
}

public int getMajor() {
return major;
}

public int getMinor() {
return minor;
}

public int getPatch() {
return patch;
}

public String getPrerelease() {
return prerelease;
}

public String getBuild() {
return build;
}

/**
* Attempts to parse a string as a semantic version according to the Semver 2.0.0 specification.
* @param input the input string
* @return a SemanticVersion instance
* @throws InvalidVersionException if the version could not be parsed
*/
public static SemanticVersion parse(String input) throws InvalidVersionException {
return parse(input, false);
}

/**
* Attempts to parse a string as a semantic version according to the Semver 2.0.0 specification, except that
* the minor and patch versions may optionally be omitted.
* @param input the input string
* @param allowMissingMinorAndPatch true if the parser should tolerate the absence of a minor and/or
* patch version; if absent, they will be treated as zero
* @return a SemanticVersion instance
* @throws InvalidVersionException if the version could not be parsed
*/
public static SemanticVersion parse(String input, boolean allowMissingMinorAndPatch) throws InvalidVersionException {
Matcher matcher = VERSION_REGEX.matcher(input);
if (!matcher.matches()) {
throw new InvalidVersionException("Invalid semantic version");
}
int major, minor, patch;
try {
major = Integer.parseInt(matcher.group("major"));
if (!allowMissingMinorAndPatch) {
if (matcher.group("minor") == null || matcher.group("patch") == null) {
throw new InvalidVersionException("Invalid semantic version");
}
}
minor = matcher.group("minor") == null ? 0 : Integer.parseInt(matcher.group("minor"));
patch = matcher.group("patch") == null ? 0 : Integer.parseInt(matcher.group("patch"));
} catch (NumberFormatException e) {
throw new InvalidVersionException("Invalid semantic version");
}
String prerelease = matcher.group("prerel");
String build = matcher.group("build");
return new SemanticVersion(major, minor, patch, prerelease, build);
}

@Override
public int compareTo(SemanticVersion other) {
return comparePrecedence(other);
}

/**
* Compares this object with another SemanticVersion according to Semver 2.0.0 precedence rules.
* @param other another SemanticVersion
* @return 0 if equal, -1 if the current object has lower precedence, or 1 if the current object has higher precedence
*/
public int comparePrecedence(SemanticVersion other) {
if (other == null) {
return 1;
}
if (major != other.major) {
return Integer.compare(major, other.major);
}
if (minor != other.minor) {
return Integer.compare(minor, other.minor);
}
if (patch != other.patch) {
return Integer.compare(patch, other.patch);
}
if (prerelease == null && other.prerelease == null) {
return 0;
}
// *no* prerelease component always has higher precedence than *any* prerelease component
if (prerelease == null) {
return 1;
}
if (other.prerelease == null) {
return -1;
}
return compareIdentifiers(prerelease.split("\\."), other.prerelease.split("\\."));
}

private int compareIdentifiers(String[] ids1, String[] ids2) {
for (int i = 0; ; i++) {
if (i >= ids1.length)
{
// x.y is always less than x.y.z
return (i >= ids2.length) ? 0 : -1;
}
if (i >= ids2.length)
{
return 1;
}
// each sub-identifier is compared numerically if both are numeric; if both are non-numeric,
// they're compared as strings; otherwise, the numeric one is the lesser one
int n1 = 0, n2 = 0, d;
boolean isNum1, isNum2;
try {
n1 = Integer.parseInt(ids1[i]);
isNum1 = true;
} catch (NumberFormatException e) {
isNum1 = false;
}
try {
n2 = Integer.parseInt(ids2[i]);
isNum2 = true;
} catch (NumberFormatException e) {
isNum2 = false;
}
if (isNum1 && isNum2)
{
d = Integer.compare(n1, n2);
}
else
{
d = isNum1 ? -1 : (isNum2 ? 1 : ids1[i].compareTo(ids2[i]));
}
if (d != 0)
{
return d;
}
}
}
}
34 changes: 23 additions & 11 deletions src/main/java/com/launchdarkly/client/VariationOrRollout.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,35 @@ Integer variationIndexForUser(LDUser user, String key, String salt) {
return null;
}

private float bucketUser(LDUser user, String key, String attr, String salt) {
static float bucketUser(LDUser user, String key, String attr, String salt) {
JsonElement userValue = user.getValueForEvaluation(attr);
String idHash;
if (userValue != null) {
if (userValue.isJsonPrimitive() && userValue.getAsJsonPrimitive().isString()) {
idHash = userValue.getAsString();
if (user.getSecondary() != null) {
idHash = idHash + "." + user.getSecondary().getAsString();
}
String hash = DigestUtils.sha1Hex(key + "." + salt + "." + idHash).substring(0, 15);
long longVal = Long.parseLong(hash, 16);
return (float) longVal / long_scale;
String idHash = getBucketableStringValue(userValue);
if (idHash != null) {
if (user.getSecondary() != null) {
idHash = idHash + "." + user.getSecondary().getAsString();
}
String hash = DigestUtils.sha1Hex(key + "." + salt + "." + idHash).substring(0, 15);
long longVal = Long.parseLong(hash, 16);
return (float) longVal / long_scale;
}
return 0F;
}

private static String getBucketableStringValue(JsonElement userValue) {
if (userValue != null && userValue.isJsonPrimitive()) {
if (userValue.getAsJsonPrimitive().isString()) {
return userValue.getAsString();
}
if (userValue.getAsJsonPrimitive().isNumber()) {
Number n = userValue.getAsJsonPrimitive().getAsNumber();
if (n instanceof Integer) {
return userValue.getAsString();
}
}
}
return null;
}

static class Rollout {
private List<WeightedVariation> variations;
private String bucketBy;
Expand Down
Loading