Skip to content

Commit e8199c2

Browse files
athiramanuAthira M
andauthored
Implement custom signal targeting for server side RC (#1108)
Co-authored-by: Athira M <[email protected]>
1 parent 5eaa9ef commit e8199c2

22 files changed

+2609
-299
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: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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.regex.PatternSyntaxException;
33+
import java.util.stream.Collectors;
34+
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
37+
38+
final class ConditionEvaluator {
39+
private static final int MAX_CONDITION_RECURSION_DEPTH = 10;
40+
private static final Logger logger = LoggerFactory.getLogger(ConditionEvaluator.class);
41+
42+
/**
43+
* Evaluates server conditions and assigns a boolean value to each condition.
44+
*
45+
* @param conditions List of conditions which are to be evaluated.
46+
* @param context A map with additional metadata used during evaluation.
47+
* @return A map of condition to evaluated value.
48+
*/
49+
@NonNull
50+
Map<String, Boolean> evaluateConditions(
51+
@NonNull List<ServerCondition> conditions,
52+
@Nullable KeysAndValues context) {
53+
checkNotNull(conditions, "List of conditions must not be null.");
54+
checkArgument(!conditions.isEmpty(), "List of conditions must not be empty.");
55+
KeysAndValues evaluationContext = context != null
56+
? context
57+
: new KeysAndValues.Builder().build();
58+
59+
Map<String, Boolean> evaluatedConditions = conditions.stream()
60+
.collect(Collectors.toMap(
61+
ServerCondition::getName,
62+
condition ->
63+
evaluateCondition(condition.getCondition(), evaluationContext, /* nestingLevel= */0)
64+
));
65+
66+
return evaluatedConditions;
67+
}
68+
69+
private boolean evaluateCondition(OneOfCondition condition, KeysAndValues context,
70+
int nestingLevel) {
71+
if (nestingLevel > MAX_CONDITION_RECURSION_DEPTH) {
72+
logger.warn("Maximum condition recursion depth exceeded.");
73+
return false;
74+
}
75+
76+
if (condition.getOrCondition() != null) {
77+
return evaluateOrCondition(condition.getOrCondition(), context, nestingLevel + 1);
78+
} else if (condition.getAndCondition() != null) {
79+
return evaluateAndCondition(condition.getAndCondition(), context, nestingLevel + 1);
80+
} else if (condition.isTrue() != null) {
81+
return true;
82+
} else if (condition.isFalse() != null) {
83+
return false;
84+
} else if (condition.getCustomSignal() != null) {
85+
return evaluateCustomSignalCondition(condition.getCustomSignal(), context);
86+
}
87+
logger.atWarn().log("Received invalid condition for evaluation.");
88+
return false;
89+
}
90+
91+
92+
private boolean evaluateOrCondition(OrCondition condition, KeysAndValues context,
93+
int nestingLevel) {
94+
return condition.getConditions().stream()
95+
.anyMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1));
96+
}
97+
98+
private boolean evaluateAndCondition(AndCondition condition, KeysAndValues context,
99+
int nestingLevel) {
100+
return condition.getConditions().stream()
101+
.allMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1));
102+
}
103+
104+
private boolean evaluateCustomSignalCondition(CustomSignalCondition condition,
105+
KeysAndValues context) {
106+
CustomSignalOperator customSignalOperator = condition.getCustomSignalOperator();
107+
String customSignalKey = condition.getCustomSignalKey();
108+
ImmutableList<String> targetCustomSignalValues = ImmutableList.copyOf(
109+
condition.getTargetCustomSignalValues());
110+
111+
if (targetCustomSignalValues.isEmpty()) {
112+
logger.warn(String.format(
113+
"Values must be assigned to all custom signal fields. Operator:%s, Key:%s, Values:%s",
114+
customSignalOperator, customSignalKey, targetCustomSignalValues));
115+
return false;
116+
}
117+
118+
String customSignalValue = context.get(customSignalKey);
119+
if (customSignalValue == null) {
120+
return false;
121+
}
122+
123+
switch (customSignalOperator) {
124+
// String operations.
125+
case STRING_CONTAINS:
126+
return compareStrings(targetCustomSignalValues, customSignalValue,
127+
(customSignal, targetSignal) -> customSignal.contains(targetSignal));
128+
case STRING_DOES_NOT_CONTAIN:
129+
return !compareStrings(targetCustomSignalValues, customSignalValue,
130+
(customSignal, targetSignal) -> customSignal.contains(targetSignal));
131+
case STRING_EXACTLY_MATCHES:
132+
return compareStrings(targetCustomSignalValues, customSignalValue,
133+
(customSignal, targetSignal) -> customSignal.equals(targetSignal));
134+
case STRING_CONTAINS_REGEX:
135+
return compareStrings(targetCustomSignalValues, customSignalValue,
136+
(customSignal, targetSignal) -> compareStringRegex(customSignal, targetSignal));
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 compareStringRegex(String customSignal, String targetSignal) {
189+
try {
190+
return Pattern.compile(targetSignal).matcher(customSignal).matches();
191+
} catch (PatternSyntaxException e) {
192+
return false;
193+
}
194+
}
195+
196+
private boolean compareNumbers(ImmutableList<String> targetValues, String customSignal,
197+
IntPredicate compareFunction) {
198+
if (targetValues.size() != 1) {
199+
logger.warn(String.format(
200+
"Target values must contain 1 element for numeric operations. Target Value: %s",
201+
targetValues));
202+
return false;
203+
}
204+
205+
try {
206+
double customSignalDouble = Double.parseDouble(customSignal);
207+
double targetValue = Double.parseDouble(targetValues.get(0));
208+
int comparisonResult = Double.compare(customSignalDouble, targetValue);
209+
return compareFunction.test(comparisonResult);
210+
} catch (NumberFormatException e) {
211+
logger.warn("Error parsing numeric values: customSignal=%s, targetValue=%s",
212+
customSignal, targetValues.get(0), e);
213+
return false;
214+
}
215+
}
216+
217+
private boolean compareSemanticVersions(ImmutableList<String> targetValues,
218+
String customSignal,
219+
IntPredicate compareFunction) {
220+
if (targetValues.size() != 1) {
221+
logger.warn(String.format("Target values must contain 1 element for semantic operation."));
222+
return false;
223+
}
224+
225+
String targetValueString = targetValues.get(0);
226+
if (!validateSemanticVersion(targetValueString)
227+
|| !validateSemanticVersion(customSignal)) {
228+
return false;
229+
}
230+
231+
List<Integer> targetVersion = parseSemanticVersion(targetValueString);
232+
List<Integer> customSignalVersion = parseSemanticVersion(customSignal);
233+
234+
int maxLength = 5;
235+
if (targetVersion.size() > maxLength || customSignalVersion.size() > maxLength) {
236+
logger.warn("Semantic version max length(%s) exceeded. Target: %s, Custom Signal: %s",
237+
maxLength, targetValueString, customSignal);
238+
return false;
239+
}
240+
241+
int comparison = compareSemanticVersions(customSignalVersion, targetVersion);
242+
return compareFunction.test(comparison);
243+
}
244+
245+
private int compareSemanticVersions(List<Integer> version1, List<Integer> version2) {
246+
int maxLength = Math.max(version1.size(), version2.size());
247+
int version1Size = version1.size();
248+
int version2Size = version2.size();
249+
250+
for (int i = 0; i < maxLength; i++) {
251+
// Default to 0 if segment is missing
252+
int v1 = i < version1Size ? version1.get(i) : 0;
253+
int v2 = i < version2Size ? version2.get(i) : 0;
254+
255+
int comparison = Integer.compare(v1, v2);
256+
if (comparison != 0) {
257+
return comparison;
258+
}
259+
}
260+
// Versions are equal
261+
return 0;
262+
}
263+
264+
private List<Integer> parseSemanticVersion(String versionString) {
265+
return Arrays.stream(versionString.split("\\."))
266+
.map(Integer::parseInt)
267+
.collect(Collectors.toList());
268+
}
269+
270+
private boolean validateSemanticVersion(String version) {
271+
Pattern pattern = Pattern.compile("^[0-9]+(?:\\.[0-9]+){0,4}$");
272+
return pattern.matcher(version).matches();
273+
}
274+
}

0 commit comments

Comments
 (0)