Skip to content

Commit b0d5090

Browse files
update: refactor DefaultCmabClient to utilize CmabClientHelper
1 parent fe75a85 commit b0d5090

File tree

2 files changed

+104
-101
lines changed

2 files changed

+104
-101
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.optimizely.ab.cmab.client;
2+
3+
import java.util.Map;
4+
import java.util.regex.Matcher;
5+
import java.util.regex.Pattern;
6+
7+
public class CmabClientHelper {
8+
public static final String CMAB_FETCH_FAILED = "CMAB decision fetch failed with status: %s";
9+
public static final String INVALID_CMAB_FETCH_RESPONSE = "Invalid CMAB fetch response";
10+
private static final Pattern VARIATION_ID_PATTERN = Pattern.compile("\"variation_id\"\\s*:\\s*\"?([^\"\\s,}]+)\"?");
11+
12+
public static String buildRequestJson(String userId, String ruleId, Map<String, Object> attributes, String cmabUuid) {
13+
StringBuilder json = new StringBuilder();
14+
json.append("{\"instances\":[{");
15+
json.append("\"visitorId\":\"").append(escapeJson(userId)).append("\",");
16+
json.append("\"experimentId\":\"").append(escapeJson(ruleId)).append("\",");
17+
json.append("\"cmabUUID\":\"").append(escapeJson(cmabUuid)).append("\",");
18+
json.append("\"attributes\":[");
19+
20+
boolean first = true;
21+
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
22+
if (!first) {
23+
json.append(",");
24+
}
25+
json.append("{\"id\":\"").append(escapeJson(entry.getKey())).append("\",");
26+
json.append("\"value\":").append(formatJsonValue(entry.getValue())).append(",");
27+
json.append("\"type\":\"custom_attribute\"}");
28+
first = false;
29+
}
30+
31+
json.append("]}]}");
32+
return json.toString();
33+
}
34+
35+
private static String escapeJson(String value) {
36+
if (value == null) {
37+
return "";
38+
}
39+
return value.replace("\\", "\\\\")
40+
.replace("\"", "\\\"")
41+
.replace("\n", "\\n")
42+
.replace("\r", "\\r")
43+
.replace("\t", "\\t");
44+
}
45+
46+
private static String formatJsonValue(Object value) {
47+
if (value == null) {
48+
return "null";
49+
} else if (value instanceof String) {
50+
return "\"" + escapeJson((String) value) + "\"";
51+
} else if (value instanceof Number || value instanceof Boolean) {
52+
return value.toString();
53+
} else {
54+
return "\"" + escapeJson(value.toString()) + "\"";
55+
}
56+
}
57+
58+
public static String parseVariationId(String jsonResponse) {
59+
// Simple regex to extract variation_id from predictions[0].variation_id
60+
Pattern pattern = Pattern.compile("\"predictions\"\\s*:\\s*\\[\\s*\\{[^}]*\"variation_id\"\\s*:\\s*\"?([^\"\\s,}]+)\"?");
61+
Matcher matcher = pattern.matcher(jsonResponse);
62+
if (matcher.find()) {
63+
return matcher.group(1);
64+
}
65+
throw new CmabInvalidResponseException(INVALID_CMAB_FETCH_RESPONSE);
66+
}
67+
68+
private static String parseVariationIdForValidation(String jsonResponse) {
69+
Matcher matcher = VARIATION_ID_PATTERN.matcher(jsonResponse);
70+
if (matcher.find()) {
71+
return matcher.group(1);
72+
}
73+
return null;
74+
}
75+
76+
public static boolean validateResponse(String responseBody) {
77+
try {
78+
return responseBody.contains("predictions") &&
79+
responseBody.contains("variation_id") &&
80+
parseVariationIdForValidation(responseBody) != null;
81+
} catch (Exception e) {
82+
return false;
83+
}
84+
}
85+
86+
public static boolean isSuccessStatusCode(int statusCode) {
87+
return statusCode >= 200 && statusCode < 300;
88+
}
89+
}

core-httpclient-impl/src/main/java/com/optimizely/ab/cmab/DefaultCmabClient.java

Lines changed: 15 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,13 @@
3636
import com.optimizely.ab.cmab.client.CmabFetchException;
3737
import com.optimizely.ab.cmab.client.CmabInvalidResponseException;
3838
import com.optimizely.ab.cmab.client.RetryConfig;
39+
import com.optimizely.ab.cmab.client.CmabClientHelper;
3940

