44import com .google .common .annotations .VisibleForTesting ;
55import com .google .gson .JsonElement ;
66import com .google .gson .JsonPrimitive ;
7+ import org .apache .commons .codec .binary .Hex ;
78import org .apache .http .annotation .ThreadSafe ;
89import org .slf4j .Logger ;
910import org .slf4j .LoggerFactory ;
1011
12+ import javax .crypto .Mac ;
13+ import javax .crypto .spec .SecretKeySpec ;
1114import java .io .Closeable ;
1215import java .io .IOException ;
16+ import java .io .UnsupportedEncodingException ;
1317import java .net .URL ;
18+ import java .security .InvalidKeyException ;
19+ import java .security .NoSuchAlgorithmException ;
1420import java .util .HashMap ;
1521import java .util .Map ;
1622import java .util .concurrent .Future ;
2632@ ThreadSafe
2733public class LDClient implements Closeable {
2834 private static final Logger logger = LoggerFactory .getLogger (LDClient .class );
35+ private static final String HMAC_ALGORITHM = "HmacSHA256" ;
36+ protected static final String CLIENT_VERSION = getClientVersion ();
37+
2938 private final LDConfig config ;
39+ private final String sdkKey ;
3040 private final FeatureRequestor requestor ;
3141 private final EventProcessor eventProcessor ;
3242 private UpdateProcessor updateProcessor ;
33- protected static final String CLIENT_VERSION = getClientVersion ();
3443
3544 /**
3645 * Creates a new client instance that connects to LaunchDarkly with the default configuration. In most
3746 * cases, you should use this constructor.
3847 *
39- * @param apiKey the API key for your account
48+ * @param sdkKey the SDK key for your LaunchDarkly environment
4049 */
41- public LDClient (String apiKey ) {
42- this (apiKey , LDConfig .DEFAULT );
50+ public LDClient (String sdkKey ) {
51+ this (sdkKey , LDConfig .DEFAULT );
4352 }
4453
4554 /**
4655 * Creates a new client to connect to LaunchDarkly with a custom configuration. This constructor
4756 * can be used to configure advanced client features, such as customizing the LaunchDarkly base URL.
4857 *
49- * @param apiKey the API key for your account
58+ * @param sdkKey the SDK key for your LaunchDarkly environment
5059 * @param config a client configuration object
5160 */
52- public LDClient (String apiKey , LDConfig config ) {
61+ public LDClient (String sdkKey , LDConfig config ) {
5362 this .config = config ;
54- this .requestor = createFeatureRequestor (apiKey , config );
55- this .eventProcessor = createEventProcessor (apiKey , config );
63+ this .sdkKey = sdkKey ;
64+ this .requestor = createFeatureRequestor (sdkKey , config );
65+ this .eventProcessor = createEventProcessor (sdkKey , config );
5666
5767 if (config .offline ) {
5868 logger .info ("Starting LaunchDarkly client in offline mode" );
@@ -66,7 +76,7 @@ public LDClient(String apiKey, LDConfig config) {
6676
6777 if (config .stream ) {
6878 logger .info ("Enabling streaming API" );
69- this .updateProcessor = createStreamProcessor (apiKey , config , requestor );
79+ this .updateProcessor = createStreamProcessor (sdkKey , config , requestor );
7080 } else {
7181 logger .info ("Disabling streaming API" );
7282 this .updateProcessor = createPollingProcessor (config );
@@ -91,18 +101,18 @@ public boolean initialized() {
91101 }
92102
93103 @ VisibleForTesting
94- protected FeatureRequestor createFeatureRequestor (String apiKey , LDConfig config ) {
95- return new FeatureRequestor (apiKey , config );
104+ protected FeatureRequestor createFeatureRequestor (String sdkKey , LDConfig config ) {
105+ return new FeatureRequestor (sdkKey , config );
96106 }
97107
98108 @ VisibleForTesting
99- protected EventProcessor createEventProcessor (String apiKey , LDConfig config ) {
100- return new EventProcessor (apiKey , config );
109+ protected EventProcessor createEventProcessor (String sdkKey , LDConfig config ) {
110+ return new EventProcessor (sdkKey , config );
101111 }
102112
103113 @ VisibleForTesting
104- protected StreamProcessor createStreamProcessor (String apiKey , LDConfig config , FeatureRequestor requestor ) {
105- return new StreamProcessor (apiKey , config , requestor );
114+ protected StreamProcessor createStreamProcessor (String sdkKey , LDConfig config , FeatureRequestor requestor ) {
115+ return new StreamProcessor (sdkKey , config , requestor );
106116 }
107117
108118 @ VisibleForTesting
@@ -174,33 +184,42 @@ private void sendFlagRequestEvent(String featureKey, LDUser user, JsonElement va
174184 }
175185
176186 /**
177- * Returns a map from feature flag keys to Boolean feature flag values for a given user. The map will contain {@code null}
178- * entries for any flags that are off or for any feature flags with non-boolean variations. If the client is offline or
179- * has not been initialized, a {@code null} map will be returned.
187+ * Returns a map from feature flag keys to {@code JsonElement} feature flag values for a given user.
188+ * If the result of a flag's evaluation would have returned the default variation, it will have a null entry
189+ * in the map. If the client is offline, has not been initialized, or a null user or user with null/empty user key a {@code null} map will be returned.
180190 * This method will not send analytics events back to LaunchDarkly.
181191 * <p>
182192 * The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service.
183193 *
184194 * @param user the end user requesting the feature flags
185- * @return a map from feature flag keys to JsonElement values for the specified user
195+ * @return a map from feature flag keys to {@code JsonElement} for the specified user
186196 */
187- public Map <String , Boolean > allFlags (LDUser user ) {
197+ public Map <String , JsonElement > allFlags (LDUser user ) {
188198 if (isOffline ()) {
199+ logger .warn ("allFlags() was called when client is in offline mode! Returning null." );
189200 return null ;
190201 }
191202
192203 if (!initialized ()) {
204+ logger .warn ("allFlags() was called before Client has been initialized! Returning null." );
205+ return null ;
206+ }
207+
208+ if (user == null || user .getKeyAsString ().isEmpty ()) {
209+ logger .warn ("allFlags() was called with null user or null/empty user key! returning null" );
193210 return null ;
194211 }
195212
196213 Map <String , FeatureFlag > flags = this .config .featureStore .all ();
197- Map <String , Boolean > result = new HashMap <>();
214+ Map <String , JsonElement > result = new HashMap <>();
198215
199- for (String key : flags .keySet ()) {
200- JsonElement evalResult = evaluate ( key , user , null );
201- if ( evalResult . isJsonPrimitive () && evalResult . getAsJsonPrimitive (). isBoolean ()) {
202- result .put (key , evalResult . getAsBoolean () );
216+ for (Map . Entry < String , FeatureFlag > entry : flags .entrySet ()) {
217+ try {
218+ JsonElement evalResult = entry . getValue (). evaluate ( user , config . featureStore ). getValue ();
219+ result .put (entry . getKey (), evalResult );
203220
221+ } catch (EvaluationException e ) {
222+ logger .error ("Exception caught when evaluating all flags:" , e );
204223 }
205224 }
206225 return result ;
@@ -315,22 +334,15 @@ private JsonElement evaluate(String featureKey, LDUser user, JsonElement default
315334 sendFlagRequestEvent (featureKey , user , defaultValue , defaultValue , null );
316335 return defaultValue ;
317336 }
318- if (featureFlag .isOn ()) {
319- FeatureFlag .EvalResult evalResult = featureFlag .evaluate (user , config .featureStore );
320- if (!isOffline ()) {
321- for (FeatureRequestEvent event : evalResult .getPrerequisiteEvents ()) {
322- eventProcessor .sendEvent (event );
323- }
324- }
325- if (evalResult .getValue () != null ) {
326- sendFlagRequestEvent (featureKey , user , evalResult .getValue (), defaultValue , featureFlag .getVersion ());
327- return evalResult .getValue ();
328- }
337+ FeatureFlag .EvalResult evalResult = featureFlag .evaluate (user , config .featureStore );
338+ if (!isOffline ()) {
339+ for (FeatureRequestEvent event : evalResult .getPrerequisiteEvents ()) {
340+ eventProcessor .sendEvent (event );
341+ }
329342 }
330- JsonElement offVariation = featureFlag .getOffVariationValue ();
331- if (offVariation != null ) {
332- sendFlagRequestEvent (featureKey , user , offVariation , defaultValue , featureFlag .getVersion ());
333- return offVariation ;
343+ if (evalResult .getValue () != null ) {
344+ sendFlagRequestEvent (featureKey , user , evalResult .getValue (), defaultValue , featureFlag .getVersion ());
345+ return evalResult .getValue ();
334346 }
335347 } catch (Exception e ) {
336348 logger .error ("Encountered exception in LaunchDarkly client" , e );
@@ -368,6 +380,25 @@ public boolean isOffline() {
368380 return config .offline ;
369381 }
370382
383+ /**
384+ * For more info: <a href=https://github.com/launchdarkly/js-client#secure-mode>https://github.com/launchdarkly/js-client#secure-mode</a>
385+ * @param user The User to be hashed along with the sdk key
386+ * @return the hash, or null if the hash could not be calculated.
387+ */
388+ public String secureModeHash (LDUser user ) {
389+ if (user == null || user .getKeyAsString ().isEmpty ()) {
390+ return null ;
391+ }
392+ try {
393+ Mac mac = Mac .getInstance (HMAC_ALGORITHM );
394+ mac .init (new SecretKeySpec (sdkKey .getBytes (), HMAC_ALGORITHM ));
395+ return Hex .encodeHexString (mac .doFinal (user .getKeyAsString ().getBytes ("UTF8" )));
396+ } catch (InvalidKeyException | UnsupportedEncodingException | NoSuchAlgorithmException e ) {
397+ logger .error ("Could not generate secure mode hash" , e );
398+ }
399+ return null ;
400+ }
401+
371402 private static String getClientVersion () {
372403 Class clazz = LDConfig .class ;
373404 String className = clazz .getSimpleName () + ".class" ;
0 commit comments