|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.bagheera; |
|
6 |
|
7 import java.io.IOException; |
|
8 import java.net.URISyntaxException; |
|
9 import java.security.GeneralSecurityException; |
|
10 import java.util.Collection; |
|
11 import java.util.concurrent.Executor; |
|
12 import java.util.concurrent.Executors; |
|
13 import java.util.regex.Pattern; |
|
14 |
|
15 import org.mozilla.gecko.sync.Utils; |
|
16 import org.mozilla.gecko.sync.net.BaseResource; |
|
17 import org.mozilla.gecko.sync.net.BaseResourceDelegate; |
|
18 import org.mozilla.gecko.sync.net.Resource; |
|
19 |
|
20 import ch.boye.httpclientandroidlib.HttpEntity; |
|
21 import ch.boye.httpclientandroidlib.HttpResponse; |
|
22 import ch.boye.httpclientandroidlib.client.ClientProtocolException; |
|
23 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; |
|
24 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; |
|
25 import ch.boye.httpclientandroidlib.protocol.HTTP; |
|
26 |
|
27 /** |
|
28 * Provides encapsulated access to a Bagheera document server. |
|
29 * The two permitted operations are: |
|
30 * * Delete a document. |
|
31 * * Upload a document, optionally deleting an expired document. |
|
32 */ |
|
33 public class BagheeraClient { |
|
34 |
|
35 protected final String serverURI; |
|
36 protected final Executor executor; |
|
37 protected static final Pattern URI_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$"); |
|
38 |
|
39 protected static String PROTOCOL_VERSION = "1.0"; |
|
40 protected static String SUBMIT_PATH = "/submit/"; |
|
41 |
|
42 /** |
|
43 * Instantiate a new client pointing at the provided server. |
|
44 * {@link #deleteDocument(String, String, BagheeraRequestDelegate)} and |
|
45 * {@link #uploadJSONDocument(String, String, String, String, BagheeraRequestDelegate)} |
|
46 * both accept delegate arguments; the {@link Executor} provided to this |
|
47 * constructor will be used to invoke callbacks on those delegates. |
|
48 * |
|
49 * @param serverURI |
|
50 * the destination server URI. |
|
51 * @param executor |
|
52 * the executor which will be used to invoke delegate callbacks. |
|
53 */ |
|
54 public BagheeraClient(final String serverURI, final Executor executor) { |
|
55 if (serverURI == null) { |
|
56 throw new IllegalArgumentException("Must provide a server URI."); |
|
57 } |
|
58 if (executor == null) { |
|
59 throw new IllegalArgumentException("Must provide a non-null executor."); |
|
60 } |
|
61 this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/"; |
|
62 this.executor = executor; |
|
63 } |
|
64 |
|
65 /** |
|
66 * Instantiate a new client pointing at the provided server. |
|
67 * Delegate callbacks will be invoked on a new background thread. |
|
68 * |
|
69 * See {@link #BagheeraClient(String, Executor)} for more details. |
|
70 * |
|
71 * @param serverURI |
|
72 * the destination server URI. |
|
73 */ |
|
74 public BagheeraClient(final String serverURI) { |
|
75 this(serverURI, Executors.newSingleThreadExecutor()); |
|
76 } |
|
77 |
|
78 /** |
|
79 * Delete the specified document from the server. |
|
80 * The delegate's callbacks will be invoked by the BagheeraClient's executor. |
|
81 */ |
|
82 public void deleteDocument(final String namespace, |
|
83 final String id, |
|
84 final BagheeraRequestDelegate delegate) throws URISyntaxException { |
|
85 if (namespace == null) { |
|
86 throw new IllegalArgumentException("Must provide namespace."); |
|
87 } |
|
88 if (id == null) { |
|
89 throw new IllegalArgumentException("Must provide id."); |
|
90 } |
|
91 |
|
92 final BaseResource resource = makeResource(namespace, id); |
|
93 resource.delegate = new BagheeraResourceDelegate(resource, namespace, id, delegate); |
|
94 resource.delete(); |
|
95 } |
|
96 |
|
97 /** |
|
98 * Upload a JSON document to a Bagheera server. The delegate's callbacks will |
|
99 * be invoked in tasks run by the client's executor. |
|
100 * |
|
101 * @param namespace |
|
102 * the namespace, such as "test" |
|
103 * @param id |
|
104 * the document ID, which is typically a UUID. |
|
105 * @param payload |
|
106 * a document, typically JSON-encoded. |
|
107 * @param oldIDs |
|
108 * an optional collection of IDs which denote documents to supersede. Can be null or empty. |
|
109 * @param delegate |
|
110 * the delegate whose methods should be invoked on success or |
|
111 * failure. |
|
112 */ |
|
113 public void uploadJSONDocument(final String namespace, |
|
114 final String id, |
|
115 final String payload, |
|
116 Collection<String> oldIDs, |
|
117 final BagheeraRequestDelegate delegate) throws URISyntaxException { |
|
118 if (namespace == null) { |
|
119 throw new IllegalArgumentException("Must provide namespace."); |
|
120 } |
|
121 if (id == null) { |
|
122 throw new IllegalArgumentException("Must provide id."); |
|
123 } |
|
124 if (payload == null) { |
|
125 throw new IllegalArgumentException("Must provide payload."); |
|
126 } |
|
127 |
|
128 final BaseResource resource = makeResource(namespace, id); |
|
129 final HttpEntity deflatedBody = DeflateHelper.deflateBody(payload); |
|
130 |
|
131 resource.delegate = new BagheeraUploadResourceDelegate(resource, namespace, id, oldIDs, delegate); |
|
132 resource.post(deflatedBody); |
|
133 } |
|
134 |
|
135 public static boolean isValidURIComponent(final String in) { |
|
136 return URI_PATTERN.matcher(in).matches(); |
|
137 } |
|
138 |
|
139 protected BaseResource makeResource(final String namespace, final String id) throws URISyntaxException { |
|
140 if (!isValidURIComponent(namespace)) { |
|
141 throw new URISyntaxException(namespace, "Illegal namespace name. Must be alphanumeric + [_-]."); |
|
142 } |
|
143 |
|
144 if (!isValidURIComponent(id)) { |
|
145 throw new URISyntaxException(id, "Illegal id value. Must be alphanumeric + [_-]."); |
|
146 } |
|
147 |
|
148 final String uri = this.serverURI + PROTOCOL_VERSION + SUBMIT_PATH + |
|
149 namespace + "/" + id; |
|
150 return new BaseResource(uri); |
|
151 } |
|
152 |
|
153 public class BagheeraResourceDelegate extends BaseResourceDelegate { |
|
154 private static final int DEFAULT_SOCKET_TIMEOUT_MSEC = 5 * 60 * 1000; // Five minutes. |
|
155 protected final BagheeraRequestDelegate delegate; |
|
156 protected final String namespace; |
|
157 protected final String id; |
|
158 |
|
159 public BagheeraResourceDelegate(final Resource resource, |
|
160 final String namespace, |
|
161 final String id, |
|
162 final BagheeraRequestDelegate delegate) { |
|
163 super(resource); |
|
164 this.namespace = namespace; |
|
165 this.id = id; |
|
166 this.delegate = delegate; |
|
167 } |
|
168 |
|
169 @Override |
|
170 public String getUserAgent() { |
|
171 return delegate.getUserAgent(); |
|
172 } |
|
173 |
|
174 @Override |
|
175 public int socketTimeout() { |
|
176 return DEFAULT_SOCKET_TIMEOUT_MSEC; |
|
177 } |
|
178 |
|
179 @Override |
|
180 public void handleHttpResponse(HttpResponse response) { |
|
181 final int status = response.getStatusLine().getStatusCode(); |
|
182 switch (status) { |
|
183 case 200: |
|
184 case 201: |
|
185 invokeHandleSuccess(status, response); |
|
186 return; |
|
187 default: |
|
188 invokeHandleFailure(status, response); |
|
189 } |
|
190 } |
|
191 |
|
192 protected void invokeHandleError(final Exception e) { |
|
193 executor.execute(new Runnable() { |
|
194 @Override |
|
195 public void run() { |
|
196 delegate.handleError(e); |
|
197 } |
|
198 }); |
|
199 } |
|
200 |
|
201 protected void invokeHandleFailure(final int status, final HttpResponse response) { |
|
202 executor.execute(new Runnable() { |
|
203 @Override |
|
204 public void run() { |
|
205 delegate.handleFailure(status, namespace, response); |
|
206 } |
|
207 }); |
|
208 } |
|
209 |
|
210 protected void invokeHandleSuccess(final int status, final HttpResponse response) { |
|
211 executor.execute(new Runnable() { |
|
212 @Override |
|
213 public void run() { |
|
214 delegate.handleSuccess(status, namespace, id, response); |
|
215 } |
|
216 }); |
|
217 } |
|
218 |
|
219 @Override |
|
220 public void handleHttpProtocolException(final ClientProtocolException e) { |
|
221 invokeHandleError(e); |
|
222 } |
|
223 |
|
224 @Override |
|
225 public void handleHttpIOException(IOException e) { |
|
226 invokeHandleError(e); |
|
227 } |
|
228 |
|
229 @Override |
|
230 public void handleTransportException(GeneralSecurityException e) { |
|
231 invokeHandleError(e); |
|
232 } |
|
233 } |
|
234 |
|
235 public final class BagheeraUploadResourceDelegate extends BagheeraResourceDelegate { |
|
236 private static final String HEADER_OBSOLETE_DOCUMENT = "X-Obsolete-Document"; |
|
237 private static final String COMPRESSED_CONTENT_TYPE = "application/json+zlib; charset=utf-8"; |
|
238 protected final Collection<String> obsoleteDocumentIDs; |
|
239 |
|
240 public BagheeraUploadResourceDelegate(Resource resource, |
|
241 String namespace, |
|
242 String id, |
|
243 Collection<String> obsoleteDocumentIDs, |
|
244 BagheeraRequestDelegate delegate) { |
|
245 super(resource, namespace, id, delegate); |
|
246 this.obsoleteDocumentIDs = obsoleteDocumentIDs; |
|
247 } |
|
248 |
|
249 @Override |
|
250 public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { |
|
251 super.addHeaders(request, client); |
|
252 request.setHeader(HTTP.CONTENT_TYPE, COMPRESSED_CONTENT_TYPE); |
|
253 if (this.obsoleteDocumentIDs != null && this.obsoleteDocumentIDs.size() > 0) { |
|
254 request.addHeader(HEADER_OBSOLETE_DOCUMENT, Utils.toCommaSeparatedString(this.obsoleteDocumentIDs)); |
|
255 } |
|
256 } |
|
257 } |
|
258 } |