From 91b188e548ecc44f28b8d87545009e4fd66b63b8 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 2 Nov 2015 21:49:48 -0800 Subject: [PATCH 1/6] Abstract feature request code into new class --- .../launchdarkly/client/FeatureRequestor.java | 120 ++++++++++++++++++ .../com/launchdarkly/client/LDClient.java | 111 ++-------------- 2 files changed, 131 insertions(+), 100 deletions(-) create mode 100644 src/main/java/com/launchdarkly/client/FeatureRequestor.java diff --git a/src/main/java/com/launchdarkly/client/FeatureRequestor.java b/src/main/java/com/launchdarkly/client/FeatureRequestor.java new file mode 100644 index 000000000..69087894a --- /dev/null +++ b/src/main/java/com/launchdarkly/client/FeatureRequestor.java @@ -0,0 +1,120 @@ +package com.launchdarkly.client; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.apache.http.HttpStatus; +import org.apache.http.client.cache.CacheResponseStatus; +import org.apache.http.client.cache.HttpCacheContext; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.cache.CacheConfig; +import org.apache.http.impl.client.cache.CachingHttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * Created by jkodumal on 11/2/15. + */ +class FeatureRequestor { + + private final String apiKey; + private final LDConfig config; + private final CloseableHttpClient client; + private static final Logger logger = LoggerFactory.getLogger(FeatureRequestor.class); + + FeatureRequestor(String apiKey, LDConfig config) { + this.apiKey = apiKey; + this.config = config; + this.client = createClient(); + } + + protected CloseableHttpClient createClient() { + CloseableHttpClient client; + PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(); + manager.setMaxTotal(100); + manager.setDefaultMaxPerRoute(20); + + CacheConfig cacheConfig = CacheConfig.custom() + .setMaxCacheEntries(1000) + .setMaxObjectSize(8192) + .setSharedCache(false) + .build(); + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(config.connectTimeout) + .setSocketTimeout(config.socketTimeout) + .setProxy(config.proxyHost) + .build(); + client = CachingHttpClients.custom() + .setCacheConfig(cacheConfig) + .setConnectionManager(manager) + .setDefaultRequestConfig(requestConfig) + .build(); + return client; + } + + FeatureRep makeRequest(String featureKey, boolean latest) throws IOException { + Gson gson = new Gson(); + HttpCacheContext context = HttpCacheContext.create(); + + String resource = latest ? "/api/eval/latest-features/" : "/api/eval/features/"; + + HttpGet request = config.getRequest(apiKey,resource + featureKey); + + CloseableHttpResponse response = null; + try { + response = client.execute(request, context); + + CacheResponseStatus responseStatus = context.getCacheResponseStatus(); + + switch (responseStatus) { + case CACHE_HIT: + logger.debug("A response was generated from the cache with " + + "no requests sent upstream"); + break; + case CACHE_MODULE_RESPONSE: + logger.debug("The response was generated directly by the " + + "caching module"); + break; + case CACHE_MISS: + logger.debug("The response came from an upstream server"); + break; + case VALIDATED: + logger.debug("The response was generated from the cache " + + "after validating the entry with the origin server"); + break; + } + + int status = response.getStatusLine().getStatusCode(); + + if (status != HttpStatus.SC_OK) { + if (status == HttpStatus.SC_UNAUTHORIZED) { + logger.error("Invalid API key"); + } else if (status == HttpStatus.SC_NOT_FOUND) { + logger.error("Unknown feature key: " + featureKey); + } else { + logger.error("Unexpected status code: " + status); + } + throw new IOException("Failed to fetch flag"); + } + + Type boolType = new TypeToken>() {}.getType(); + + FeatureRep result = gson.fromJson(EntityUtils.toString(response.getEntity()), boolType); + return result; + } + finally { + try { + if (response != null) response.close(); + } catch (IOException e) { + } + } + } +} diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index c0f34db02..c7add8f1a 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -1,32 +1,17 @@ package com.launchdarkly.client; -import com.google.gson.Gson; import com.google.gson.JsonElement; -import com.google.gson.reflect.TypeToken; -import org.apache.http.HttpStatus; import org.apache.http.annotation.ThreadSafe; -import org.apache.http.client.cache.CacheResponseStatus; -import org.apache.http.client.cache.HttpCacheContext; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.cache.CacheConfig; -import org.apache.http.impl.client.cache.CachingHttpClients; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.IOException; -import java.lang.reflect.Type; import java.net.URL; import java.util.jar.Attributes; import java.util.jar.Manifest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * * A client for the LaunchDarkly API. Client instances are thread-safe. Applications should instantiate @@ -37,10 +22,9 @@ public class LDClient implements Closeable { private static final Logger logger = LoggerFactory.getLogger(LDClient.class); private final LDConfig config; - private final CloseableHttpClient client; + private final FeatureRequestor requestor; private final EventProcessor eventProcessor; private final StreamProcessor streamProcessor; - private final String apiKey; protected static final String CLIENT_VERSION = getClientVersion(); private volatile boolean offline = false; @@ -63,9 +47,8 @@ public LDClient(String apiKey) { * @param config a client configuration object */ public LDClient(String apiKey, LDConfig config) { - this.apiKey = apiKey; this.config = config; - this.client = createClient(); + this.requestor = createFeatureRequestor(apiKey, config); this.eventProcessor = createEventProcessor(apiKey, config); if (config.stream) { @@ -78,6 +61,10 @@ public LDClient(String apiKey, LDConfig config) { } } + protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { + return new FeatureRequestor(apiKey, config); + } + protected EventProcessor createEventProcessor(String apiKey, LDConfig config) { return new EventProcessor(apiKey, config); } @@ -86,30 +73,6 @@ protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config) return new StreamProcessor(apiKey, config); } - protected CloseableHttpClient createClient() { - CloseableHttpClient client; - PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(); - manager.setMaxTotal(100); - manager.setDefaultMaxPerRoute(20); - - CacheConfig cacheConfig = CacheConfig.custom() - .setMaxCacheEntries(1000) - .setMaxObjectSize(8192) - .setSharedCache(false) - .build(); - - RequestConfig requestConfig = RequestConfig.custom() - .setConnectTimeout(config.connectTimeout) - .setSocketTimeout(config.socketTimeout) - .setProxy(config.proxyHost) - .build(); - client = CachingHttpClients.custom() - .setCacheConfig(cacheConfig) - .setConnectionManager(manager) - .setDefaultRequestConfig(requestConfig) - .build(); - return client; - } /** * Tracks that a user performed an event. @@ -191,13 +154,14 @@ public boolean toggle(String featureKey, LDUser user, boolean defaultValue) { logger.debug("Using feature flag stored from streaming API"); result = (FeatureRep) this.streamProcessor.getFeature(featureKey); if (config.debugStreaming) { - FeatureRep pollingResult = fetchFeature(featureKey); + FeatureRep pollingResult = requestor.makeRequest(featureKey, true); if (!result.equals(pollingResult)) { logger.warn("Mismatch between streaming and polling feature! Streaming: {} Polling: {}", result, pollingResult); } } } else { - result = fetchFeature(featureKey); + // If streaming is enabled, always get the latest version of the feature while polling + result = requestor.makeRequest(featureKey, this.config.stream); } if (result == null) { logger.warn("Unknown feature flag " + featureKey + "; returning default value"); @@ -221,60 +185,7 @@ public boolean toggle(String featureKey, LDUser user, boolean defaultValue) { } } - private FeatureRep fetchFeature(String featureKey) throws IOException { - Gson gson = new Gson(); - HttpCacheContext context = HttpCacheContext.create(); - HttpGet request = config.getRequest(apiKey, "/api/eval/features/" + featureKey); - - CloseableHttpResponse response = null; - try { - response = client.execute(request, context); - - CacheResponseStatus responseStatus = context.getCacheResponseStatus(); - - switch (responseStatus) { - case CACHE_HIT: - logger.debug("A response was generated from the cache with " + - "no requests sent upstream"); - break; - case CACHE_MODULE_RESPONSE: - logger.debug("The response was generated directly by the " + - "caching module"); - break; - case CACHE_MISS: - logger.debug("The response came from an upstream server"); - break; - case VALIDATED: - logger.debug("The response was generated from the cache " + - "after validating the entry with the origin server"); - break; - } - - int status = response.getStatusLine().getStatusCode(); - - if (status != HttpStatus.SC_OK) { - if (status == HttpStatus.SC_UNAUTHORIZED) { - logger.error("Invalid API key"); - } else if (status == HttpStatus.SC_NOT_FOUND) { - logger.error("Unknown feature key: " + featureKey); - } else { - logger.error("Unexpected status code: " + status); - } - throw new IOException("Failed to fetch flag"); - } - - Type boolType = new TypeToken>() {}.getType(); - FeatureRep result = gson.fromJson(EntityUtils.toString(response.getEntity()), boolType); - return result; - } - finally { - try { - if (response != null) response.close(); - } catch (IOException e) { - } - } - } /** * Closes the LaunchDarkly client event processing thread and flushes all pending events. This should only From 87463747516df49d88e0e56e8a4ba8857150f137 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 2 Nov 2015 22:19:55 -0800 Subject: [PATCH 2/6] Support for indirect patch events --- .../com/launchdarkly/client/LDClient.java | 6 +++--- .../launchdarkly/client/StreamProcessor.java | 20 +++++++++++++++++-- .../com/launchdarkly/client/LDClientTest.java | 18 +++++++---------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index c7add8f1a..6a5d037d4 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -53,7 +53,7 @@ public LDClient(String apiKey, LDConfig config) { if (config.stream) { logger.debug("Enabling streaming API"); - this.streamProcessor = createStreamProcessor(apiKey, config); + this.streamProcessor = createStreamProcessor(apiKey, config, requestor); this.streamProcessor.subscribe(); } else { logger.debug("Streaming API disabled"); @@ -69,8 +69,8 @@ protected EventProcessor createEventProcessor(String apiKey, LDConfig config) { return new EventProcessor(apiKey, config); } - protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config) { - return new StreamProcessor(apiKey, config); + protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { + return new StreamProcessor(apiKey, config, requestor); } diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java index 7ecf604b0..76d3ac1ef 100644 --- a/src/main/java/com/launchdarkly/client/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java @@ -5,6 +5,8 @@ import org.glassfish.jersey.internal.util.collection.StringKeyIgnoreCaseMultivaluedMap; import org.glassfish.jersey.media.sse.InboundEvent; import org.glassfish.jersey.media.sse.SseFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; @@ -19,18 +21,23 @@ class StreamProcessor implements Closeable { private static final String PUT = "put"; private static final String PATCH = "patch"; private static final String DELETE = "delete"; + private static final String INDIRECT_PATCH = "indirect/patch"; + private static final Logger logger = LoggerFactory.getLogger(StreamProcessor.class); private final Client client; private final FeatureStore store; private final LDConfig config; private final String apiKey; + private final FeatureRequestor requestor; private EventSource es; - StreamProcessor(String apiKey, LDConfig config) { + + StreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { this.client = ClientBuilder.newBuilder().register(SseFeature.class).build(); this.store = new InMemoryFeatureStore(); this.config = config; this.apiKey = apiKey; + this.requestor = requestor; } void subscribe() { @@ -58,8 +65,17 @@ else if (event.getName().equals(DELETE)) { FeatureDeleteData data = gson.fromJson(event.readData(), FeatureDeleteData.class); store.delete(data.key(), data.version()); } + else if (event.getName().equals(INDIRECT_PATCH)) { + String key = gson.fromJson(event.readData(), String.class); + try { + FeatureRep feature = requestor.makeRequest(key, true); + store.upsert(key, feature); + } catch (IOException e) { + logger.error("Encountered exception in LaunchDarkly client", e); + } + } else { - // TODO log an error + logger.warn("Unexpected event found in stream: " + event.getName()); } } }; diff --git a/src/test/java/com/launchdarkly/client/LDClientTest.java b/src/test/java/com/launchdarkly/client/LDClientTest.java index c95a2f6a0..3e3bae21e 100644 --- a/src/test/java/com/launchdarkly/client/LDClientTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientTest.java @@ -20,28 +20,24 @@ public class LDClientTest extends EasyMockSupport { private CloseableHttpClient httpClient = createMock(CloseableHttpClient.class); - private EventProcessor eventProcessor = createMock(EventProcessor.class); + private FeatureRequestor requestor = createMock(FeatureRequestor.class); LDClient client = new LDClient("API_KEY") { - @Override - protected CloseableHttpClient createClient() { - return httpClient; - } @Override - protected EventProcessor createEventProcessor(String apiKey, LDConfig config) { - return eventProcessor; + protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { + return requestor; } }; @Test public void testExceptionThrownByHttpClientReturnsDefaultValue() throws IOException { - expect(httpClient.execute(anyObject(HttpUriRequest.class), anyObject(HttpContext.class))).andThrow(new RuntimeException()); - replay(httpClient); + expect(requestor.makeRequest(anyString(), anyBoolean())).andThrow(new IOException()); + replay(requestor); - boolean result = client.getFlag("test", new LDUser("test.key"), true); + boolean result = client.toggle("test", new LDUser("test.key"), true); assertEquals(true, result); - verify(httpClient); + verify(requestor); } } From 57b1b6e1b50f51e567522a981ef356e651bdd51e Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 2 Nov 2015 23:15:00 -0800 Subject: [PATCH 3/6] Increase the maximum cache object size --- src/main/java/com/launchdarkly/client/FeatureRequestor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/launchdarkly/client/FeatureRequestor.java b/src/main/java/com/launchdarkly/client/FeatureRequestor.java index 69087894a..33172cbd9 100644 --- a/src/main/java/com/launchdarkly/client/FeatureRequestor.java +++ b/src/main/java/com/launchdarkly/client/FeatureRequestor.java @@ -43,7 +43,7 @@ protected CloseableHttpClient createClient() { CacheConfig cacheConfig = CacheConfig.custom() .setMaxCacheEntries(1000) - .setMaxObjectSize(8192) + .setMaxObjectSize(131072) .setSharedCache(false) .build(); From dbe4cf8de92038fc388d409a3f83a3f1efcf6c04 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Tue, 3 Nov 2015 07:23:23 -0800 Subject: [PATCH 4/6] Change variable name to better reflect intent --- src/main/java/com/launchdarkly/client/FeatureRequestor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/launchdarkly/client/FeatureRequestor.java b/src/main/java/com/launchdarkly/client/FeatureRequestor.java index 33172cbd9..cc4f30ce7 100644 --- a/src/main/java/com/launchdarkly/client/FeatureRequestor.java +++ b/src/main/java/com/launchdarkly/client/FeatureRequestor.java @@ -105,9 +105,9 @@ FeatureRep makeRequest(String featureKey, boolean latest) throws IOExcept throw new IOException("Failed to fetch flag"); } - Type boolType = new TypeToken>() {}.getType(); + Type type = new TypeToken>() {}.getType(); - FeatureRep result = gson.fromJson(EntityUtils.toString(response.getEntity()), boolType); + FeatureRep result = gson.fromJson(EntityUtils.toString(response.getEntity()), type); return result; } finally { From ac3fcb29cfad7b0d043d3128c8ae131b7007c970 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Tue, 3 Nov 2015 20:47:58 -0800 Subject: [PATCH 5/6] Support for indirect put events --- .../launchdarkly/client/FeatureRequestor.java | 102 ++++++++++++------ .../launchdarkly/client/StreamProcessor.java | 11 +- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/launchdarkly/client/FeatureRequestor.java b/src/main/java/com/launchdarkly/client/FeatureRequestor.java index cc4f30ce7..2bab25df6 100644 --- a/src/main/java/com/launchdarkly/client/FeatureRequestor.java +++ b/src/main/java/com/launchdarkly/client/FeatureRequestor.java @@ -18,10 +18,8 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.util.Map; -/** - * Created by jkodumal on 11/2/15. - */ class FeatureRequestor { private final String apiKey; @@ -60,50 +58,90 @@ protected CloseableHttpClient createClient() { return client; } - FeatureRep makeRequest(String featureKey, boolean latest) throws IOException { + Map> makeAllRequest(boolean latest) throws IOException { Gson gson = new Gson(); HttpCacheContext context = HttpCacheContext.create(); - String resource = latest ? "/api/eval/latest-features/" : "/api/eval/features/"; + String resource = latest ? "/api/eval/latest-features" : "/api/eval/features"; - HttpGet request = config.getRequest(apiKey,resource + featureKey); + HttpGet request = config.getRequest(apiKey, resource); CloseableHttpResponse response = null; try { response = client.execute(request, context); - CacheResponseStatus responseStatus = context.getCacheResponseStatus(); - - switch (responseStatus) { - case CACHE_HIT: - logger.debug("A response was generated from the cache with " + - "no requests sent upstream"); - break; - case CACHE_MODULE_RESPONSE: - logger.debug("The response was generated directly by the " + - "caching module"); - break; - case CACHE_MISS: - logger.debug("The response came from an upstream server"); - break; - case VALIDATED: - logger.debug("The response was generated from the cache " + - "after validating the entry with the origin server"); - break; + logCacheResponse(context.getCacheResponseStatus()); + + handleResponseStatus(response.getStatusLine().getStatusCode(), null); + + Type type = new TypeToken>>() {}.getType(); + + Map> result = gson.fromJson(EntityUtils.toString(response.getEntity()), type); + return result; + } + finally { + try { + if (response != null) response.close(); + } catch (IOException e) { } + } + } - int status = response.getStatusLine().getStatusCode(); + void logCacheResponse(CacheResponseStatus status) { + switch (status) { + case CACHE_HIT: + logger.debug("A response was generated from the cache with " + + "no requests sent upstream"); + break; + case CACHE_MODULE_RESPONSE: + logger.debug("The response was generated directly by the " + + "caching module"); + break; + case CACHE_MISS: + logger.debug("The response came from an upstream server"); + break; + case VALIDATED: + logger.debug("The response was generated from the cache " + + "after validating the entry with the origin server"); + break; + } + } - if (status != HttpStatus.SC_OK) { - if (status == HttpStatus.SC_UNAUTHORIZED) { - logger.error("Invalid API key"); - } else if (status == HttpStatus.SC_NOT_FOUND) { + void handleResponseStatus(int status, String featureKey) throws IOException { + + if (status != HttpStatus.SC_OK) { + if (status == HttpStatus.SC_UNAUTHORIZED) { + logger.error("Invalid API key"); + } else if (status == HttpStatus.SC_NOT_FOUND) { + if (featureKey != null) { logger.error("Unknown feature key: " + featureKey); - } else { - logger.error("Unexpected status code: " + status); } - throw new IOException("Failed to fetch flag"); + else { + logger.error("Resource not found"); + } + } else { + logger.error("Unexpected status code: " + status); } + throw new IOException("Failed to fetch flag"); + } + + } + + FeatureRep makeRequest(String featureKey, boolean latest) throws IOException { + Gson gson = new Gson(); + HttpCacheContext context = HttpCacheContext.create(); + + String resource = latest ? "/api/eval/latest-features/" : "/api/eval/features/"; + + HttpGet request = config.getRequest(apiKey,resource + featureKey); + + CloseableHttpResponse response = null; + try { + response = client.execute(request, context); + + logCacheResponse(context.getCacheResponseStatus()); + + handleResponseStatus(response.getStatusLine().getStatusCode(), featureKey); Type type = new TypeToken>() {}.getType(); diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java index 76d3ac1ef..d91554e6a 100644 --- a/src/main/java/com/launchdarkly/client/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java @@ -21,6 +21,7 @@ class StreamProcessor implements Closeable { private static final String PUT = "put"; private static final String PATCH = "patch"; private static final String DELETE = "delete"; + private static final String INDIRECT_PUT = "indirect/put"; private static final String INDIRECT_PATCH = "indirect/patch"; private static final Logger logger = LoggerFactory.getLogger(StreamProcessor.class); @@ -65,8 +66,16 @@ else if (event.getName().equals(DELETE)) { FeatureDeleteData data = gson.fromJson(event.readData(), FeatureDeleteData.class); store.delete(data.key(), data.version()); } + else if (event.getName().equals(INDIRECT_PUT)) { + try { + Map> features = requestor.makeAllRequest(true); + store.init(features); + } catch (IOException e) { + logger.error("Encountered exception in LaunchDarkly client", e); + } + } else if (event.getName().equals(INDIRECT_PATCH)) { - String key = gson.fromJson(event.readData(), String.class); + String key = event.readData(); try { FeatureRep feature = requestor.makeRequest(key, true); store.upsert(key, feature); From a3dbb7b1c4e2d4c746f3cdfe53b7bf0dbd721100 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Wed, 4 Nov 2015 17:27:39 -0800 Subject: [PATCH 6/6] Fix potential NPEs in building Users --- build.gradle | 2 +- .../java/com/launchdarkly/client/LDUser.java | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 17ca58cb5..2466c904f 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ repositories { allprojects { group = 'com.launchdarkly' - version = "0.14.0" + version = "0.15.0-SNAPSHOT" sourceCompatibility = 1.6 targetCompatibility = 1.6 } diff --git a/src/main/java/com/launchdarkly/client/LDUser.java b/src/main/java/com/launchdarkly/client/LDUser.java index 92617894c..76fe8a9ac 100644 --- a/src/main/java/com/launchdarkly/client/LDUser.java +++ b/src/main/java/com/launchdarkly/client/LDUser.java @@ -254,7 +254,9 @@ public Builder email(String email) { * @return the builder */ public Builder custom(String k, String v) { - custom.put(k, new JsonPrimitive(v)); + if (key != null && v != null) { + custom.put(k, new JsonPrimitive(v)); + } return this; } @@ -265,7 +267,9 @@ public Builder custom(String k, String v) { * @return the builder */ public Builder custom(String k, Number n) { - custom.put(k, new JsonPrimitive(n)); + if (key != null && n != null) { + custom.put(k, new JsonPrimitive(n)); + } return this; } @@ -276,7 +280,9 @@ public Builder custom(String k, Number n) { * @return the builder */ public Builder custom(String k, Boolean b) { - custom.put(k, new JsonPrimitive(b)); + if (key != null && b != null) { + custom.put(k, new JsonPrimitive(b)); + } return this; } @@ -289,7 +295,9 @@ public Builder custom(String k, Boolean b) { public Builder custom(String k, List vs) { JsonArray array = new JsonArray(); for (String v : vs) { - array.add(new JsonPrimitive(v)); + if (v != null) { + array.add(new JsonPrimitive(v)); + } } custom.put(k, array); return this;