1818
1919import com .optimizely .ab .HttpClientUtils ;
2020import com .optimizely .ab .OptimizelyHttpClient ;
21- import com .optimizely .ab .OptimizelyRuntimeException ;
2221import com .optimizely .ab .annotations .VisibleForTesting ;
2322import com .optimizely .ab .config .parser .ConfigParseException ;
24- import org .apache .http .HttpEntity ;
25- import org .apache .http .HttpResponse ;
23+ import org .apache .http .*;
2624import org .apache .http .client .ClientProtocolException ;
27- import org .apache .http .client .ResponseHandler ;
2825import org .apache .http .client .methods .HttpGet ;
2926import org .apache .http .util .EntityUtils ;
3027import org .slf4j .Logger ;
3128import org .slf4j .LoggerFactory ;
3229
33- import javax .annotation .CheckForNull ;
3430import java .io .IOException ;
3531import java .net .URI ;
3632import java .util .concurrent .TimeUnit ;
3733
3834/**
39- * HttpProjectConfigManager is an implementation of a {@link PollingProjectConfigManager}
35+ * HttpProjectConfigManager is an implementation of a ProjectConfigManager
4036 * backed by a datafile. Currently this is loosely tied to Apache HttpClient
4137 * implementation which is the client of choice in this package.
38+ *
39+ * Note that this implementation is blocking and stateless. This is best used in
40+ * conjunction with the {@link PollingProjectConfigManager} to provide caching
41+ * and asynchronous fetching.
4242 */
4343public class HttpProjectConfigManager extends PollingProjectConfigManager {
4444
4545 private static final Logger logger = LoggerFactory .getLogger (HttpProjectConfigManager .class );
4646
4747 private final OptimizelyHttpClient httpClient ;
4848 private final URI uri ;
49- private final ResponseHandler < String > responseHandler = new ProjectConfigResponseHandler () ;
49+ private String datafileLastModified ;
5050
51- private HttpProjectConfigManager (long period , TimeUnit timeUnit , OptimizelyHttpClient httpClient , String url , long blockingTimeoutPeriod , TimeUnit blockingTimeoutUnit ) {
52- super (period , timeUnit , blockingTimeoutPeriod , blockingTimeoutUnit );
51+ private HttpProjectConfigManager (long period , TimeUnit timeUnit , OptimizelyHttpClient httpClient , String url ) {
52+ super (period , timeUnit );
5353 this .httpClient = httpClient ;
5454 this .uri = URI .create (url );
5555 }
@@ -58,16 +58,58 @@ public URI getUri() {
5858 return uri ;
5959 }
6060
61+ @ VisibleForTesting
62+ public String getLastModified () {
63+ return datafileLastModified ;
64+ }
65+
66+ public String getDatafileFromResponse (HttpResponse response ) throws NullPointerException , IOException {
67+ StatusLine statusLine = response .getStatusLine ();
68+
69+ if (statusLine == null ) {
70+ throw new ClientProtocolException ("unexpected response from event endpoint, status is null" );
71+ }
72+
73+ int status = statusLine .getStatusCode ();
74+
75+ // Datafile has not updated
76+ if (status == HttpStatus .SC_NOT_MODIFIED ) {
77+ logger .debug ("Not updating ProjectConfig as datafile has not updated since " + datafileLastModified );
78+ return null ;
79+ }
80+
81+ if (status >= 200 && status < 300 ) {
82+ // read the response, so we can close the connection
83+ HttpEntity entity = response .getEntity ();
84+ Header lastModifiedHeader = response .getFirstHeader (HttpHeaders .LAST_MODIFIED );
85+ if (lastModifiedHeader != null ) {
86+ datafileLastModified = lastModifiedHeader .getValue ();
87+ }
88+ return EntityUtils .toString (entity , "UTF-8" );
89+ } else {
90+ throw new ClientProtocolException ("unexpected response from event endpoint, status: " + status );
91+ }
92+ }
93+
6194 static ProjectConfig parseProjectConfig (String datafile ) throws ConfigParseException {
6295 return new DatafileProjectConfig .Builder ().withDatafile (datafile ).build ();
6396 }
6497
6598 @ Override
6699 protected ProjectConfig poll () {
67100 HttpGet httpGet = new HttpGet (uri );
101+
102+ if (datafileLastModified != null ) {
103+ httpGet .setHeader (HttpHeaders .IF_MODIFIED_SINCE , datafileLastModified );
104+ }
105+
68106 logger .info ("Fetching datafile from: {}" , httpGet .getURI ());
69107 try {
70- String datafile = httpClient .execute (httpGet , responseHandler );
108+ HttpResponse response = httpClient .execute (httpGet );
109+ String datafile = getDatafileFromResponse (response );
110+ if (datafile == null ) {
111+ return null ;
112+ }
71113 return parseProjectConfig (datafile );
72114 } catch (ConfigParseException | IOException e ) {
73115 logger .error ("Error fetching datafile" , e );
@@ -86,13 +128,9 @@ public static class Builder {
86128 private String url ;
87129 private String format = "https://cdn.optimizely.com/datafiles/%s.json" ;
88130 private OptimizelyHttpClient httpClient ;
89-
90131 private long period = 5 ;
91132 private TimeUnit timeUnit = TimeUnit .MINUTES ;
92133
93- private long blockingTimeoutPeriod = 10 ;
94- private TimeUnit blockingTimeoutUnit = TimeUnit .SECONDS ;
95-
96134 public Builder withDatafile (String datafile ) {
97135 this .datafile = datafile ;
98136 return this ;
@@ -118,23 +156,6 @@ public Builder withOptimizelyHttpClient(OptimizelyHttpClient httpClient) {
118156 return this ;
119157 }
120158
121- /**
122- * Configure time to block before Completing the future. This timeout is used on the first call
123- * to {@link PollingProjectConfigManager#getConfig()}. If the timeout is exceeded then the
124- * PollingProjectConfigManager will begin returning null immediately until the call to Poll
125- * succeeds.
126- */
127- public Builder withBlockingTimeout (long period , TimeUnit timeUnit ) {
128- if (timeUnit == null ) {
129- throw new NullPointerException ("Must provide valid timeUnit" );
130- }
131-
132- this .blockingTimeoutPeriod = period ;
133- this .blockingTimeoutUnit = timeUnit ;
134-
135- return this ;
136- }
137-
138159 public Builder withPollingInterval (long period , TimeUnit timeUnit ) {
139160 if (timeUnit == null ) {
140161 throw new NullPointerException ("Must provide valid timeUnit" );
@@ -165,15 +186,17 @@ public HttpProjectConfigManager build(boolean defer) {
165186 httpClient = HttpClientUtils .getDefaultHttpClient ();
166187 }
167188
168- if (url == null ) {
169- if (sdkKey == null ) {
170- throw new NullPointerException ("sdkKey cannot be null" );
171- }
189+ if (url != null ) {
190+ return new HttpProjectConfigManager (period , timeUnit , httpClient , url );
191+ }
172192
173- url = String .format (format , sdkKey );
193+ if (sdkKey == null ) {
194+ throw new NullPointerException ("sdkKey cannot be null" );
174195 }
175196
176- HttpProjectConfigManager httpProjectManager = new HttpProjectConfigManager (period , timeUnit , httpClient , url , blockingTimeoutPeriod , blockingTimeoutUnit );
197+ url = String .format (format , sdkKey );
198+
199+ HttpProjectConfigManager httpProjectManager = new HttpProjectConfigManager (period , timeUnit , httpClient , url );
177200
178201 if (datafile != null ) {
179202 try {
@@ -194,24 +217,4 @@ public HttpProjectConfigManager build(boolean defer) {
194217 return httpProjectManager ;
195218 }
196219 }
197-
198- /**
199- * Handler for the event request that returns nothing (i.e., Void)
200- */
201- static final class ProjectConfigResponseHandler implements ResponseHandler <String > {
202-
203- @ Override
204- @ CheckForNull
205- public String handleResponse (HttpResponse response ) throws IOException {
206- int status = response .getStatusLine ().getStatusCode ();
207- if (status >= 200 && status < 300 ) {
208- // read the response, so we can close the connection
209- HttpEntity entity = response .getEntity ();
210- return EntityUtils .toString (entity , "UTF-8" );
211- } else {
212- // TODO handle unmodifed response.
213- throw new ClientProtocolException ("unexpected response from event endpoint, status: " + status );
214- }
215- }
216- }
217- }
220+ }
0 commit comments