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

248 lines
8.4 KiB
Java

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.bagheera;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
import ch.boye.httpclientandroidlib.protocol.HTTP;
/**
* Provides encapsulated access to a Bagheera document server.
* The two permitted operations are:
* * Delete a document.
* * Upload a document, optionally deleting an expired document.
*/
public class BagheeraClient {
protected final String serverURI;
protected final Executor executor;
protected static final Pattern URI_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
protected static String PROTOCOL_VERSION = "1.0";
protected static String SUBMIT_PATH = "/submit/";
/**
* Instantiate a new client pointing at the provided server.
* {@link #deleteDocument(String, String, BagheeraRequestDelegate)} and
* {@link #uploadJSONDocument(String, String, String, String, BagheeraRequestDelegate)}
* both accept delegate arguments; the {@link Executor} provided to this
* constructor will be used to invoke callbacks on those delegates.
*
* @param serverURI
* the destination server URI.
* @param executor
* the executor which will be used to invoke delegate callbacks.
*/
public BagheeraClient(final String serverURI, final Executor executor) {
if (serverURI == null) {
throw new IllegalArgumentException("Must provide a server URI.");
}
if (executor == null) {
throw new IllegalArgumentException("Must provide a non-null executor.");
}
this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
this.executor = executor;
}
/**
* Instantiate a new client pointing at the provided server.
* Delegate callbacks will be invoked on a new background thread.
*
* See {@link #BagheeraClient(String, Executor)} for more details.
*
* @param serverURI
* the destination server URI.
*/
public BagheeraClient(final String serverURI) {
this(serverURI, Executors.newSingleThreadExecutor());
}
/**
* Delete the specified document from the server.
* The delegate's callbacks will be invoked by the BagheeraClient's executor.
*/
public void deleteDocument(final String namespace,
final String id,
final BagheeraRequestDelegate delegate) throws URISyntaxException {
if (namespace == null) {
throw new IllegalArgumentException("Must provide namespace.");
}
if (id == null) {
throw new IllegalArgumentException("Must provide id.");
}
final BaseResource resource = makeResource(namespace, id);
resource.delegate = new BagheeraResourceDelegate(resource, delegate);
resource.delete();
}
/**
* Upload a JSON document to a Bagheera server. The delegate's callbacks will
* be invoked in tasks run by the client's executor.
*
* @param namespace
* the namespace, such as "test"
* @param id
* the document ID, which is typically a UUID.
* @param payload
* a document, typically JSON-encoded.
* @param oldID
* an optional ID which denotes a document to supersede. Can be null.
* @param delegate
* the delegate whose methods should be invoked on success or
* failure.
*/
public void uploadJSONDocument(final String namespace,
final String id,
final String payload,
final String oldID,
final BagheeraRequestDelegate delegate) throws URISyntaxException {
if (namespace == null) {
throw new IllegalArgumentException("Must provide namespace.");
}
if (id == null) {
throw new IllegalArgumentException("Must provide id.");
}
if (payload == null) {
throw new IllegalArgumentException("Must provide payload.");
}
final BaseResource resource = makeResource(namespace, id);
final HttpEntity deflatedBody = DeflateHelper.deflateBody(payload);
resource.delegate = new BagheeraUploadResourceDelegate(resource, oldID, delegate);
resource.post(deflatedBody);
}
public static boolean isValidURIComponent(final String in) {
return URI_PATTERN.matcher(in).matches();
}
protected BaseResource makeResource(final String namespace, final String id) throws URISyntaxException {
if (!isValidURIComponent(namespace)) {
throw new URISyntaxException(namespace, "Illegal namespace name. Must be alphanumeric + [_-].");
}
if (!isValidURIComponent(id)) {
throw new URISyntaxException(id, "Illegal id value. Must be alphanumeric + [_-].");
}
final String uri = this.serverURI + PROTOCOL_VERSION + SUBMIT_PATH +
namespace + "/" + id;
return new BaseResource(uri);
}
public class BagheeraResourceDelegate extends BaseResourceDelegate {
private static final int DEFAULT_SOCKET_TIMEOUT_MSEC = 5 * 60 * 1000; // Five minutes.
protected BagheeraRequestDelegate delegate;
public BagheeraResourceDelegate(Resource resource) {
super(resource);
}
public BagheeraResourceDelegate(final Resource resource,
final BagheeraRequestDelegate delegate) {
this(resource);
this.delegate = delegate;
}
@Override
public int socketTimeout() {
return DEFAULT_SOCKET_TIMEOUT_MSEC;
}
@Override
public void handleHttpResponse(HttpResponse response) {
final int status = response.getStatusLine().getStatusCode();
switch (status) {
case 200:
case 201:
invokeHandleSuccess(status, response);
return;
default:
invokeHandleFailure(status, response);
}
}
protected void invokeHandleError(final Exception e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
protected void invokeHandleFailure(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(status, response);
}
});
}
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleSuccess(status, response);
}
});
}
@Override
public void handleHttpProtocolException(final ClientProtocolException e) {
invokeHandleError(e);
}
@Override
public void handleHttpIOException(IOException e) {
invokeHandleError(e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
invokeHandleError(e);
}
}
public final class BagheeraUploadResourceDelegate extends BagheeraResourceDelegate {
private static final String HEADER_OBSOLETE_DOCUMENT = "X-Obsolete-Document";
private static final String COMPRESSED_CONTENT_TYPE = "application/json+zlib; charset=utf-8";
protected String obsoleteDocumentID;
public BagheeraUploadResourceDelegate(Resource resource,
String obsoleteDocumentID,
BagheeraRequestDelegate delegate) {
super(resource, delegate);
this.obsoleteDocumentID = obsoleteDocumentID;
}
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
super.addHeaders(request, client);
request.setHeader(HTTP.CONTENT_TYPE, COMPRESSED_CONTENT_TYPE);
if (this.obsoleteDocumentID != null) {
request.addHeader(HEADER_OBSOLETE_DOCUMENT, this.obsoleteDocumentID);
}
}
}
}