11package com .datadog .featureflag ;
22
3+ import com .datadog .featureflag .ufc .v1 .Allocation ;
4+ import com .datadog .featureflag .ufc .v1 .ConditionConfiguration ;
5+ import com .datadog .featureflag .ufc .v1 .ConditionOperator ;
6+ import com .datadog .featureflag .ufc .v1 .Flag ;
7+ import com .datadog .featureflag .ufc .v1 .Rule ;
38import com .datadog .featureflag .ufc .v1 .ServerConfiguration ;
9+ import com .datadog .featureflag .ufc .v1 .Shard ;
10+ import com .datadog .featureflag .ufc .v1 .ShardRange ;
11+ import com .datadog .featureflag .ufc .v1 .Split ;
12+ import com .datadog .featureflag .ufc .v1 .Variant ;
413import datadog .trace .api .featureflag .FeatureFlagEvaluator ;
14+ import java .nio .charset .StandardCharsets ;
15+ import java .security .MessageDigest ;
16+ import java .security .NoSuchAlgorithmException ;
17+ import java .text .ParseException ;
18+ import java .text .SimpleDateFormat ;
19+ import java .util .Collection ;
520import java .util .Collections ;
21+ import java .util .Date ;
22+ import java .util .HashMap ;
23+ import java .util .List ;
24+ import java .util .Map ;
625import java .util .Set ;
26+ import java .util .TimeZone ;
727import java .util .WeakHashMap ;
828import java .util .concurrent .atomic .AtomicReference ;
929import java .util .function .Consumer ;
30+ import java .util .regex .Pattern ;
1031
1132public class FeatureFlagEvaluatorImpl
1233 implements FeatureFlagEvaluator , Consumer <ServerConfiguration > {
@@ -17,36 +38,330 @@ public class FeatureFlagEvaluatorImpl
1738 @ Override
1839 public Resolution <Boolean > evaluate (
1940 final String key , final Boolean defaultValue , final Context context ) {
20- // TODO
21- throw new UnsupportedOperationException ();
41+ return evaluateInternal (key , defaultValue , context );
2242 }
2343
2444 @ Override
2545 public Resolution <Integer > evaluate (
2646 final String key , final Integer defaultValue , final Context context ) {
27- // TODO
28- throw new UnsupportedOperationException ();
47+ return evaluateInternal (key , defaultValue , context );
2948 }
3049
3150 @ Override
3251 public Resolution <Double > evaluate (
3352 final String key , final Double defaultValue , final Context context ) {
34- // TODO
35- throw new UnsupportedOperationException ();
53+ return evaluateInternal (key , defaultValue , context );
3654 }
3755
3856 @ Override
3957 public Resolution <String > evaluate (
4058 final String key , final String defaultValue , final Context context ) {
41- // TODO
42- throw new UnsupportedOperationException ();
59+ return evaluateInternal (key , defaultValue , context );
4360 }
4461
4562 @ Override
4663 public Resolution <Object > evaluate (
4764 final String key , final Object defaultValue , final Context context ) {
48- // TODO
49- throw new UnsupportedOperationException ();
65+ return evaluateInternal (key , defaultValue , context );
66+ }
67+
68+ private <T > Resolution <T > evaluateInternal (
69+ final String key , final T defaultValue , final Context context ) {
70+ final ServerConfiguration config = configuration .get ();
71+
72+ if (config == null ) {
73+ // TODO: log warning and telemetry
74+ return Resolution .notInitialized (defaultValue );
75+ }
76+
77+ if (context == null || context .getTargetingKey () == null ) {
78+ // TODO: log error and telemetry
79+ return Resolution .error (defaultValue );
80+ }
81+
82+ final Flag flag = config .flags .get (key );
83+ if (flag == null ) {
84+ // TODO: log warning and telemetry
85+ return Resolution .defaultResolution (defaultValue );
86+ }
87+
88+ if (!flag .enabled ) {
89+ // TODO: log warning and telemetry
90+ return new Resolution <>(defaultValue ).setReason (ResolutionReason .DISABLED );
91+ }
92+
93+ if (flag .allocations == null || flag .allocations .isEmpty ()) {
94+ // TODO: log warning and telemetry
95+ return Resolution .defaultResolution (defaultValue );
96+ }
97+
98+ final Date now = new Date ();
99+ final String targetingKey = context .getTargetingKey ();
100+
101+ for (final Allocation allocation : flag .allocations ) {
102+ if (!isAllocationActive (allocation , now )) {
103+ continue ;
104+ }
105+
106+ if (allocation .rules != null && !allocation .rules .isEmpty ()) {
107+ if (!evaluateRules (allocation .rules , context )) {
108+ continue ;
109+ }
110+ }
111+
112+ if (allocation .splits != null ) {
113+ for (final Split split : allocation .splits ) {
114+ if (split .shards != null ) {
115+ if (split .shards .isEmpty ()) {
116+ return resolveVariant (flag , split .variationKey , allocation );
117+ }
118+
119+ // To match a split, subject must match ALL underlying shards
120+ boolean allShardsMatch = true ;
121+ for (final Shard shard : split .shards ) {
122+ if (!matchesShard (shard , targetingKey )) {
123+ allShardsMatch = false ;
124+ break ;
125+ }
126+ }
127+ if (allShardsMatch ) {
128+ return resolveVariant (flag , split .variationKey , allocation );
129+ }
130+ }
131+ }
132+ }
133+ }
134+
135+ return Resolution .defaultResolution (defaultValue );
136+ }
137+
138+ private boolean isAllocationActive (final Allocation allocation , final Date now ) {
139+ if (allocation .startAt != null ) {
140+ final Date startDate = parseDate (allocation .startAt );
141+ if (startDate != null && now .before (startDate )) {
142+ return false ;
143+ }
144+ }
145+
146+ if (allocation .endAt != null ) {
147+ final Date endDate = parseDate (allocation .endAt );
148+ if (endDate != null && now .after (endDate )) {
149+ return false ;
150+ }
151+ }
152+
153+ return true ;
154+ }
155+
156+ private boolean evaluateRules (final List <Rule > rules , final Context context ) {
157+ for (final Rule rule : rules ) {
158+ if (rule .conditions == null || rule .conditions .isEmpty ()) {
159+ continue ;
160+ }
161+
162+ boolean allConditionsMatch = true ;
163+ for (final ConditionConfiguration condition : rule .conditions ) {
164+ if (!evaluateCondition (condition , context )) {
165+ allConditionsMatch = false ;
166+ break ;
167+ }
168+ }
169+
170+ if (allConditionsMatch ) {
171+ return true ;
172+ }
173+ }
174+ return false ;
175+ }
176+
177+ private boolean evaluateCondition (final ConditionConfiguration condition , final Context context ) {
178+ if (condition .operator == ConditionOperator .IS_NULL ) {
179+ final Object value = resolveAttribute (condition .attribute , context );
180+ boolean isNull = value == null ;
181+ // condition.value determines if we're checking for null (true) or not null (false)
182+ boolean expectedNull = condition .value instanceof Boolean ? (Boolean ) condition .value : true ;
183+ return isNull == expectedNull ;
184+ }
185+
186+ final Object attributeValue = resolveAttribute (condition .attribute , context );
187+ if (attributeValue == null ) {
188+ return false ;
189+ }
190+
191+ switch (condition .operator ) {
192+ case MATCHES :
193+ return matchesRegex (attributeValue , condition .value );
194+ case NOT_MATCHES :
195+ return !matchesRegex (attributeValue , condition .value );
196+ case ONE_OF :
197+ return isOneOf (attributeValue , condition .value );
198+ case NOT_ONE_OF :
199+ return !isOneOf (attributeValue , condition .value );
200+ case GTE :
201+ return compareNumber (attributeValue , condition .value , (a , b ) -> a >= b );
202+ case GT :
203+ return compareNumber (attributeValue , condition .value , (a , b ) -> a > b );
204+ case LTE :
205+ return compareNumber (attributeValue , condition .value , (a , b ) -> a <= b );
206+ case LT :
207+ return compareNumber (attributeValue , condition .value , (a , b ) -> a < b );
208+ default :
209+ return false ;
210+ }
211+ }
212+
213+ private boolean matchesRegex (final Object attributeValue , final Object conditionValue ) {
214+ if (!(conditionValue instanceof String )) {
215+ return false ;
216+ }
217+ try {
218+ final Pattern pattern = Pattern .compile ((String ) conditionValue );
219+ return pattern .matcher (String .valueOf (attributeValue )).find ();
220+ } catch (Exception e ) {
221+ return false ;
222+ }
223+ }
224+
225+ private boolean isOneOf (final Object attributeValue , final Object conditionValue ) {
226+ if (!(conditionValue instanceof Collection )) {
227+ return false ;
228+ }
229+ final Collection <?> values = (Collection <?>) conditionValue ;
230+ for (final Object value : values ) {
231+ if (valuesEqual (attributeValue , value )) {
232+ return true ;
233+ }
234+ }
235+ return false ;
236+ }
237+
238+ private boolean valuesEqual (final Object a , final Object b ) {
239+ if (a == null && b == null ) {
240+ return true ;
241+ }
242+ if (a == null || b == null ) {
243+ return false ;
244+ }
245+
246+ if (a instanceof Number || b instanceof Number ) {
247+ return numbersEqual (a , b );
248+ }
249+
250+ return String .valueOf (a ).equals (String .valueOf (b ));
251+ }
252+
253+ private boolean numbersEqual (final Object a , final Object b ) {
254+ try {
255+ return toDouble (a ) == toDouble (b );
256+ } catch (Exception e ) {
257+ return String .valueOf (a ).equals (String .valueOf (b ));
258+ }
259+ }
260+
261+ private boolean compareNumber (
262+ final Object attributeValue , final Object conditionValue , NumberComparator comparator ) {
263+ try {
264+ final double a = toDouble (attributeValue );
265+ final double b = toDouble (conditionValue );
266+ return comparator .compare (a , b );
267+ } catch (Exception e ) {
268+ return false ;
269+ }
270+ }
271+
272+ private double toDouble (final Object value ) {
273+ if (value instanceof Number ) {
274+ return ((Number ) value ).doubleValue ();
275+ }
276+ return Double .parseDouble (String .valueOf (value ));
277+ }
278+
279+ private boolean matchesShard (final Shard shard , final String targetingKey ) {
280+ final int assignedShard = getShard (shard .salt , targetingKey , shard .totalShards );
281+ for (final ShardRange range : shard .ranges ) {
282+ if (assignedShard >= range .start && assignedShard < range .end ) {
283+ return true ;
284+ }
285+ }
286+ return false ;
287+ }
288+
289+ private int getShard (final String salt , final String targetingKey , final int totalShards ) {
290+ final String hashKey = salt + "-" + targetingKey ;
291+ final String md5Hash = getMD5Hash (hashKey );
292+ final String first8Chars = md5Hash .substring (0 , Math .min (8 , md5Hash .length ()));
293+ final long intFromHash = Long .parseLong (first8Chars , 16 );
294+ return (int ) (intFromHash % totalShards );
295+ }
296+
297+ private String getMD5Hash (final String input ) {
298+ try {
299+ final MessageDigest md = MessageDigest .getInstance ("MD5" );
300+ final byte [] hashBytes = md .digest (input .getBytes (StandardCharsets .UTF_8 ));
301+ final StringBuilder hexString = new StringBuilder ();
302+ for (byte b : hashBytes ) {
303+ final String hex = Integer .toHexString (0xff & b );
304+ if (hex .length () == 1 ) {
305+ hexString .append ('0' );
306+ }
307+ hexString .append (hex );
308+ }
309+ return hexString .toString ();
310+ } catch (NoSuchAlgorithmException e ) {
311+ throw new RuntimeException ("MD5 algorithm not available" , e );
312+ }
313+ }
314+
315+ @ SuppressWarnings ("unchecked" )
316+ private <T > Resolution <T > resolveVariant (
317+ final Flag flag , final String variationKey , final Allocation allocation ) {
318+ final Variant variant = flag .variations .get (variationKey );
319+ if (variant == null ) {
320+ return Resolution .error ((T ) null );
321+ }
322+
323+ final Map <String , Object > metadata = new HashMap <>();
324+ metadata .put ("flagKey" , flag .key );
325+ metadata .put ("variationType" , flag .variationType .name ());
326+ if (allocation .doLog != null && allocation .doLog ) {
327+ metadata .put ("allocationKey" , allocation .key );
328+ }
329+
330+ return Resolution .targetingMatch ((T ) variant .value )
331+ .setVariant (variant .key )
332+ .setFlagMetadata (metadata );
333+ }
334+
335+ private Date parseDate (final String dateString ) {
336+ if (dateString == null ) {
337+ return null ;
338+ }
339+ try {
340+ final SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" );
341+ sdf .setTimeZone (TimeZone .getTimeZone ("UTC" ));
342+ return sdf .parse (dateString );
343+ } catch (ParseException e ) {
344+ try {
345+ final SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd'T'HH:mm:ss'Z'" );
346+ sdf .setTimeZone (TimeZone .getTimeZone ("UTC" ));
347+ return sdf .parse (dateString );
348+ } catch (ParseException e2 ) {
349+ return null ;
350+ }
351+ }
352+ }
353+
354+ @ FunctionalInterface
355+ private interface NumberComparator {
356+ boolean compare (double a , double b );
357+ }
358+
359+ private Object resolveAttribute (final String name , final Context context ) {
360+ // Special handling for "id" attribute: if not explicitly provided, use targeting key
361+ if ("id" .equals (name ) && !context .keySet ().contains (name )) {
362+ return context .getTargetingKey ();
363+ }
364+ return context .getValue (name );
50365 }
51366
52367 @ Override
0 commit comments