Skip to content
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
48 changes: 46 additions & 2 deletions client/src/main/java/io/split/client/SplitClientConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.split.client;

import io.split.client.dtos.FallbackTreatment;
import io.split.client.dtos.FallbackTreatmentsConfiguration;
import io.split.client.dtos.ProxyConfiguration;
import io.split.client.impressions.ImpressionListener;
import io.split.client.impressions.ImpressionsManager;
Expand All @@ -16,10 +18,13 @@
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ThreadFactory;
import java.io.InputStream;

import static io.split.inputValidation.FallbackTreatmentValidator.isValidByFlagTreatment;
import static io.split.inputValidation.FallbackTreatmentValidator.isValidTreatment;
import static io.split.inputValidation.FlagSetsValidator.cleanup;

/**
Expand Down Expand Up @@ -91,6 +96,7 @@ private HttpScheme() {
private final CustomStorageWrapper _customStorageWrapper;
private final StorageMode _storageMode;
private final ThreadFactory _threadFactory;
private final FallbackTreatmentsConfiguration _fallbackTreatments;

// Proxy configs
private final ProxyConfiguration _proxyConfiguration;
Expand Down Expand Up @@ -163,7 +169,8 @@ private SplitClientConfig(String endpoint,
HashSet<String> flagSetsFilter,
int invalidSets,
CustomHeaderDecorator customHeaderDecorator,
CustomHttpModule alternativeHTTPModule) {
CustomHttpModule alternativeHTTPModule,
FallbackTreatmentsConfiguration fallbackTreatments) {
_endpoint = endpoint;
_eventsEndpoint = eventsEndpoint;
_featuresRefreshRate = pollForFeatureChangesEveryNSeconds;
Expand Down Expand Up @@ -218,6 +225,7 @@ private SplitClientConfig(String endpoint,
_invalidSets = invalidSets;
_customHeaderDecorator = customHeaderDecorator;
_alternativeHTTPModule = alternativeHTTPModule;
_fallbackTreatments = fallbackTreatments;

Properties props = new Properties();
try {
Expand Down Expand Up @@ -436,6 +444,8 @@ public boolean isSdkEndpointOverridden() {

public CustomHttpModule alternativeHTTPModule() { return _alternativeHTTPModule; }

public FallbackTreatmentsConfiguration fallbackTreatments() { return _fallbackTreatments; }

public static final class Builder {
private String _endpoint = SDK_ENDPOINT;
private boolean _endpointSet = false;
Expand Down Expand Up @@ -494,6 +504,7 @@ public static final class Builder {
private int _invalidSetsCount = 0;
private CustomHeaderDecorator _customHeaderDecorator = null;
private CustomHttpModule _alternativeHTTPModule = null;
private FallbackTreatmentsConfiguration _fallbackTreatments;

public Builder() {
}
Expand Down Expand Up @@ -1022,6 +1033,17 @@ public Builder alternativeHTTPModule(CustomHttpModule alternativeHTTPModule) {
return this;
}

/**
* Fallback Treatments
*
* @param fallbackTreatments
* @return this builder
*/
public Builder fallbackTreatments(FallbackTreatmentsConfiguration fallbackTreatments) {
_fallbackTreatments = fallbackTreatments;
return this;
}

