diff --git a/unleash-client-java/pom.xml b/unleash-client-java/pom.xml
index becac166be..f43b1dea18 100644
--- a/unleash-client-java/pom.xml
+++ b/unleash-client-java/pom.xml
@@ -19,6 +19,17 @@
+
+
+
+ com.google.code.gson
+ gson
+ 2.2.4
+
+
org.apache.logging.log4j
log4j-api
@@ -31,12 +42,22 @@
4.3.1
+
junit
junit
4.11
+ test
+
+ org.mockito
+ mockito-all
+ 1.9.5
+ test
+
+
+
diff --git a/unleash-client-java/src/main/no/finn/unleash/Toggle.java b/unleash-client-java/src/main/no/finn/unleash/Toggle.java
new file mode 100644
index 0000000000..0b8cf1ecb2
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/Toggle.java
@@ -0,0 +1,33 @@
+package no.finn.unleash;
+
+import java.util.Map;
+
+public final class Toggle {
+ private final String name;
+ private final boolean enabled;
+ private final String strategy;
+ private final Map parameters;
+
+ public Toggle(String name, boolean enabled, String strategy, Map parameters) {
+ this.name = name;
+ this.enabled = enabled;
+ this.strategy = strategy;
+ this.parameters = parameters;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public String getStrategy() {
+ return strategy;
+ }
+
+ public Map getParameters() {
+ return parameters;
+ }
+}
diff --git a/unleash-client-java/src/main/no/finn/unleash/Unleash.java b/unleash-client-java/src/main/no/finn/unleash/Unleash.java
index 34185b5c81..384220552b 100644
--- a/unleash-client-java/src/main/no/finn/unleash/Unleash.java
+++ b/unleash-client-java/src/main/no/finn/unleash/Unleash.java
@@ -1,4 +1,66 @@
package no.finn.unleash;
+import java.util.HashMap;
+import java.util.Map;
+import no.finn.unleash.repository.ToggleException;
+import no.finn.unleash.repository.ToggleRepository;
+import no.finn.unleash.strategy.DefaultStrategy;
+import no.finn.unleash.strategy.Strategy;
+import no.finn.unleash.strategy.UnknownStrategy;
+
public final class Unleash {
+ private final ToggleRepository toggleRepository;
+ private final Map strategyMap;
+ private final UnknownStrategy unknownStrategy = new UnknownStrategy();
+ private final DefaultStrategy defaultStrategy = new DefaultStrategy();
+
+
+ public Unleash(ToggleRepository toggleRepository, Strategy... strategies) {
+ this.toggleRepository = toggleRepository;
+ this.strategyMap = buildStrategyMap(strategies);
+ }
+
+ public boolean isEnabled(final String toggleName) {
+ return isEnabled(toggleName, false);
+ }
+
+ public boolean isEnabled(final String toggleName, final boolean defaultSetting) {
+ try {
+ Toggle toggle = toggleRepository.getToggle(toggleName);
+
+ if(toggle == null) {
+ return defaultSetting;
+ }
+
+ Strategy strategy = getStrategy(toggle.getStrategy());
+ return toggle.isEnabled() && strategy.isEnabled(toggle.getParameters());
+
+ } catch (ToggleException rx) {
+ return defaultSetting;
+ }
+ }
+
+ private Map buildStrategyMap(Strategy[] strategies) {
+ Map map = new HashMap<>();
+
+ map.put(defaultStrategy.getName(), defaultStrategy);
+
+ if(strategies != null) {
+ for(Strategy strategy : strategies) {
+ map.put(strategy.getName(), strategy);
+ }
+ }
+
+ return map;
+ }
+
+
+
+ private Strategy getStrategy(String strategy) {
+ if(strategyMap.containsKey(strategy)) {
+ return strategyMap.get(strategy);
+ } else {
+ return unknownStrategy;
+ }
+ }
}
diff --git a/unleash-client-java/src/main/no/finn/unleash/repository/HTTPToggleRepository.java b/unleash-client-java/src/main/no/finn/unleash/repository/HTTPToggleRepository.java
new file mode 100644
index 0000000000..65a579cff3
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/repository/HTTPToggleRepository.java
@@ -0,0 +1,57 @@
+package no.finn.unleash.repository;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import no.finn.unleash.Toggle;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+
+public class HTTPToggleRepository implements ToggleRepository {
+ private static final Log LOG = LogFactory.getLog(HTTPToggleRepository.class);
+
+ private final HttpClient httpClient;
+ private final String serverEndpoint;
+
+ public HTTPToggleRepository(final String serverEndpoint) {
+ this.serverEndpoint = serverEndpoint;
+ this.httpClient = HttpClients.createDefault();
+ }
+
+ @Override
+ public Toggle getToggle(final String name) throws ToggleException {
+ try {
+ for (Toggle toggle : fetchToggles()) {
+ if (name.equals(toggle.getName())) {
+ return toggle;
+ }
+ }
+ } catch (IOException e) {
+ LOG.warn("Could not fetch toggles via HTTP", e);
+ throw new ToggleException("Could not fetch toggles via HTTP");
+ }
+
+ throw new ToggleException("unknown toggle: " + name);
+ }
+
+ @Override
+ public Collection getToggles() {
+ try {
+ return fetchToggles();
+ } catch (IOException e) {
+ LOG.warn("Could not fetch toggles via HTTP");
+ throw new ToggleException("Could not fetch toggles via HTTP");
+ }
+ }
+
+ private List fetchToggles() throws IOException {
+ HttpResponse httpResponse = httpClient.execute(new HttpGet(serverEndpoint));
+ final String jsonString = EntityUtils.toString(httpResponse.getEntity());
+ return JsonParser.toListOfToggles(jsonString);
+ }
+}
diff --git a/unleash-client-java/src/main/no/finn/unleash/repository/JsonParser.java b/unleash-client-java/src/main/no/finn/unleash/repository/JsonParser.java
new file mode 100644
index 0000000000..f8390ccab0
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/repository/JsonParser.java
@@ -0,0 +1,27 @@
+package no.finn.unleash.repository;
+
+import java.util.Collection;
+import java.util.List;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import no.finn.unleash.Toggle;
+
+public final class JsonParser {
+
+ public static Toggle toToggle(String jsonString) {
+ Gson gson = new GsonBuilder().create();
+ return gson.fromJson(jsonString, Toggle.class);
+ }
+
+ public static String toJsonString(Collection toggles) {
+ Gson gson = new GsonBuilder().create();
+ return gson.toJson(toggles);
+ }
+
+
+ public static List toListOfToggles(String jsonString) {
+ Gson gson = new GsonBuilder().create();
+ return gson.fromJson(jsonString, new TypeToken>(){}.getType());
+ }
+}
diff --git a/unleash-client-java/src/main/no/finn/unleash/repository/PollingToggleRepository.java b/unleash-client-java/src/main/no/finn/unleash/repository/PollingToggleRepository.java
new file mode 100644
index 0000000000..d762d14196
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/repository/PollingToggleRepository.java
@@ -0,0 +1,136 @@
+package no.finn.unleash.repository;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import no.finn.unleash.Toggle;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class PollingToggleRepository implements ToggleRepository {
+ private static final Log LOG = LogFactory.getLog(PollingToggleRepository.class);
+ private static final ScheduledThreadPoolExecutor TIMER = new ScheduledThreadPoolExecutor(
+ 1,
+ new ThreadFactory() {
+ @Override
+ public Thread newThread(final Runnable r) {
+ Thread thread = Executors.defaultThreadFactory().newThread(r);
+ thread.setName("unleash-toggle-repository");
+ thread.setDaemon(true);
+ return thread;
+ }
+ });
+
+ static {
+ TIMER.setRemoveOnCancelPolicy(true);
+ }
+
+ private final ToggleRepository toggleRepository;
+ private final int pollIntervalSeconds;
+ private Map togglesCache;
+
+
+ public PollingToggleRepository(final ToggleRepository toggleRepository, final int pollIntervalSeconds){
+ this.toggleRepository = toggleRepository;
+ this.pollIntervalSeconds = pollIntervalSeconds;
+
+ this.togglesCache = new HashMap<>();
+ updateTogglesCache();
+ startBackgroundPolling();
+ }
+
+ @Override
+ public Toggle getToggle(final String name){
+ return togglesCache.get(name);
+ }
+
+ @Override
+ public Collection getToggles() {
+ return Collections.unmodifiableCollection(togglesCache.values());
+ }
+
+ private void updateTogglesCache() {
+ try {
+ Map freshToggleMap = new HashMap<>();
+
+ for(Toggle toggle : fetchToggles()) {
+ freshToggleMap.put(toggle.getName(), toggle);
+ }
+
+ this.togglesCache = Collections.unmodifiableMap(freshToggleMap);
+
+ } catch (ToggleException e) {
+ //Do nothing
+ }
+ }
+
+ private ScheduledFuture startBackgroundPolling() {
+ try {
+ return TIMER.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ updateTogglesCache();
+ }
+ }, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS);
+ } catch (RejectedExecutionException ex) {
+ LOG.error("Unleash background task crashed");
+ return null;
+ }
+ }
+
+
+ private Collection fetchToggles() throws ToggleException {
+ try {
+ Collection toggles = toggleRepository.getToggles();
+ storeRepoAsTempFile(JsonParser.toJsonString(toggles));
+ return toggles;
+ } catch (ToggleException ex) {
+ if(togglesCache.isEmpty()) {
+ return loadFromTempFile();
+ }
+ throw ex;
+ }
+ }
+
+
+ private List loadFromTempFile() throws ToggleException {
+ LOG.info("Unleash will try to load feature toggle states from temporary backup");
+ try(FileReader reader = new FileReader(pathToTmpBackupFile())) {
+ BufferedReader br = new BufferedReader(reader);
+ StringBuilder builder = new StringBuilder();
+ String line;
+ while((line = br.readLine()) != null) {
+ builder.append(line);
+ }
+ return JsonParser.toListOfToggles(builder.toString());
+ } catch (IOException e) {
+ LOG.error("Unleash was unable to feature toggle repo from temporary backup: " + pathToTmpBackupFile());
+ throw new ToggleException("Unleash was unable to feature toggle states from temporary backup");
+ }
+ }
+
+ private void storeRepoAsTempFile(final String serverResponse) {
+ try(FileWriter writer = new FileWriter(pathToTmpBackupFile())) {
+ writer.write(serverResponse);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static String pathToTmpBackupFile() {
+ return System.getProperty("java.io.tmpdir") + File.separatorChar + "unleash-repo.json";
+ }
+}
diff --git a/unleash-client-java/src/main/no/finn/unleash/repository/ToggleException.java b/unleash-client-java/src/main/no/finn/unleash/repository/ToggleException.java
new file mode 100644
index 0000000000..531fcbcca6
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/repository/ToggleException.java
@@ -0,0 +1,7 @@
+package no.finn.unleash.repository;
+
+public class ToggleException extends RuntimeException {
+ public ToggleException(String message) {
+ super(message);
+ }
+}
diff --git a/unleash-client-java/src/main/no/finn/unleash/repository/ToggleRepository.java b/unleash-client-java/src/main/no/finn/unleash/repository/ToggleRepository.java
new file mode 100644
index 0000000000..4a49ef2c01
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/repository/ToggleRepository.java
@@ -0,0 +1,10 @@
+package no.finn.unleash.repository;
+
+import java.util.Collection;
+import no.finn.unleash.Toggle;
+
+public interface ToggleRepository {
+ Toggle getToggle(String name) throws ToggleException;
+
+ Collection getToggles() throws ToggleException;
+}
diff --git a/unleash-client-java/src/main/no/finn/unleash/strategy/DefaultStrategy.java b/unleash-client-java/src/main/no/finn/unleash/strategy/DefaultStrategy.java
new file mode 100644
index 0000000000..f0db305f5b
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/strategy/DefaultStrategy.java
@@ -0,0 +1,17 @@
+package no.finn.unleash.strategy;
+
+import java.util.Map;
+
+public final class DefaultStrategy implements Strategy {
+ public static final String NAME = "default";
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public boolean isEnabled(Map parameters) {
+ return true;
+ }
+}
diff --git a/unleash-client-java/src/main/no/finn/unleash/strategy/Strategy.java b/unleash-client-java/src/main/no/finn/unleash/strategy/Strategy.java
new file mode 100644
index 0000000000..81bc2136f8
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/strategy/Strategy.java
@@ -0,0 +1,9 @@
+package no.finn.unleash.strategy;
+
+import java.util.Map;
+
+public interface Strategy {
+ String getName();
+
+ boolean isEnabled(Map parameters);
+}
diff --git a/unleash-client-java/src/main/no/finn/unleash/strategy/UnknownStrategy.java b/unleash-client-java/src/main/no/finn/unleash/strategy/UnknownStrategy.java
new file mode 100644
index 0000000000..4fb166a470
--- /dev/null
+++ b/unleash-client-java/src/main/no/finn/unleash/strategy/UnknownStrategy.java
@@ -0,0 +1,17 @@
+package no.finn.unleash.strategy;
+
+import java.util.Map;
+
+public final class UnknownStrategy implements Strategy {
+ public static final String NAME = "unknown";
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public boolean isEnabled(Map parameters) {
+ return false;
+ }
+}
diff --git a/unleash-client-java/src/test/no/finn/unleash/UnleashTest.java b/unleash-client-java/src/test/no/finn/unleash/UnleashTest.java
index 7cfc113a87..a38256681e 100644
--- a/unleash-client-java/src/test/no/finn/unleash/UnleashTest.java
+++ b/unleash-client-java/src/test/no/finn/unleash/UnleashTest.java
@@ -1,13 +1,67 @@
package no.finn.unleash;
+import no.finn.unleash.repository.ToggleException;
+import no.finn.unleash.repository.ToggleRepository;
+import no.finn.unleash.strategy.Strategy;
+import org.junit.Before;
import org.junit.Test;
-import static org.junit.Assert.assertTrue;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.*;
public class UnleashTest {
+ private ToggleRepository toggleRepository;
+ private Unleash unleash;
+
+ @Before
+ public void setup() {
+ toggleRepository = mock(ToggleRepository.class);
+ unleash = new Unleash(toggleRepository);
+ }
+
@Test
- public void helloWorld() {
- assertTrue(true);
+ public void known_toogle_and_strategy_should_be_active() {
+ when(toggleRepository.getToggle("test")).thenReturn(new Toggle("test", true, "default", null));
+
+ assertThat(unleash.isEnabled("test"), is(true));
+ }
+
+ @Test
+ public void unknown_strategy_should_be_considered_inactive() {
+ when(toggleRepository.getToggle("test")).thenReturn(new Toggle("test", true, "whoot_strat", null));
+
+ assertThat(unleash.isEnabled("test"), is(false));
+ }
+
+ @Test
+ public void failing_repository_should_obey_default() {
+ when(toggleRepository.getToggle("test")).thenThrow(new ToggleException("service down"));
+
+ assertThat(unleash.isEnabled("test", true), is(true));
+ }
+
+ @Test
+ public void unknown_feature_should_be_considered_inactive() {
+ when(toggleRepository.getToggle("test")).thenReturn(null);
+
+ assertThat(unleash.isEnabled("test"), is(false));
+ }
+
+ @Test
+ public void should_register_custom_strategies() {
+ //custom strategy
+ Strategy customStrategy = mock(Strategy.class);
+ when(customStrategy.getName()).thenReturn("custom");
+
+ //register custom strategy
+ unleash = new Unleash(toggleRepository, customStrategy);
+ when(toggleRepository.getToggle("test")).thenReturn(new Toggle("test", true, "custom", null));
+
+ unleash.isEnabled("test");
+
+ verify(customStrategy, times(1)).isEnabled(anyMap());
}
}