зеркало из https://github.com/mozilla/gecko-dev.git
Bug 840128 - Android client for Bagheera. r=nalexander
This commit is contained in:
Родитель
027a1f1657
Коммит
2e63bd0796
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче