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