Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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/. */
5 package org.mozilla.gecko.background.bagheera;
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;
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;
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;
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 {
35 protected final String serverURI;
36 protected final Executor executor;
37 protected static final Pattern URI_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
39 protected static String PROTOCOL_VERSION = "1.0";
40 protected static String SUBMIT_PATH = "/submit/";
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 }
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 }
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 }
92 final BaseResource resource = makeResource(namespace, id);
93 resource.delegate = new BagheeraResourceDelegate(resource, namespace, id, delegate);
94 resource.delete();
95 }
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 }
128 final BaseResource resource = makeResource(namespace, id);
129 final HttpEntity deflatedBody = DeflateHelper.deflateBody(payload);
131 resource.delegate = new BagheeraUploadResourceDelegate(resource, namespace, id, oldIDs, delegate);
132 resource.post(deflatedBody);
133 }
135 public static boolean isValidURIComponent(final String in) {
136 return URI_PATTERN.matcher(in).matches();
137 }
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 }
144 if (!isValidURIComponent(id)) {
145 throw new URISyntaxException(id, "Illegal id value. Must be alphanumeric + [_-].");
146 }
148 final String uri = this.serverURI + PROTOCOL_VERSION + SUBMIT_PATH +
149 namespace + "/" + id;
150 return new BaseResource(uri);
151 }
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;
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 }
169 @Override
170 public String getUserAgent() {
171 return delegate.getUserAgent();
172 }
174 @Override
175 public int socketTimeout() {
176 return DEFAULT_SOCKET_TIMEOUT_MSEC;
177 }
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 }
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 }
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 }
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 }
219 @Override
220 public void handleHttpProtocolException(final ClientProtocolException e) {
221 invokeHandleError(e);
222 }
224 @Override
225 public void handleHttpIOException(IOException e) {
226 invokeHandleError(e);
227 }
229 @Override
230 public void handleTransportException(GeneralSecurityException e) {
231 invokeHandleError(e);
232 }
233 }
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;
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 }
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 }