Skip to content

Commit ec69def

Browse files
feat: add logic for offline mode in java (#141)
* Add logic for offline mode in java * checkstyle fixes * Remove unnecessary exception * REmove unnecessary changes spotted in self review * PR feedback
1 parent 1998b24 commit ec69def

File tree

8 files changed

+284
-25
lines changed

8 files changed

+284
-25
lines changed

src/main/java/com/flagsmith/FlagsmithClient.java

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public void updateEnvironment() {
7272
* @return
7373
*/
7474
public Flags getEnvironmentFlags() throws FlagsmithClientError {
75-
if (flagsmithSdk.getConfig().getEnableLocalEvaluation()) {
75+
if (getShouldUseEnvironmentDocument()) {
7676
return getEnvironmentFlagsFromDocument();
7777
}
7878

@@ -105,7 +105,7 @@ public Flags getIdentityFlags(String identifier)
105105
*/
106106
public Flags getIdentityFlags(String identifier, Map<String, Object> traits)
107107
throws FlagsmithClientError {
108-
if (flagsmithSdk.getConfig().getEnableLocalEvaluation()) {
108+
if (getShouldUseEnvironmentDocument()) {
109109
return getIdentityFlagsFromDocument(identifier, traits);
110110
}
111111

@@ -165,23 +165,23 @@ public void close() {
165165

166166
private Flags getEnvironmentFlagsFromDocument() throws FlagsmithClientError {
167167
if (environment == null) {
168-
if (flagsmithSdk.getConfig().getFlagsmithFlagDefaults() == null) {
168+
if (getConfig().getFlagsmithFlagDefaults() == null) {
169169
throw new FlagsmithClientError("Unable to get flags. No environment present.");
170170
}
171171
return getDefaultFlags();
172172
}
173173

174174
return Flags.fromFeatureStateModels(
175175
Engine.getEnvironmentFeatureStates(environment),
176-
flagsmithSdk.getConfig().getAnalyticsProcessor(),
176+
getConfig().getAnalyticsProcessor(),
177177
null,
178-
flagsmithSdk.getConfig().getFlagsmithFlagDefaults());
178+
getConfig().getFlagsmithFlagDefaults());
179179
}
180180

181181
private Flags getIdentityFlagsFromDocument(String identifier, Map<String, Object> traits)
182182
throws FlagsmithClientError {
183183
if (environment == null) {
184-
if (flagsmithSdk.getConfig().getFlagsmithFlagDefaults() == null) {
184+
if (getConfig().getFlagsmithFlagDefaults() == null) {
185185
throw new FlagsmithClientError("Unable to get flags. No environment present.");
186186
}
187187
return getDefaultFlags();
@@ -192,17 +192,23 @@ private Flags getIdentityFlagsFromDocument(String identifier, Map<String, Object
192192

193193
return Flags.fromFeatureStateModels(
194194
featureStates,
195-
flagsmithSdk.getConfig().getAnalyticsProcessor(),
195+
getConfig().getAnalyticsProcessor(),
196196
identity.getCompositeKey(),
197-
flagsmithSdk.getConfig().getFlagsmithFlagDefaults());
197+
getConfig().getFlagsmithFlagDefaults());
198198
}
199199

200200
private Flags getEnvironmentFlagsFromApi() throws FlagsmithApiError {
201201
try {
202202
return flagsmithSdk.getFeatureFlags(Boolean.TRUE);
203203
} catch (Exception e) {
204-
if (flagsmithSdk.getConfig().getFlagsmithFlagDefaults() != null) {
204+
if (getConfig().getFlagsmithFlagDefaults() != null) {
205205
return getDefaultFlags();
206+
} else if (environment != null) {
207+
try {
208+
return getEnvironmentFlagsFromDocument();
209+
} catch (FlagsmithClientError ce) {
210+
// Do nothing and fall through to FlagsmithApiError
211+
}
206212
}
207213

208214
throw new FlagsmithApiError("Failed to get feature flags.");
@@ -225,8 +231,14 @@ private Flags getIdentityFlagsFromApi(String identifier, Map<String, Object> tra
225231
traitsList,
226232
Boolean.TRUE);
227233
} catch (Exception e) {
228-
if (flagsmithSdk.getConfig().getFlagsmithFlagDefaults() != null) {
234+
if (getConfig().getFlagsmithFlagDefaults() != null) {
229235
return getDefaultFlags();
236+
} else if (environment != null) {
237+
try {
238+
return getIdentityFlagsFromDocument(identifier, traits);
239+
} catch (FlagsmithClientError ce) {
240+
// Do nothing and fall through to FlagsmithApiError
241+
}
230242
}
231243

232244
throw new FlagsmithApiError("Failed to get feature flags.");
@@ -258,19 +270,23 @@ private IdentityModel buildIdentityModel(String identifier, Map<String, Object>
258270

259271
private Flags getDefaultFlags() {
260272
Flags flags = new Flags();
261-
flags.setDefaultFlagHandler(flagsmithSdk.getConfig().getFlagsmithFlagDefaults());
273+
flags.setDefaultFlagHandler(getConfig().getFlagsmithFlagDefaults());
262274
return flags;
263275
}
264276

265277
private String getEnvironmentUpdateErrorMessage() {
266278
if (this.environment == null) {
267279
return "Unable to update environment from API. "
268-
+ "No environment configured - using defaultHandler if configured.";
280+
+ "No environment configured - using defaultHandler if configured.";
269281
} else {
270282
return "Unable to update environment from API. Continuing to use previous copy.";
271283
}
272284
}
273285

286+
private FlagsmithConfig getConfig() {
287+
return flagsmithSdk.getConfig();
288+
}
289+
274290
/**
275291
* Returns a FlagsmithCache cache object that encapsulates methods to manipulate
276292
* the cache.
@@ -281,6 +297,15 @@ public FlagsmithCache getCache() {
281297
return this.flagsmithSdk.getCache();
282298
}
283299

300+
/**
301+
* Returns a boolean indicating whether the flags should be retrieved from a
302+
* locally stored environment document instead of retrieved from the API.
303+
*/
304+
private Boolean getShouldUseEnvironmentDocument() {
305+
FlagsmithConfig config = getConfig();
306+
return config.getEnableLocalEvaluation() | config.getOfflineMode();
307+
}
308+
284309
public static class Builder {
285310

286311
private final FlagsmithClient client;
@@ -451,33 +476,40 @@ public Builder withFlagsmithApiWrapper(FlagsmithApiWrapper flagsmithApiWrapper)
451476
* @return a FlagsmithClient
452477
*/
453478
public FlagsmithClient build() {
454-
final FlagsmithApiWrapper flagsmithApiWrapper;
479+
if (configuration.getOfflineMode()) {
480+
if (configuration.getOfflineHandler() == null) {
481+
throw new FlagsmithRuntimeError("Offline handler must be provided to use offline mode.");
482+
}
483+
}
455484

456485
if (this.flagsmithApiWrapper != null) {
457-
flagsmithApiWrapper = this.flagsmithApiWrapper;
486+
client.flagsmithSdk = this.flagsmithApiWrapper;
458487
} else if (cacheConfig != null) {
459-
flagsmithApiWrapper = new FlagsmithApiWrapper(
488+
client.flagsmithSdk = new FlagsmithApiWrapper(
460489
cacheConfig.getCache(),
461490
this.configuration,
462491
this.customHeaders,
463492
client.logger,
464493
apiKey);
465494
} else {
466-
flagsmithApiWrapper = new FlagsmithApiWrapper(
495+
client.flagsmithSdk = new FlagsmithApiWrapper(
467496
this.configuration,
468497
this.customHeaders,
469498
client.logger,
470499
apiKey);
471500
}
472501

473-
client.flagsmithSdk = flagsmithApiWrapper;
474-
475502
if (configuration.getAnalyticsProcessor() != null) {
476-
configuration.getAnalyticsProcessor().setApi(flagsmithApiWrapper);
503+
configuration.getAnalyticsProcessor().setApi(client.flagsmithSdk);
477504
configuration.getAnalyticsProcessor().setLogger(client.logger);
478505
}
479506

480507
if (configuration.getEnableLocalEvaluation()) {
508+
if (configuration.getOfflineHandler() != null) {
509+
throw new FlagsmithRuntimeError(
510+
"Local evaluation and offline handler cannot be used together.");
511+
}
512+
481513
if (!apiKey.startsWith("ser.")) {
482514
throw new FlagsmithRuntimeError(
483515
"In order to use local evaluation, please generate a server key "
@@ -495,6 +527,14 @@ public FlagsmithClient build() {
495527
client.pollingManager.startPolling();
496528
}
497529

530+
if (configuration.getOfflineHandler() != null) {
531+
if (configuration.getFlagsmithFlagDefaults() != null) {
532+
throw new FlagsmithRuntimeError(
533+
"Cannot use both default flag handler and offline handler.");
534+
}
535+
client.environment = configuration.getOfflineHandler().getEnvironment();
536+
}
537+
498538
return this.client;
499539
}
500540
}

src/main/java/com/flagsmith/config/FlagsmithConfig.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.flagsmith.config;
22

33
import com.flagsmith.FlagsmithFlagDefaults;
4+
import com.flagsmith.interfaces.IOfflineHandler;
45
import com.flagsmith.threads.AnalyticsProcessor;
56

67
import java.net.Proxy;
@@ -42,6 +43,8 @@ public final class FlagsmithConfig {
4243
private AnalyticsProcessor analyticsProcessor;
4344
private FlagsmithFlagDefaults flagsmithFlagDefaults = null;
4445
private Boolean raiseUpdateEnvironmentErrorsOnStartup = true;
46+
private Boolean offlineMode = false;
47+
private IOfflineHandler offlineHandler = null;
4548

4649
protected FlagsmithConfig(Builder builder) {
4750
this.baseUri = builder.baseUri;
@@ -75,6 +78,9 @@ protected FlagsmithConfig(Builder builder) {
7578
analyticsProcessor = new AnalyticsProcessor(httpClient);
7679
}
7780
}
81+
82+
this.offlineMode = builder.offlineMode;
83+
this.offlineHandler = builder.offlineHandler;
7884
}
7985

8086
public static FlagsmithConfig.Builder newBuilder() {
@@ -103,6 +109,9 @@ public static class Builder {
103109
private Integer environmentRefreshIntervalSeconds = DEFAULT_ENVIRONMENT_REFRESH_SECONDS;
104110
private Boolean enableAnalytics = Boolean.FALSE;
105111

112+
private Boolean offlineMode = Boolean.FALSE;
113+
private IOfflineHandler offlineHandler;
114+
106115
private Builder() {
107116
}
108117

@@ -211,8 +220,8 @@ public Builder withLocalEvaluation(Boolean localEvaluation) {
211220
}
212221

213222
/**
214-
* set environment refresh rate with polling manager. Only needed when local evaluation is
215-
* true.
223+
* set environment refresh rate with polling manager. Only needed when local
224+
* evaluation is true.
216225
*
217226
* @param seconds seconds
218227
* @return
@@ -234,7 +243,6 @@ public Builder withAnalyticsProcessor(AnalyticsProcessor processor) {
234243
return this;
235244
}
236245

237-
238246
/**
239247
* Enable Analytics Processor.
240248
*
@@ -246,6 +254,28 @@ public Builder withEnableAnalytics(Boolean enable) {
246254
return this;
247255
}
248256

257+
/**
258+
* Enable offline mode.
259+
*
260+
* @param offlineMode boolean to enable offline mode
261+
* @return
262+
*/
263+
public Builder withOfflineMode(Boolean offlineMode) {
264+
this.offlineMode = offlineMode;
265+
return this;
266+
}
267+
268+
/**
269+
* Set the offline handler (used as a fallback or with offlineMode).
270+
*
271+
* @param offlineHandler the offline handler
272+
* @return
273+
*/
274+
public Builder withOfflineHandler(IOfflineHandler offlineHandler) {
275+
this.offlineHandler = offlineHandler;
276+
return this;
277+
}
278+
249279
public FlagsmithConfig build() {
250280
return new FlagsmithConfig(this);
251281
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.flagsmith.interfaces;
2+
3+
import com.flagsmith.flagengine.environments.EnvironmentModel;
4+
5+
public interface IOfflineHandler {
6+
EnvironmentModel getEnvironment();
7+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.flagsmith.offline;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.flagsmith.MapperFactory;
5+
import com.flagsmith.exceptions.FlagsmithClientError;
6+
import com.flagsmith.flagengine.environments.EnvironmentModel;
7+
import com.flagsmith.interfaces.IOfflineHandler;
8+
9+
import java.io.File;
10+
import java.io.IOException;
11+
12+
public class LocalFileHandler implements IOfflineHandler {
13+
private EnvironmentModel environmentModel;
14+
private ObjectMapper objectMapper = MapperFactory.getMapper();
15+
16+
/**
17+
* Instantiate a LocalFileHandler for use as an OfflineHandler.
18+
*
19+
* @param filePath - path to a json file containing the environment data.
20+
* @throws FlagsmithClientError
21+
*
22+
*/
23+
public LocalFileHandler(String filePath) throws FlagsmithClientError {
24+
File file = new File(filePath);
25+
try {
26+
environmentModel = objectMapper.readValue(file, EnvironmentModel.class);
27+
} catch (IOException e) {
28+
throw new FlagsmithClientError("Unable to read environment from file " + filePath);
29+
}
30+
}
31+
32+
public EnvironmentModel getEnvironment() {
33+
return environmentModel;
34+
}
35+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.flagsmith;
2+
3+
import com.flagsmith.flagengine.environments.EnvironmentModel;
4+
import com.flagsmith.interfaces.IOfflineHandler;
5+
6+
public class DummyOfflineHandler implements IOfflineHandler {
7+
public EnvironmentModel getEnvironment() {
8+
return FlagsmithTestHelper.environmentModel();
9+
}
10+
}

0 commit comments

Comments
 (0)