Bug 840128 - Android client for Bagheera. r=nalexander

This commit is contained in:
Richard Newman 2013-03-04 18:38:24 -08:00
Родитель 027a1f1657
Коммит 2e63bd0796
7 изменённых файлов: 446 добавлений и 0 удалений

Просмотреть файл

@ -20,6 +20,10 @@ SYNC_JAVA_FILES := \
background/announcements/AnnouncementsService.java \
background/announcements/AnnouncementsStartReceiver.java \
background/BackgroundService.java \
background/bagheera/BagheeraClient.java \
background/bagheera/BagheeraRequestDelegate.java \
background/bagheera/BoundedByteArrayEntity.java \
background/bagheera/DeflateHelper.java \
background/common/log/Logger.java \
background/common/log/writers/AndroidLevelCachingLogWriter.java \
background/common/log/writers/AndroidLogWriter.java \

Просмотреть файл

@ -0,0 +1,247 @@
/* 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);
}
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
/* 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 ch.boye.httpclientandroidlib.HttpResponse;
public interface BagheeraRequestDelegate {
void handleSuccess(int status, HttpResponse response);
void handleError(Exception e);
void handleFailure(int status, HttpResponse response);
}

Просмотреть файл

@ -0,0 +1,88 @@
/* 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.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity;
import ch.boye.httpclientandroidlib.entity.ByteArrayEntity;
/**
* An entity that acts like {@link ByteArrayEntity}, but exposes a window onto
* the byte array that is a subsection of the array. The purpose of this is to
* allow a smaller entity to be created without having to resize the source
* array.
*/
public class BoundedByteArrayEntity extends AbstractHttpEntity implements
Cloneable {
protected final byte[] content;
protected final int start;
protected final int end;
protected final int length;
/**
* Create a new entity that behaves exactly like a {@link ByteArrayEntity}
* created with a copy of <code>b</code> truncated to (
* <code>end - start</code>) bytes, starting at <code>start</code>.
*
* @param b the byte array to use.
* @param start the start index.
* @param end the end index.
*/
public BoundedByteArrayEntity(final byte[] b, final int start, final int end) {
if (b == null) {
throw new IllegalArgumentException("Source byte array may not be null.");
}
if (end < start ||
start < 0 ||
end < 0 ||
start > b.length ||
end > b.length) {
throw new IllegalArgumentException("Bounds out of range.");
}
this.content = b;
this.start = start;
this.end = end;
this.length = end - start;
}
@Override
public boolean isRepeatable() {
return true;
}
@Override
public long getContentLength() {
return this.length;
}
@Override
public InputStream getContent() {
return new ByteArrayInputStream(this.content, this.start, this.length);
}
@Override
public void writeTo(final OutputStream outstream) throws IOException {
if (outstream == null) {
throw new IllegalArgumentException("Output stream may not be null.");
}
outstream.write(this.content);
outstream.flush();
}
@Override
public boolean isStreaming() {
return false;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

Просмотреть файл

@ -0,0 +1,78 @@
/* 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.UnsupportedEncodingException;
import java.util.zip.Deflater;
import ch.boye.httpclientandroidlib.HttpEntity;
public class DeflateHelper {
/**
* Conservative upper bound for zlib size, equivalent to the first few lines
* in zlib's deflateBound function.
*
* Includes zlib header.
*
* @param sourceLen
* the number of bytes to compress.
* @return the number of bytes to allocate for the compressed output.
*/
public static int deflateBound(final int sourceLen) {
return sourceLen + ((sourceLen + 7) >> 3) + ((sourceLen + 63) >> 6) + 5 + 6;
}
/**
* Deflate the input into the output array, returning the number of bytes
* written to output.
*/
public static int deflate(byte[] input, byte[] output) {
final Deflater deflater = new Deflater();
deflater.setInput(input);
deflater.finish();
final int length = deflater.deflate(output);
deflater.end();
return length;
}
/**
* Deflate the input, returning an HttpEntity that offers an accurate window
* on the output.
*
* Note that this method does not trim the output array. (Test code can use
* {@link TestDeflation#deflateTrimmed(byte[])}.)
*
* Trimming would be more efficient for long-term space use, but we expect this
* entity to be transient.
*
* Note also that deflate can require <b>more</b> space than the input.
* {@link #deflateBound(int)} tells us the most it will use.
*
* @param bytes the input to deflate.
* @return the deflated input as an entity.
*/
@SuppressWarnings("javadoc")
public static HttpEntity deflateBytes(final byte[] bytes) {
// We would like to use DeflaterInputStream here, but it's minSDK=9, and we
// still target 8. It would also force us to use chunked Transfer-Encoding,
// so perhaps it's for the best!
final byte[] out = new byte[deflateBound(bytes.length)];
final int outLength = deflate(bytes, out);
return new BoundedByteArrayEntity(out, 0, outLength);
}
public static HttpEntity deflateBody(final String payload) {
final byte[] bytes;
try {
bytes = payload.getBytes("UTF-8");
} catch (UnsupportedEncodingException ex) {
// This will never happen. Thanks, Java!
throw new RuntimeException(ex);
}
return deflateBytes(bytes);
}
}

Просмотреть файл

@ -4,6 +4,7 @@
package org.mozilla.gecko.background.common.log;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
@ -12,6 +13,7 @@ import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.writers.AndroidLevelCachingLogWriter;
import org.mozilla.gecko.background.common.log.writers.AndroidLogWriter;
import org.mozilla.gecko.background.common.log.writers.LogWriter;
import org.mozilla.gecko.background.common.log.writers.PrintLogWriter;
import org.mozilla.gecko.background.common.log.writers.SimpleTagLogWriter;
import org.mozilla.gecko.background.common.log.writers.ThreadLocalTagLogWriter;
@ -117,6 +119,16 @@ public class Logger {
logWriters.addAll(Logger.defaultLogWriters());
}
/**
* Start writing log output to stdout.
* <p>
* Use <code>resetLogging</code> to stop logging to stdout.
*/
public static synchronized void startLoggingToConsole() {
setThreadLogTag("Test");
startLoggingTo(new PrintLogWriter(new PrintWriter(System.out, true)));
}
// Synchronized version for other classes to use.
public static synchronized boolean shouldLogVerbose(String logTag) {
for (LogWriter logWriter : logWriters) {

Просмотреть файл

@ -8,6 +8,10 @@ background/announcements/AnnouncementsFetchResourceDelegate.java
background/announcements/AnnouncementsService.java
background/announcements/AnnouncementsStartReceiver.java
background/BackgroundService.java
background/bagheera/BagheeraClient.java
background/bagheera/BagheeraRequestDelegate.java
background/bagheera/BoundedByteArrayEntity.java
background/bagheera/DeflateHelper.java
background/common/log/Logger.java
background/common/log/writers/AndroidLevelCachingLogWriter.java
background/common/log/writers/AndroidLogWriter.java