4041
public class DefaultCmabClient implements CmabClient {
4142

4243
private static final Logger logger = LoggerFactory.getLogger(DefaultCmabClient.class);
4344
private static final int DEFAULT_TIMEOUT_MS = 10000;
44-
// Update constants to match JS error messages format
45-
private static final String CMAB_FETCH_FAILED = "CMAB decision fetch failed with status: %s";
46-
private static final String INVALID_CMAB_FETCH_RESPONSE = "Invalid CMAB fetch response";
47-
private static final Pattern VARIATION_ID_PATTERN = Pattern.compile("\"variation_id\"\\s*:\\s*\"?([^\"\\s,}]+)\"?");
45+
4846
private static final String CMAB_PREDICTION_ENDPOINT = "https://prediction.cmab.optimizely.com/predict/%s";
4947

5048
private final OptimizelyHttpClient httpClient;
@@ -81,7 +79,7 @@ private OptimizelyHttpClient createDefaultHttpClient() {
8179
public String fetchDecision(String ruleId, String userId, Map<String, Object> attributes, String cmabUuid) {
8280
// Implementation will use this.httpClient and this.retryConfig
8381
String url = String.format(CMAB_PREDICTION_ENDPOINT, ruleId);
84-
String requestBody = buildRequestJson(userId, ruleId, attributes, cmabUuid);
82+
String requestBody = CmabClientHelper.buildRequestJson(userId, ruleId, attributes, cmabUuid);
8583

8684
// Use retry logic if configured, otherwise single request
8785
if (retryConfig != null && retryConfig.getMaxRetries() > 0) {
@@ -96,7 +94,7 @@ private String doFetch(String url, String requestBody) {
9694
try {
9795
request.setEntity(new StringEntity(requestBody));
9896
} catch (UnsupportedEncodingException e) {
99-
String errorMessage = String.format(CMAB_FETCH_FAILED, e.getMessage());
97+
String errorMessage = String.format(CmabClientHelper.CMAB_FETCH_FAILED, e.getMessage());
10098
logger.error(errorMessage);
10199
throw new CmabFetchException(errorMessage);
102100
}
@@ -105,9 +103,9 @@ private String doFetch(String url, String requestBody) {
105103
try {
106104
response = httpClient.execute(request);
107105

108-
if (!isSuccessStatusCode(response.getStatusLine().getStatusCode())) {
106+
if (!CmabClientHelper.isSuccessStatusCode(response.getStatusLine().getStatusCode())) {
109107
StatusLine statusLine = response.getStatusLine();
110-
String errorMessage = String.format(CMAB_FETCH_FAILED, statusLine.getReasonPhrase());
108+
String errorMessage = String.format(CmabClientHelper.CMAB_FETCH_FAILED, statusLine.getReasonPhrase());
111109
logger.error(errorMessage);
112110
throw new CmabFetchException(errorMessage);
113111
}
@@ -116,18 +114,18 @@ private String doFetch(String url, String requestBody) {
116114
try {
117115
responseBody = EntityUtils.toString(response.getEntity());
118116

119-
if (!validateResponse(responseBody)) {
120-
logger.error(INVALID_CMAB_FETCH_RESPONSE);
121-
throw new CmabInvalidResponseException(INVALID_CMAB_FETCH_RESPONSE);
117+
if (!CmabClientHelper.validateResponse(responseBody)) {
118+
logger.error(CmabClientHelper.INVALID_CMAB_FETCH_RESPONSE);
119+
throw new CmabInvalidResponseException(CmabClientHelper.INVALID_CMAB_FETCH_RESPONSE);
122120
}
123-
return parseVariationId(responseBody);
121+
return CmabClientHelper.parseVariationId(responseBody);
124122
} catch (IOException | ParseException e) {
125-
logger.error(CMAB_FETCH_FAILED);
126-
throw new CmabInvalidResponseException(INVALID_CMAB_FETCH_RESPONSE);
123+
logger.error(CmabClientHelper.CMAB_FETCH_FAILED);
124+
throw new CmabInvalidResponseException(CmabClientHelper.INVALID_CMAB_FETCH_RESPONSE);
127125
}
128126

129127
} catch (IOException e) {
130-
String errorMessage = String.format(CMAB_FETCH_FAILED, e.getMessage());
128+
String errorMessage = String.format(CmabClientHelper.CMAB_FETCH_FAILED, e.getMessage());
131129
logger.error(errorMessage);
132130
throw new CmabFetchException(errorMessage);
133131
} finally {
@@ -158,7 +156,7 @@ private String doFetchWithRetry(String url, String requestBody, int maxRetries)
158156
Thread.sleep((long) backoff);
159157
} catch (InterruptedException ie) {
160158
Thread.currentThread().interrupt();
161-
String errorMessage = String.format(CMAB_FETCH_FAILED, "Request interrupted during retry");
159+
String errorMessage = String.format(CmabClientHelper.CMAB_FETCH_FAILED, "Request interrupted during retry");
162160
logger.error(errorMessage);
163161
throw new CmabFetchException(errorMessage, ie);
164162
}
@@ -172,94 +170,10 @@ private String doFetchWithRetry(String url, String requestBody, int maxRetries)
172170
}
173171

174172
// If we get here, all retries were exhausted
175-
String errorMessage = String.format(CMAB_FETCH_FAILED, "Exhausted all retries for CMAB request");
173+
String errorMessage = String.format(CmabClientHelper.CMAB_FETCH_FAILED, "Exhausted all retries for CMAB request");
176174
logger.error(errorMessage);
177175
throw new CmabFetchException(errorMessage, lastException);
178176
}
179-
180-
private String buildRequestJson(String userId, String ruleId, Map<String, Object> attributes, String cmabUuid) {
181-
StringBuilder json = new StringBuilder();
182-
json.append("{\"instances\":[{");
183-
json.append("\"visitorId\":\"").append(escapeJson(userId)).append("\",");
184-
json.append("\"experimentId\":\"").append(escapeJson(ruleId)).append("\",");
185-
json.append("\"cmabUUID\":\"").append(escapeJson(cmabUuid)).append("\",");
186-
json.append("\"attributes\":[");
187-
188-
boolean first = true;
189-
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
190-
if (!first) {
191-
json.append(",");
192-
}
193-
json.append("{\"id\":\"").append(escapeJson(entry.getKey())).append("\",");
194-
json.append("\"value\":").append(formatJsonValue(entry.getValue())).append(",");
195-
json.append("\"type\":\"custom_attribute\"}");
196-
first = false;
197-
}
198-
199-
json.append("]}]}");
200-
return json.toString();
201-
}
202-
203-
private String escapeJson(String value) {
204-
if (value == null) {
205-
return "";
206-
}
207-
return value.replace("\\", "\\\\")
208-
.replace("\"", "\\\"")
209-
.replace("\n", "\\n")
210-
.replace("\r", "\\r")
211-
.replace("\t", "\\t");
212-
}
213-
214-
private String formatJsonValue(Object value) {
215-
if (value == null) {
216-
return "null";
217-
} else if (value instanceof String) {
218-
return "\"" + escapeJson((String) value) + "\"";
219-
} else if (value instanceof Number || value instanceof Boolean) {
220-
return value.toString();
221-
} else {
222-
return "\"" + escapeJson(value.toString()) + "\"";
223-
}
224-
}
225-
226-
// Helper methods
227-
private boolean isSuccessStatusCode(int statusCode) {
228-
return statusCode >= 200 && statusCode < 300;
229-
}
230-
231-
private boolean validateResponse(String responseBody) {
232-
try {
233-
return responseBody.contains("predictions") &&
234-
responseBody.contains("variation_id") &&
235-
parseVariationIdForValidation(responseBody) != null;
236-
} catch (Exception e) {
237-
return false;
238-
}
239-
}
240-
241-
private boolean shouldRetry(Exception exception) {
242-
return (exception instanceof CmabFetchException) ||
243-
(exception instanceof CmabInvalidResponseException);
244-
}
245-
246-
private String parseVariationIdForValidation(String jsonResponse) {
247-
Matcher matcher = VARIATION_ID_PATTERN.matcher(jsonResponse);
248-
if (matcher.find()) {
249-
return matcher.group(1);
250-
}
251-
return null;
252-
}
253-
254-
private String parseVariationId(String jsonResponse) {
255-
// Simple regex to extract variation_id from predictions[0].variation_id
256-
Pattern pattern = Pattern.compile("\"predictions\"\\s*:\\s*\\[\\s*\\{[^}]*\"variation_id\"\\s*:\\s*\"?([^\"\\s,}]+)\"?");
257-
Matcher matcher = pattern.matcher(jsonResponse);
258-
if (matcher.find()) {
259-
return matcher.group(1);
260-
}
261-
throw new CmabInvalidResponseException(INVALID_CMAB_FETCH_RESPONSE);
262-
}
263177

264178
private static void closeHttpResponse(CloseableHttpResponse response) {
265179
if (response != null) {

0 commit comments

Comments
 (0)