1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/background/bagheera/BagheeraClient.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,258 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.background.bagheera; 1.9 + 1.10 +import java.io.IOException; 1.11 +import java.net.URISyntaxException; 1.12 +import java.security.GeneralSecurityException; 1.13 +import java.util.Collection; 1.14 +import java.util.concurrent.Executor; 1.15 +import java.util.concurrent.Executors; 1.16 +import java.util.regex.Pattern; 1.17 + 1.18 +import org.mozilla.gecko.sync.Utils; 1.19 +import org.mozilla.gecko.sync.net.BaseResource; 1.20 +import org.mozilla.gecko.sync.net.BaseResourceDelegate; 1.21 +import org.mozilla.gecko.sync.net.Resource; 1.22 + 1.23 +import ch.boye.httpclientandroidlib.HttpEntity; 1.24 +import ch.boye.httpclientandroidlib.HttpResponse; 1.25 +import ch.boye.httpclientandroidlib.client.ClientProtocolException; 1.26 +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; 1.27 +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; 1.28 +import ch.boye.httpclientandroidlib.protocol.HTTP; 1.29 + 1.30 +/** 1.31 + * Provides encapsulated access to a Bagheera document server. 1.32 + * The two permitted operations are: 1.33 + * * Delete a document. 1.34 + * * Upload a document, optionally deleting an expired document. 1.35 + */ 1.36 +public class BagheeraClient { 1.37 + 1.38 + protected final String serverURI; 1.39 + protected final Executor executor; 1.40 + protected static final Pattern URI_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$"); 1.41 + 1.42 + protected static String PROTOCOL_VERSION = "1.0"; 1.43 + protected static String SUBMIT_PATH = "/submit/"; 1.44 + 1.45 + /** 1.46 + * Instantiate a new client pointing at the provided server. 1.47 + * {@link #deleteDocument(String, String, BagheeraRequestDelegate)} and 1.48 + * {@link #uploadJSONDocument(String, String, String, String, BagheeraRequestDelegate)} 1.49 + * both accept delegate arguments; the {@link Executor} provided to this 1.50 + * constructor will be used to invoke callbacks on those delegates. 1.51 + * 1.52 + * @param serverURI 1.53 + * the destination server URI. 1.54 + * @param executor 1.55 + * the executor which will be used to invoke delegate callbacks. 1.56 + */ 1.57 + public BagheeraClient(final String serverURI, final Executor executor) { 1.58 + if (serverURI == null) { 1.59 + throw new IllegalArgumentException("Must provide a server URI."); 1.60 + } 1.61 + if (executor == null) { 1.62 + throw new IllegalArgumentException("Must provide a non-null executor."); 1.63 + } 1.64 + this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/"; 1.65 + this.executor = executor; 1.66 + } 1.67 + 1.68 + /** 1.69 + * Instantiate a new client pointing at the provided server. 1.70 + * Delegate callbacks will be invoked on a new background thread. 1.71 + * 1.72 + * See {@link #BagheeraClient(String, Executor)} for more details. 1.73 + * 1.74 + * @param serverURI 1.75 + * the destination server URI. 1.76 + */ 1.77 + public BagheeraClient(final String serverURI) { 1.78 + this(serverURI, Executors.newSingleThreadExecutor()); 1.79 + } 1.80 + 1.81 + /** 1.82 + * Delete the specified document from the server. 1.83 + * The delegate's callbacks will be invoked by the BagheeraClient's executor. 1.84 + */ 1.85 + public void deleteDocument(final String namespace, 1.86 + final String id, 1.87 + final BagheeraRequestDelegate delegate) throws URISyntaxException { 1.88 + if (namespace == null) { 1.89 + throw new IllegalArgumentException("Must provide namespace."); 1.90 + } 1.91 + if (id == null) { 1.92 + throw new IllegalArgumentException("Must provide id."); 1.93 + } 1.94 + 1.95 + final BaseResource resource = makeResource(namespace, id); 1.96 + resource.delegate = new BagheeraResourceDelegate(resource, namespace, id, delegate); 1.97 + resource.delete(); 1.98 + } 1.99 + 1.100 + /** 1.101 + * Upload a JSON document to a Bagheera server. The delegate's callbacks will 1.102 + * be invoked in tasks run by the client's executor. 1.103 + * 1.104 + * @param namespace 1.105 + * the namespace, such as "test" 1.106 + * @param id 1.107 + * the document ID, which is typically a UUID. 1.108 + * @param payload 1.109 + * a document, typically JSON-encoded. 1.110 + * @param oldIDs 1.111 + * an optional collection of IDs which denote documents to supersede. Can be null or empty. 1.112 + * @param delegate 1.113 + * the delegate whose methods should be invoked on success or 1.114 + * failure. 1.115 + */ 1.116 + public void uploadJSONDocument(final String namespace, 1.117 + final String id, 1.118 + final String payload, 1.119 + Collection<String> oldIDs, 1.120 + final BagheeraRequestDelegate delegate) throws URISyntaxException { 1.121 + if (namespace == null) { 1.122 + throw new IllegalArgumentException("Must provide namespace."); 1.123 + } 1.124 + if (id == null) { 1.125 + throw new IllegalArgumentException("Must provide id."); 1.126 + } 1.127 + if (payload == null) { 1.128 + throw new IllegalArgumentException("Must provide payload."); 1.129 + } 1.130 + 1.131 + final BaseResource resource = makeResource(namespace, id); 1.132 + final HttpEntity deflatedBody = DeflateHelper.deflateBody(payload); 1.133 + 1.134 + resource.delegate = new BagheeraUploadResourceDelegate(resource, namespace, id, oldIDs, delegate); 1.135 + resource.post(deflatedBody); 1.136 + } 1.137 + 1.138 + public static boolean isValidURIComponent(final String in) { 1.139 + return URI_PATTERN.matcher(in).matches(); 1.140 + } 1.141 + 1.142 + protected BaseResource makeResource(final String namespace, final String id) throws URISyntaxException { 1.143 + if (!isValidURIComponent(namespace)) { 1.144 + throw new URISyntaxException(namespace, "Illegal namespace name. Must be alphanumeric + [_-]."); 1.145 + } 1.146 + 1.147 + if (!isValidURIComponent(id)) { 1.148 + throw new URISyntaxException(id, "Illegal id value. Must be alphanumeric + [_-]."); 1.149 + } 1.150 + 1.151 + final String uri = this.serverURI + PROTOCOL_VERSION + SUBMIT_PATH + 1.152 + namespace + "/" + id; 1.153 + return new BaseResource(uri); 1.154 + } 1.155 + 1.156 + public class BagheeraResourceDelegate extends BaseResourceDelegate { 1.157 + private static final int DEFAULT_SOCKET_TIMEOUT_MSEC = 5 * 60 * 1000; // Five minutes. 1.158 + protected final BagheeraRequestDelegate delegate; 1.159 + protected final String namespace; 1.160 + protected final String id; 1.161 + 1.162 + public BagheeraResourceDelegate(final Resource resource, 1.163 + final String namespace, 1.164 + final String id, 1.165 + final BagheeraRequestDelegate delegate) { 1.166 + super(resource); 1.167 + this.namespace = namespace; 1.168 + this.id = id; 1.169 + this.delegate = delegate; 1.170 + } 1.171 + 1.172 + @Override 1.173 + public String getUserAgent() { 1.174 + return delegate.getUserAgent(); 1.175 + } 1.176 + 1.177 + @Override 1.178 + public int socketTimeout() { 1.179 + return DEFAULT_SOCKET_TIMEOUT_MSEC; 1.180 + } 1.181 + 1.182 + @Override 1.183 + public void handleHttpResponse(HttpResponse response) { 1.184 + final int status = response.getStatusLine().getStatusCode(); 1.185 + switch (status) { 1.186 + case 200: 1.187 + case 201: 1.188 + invokeHandleSuccess(status, response); 1.189 + return; 1.190 + default: 1.191 + invokeHandleFailure(status, response); 1.192 + } 1.193 + } 1.194 + 1.195 + protected void invokeHandleError(final Exception e) { 1.196 + executor.execute(new Runnable() { 1.197 + @Override 1.198 + public void run() { 1.199 + delegate.handleError(e); 1.200 + } 1.201 + }); 1.202 + } 1.203 + 1.204 + protected void invokeHandleFailure(final int status, final HttpResponse response) { 1.205 + executor.execute(new Runnable() { 1.206 + @Override 1.207 + public void run() { 1.208 + delegate.handleFailure(status, namespace, response); 1.209 + } 1.210 + }); 1.211 + } 1.212 + 1.213 + protected void invokeHandleSuccess(final int status, final HttpResponse response) { 1.214 + executor.execute(new Runnable() { 1.215 + @Override 1.216 + public void run() { 1.217 + delegate.handleSuccess(status, namespace, id, response); 1.218 + } 1.219 + }); 1.220 + } 1.221 + 1.222 + @Override 1.223 + public void handleHttpProtocolException(final ClientProtocolException e) { 1.224 + invokeHandleError(e); 1.225 + } 1.226 + 1.227 + @Override 1.228 + public void handleHttpIOException(IOException e) { 1.229 + invokeHandleError(e); 1.230 + } 1.231 + 1.232 + @Override 1.233 + public void handleTransportException(GeneralSecurityException e) { 1.234 + invokeHandleError(e); 1.235 + } 1.236 + } 1.237 + 1.238 + public final class BagheeraUploadResourceDelegate extends BagheeraResourceDelegate { 1.239 + private static final String HEADER_OBSOLETE_DOCUMENT = "X-Obsolete-Document"; 1.240 + private static final String COMPRESSED_CONTENT_TYPE = "application/json+zlib; charset=utf-8"; 1.241 + protected final Collection<String> obsoleteDocumentIDs; 1.242 + 1.243 + public BagheeraUploadResourceDelegate(Resource resource, 1.244 + String namespace, 1.245 + String id, 1.246 + Collection<String> obsoleteDocumentIDs, 1.247 + BagheeraRequestDelegate delegate) { 1.248 + super(resource, namespace, id, delegate); 1.249 + this.obsoleteDocumentIDs = obsoleteDocumentIDs; 1.250 + } 1.251 + 1.252 + @Override 1.253 + public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { 1.254 + super.addHeaders(request, client); 1.255 + request.setHeader(HTTP.CONTENT_TYPE, COMPRESSED_CONTENT_TYPE); 1.256 + if (this.obsoleteDocumentIDs != null && this.obsoleteDocumentIDs.size() > 0) { 1.257 + request.addHeader(HEADER_OBSOLETE_DOCUMENT, Utils.toCommaSeparatedString(this.obsoleteDocumentIDs)); 1.258 + } 1.259 + } 1.260 + } 1.261 +}