mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Extended java-client to support the following:
* Unleash - the actual unleash client exposed to the clients. * Toggle - represents a concrete feature toggle. * ToggleRepository - the logic to communicate with the unleash-server. * Strategy - implements the logic associated with strategies. I have also included to common strategies: "Default" and "Unknown". related to issue #17.
This commit is contained in:
		
							parent
							
								
									2ca8291eac
								
							
						
					
					
						commit
						0ed61a1f0f
					
				| @ -19,6 +19,17 @@ | ||||
|     </properties> | ||||
| 
 | ||||
|     <dependencies> | ||||
| 
 | ||||
|         <!-- | ||||
|           TODO: we should write our own manual json serialize/deserializer | ||||
|           to avvoid this dependency | ||||
|         --> | ||||
|         <dependency> | ||||
|             <groupId>com.google.code.gson</groupId> | ||||
|             <artifactId>gson</artifactId> | ||||
|             <version>2.2.4</version> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>org.apache.logging.log4j</groupId> | ||||
|             <artifactId>log4j-api</artifactId> | ||||
| @ -31,12 +42,22 @@ | ||||
|             <version>4.3.1</version> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <!-- Test dependencies --> | ||||
|         <dependency> | ||||
|             <groupId>junit</groupId> | ||||
|             <artifactId>junit</artifactId> | ||||
|             <version>4.11</version> | ||||
|             <scope>test</scope> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>org.mockito</groupId> | ||||
|             <artifactId>mockito-all</artifactId> | ||||
|             <version>1.9.5</version> | ||||
|             <scope>test</scope> | ||||
|         </dependency> | ||||
| 
 | ||||
| 
 | ||||
|     </dependencies> | ||||
| 
 | ||||
|     <build> | ||||
|  | ||||
							
								
								
									
										33
									
								
								unleash-client-java/src/main/no/finn/unleash/Toggle.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								unleash-client-java/src/main/no/finn/unleash/Toggle.java
									
									
									
									
									
										Normal file
									
								
							| @ -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<String, String> parameters; | ||||
| 
 | ||||
|     public Toggle(String name, boolean enabled, String strategy, Map<String, String> 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<String, String> getParameters() { | ||||
|         return parameters; | ||||
|     } | ||||
| } | ||||
| @ -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<String, Strategy> 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<String, Strategy> buildStrategyMap(Strategy[] strategies) { | ||||
|         Map<String, Strategy> 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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<Toggle> 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<Toggle> fetchToggles() throws IOException { | ||||
|         HttpResponse httpResponse = httpClient.execute(new HttpGet(serverEndpoint)); | ||||
|         final String jsonString = EntityUtils.toString(httpResponse.getEntity()); | ||||
|         return JsonParser.toListOfToggles(jsonString); | ||||
|     } | ||||
| } | ||||
| @ -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<Toggle> toggles) { | ||||
|         Gson gson = new GsonBuilder().create(); | ||||
|         return gson.toJson(toggles); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static List<Toggle> toListOfToggles(String jsonString) { | ||||
|         Gson gson = new GsonBuilder().create(); | ||||
|         return gson.fromJson(jsonString, new TypeToken<List<Toggle>>(){}.getType()); | ||||
|     } | ||||
| } | ||||
| @ -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<String, Toggle> 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<Toggle> getToggles() { | ||||
|         return Collections.unmodifiableCollection(togglesCache.values()); | ||||
|     } | ||||
| 
 | ||||
|     private void updateTogglesCache() { | ||||
|         try { | ||||
|             Map<String, Toggle> 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<Toggle> fetchToggles() throws ToggleException { | ||||
|         try { | ||||
|             Collection<Toggle> toggles = toggleRepository.getToggles(); | ||||
|             storeRepoAsTempFile(JsonParser.toJsonString(toggles)); | ||||
|             return toggles; | ||||
|         } catch (ToggleException ex) { | ||||
|             if(togglesCache.isEmpty()) { | ||||
|                 return loadFromTempFile(); | ||||
|             } | ||||
|             throw ex; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private List<Toggle> 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"; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| package no.finn.unleash.repository; | ||||
| 
 | ||||
| public class ToggleException extends RuntimeException { | ||||
|     public ToggleException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| @ -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<Toggle> getToggles() throws ToggleException; | ||||
| } | ||||
| @ -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<String, String> parameters) { | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| package no.finn.unleash.strategy; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| 
 | ||||
| public interface Strategy { | ||||
|     String getName(); | ||||
| 
 | ||||
|     boolean isEnabled(Map<String, String> parameters); | ||||
| } | ||||
| @ -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<String, String> parameters) { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| @ -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()); | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user