Skip to content

Commit 5fbbede

Browse files
author
Athira M
committed
Implement custom signal targeting for server side RC
1 parent 5eaa9ef commit 5fbbede

22 files changed

+2478
-298
lines changed

src/main/java/com/google/firebase/remoteconfig/AndCondition.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
/*
32
* Copyright 2025 Google LLC
43
*
@@ -61,4 +60,3 @@ AndConditionResponse toAndConditionResponse() {
6160
.collect(Collectors.toList()));
6261
}
6362
}
64-
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.remoteconfig;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
22+
import com.google.common.collect.ImmutableList;
23+
import com.google.firebase.internal.NonNull;
24+
import com.google.firebase.internal.Nullable;
25+
26+
import java.util.Arrays;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.function.BiPredicate;
30+
import java.util.function.IntPredicate;
31+
import java.util.regex.Pattern;
32+
import java.util.stream.Collectors;
33+
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
36+
37+
final class ConditionEvaluator {
38+
private static final int MAX_CONDITION_RECURSION_DEPTH = 10;
39+
private static final Logger logger = LoggerFactory.getLogger(ConditionEvaluator.class);
40+
41+
/**
42+
* Evaluates server conditions and assigns a boolean value to each condition.
43+
*
44+
* @param conditions List of conditions which are to be evaluated.
45+
* @param context A map with additional metadata used during evaluation.
46+
* @return A map of condition to evaluated value.
47+
*/
48+
@NonNull
49+
Map<String, Boolean> evaluateConditions(
50+
@NonNull List<ServerCondition> conditions,
51+
@Nullable KeysAndValues context) {
52+
checkNotNull(conditions, "List of conditions must not be null.");
53+
checkArgument(!conditions.isEmpty(), "List of conditions must not be empty.");
54+
KeysAndValues evaluationContext = context != null
55+
? context
56+
: new KeysAndValues.Builder().build();
57+
58+
Map<String, Boolean> evaluatedConditions = conditions.stream()
59+
.collect(Collectors.toMap(
60+
ServerCondition::getName,
61+
condition ->
62+
evaluateCondition(condition.getCondition(), evaluationContext, /* nestingLevel= */0)
63+
));
64+
65+
return evaluatedConditions;
66+
}
67+
68+
private boolean evaluateCondition(OneOfCondition condition, KeysAndValues context,
69+
int nestingLevel) {
70+
if (nestingLevel > MAX_CONDITION_RECURSION_DEPTH) {
71+
logger.warn("Maximum condition recursion depth exceeded.");
72+
return false;
73+
}
74+
75+
if (condition.getOrCondition() != null) {
76+
return evaluateOrCondition(condition.getOrCondition(), context, nestingLevel + 1);
77+
} else if (condition.getAndCondition() != null) {
78+
return evaluateAndCondition(condition.getAndCondition(), context, nestingLevel + 1);
79+
} else if (condition.isTrue() != null) {
80+
return true;
81+
} else if (condition.isFalse() != null) {
82+
return false;
83+
} else if (condition.getCustomSignal() != null) {
84+
return evaluateCustomSignalCondition(condition.getCustomSignal(), context);
85+
}
86+
logger.atWarn().log("Received invalid condition for evaluation.");
87+
return false;
88+
}
89+
90+
91+
private boolean evaluateOrCondition(OrCondition condition, KeysAndValues context,
92+
int nestingLevel) {
93+
return condition.getConditions().stream()
94+
.anyMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1));
95+
}
96+
97+
private boolean evaluateAndCondition(AndCondition condition, KeysAndValues context,
98+
int nestingLevel) {
99+
return condition.getConditions().stream()
100+
.allMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1));
101+
}
102+
103+
private boolean evaluateCustomSignalCondition(CustomSignalCondition condition,
104+
KeysAndValues context) {
105+
CustomSignalOperator customSignalOperator = condition.getCustomSignalOperator();
106+
String customSignalKey = condition.getCustomSignalKey();
107+
ImmutableList<String> targetCustomSignalValues = ImmutableList.copyOf(
108+
condition.getTargetCustomSignalValues());
109+
110+
if (targetCustomSignalValues.isEmpty()) {
111+
logger.warn(String.format(
112+
"Values must be assigned to all custom signal fields. Operator:%s, Key:%s, Values:%s",
113+
customSignalOperator, customSignalKey, targetCustomSignalValues));
114+
return false;
115+
}
116+
117+
String customSignalValue = context.get(customSignalKey);
118+
if (customSignalValue == null) {
119+
return false;
120+
}
121+
122+
switch (customSignalOperator) {
123+
// String operations.
124+
case STRING_CONTAINS:
125+
return compareStrings(targetCustomSignalValues, customSignalValue,
126+
(customSignal, targetSignal) -> customSignal.contains(targetSignal));
127+
case STRING_DOES_NOT_CONTAIN:
128+
return !compareStrings(targetCustomSignalValues, customSignalValue,
129+
(customSignal, targetSignal) -> customSignal.contains(targetSignal));
130+
case STRING_EXACTLY_MATCHES:
131+
return compareStrings(targetCustomSignalValues, customSignalValue,
132+
(customSignal, targetSignal) -> customSignal.equals(targetSignal));
133+
case STRING_CONTAINS_REGEX:
134+
return compareStrings(targetCustomSignalValues, customSignalValue,
135+
(customSignal, targetSignal) -> Pattern.compile(targetSignal)
136+
.matcher(customSignal).matches());
137+
138+
// Numeric operations.
139+
case NUMERIC_LESS_THAN:
140+
return compareNumbers(targetCustomSignalValues, customSignalValue,
141+
(result) -> result < 0);
142+
case NUMERIC_LESS_EQUAL:
143+
return compareNumbers(targetCustomSignalValues, customSignalValue,
144+
(result) -> result <= 0);
145+
case NUMERIC_EQUAL:
146+
return compareNumbers(targetCustomSignalValues, customSignalValue,
147+
(result) -> result == 0);
148+
case NUMERIC_NOT_EQUAL:
149+
return compareNumbers(targetCustomSignalValues, customSignalValue,
150+
(result) -> result != 0);
151+
case NUMERIC_GREATER_THAN:
152+
return compareNumbers(targetCustomSignalValues, customSignalValue,
153+
(result) -> result > 0);
154+
case NUMERIC_GREATER_EQUAL:
155+
return compareNumbers(targetCustomSignalValues, customSignalValue,
156+
(result) -> result >= 0);
157+
158+
// Semantic operations.
159+
case SEMANTIC_VERSION_EQUAL:
160+
return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
161+
(result) -> result == 0);
162+
case SEMANTIC_VERSION_GREATER_EQUAL:
163+
return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
164+
(result) -> result >= 0);
165+
case SEMANTIC_VERSION_GREATER_THAN:
166+
return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
167+
(result) -> result > 0);
168+
case SEMANTIC_VERSION_LESS_EQUAL:
169+
return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
170+
(result) -> result <= 0);
171+
case SEMANTIC_VERSION_LESS_THAN:
172+
return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
173+
(result) -> result < 0);
174+
case SEMANTIC_VERSION_NOT_EQUAL:
175+
return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
176+
(result) -> result != 0);
177+
default:
178+
return false;
179+
}
180+
}
181+
182+
private boolean compareStrings(ImmutableList<String> targetValues, String customSignal,
183+
BiPredicate<String, String> compareFunction) {
184+
return targetValues.stream().anyMatch(targetValue ->
185+
compareFunction.test(customSignal, targetValue));
186+
}
187+
188+
private boolean compareNumbers(ImmutableList<String> targetValues, String customSignal,
189+
IntPredicate compareFunction) {
190+
if (targetValues.size() != 1) {
191+
logger.warn(String.format(
192+
"Target values must contain 1 element for numeric operations. Target Value: %s",
193+
targetValues));
194+
return false;
195+
}
196+
197+
try {
198+
double customSignalDouble = Double.parseDouble(customSignal);
199+
double targetValue = Double.parseDouble(targetValues.get(0));
200+
int comparisonResult = Double.compare(customSignalDouble, targetValue);
201+
return compareFunction.test(comparisonResult);
202+
} catch (NumberFormatException e) {
203+
logger.warn("Error parsing numeric values: customSignal=%s, targetValue=%s",
204+
customSignal, targetValues.get(0), e);
205+
return false;
206+
}
207+
}
208+
209+
private boolean compareSemanticVersions(ImmutableList<String> targetValues,
210+
String customSignal,
211+
IntPredicate compareFunction) {
212+
if (targetValues.size() != 1) {
213+
logger.warn(String.format("Target values must contain 1 element for semantic operation."));
214+
return false;
215+
}
216+
217+
String targetValueString = targetValues.get(0);
218+
if (!validateSemanticVersion(targetValueString)
219+
|| !validateSemanticVersion(customSignal)) {
220+
return false;
221+
}
222+
223+
List<Integer> targetVersion = parseSemanticVersion(targetValueString);
224+
List<Integer> customSignalVersion = parseSemanticVersion(customSignal);
225+
226+
int maxLength = 5;
227+
if (targetVersion.size() > maxLength || customSignalVersion.size() > maxLength) {
228+
logger.warn("Semantic version max length(%s) exceeded. Target: %s, Custom Signal: %s",
229+
maxLength, targetValueString, customSignal);
230+
return false;
231+
}
232+
233+
int comparison = compareSemanticVersions(customSignalVersion, targetVersion);
234+
return compareFunction.test(comparison);
235+
}
236+
237+
private int compareSemanticVersions(List<Integer> version1, List<Integer> version2) {
238+
int maxLength = Math.max(version1.size(), version2.size());
239+
int version1Size = version1.size();
240+
int version2Size = version2.size();
241+
242+
for (int i = 0; i < maxLength; i++) {
243+
// Default to 0 if segment is missing
244+
int v1 = i < version1Size ? version1.get(i) : 0;
245+
int v2 = i < version2Size ? version2.get(i) : 0;
246+
247+
int comparison = Integer.compare(v1, v2);
248+
if (comparison != 0) {
249+
return comparison;
250+
}
251+
}
252+
// Versions are equal
253+
return 0;
254+
}
255+
256+
private List<Integer> parseSemanticVersion(String versionString) {
257+
return Arrays.stream(versionString.split("\\."))
258+
.map(Integer::parseInt)
259+
.collect(Collectors.toList());
260+
}
261+
262+
private boolean validateSemanticVersion(String version) {
263+
Pattern pattern = Pattern.compile("^[0-9]+(?:\\.[0-9]+){0,4}$");
264+
return pattern.matcher(version).matches();
265+
}
266+
}

0 commit comments

Comments
 (0)