3636import com .optimizely .ab .cmab .client .CmabFetchException ;
3737import com .optimizely .ab .cmab .client .CmabInvalidResponseException ;
3838import com .optimizely .ab .cmab .client .RetryConfig ;
39+ import com .optimizely .ab .cmab .client .CmabClientHelper ;
3940
4041public 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