mobile/android/base/background/bagheera/BagheeraClient.java

changeset 0
6474c204b198
     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 +}

mercurial