18
18
19
19
import com .optimizely .ab .HttpClientUtils ;
20
20
import com .optimizely .ab .OptimizelyHttpClient ;
21
- import com .optimizely .ab .OptimizelyRuntimeException ;
22
21
import com .optimizely .ab .annotations .VisibleForTesting ;
23
22
import com .optimizely .ab .config .parser .ConfigParseException ;
24
- import org .apache .http .HttpEntity ;
25
- import org .apache .http .HttpResponse ;
23
+ import org .apache .http .*;
26
24
import org .apache .http .client .ClientProtocolException ;
27
- import org .apache .http .client .ResponseHandler ;
28
25
import org .apache .http .client .methods .HttpGet ;
29
26
import org .apache .http .util .EntityUtils ;
30
27
import org .slf4j .Logger ;
31
28
import org .slf4j .LoggerFactory ;
32
29
33
- import javax .annotation .CheckForNull ;
34
30
import java .io .IOException ;
35
31
import java .net .URI ;
36
32
import java .util .concurrent .TimeUnit ;
37
33
38
34
/**
39
- * HttpProjectConfigManager is an implementation of a {@link PollingProjectConfigManager}
35
+ * HttpProjectConfigManager is an implementation of a ProjectConfigManager
40
36
* backed by a datafile. Currently this is loosely tied to Apache HttpClient
41
37
* 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.
42
42
*/
43
43
public class HttpProjectConfigManager extends PollingProjectConfigManager {
44
44
45
45
private static final Logger logger = LoggerFactory .getLogger (HttpProjectConfigManager .class );
46
46
47
47
private final OptimizelyHttpClient httpClient ;
48
48
private final URI uri ;
49
- private final ResponseHandler < String > responseHandler = new ProjectConfigResponseHandler () ;
49
+ private String datafileLastModified ;
50
50
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 );
53
53
this .httpClient = httpClient ;
54
54
this .uri = URI .create (url );
55
55
}
@@ -58,16 +58,58 @@ public URI getUri() {
58
58
return uri ;
59
59
}
60
60
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
+
61
94
static ProjectConfig parseProjectConfig (String datafile ) throws ConfigParseException {
62
95
return new DatafileProjectConfig .Builder ().withDatafile (datafile ).build ();
63
96
}
64
97
65
98
@ Override
66
99
protected ProjectConfig poll () {
67
100
HttpGet httpGet = new HttpGet (uri );
101
+
102
+ if (datafileLastModified != null ) {
103
+ httpGet .setHeader (HttpHeaders .IF_MODIFIED_SINCE , datafileLastModified );
104
+ }
105
+
68
106
logger .info ("Fetching datafile from: {}" , httpGet .getURI ());
69
107
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
+ }
71
113
return parseProjectConfig (datafile );
72
114
} catch (ConfigParseException | IOException e ) {
73
115
logger .error ("Error fetching datafile" , e );
@@ -86,13 +128,9 @@ public static class Builder {
86
128
private String url ;
87
129
private String format = "https://cdn.optimizely.com/datafiles/%s.json" ;
88
130
private OptimizelyHttpClient httpClient ;
89
-
90
131
private long period = 5 ;
91
132
private TimeUnit timeUnit = TimeUnit .MINUTES ;
92
133
93
- private long blockingTimeoutPeriod = 10 ;
94
- private TimeUnit blockingTimeoutUnit = TimeUnit .SECONDS ;
95
-
96
134
public Builder withDatafile (String datafile ) {
97
135
this .datafile = datafile ;
98
136
return this ;
@@ -118,23 +156,6 @@ public Builder withOptimizelyHttpClient(OptimizelyHttpClient httpClient) {
118
156
return this ;
119
157
}
120
158
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
-
138
159
public Builder withPollingInterval (long period , TimeUnit timeUnit ) {
139
160
if (timeUnit == null ) {
140
161
throw new NullPointerException ("Must provide valid timeUnit" );
@@ -165,15 +186,17 @@ public HttpProjectConfigManager build(boolean defer) {
165
186
httpClient = HttpClientUtils .getDefaultHttpClient ();
166
187
}
167
188
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
+ }
172
192
173
- url = String .format (format , sdkKey );
193
+ if (sdkKey == null ) {
194
+ throw new NullPointerException ("sdkKey cannot be null" );
174
195
}
175
196
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 );
177
200
178
201
if (datafile != null ) {
179
202
try {
@@ -194,24 +217,4 @@ public HttpProjectConfigManager build(boolean defer) {
194
217
return httpProjectManager ;
195
218
}
196
219
}
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