/**
* Thread Factory
*
Expand Down Expand Up @@ -1158,6 +1180,25 @@ private void verifyProxy() {
}
}

private void verifyFallbackTreatments() {
if (_fallbackTreatments == null)
return;

FallbackTreatment processedGlobalFallbackTreatment = _fallbackTreatments.getGlobalFallbackTreatment();
Map<String, FallbackTreatment> processedByFlagFallbackTreatment = _fallbackTreatments.getByFlagFallbackTreatment();

if (_fallbackTreatments.getGlobalFallbackTreatment() != null) {
processedGlobalFallbackTreatment = new FallbackTreatment(
isValidTreatment(_fallbackTreatments.getGlobalFallbackTreatment().getTreatment(), "Fallback treatments"),
_fallbackTreatments.getGlobalFallbackTreatment().getConfig());
}

if (_fallbackTreatments.getByFlagFallbackTreatment() != null) {
processedByFlagFallbackTreatment = isValidByFlagTreatment(_fallbackTreatments.getByFlagFallbackTreatment(), "config");
}
_fallbackTreatments = new FallbackTreatmentsConfiguration(processedGlobalFallbackTreatment, processedByFlagFallbackTreatment);
}

public SplitClientConfig build() {

verifyRates();
Expand All @@ -1172,6 +1213,8 @@ public SplitClientConfig build() {

verifyProxy();

verifyFallbackTreatments();

if (_numThreadsForSegmentFetch <= 0) {
throw new IllegalArgumentException("Number of threads for fetching segments MUST be greater than zero");
}
Expand Down Expand Up @@ -1230,7 +1273,8 @@ public SplitClientConfig build() {
_flagSetsFilter,
_invalidSetsCount,
_customHeaderDecorator,
_alternativeHTTPModule);
_alternativeHTTPModule,
_fallbackTreatments);
}
}
}
33 changes: 33 additions & 0 deletions client/src/main/java/io/split/client/dtos/FallbackTreatment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.split.client.dtos;

import java.util.Map;

public class FallbackTreatment {
private final Map<String, Object> _config;
private final String _treatment;
private final String _label;

public FallbackTreatment(String treatment, Map<String, Object> config) {
_treatment = treatment;
_config = config;
_label = "fallback - ";
}

public FallbackTreatment(String treatment) {
_treatment = treatment;
_config = null;
_label = "fallback - ";
}

public Map<String, Object> getConfig() {
return _config;
}

public String getTreatment() {
return _treatment;
}

public String getLabel() {
return _label;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.split.client.dtos;

import java.util.Map;

public class FallbackTreatmentsConfiguration {
private final FallbackTreatment _globalFallbackTreatment;
private final Map<String, FallbackTreatment> _byFlagFallbackTreatment;

public FallbackTreatmentsConfiguration(FallbackTreatment globalFallbackTreatment, Map<String, FallbackTreatment> byFlagFallbackTreatment) {
_globalFallbackTreatment = globalFallbackTreatment;
_byFlagFallbackTreatment = byFlagFallbackTreatment;
}

public FallbackTreatment getGlobalFallbackTreatment() {
return _globalFallbackTreatment;
}

public Map<String, FallbackTreatment> getByFlagFallbackTreatment() { return _byFlagFallbackTreatment;}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.split.inputValidation;

import io.split.client.dtos.FallbackTreatment;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

import static io.split.inputValidation.SplitNameValidator.isValid;

public class FallbackTreatmentValidator {
private static final Logger _log = LoggerFactory.getLogger(FallbackTreatmentValidator.class);
private static final Pattern TREATMENT_MATCHER = Pattern.compile("^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$");
private static final int MAX_LENGTH = 100;

public static String isValidTreatment(String name, String method) {
if (name == null) {
_log.error(String.format("%s: you passed a null treatment, fallback treatment must be a non-empty string", method));
return null;
}

if (name.isEmpty()) {
_log.error(String.format("%s: you passed an empty treatment, fallback treatment must be a non-empty string", method));
return null;
}

String trimmed = name.trim();
if (!trimmed.equals(name)) {
_log.warn(String.format("%s: fallback treatment %s has extra whitespace, trimming", method, name));
name = trimmed;
}

if (name.length() > MAX_LENGTH) {
return null;
}

if (!TREATMENT_MATCHER.matcher(name).find()) {
_log.error(String.format("%s: you passed %s, treatment must adhere to the regular expression " +
"^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$", method, name));
return null;
}

return name;
}

public static Map<String, FallbackTreatment> isValidByFlagTreatment(Map<String, FallbackTreatment> byFlagTreatment, String method) {
Map<String, FallbackTreatment> result = new HashMap<>();
for (Map.Entry<String, FallbackTreatment> entry : byFlagTreatment.entrySet()) {
Optional<String> feature_name = isValid(entry.getKey(), "Validator");
if (feature_name.equals(Optional.empty())) {
continue;
}

FallbackTreatment fallbackTreatment = entry.getValue();
String treatment = isValidTreatment(fallbackTreatment.getTreatment(), "Validator");
if (treatment == null) {
continue;
}

result.put(feature_name.get(), new FallbackTreatment(treatment, fallbackTreatment.getConfig()));
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class SplitNameValidator {
private static final Logger _log = LoggerFactory.getLogger(SplitNameValidator.class);
private static final int MAX_LENGTH = 100;
private static final Pattern NAME_MATCHER = Pattern.compile("^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$");

public static Optional<String> isValid(String name, String method) {
if (name == null) {
Expand All @@ -28,6 +31,16 @@ public static Optional<String> isValid(String name, String method) {
name = trimmed;
}

if (name.length() > MAX_LENGTH) {
return Optional.empty();
}

if (!NAME_MATCHER.matcher(name).find()) {
_log.error(String.format("%s: you passed %s, feature flag name must adhere to the regular expression " +
"^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$", method, name));
return Optional.empty();
}

return Optional.of(name);
}

Expand Down
28 changes: 25 additions & 3 deletions client/src/test/java/io/split/client/SplitClientConfigTest.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package io.split.client;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.split.client.dtos.BasicCredentialsProvider;
import io.split.client.dtos.BearerCredentialsProvider;
import io.split.client.dtos.RequestContext;
import io.split.client.dtos.FallbackTreatmentsConfiguration;
import io.split.client.dtos.FallbackTreatment;
import io.split.client.dtos.ProxyConfiguration;
import io.split.client.impressions.Impression;
import io.split.client.impressions.ImpressionListener;
import io.split.client.impressions.ImpressionsManager;
import io.split.client.dtos.RequestContext;
import io.split.integrations.IntegrationsConfig;
import org.junit.Assert;
import org.junit.Test;
Expand All @@ -17,6 +17,7 @@
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -362,4 +363,25 @@ public void mustUseP12PassKeyWithProxyMtls() throws MalformedURLException, FileN
.build())
.build();
}

@Test
public void fallbackTreatmentCheckRegex() {
SplitClientConfig config = SplitClientConfig.builder()
.fallbackTreatments(new FallbackTreatmentsConfiguration(new FallbackTreatment("12#2"), null))
.build();
Assert.assertEquals(null, config.fallbackTreatments().getGlobalFallbackTreatment().getTreatment());

config = SplitClientConfig.builder()
.fallbackTreatments(new FallbackTreatmentsConfiguration(null, new HashMap<String, FallbackTreatment>() {{ put("flag", new FallbackTreatment("12#2")); }} ))
.build();
Assert.assertEquals(0, config.fallbackTreatments().getByFlagFallbackTreatment().size());

config = SplitClientConfig.builder()
.fallbackTreatments(new FallbackTreatmentsConfiguration(
new FallbackTreatment("on"),
new HashMap<String, FallbackTreatment>() {{ put("flag", new FallbackTreatment("off")); }} ))
.build();
Assert.assertEquals("on", config.fallbackTreatments().getGlobalFallbackTreatment().getTreatment());
Assert.assertEquals("off", config.fallbackTreatments().getByFlagFallbackTreatment().get("flag").getTreatment());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -712,10 +712,10 @@ public void testPluggableMode() throws IOException, URISyntaxException {
Assert.assertTrue(events.stream().anyMatch(e -> "keyProperties".equals(e.getEventDto().key) && e.getEventDto().properties != null));

Assert.assertEquals(3, splits.size());
Assert.assertTrue(splits.stream().anyMatch(sw -> "first.name".equals(sw.name)));
Assert.assertTrue(splits.stream().anyMatch(sw -> "second.name".equals(sw.name)));
Assert.assertEquals("on", client.getTreatment("key", "first.name"));
Assert.assertEquals("off", client.getTreatmentWithConfig("FakeKey", "second.name").treatment());
Assert.assertTrue(splits.stream().anyMatch(sw -> "first-name".equals(sw.name)));
Assert.assertTrue(splits.stream().anyMatch(sw -> "second-name".equals(sw.name)));
Assert.assertEquals("on", client.getTreatment("key", "first-name"));
Assert.assertEquals("off", client.getTreatmentWithConfig("FakeKey", "second-name").treatment());
Assert.assertEquals("control", client.getTreatment("FakeKey", "noSplit"));
Assert.assertEquals("on", client.getTreatment("bilal@@split.io", "rbs_flag", new HashMap<String, Object>() {{
put("email", "bilal@@split.io");
Expand All @@ -726,8 +726,8 @@ public void testPluggableMode() throws IOException, URISyntaxException {

List<ImpressionConsumer> impressions = customStorageWrapper.getImps();
Assert.assertEquals(4, impressions.size());
Assert.assertTrue(impressions.stream().anyMatch(imp -> "first.name".equals(imp.getKeyImpression().feature) && "on".equals(imp.getKeyImpression().treatment)));
Assert.assertTrue(impressions.stream().anyMatch(imp -> "second.name".equals(imp.getKeyImpression().feature) && "off".equals(imp.getKeyImpression().treatment)));
Assert.assertTrue(impressions.stream().anyMatch(imp -> "first-name".equals(imp.getKeyImpression().feature) && "on".equals(imp.getKeyImpression().treatment)));
Assert.assertTrue(impressions.stream().anyMatch(imp -> "second-name".equals(imp.getKeyImpression().feature) && "off".equals(imp.getKeyImpression().treatment)));

Map<String, Long> latencies = customStorageWrapper.getLatencies();

Expand Down
Loading