diff --git a/Parse/src/main/java/com/parse/ParseLogInterceptor.java b/Parse/src/main/java/com/parse/ParseLogInterceptor.java deleted file mode 100644 index ad729e4cb..000000000 --- a/Parse/src/main/java/com/parse/ParseLogInterceptor.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright (c) 2015-present, Parse, LLC. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -package com.parse; - -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; -import java.util.zip.GZIPInputStream; - -import bolts.Task; - -/** - * {@code ParseLogInterceptor} is used to log the request and response information to the given - * logger. - */ -/** package */ class ParseLogInterceptor implements ParseNetworkInterceptor { - - private final static String TAG = "ParseLogNetworkInterceptor"; - private final static String LOG_PARAGRAPH_BREAKER = "--------------"; - private final static String KEY_TYPE = "Type"; - private final static String KEY_HEADERS = "Headers"; - private final static String KEY_URL = "Url"; - private final static String KEY_METHOD = "Method"; - private final static String KEY_REQUEST_ID = "Request-Id"; - private final static String KEY_CONTENT_LENGTH = "Content-Length"; - private final static String KEY_CONTENT_TYPE = "Content-Type"; - private final static String KEY_BODY = "Body"; - private final static String KEY_STATUS_CODE = "Status-Code"; - private final static String KEY_REASON_PHRASE = "Reason-Phrase"; - private final static String KEY_ERROR = "Error"; - private final static String TYPE_REQUEST = "Request"; - private final static String TYPE_RESPONSE = "Response"; - private final static String TYPE_ERROR = "Error"; - - private final static String IGNORED_BODY_INFO = "Ignored"; - - private static final String GZIP_ENCODING = "gzip"; - - /** - * The {@code Logger} to log the request and response information. - */ - public static abstract class Logger { - public static String NEW_LINE = "\n"; - - // The reason we need a lock here is since multiple network threads may write to the - // logger concurrently, the message of different threads may intertwined. We need this - // lock to keep the messages printed by different threads separated - private ReentrantLock lock; - - public Logger() { - lock = new ReentrantLock(); - } - - public abstract void write(String str); - - public void lock() { - lock.lock(); - } - - public void unlock() { - lock.unlock(); - } - - public void write(String name, String value) { - write(name + " : " + value); - } - - public void writeLine(String str) { - write(str); - write(NEW_LINE); - } - - public void writeLine(String name, String value) { - writeLine(name + " : " + value); - } - } - - /** - * Android logcat implementation of {@code Logger}. - */ - private static class LogcatLogger extends Logger { - - private static int MAX_MESSAGE_LENGTH = 4000; - - @Override - public void write(String str) { - // Logcat can only print the limited number of characters in one line, so when we have a long - // message, we need to split them to multiple lines - int start = 0; - while (start < str.length()) { - int end = Math.min(start + MAX_MESSAGE_LENGTH, str.length()); - Log.i(TAG, str.substring(start, end)); - start = end; - } - } - - @Override - public void writeLine(String str) { - // Logcat actually writes in a new line every time, so we need to rewrite it - write(str); - } - } - - /** - * A helper stream to proxy the original inputStream to other inputStream. When the original - * stream is read, another proxied stream can also be read for the same content. - */ - private static class ProxyInputStream extends InputStream { - private final InputStream originalInput; - private final PipedInputStream proxyInput; - private final PipedOutputStream proxyOutput; - - public ProxyInputStream( - InputStream originalInput, final InterceptCallback callback) throws IOException { - this.originalInput = originalInput; - PipedInputStream tempProxyInput = new PipedInputStream(); - PipedOutputStream tempProxyOutput = null; - try { - tempProxyOutput = new PipedOutputStream(tempProxyInput); - } catch (IOException e) { - callback.done(null, e); - ParseIOUtils.closeQuietly(tempProxyOutput); - ParseIOUtils.closeQuietly(tempProxyInput); - throw e; - } - proxyInput = tempProxyInput; - proxyOutput = tempProxyOutput; - - // We need to make sure we read and write proxyInput/Output in separate threads, otherwise - // there will be a deadlock - Task.call(new Callable() { - @Override - public Void call() throws Exception { - callback.done(proxyInput, null); - return null; - } - }, ParseExecutors.io()); - } - - @Override - public int read() throws IOException { - try { - int n = originalInput.read(); - if (n == -1) { - // Hit the end of the stream. - ParseIOUtils.closeQuietly(proxyOutput); - } else { - proxyOutput.write(n); - } - return n; - } catch (IOException e) { - // If we have problems in read from original inputStream or write to the proxyOutputStream, - // we simply close the proxy stream and throw the exception - ParseIOUtils.closeQuietly(proxyOutput); - throw e; - } - } - } - - private static boolean isContentTypePrintable(String contentType) { - return (contentType != null) && (contentType.contains("json") || contentType.contains("text")); - } - - private static boolean isGzipEncoding(ParseHttpResponse response) { - return GZIP_ENCODING.equals(response.getHeader("Content-Encoding")); - } - - private static String formatBytes(byte[] bytes, String contentType) { - // We handle json separately since it is the most common body - if (contentType.contains("json")) { - try { - return new JSONObject(new String(bytes)).toString(4); - } catch (JSONException e) { - return new String(bytes).trim(); - } - } else if (contentType.contains("text")) { - return new String(bytes).trim(); - } else { - throw new IllegalStateException("We can not print this " + contentType); - } - } - - private interface InterceptCallback extends ParseCallback2 { - - @Override - void done(InputStream proxyInputStream, IOException e); - } - - - private Logger logger; - - /** - * Set the logger the interceptor uses. The default one is Android logcat logger. - * @param logger - * The logger the interceptor uses. - */ - public void setLogger(Logger logger) { - if (this.logger == null) { - this.logger = logger; - } else { - throw new IllegalStateException( - "Another logger was already registered: " + this.logger); - } - } - - private Logger getLogger() { - if (logger == null) { - logger = new LogcatLogger(); - } - return logger; - } - - // Request Id generator - private final AtomicInteger nextRequestId = new AtomicInteger(0); - - @Override - public ParseHttpResponse intercept(Chain chain) throws IOException { - // Intercept request - final String requestId = String.valueOf(nextRequestId.getAndIncrement()); - ParseHttpRequest request = chain.getRequest(); - - logRequestInfo(getLogger(), requestId, request); - - // Developers need to manually call this - ParseHttpResponse tempResponse; - try { - tempResponse = chain.proceed(request); - } catch (IOException e) { - // Log error when we can not get response from server - logError(getLogger(), requestId, e.getMessage()); - throw e; - } - - final ParseHttpResponse response = tempResponse; - InputStream newResponseBodyStream = response.getContent(); - // For response content, if developers care time of the response(latency, sending and receiving - // time etc) or need the original networkStream to do something, they have to proxy the - // response - if (isContentTypePrintable(response.getContentType())) { - newResponseBodyStream = new ProxyInputStream(response.getContent(), new InterceptCallback() { - @Override - public void done(InputStream proxyInput, IOException e) { - try { - if (e != null) { - return; - } - - // This inputStream will be blocked until we write to the ProxyOutputStream - // in ProxyInputStream - InputStream decompressedInput = isGzipEncoding(response) ? - new GZIPInputStream(proxyInput) : proxyInput; - ByteArrayOutputStream decompressedOutput = new ByteArrayOutputStream(); - ParseIOUtils.copy(decompressedInput, decompressedOutput); - byte[] bodyBytes = decompressedOutput.toByteArray(); - String responseBodyInfo = formatBytes(bodyBytes, response.getContentType()); - logResponseInfo(getLogger(), requestId, response, responseBodyInfo); - - ParseIOUtils.closeQuietly(decompressedInput); - ParseIOUtils.closeQuietly(proxyInput); - } catch (IOException e1) { - // Log error when we can't read body stream - logError(getLogger(), requestId, e1.getMessage()); - } finally { - ParseIOUtils.closeQuietly(proxyInput); - } - } - }); - } else { - logResponseInfo(getLogger(), requestId, response, IGNORED_BODY_INFO); - } - - return new ParseHttpResponse.Builder(response) - .setContent(newResponseBodyStream) - .build(); - } - - private void logRequestInfo( - Logger logger, String requestId, ParseHttpRequest request) throws IOException { - logger.lock(); - logger.writeLine(KEY_TYPE, TYPE_REQUEST); - logger.writeLine(KEY_REQUEST_ID, requestId); - logger.writeLine(KEY_URL, request.getUrl()); - logger.writeLine(KEY_METHOD, request.getMethod().toString()); - // Add missing headers - Map headers = new HashMap<>(request.getAllHeaders()); - if (request.getBody() != null) { - headers.put(KEY_CONTENT_LENGTH, String.valueOf(request.getBody().getContentLength())); - headers.put(KEY_CONTENT_TYPE, request.getBody().getContentType()); - } - logger.writeLine(KEY_HEADERS, headers.toString()); - - // Body - if (request.getBody() != null) { - String requestBodyInfo; - String contentType = request.getBody().getContentType(); - if (isContentTypePrintable(contentType)) { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - request.getBody().writeTo(output); - requestBodyInfo = formatBytes(output.toByteArray(), contentType); - } else { - requestBodyInfo = IGNORED_BODY_INFO; - } - logger.writeLine(KEY_BODY, requestBodyInfo); - } - - logger.writeLine(LOG_PARAGRAPH_BREAKER); - logger.unlock(); - } - - // Since we can not read the content of the response directly, we need an additional parameter - // to pass the responseBody after we get it asynchronously. - private void logResponseInfo( - Logger logger, String requestId, ParseHttpResponse response, String responseBodyInfo) { - logger.lock(); - logger.writeLine(KEY_TYPE, TYPE_RESPONSE); - logger.writeLine(KEY_REQUEST_ID, requestId); - logger.writeLine(KEY_STATUS_CODE, String.valueOf(response.getStatusCode())); - logger.writeLine(KEY_REASON_PHRASE, response.getReasonPhrase()); - logger.writeLine(KEY_HEADERS, response.getAllHeaders().toString()); - - // Body - if (responseBodyInfo != null) { - logger.writeLine(KEY_BODY, responseBodyInfo); - } - - logger.writeLine(LOG_PARAGRAPH_BREAKER); - logger.unlock(); - } - - private void logError(Logger logger, String requestId, String message) { - logger.lock(); - logger.writeLine(KEY_TYPE, TYPE_ERROR); - logger.writeLine(KEY_REQUEST_ID, requestId); - logger.writeLine(KEY_ERROR, message); - logger.writeLine(LOG_PARAGRAPH_BREAKER); - logger.unlock(); - } -} diff --git a/Parse/src/main/java/com/parse/ParseStethoInterceptor.java b/Parse/src/main/java/com/parse/ParseStethoInterceptor.java deleted file mode 100644 index 8f3bc08ee..000000000 --- a/Parse/src/main/java/com/parse/ParseStethoInterceptor.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (c) 2015-present, Parse, LLC. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -package com.parse; - -import com.facebook.stetho.inspector.network.DefaultResponseHandler; -import com.facebook.stetho.inspector.network.NetworkEventReporter; -import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.annotation.Nullable; - -/** - * {@code ParseStethoInterceptor} is used to log the request and response through Stetho to chrome - * browser debugger. - */ -/** package */ class ParseStethoInterceptor implements ParseNetworkInterceptor { - - private static final String CONTENT_LENGTH_HEADER = "Content-Length"; - private static final String CONTENT_TYPE_HEADER = "Content-Type"; - - // Implementation of Stetho request - private static class ParseInterceptorHttpRequest - implements NetworkEventReporter.InspectorRequest { - private final String requestId; - private final ParseHttpRequest request; - private byte[] body; - private boolean hasGeneratedBody; - // Since Stetho use index to get header, we use a list to store them - private List headers; - - public ParseInterceptorHttpRequest(String requestId, ParseHttpRequest request) { - this.requestId = requestId; - this.request = request; - - // Add content-length and content-type header to the interceptor. These headers are added when - // a real httpclient send the request. Since we still want users to see these headers, we - // manually add them to Interceptor request if they are not in the header list - headers = new ArrayList<>(); - for (Map.Entry headerEntry : request.getAllHeaders().entrySet()) { - headers.add(headerEntry.getKey()); - headers.add(headerEntry.getValue()); - } - if (request.getBody() != null) { - if (!headers.contains(CONTENT_LENGTH_HEADER)) { - headers.add(CONTENT_LENGTH_HEADER); - headers.add(String.valueOf(request.getBody().getContentLength())); - } - // If user does not set contentType when create ParseFile, it may be null - if (request.getBody().getContentType() != null && !headers.contains(CONTENT_TYPE_HEADER)) { - headers.add(CONTENT_TYPE_HEADER); - headers.add(request.getBody().getContentType()); - } - } - } - - @Override - public String id() { - return requestId; - } - - @Override - public String friendlyName() { - return null; - } - - @Nullable - @Override - public Integer friendlyNameExtra() { - return null; - } - - @Override - public String url() { - return request.getUrl(); - } - - @Override - public String method() { - return request.getMethod().toString(); - } - - @Nullable - @Override - public byte[] body() throws IOException { - if (!hasGeneratedBody) { - hasGeneratedBody = true; - body = generateBody(); - } - return body; - } - - private byte[] generateBody() throws IOException { - ParseHttpBody body = request.getBody(); - if (body == null) { - return null; - } - ByteArrayOutputStream out = new ByteArrayOutputStream(); - body.writeTo(out); - return out.toByteArray(); - } - - @Override - public int headerCount() { - return headers.size() / 2; - } - - @Override - public String headerName(int index) { - return headers.get(index * 2); - } - - @Override - public String headerValue(int index) { - return headers.get(index * 2 + 1); - } - - @Nullable - @Override - public String firstHeaderValue(String name) { - int index = headers.indexOf(name); - return index >= 0 ? headers.get(index + 1) : null; - } - } - - // Implementation of Stetho response - private static class ParseInspectorHttpResponse - implements NetworkEventReporter.InspectorResponse { - private final String requestId; - private final ParseHttpRequest request; - private final ParseHttpResponse response; - // Since stetho use index to get header, we use a list to store them - private List responseHeaders; - - public ParseInspectorHttpResponse( - String requestId, - ParseHttpRequest request, - ParseHttpResponse response) { - this.requestId = requestId; - this.request = request; - this.response = response; - responseHeaders = new ArrayList<>(); - for (Map.Entry headerEntry : response.getAllHeaders().entrySet()) { - responseHeaders.add(headerEntry.getKey()); - responseHeaders.add(headerEntry.getValue()); - } - } - - @Override - public String requestId() { - return requestId; - } - - @Override - public String url() { - return request.getUrl(); - } - - @Override - public int statusCode() { - return response.getStatusCode(); - } - - @Override - public String reasonPhrase() { - return response.getReasonPhrase(); - } - - @Override - public boolean connectionReused() { - // Follow Stetho URLConnectionInspectorResponse - return false; - } - - @Override - public int connectionId() { - // Follow Stetho URLConnectionInspectorResponse - return requestId.hashCode(); - } - - @Override - public boolean fromDiskCache() { - // Follow Stetho URLConnectionInspectorResponse - return false; - } - - @Override - public int headerCount() { - return response.getAllHeaders().size(); - } - - @Override - public String headerName(int index) { - return responseHeaders.get(index * 2); - } - - @Override - public String headerValue(int index) { - return responseHeaders.get(index * 2 + 1); - } - - @Nullable - @Override - public String firstHeaderValue(String name) { - int index = responseHeaders.indexOf(name); - return index >= 0 ? responseHeaders.get(index + 1) : null; - } - } - - // Stetho reporter - private final NetworkEventReporter stethoEventReporter = NetworkEventReporterImpl.get(); - - // Request Id generator - private final AtomicInteger nextRequestId = new AtomicInteger(0); - - @Override - public ParseHttpResponse intercept(Chain chain) throws IOException { - // Intercept request - String requestId = String.valueOf(nextRequestId.getAndIncrement()); - ParseHttpRequest request = chain.getRequest(); - - // If Stetho debugger is available (chrome debugger is open), intercept the request - if (stethoEventReporter.isEnabled()) { - ParseInterceptorHttpRequest inspectorRequest = - new ParseInterceptorHttpRequest(requestId, chain.getRequest()); - stethoEventReporter.requestWillBeSent(inspectorRequest); - } - - - ParseHttpResponse response; - try { - response = chain.proceed(request); - } catch (IOException e) { - // If Stetho debugger is available (chrome debugger is open), intercept the exception - if (stethoEventReporter.isEnabled()) { - stethoEventReporter.httpExchangeFailed(requestId, e.toString()); - } - throw e; - } - - if (stethoEventReporter.isEnabled()) { - // If Stetho debugger is available (chrome debugger is open), intercept the response body - if (request.getBody() != null) { - stethoEventReporter.dataSent(requestId, (int)(request.getBody().getContentLength()), - (int)(request.getBody().getContentLength())); - } - - // If Stetho debugger is available (chrome debugger is open), intercept the response headers - stethoEventReporter.responseHeadersReceived( - new ParseInspectorHttpResponse(requestId, request, response)); - - InputStream responseStream = null; - if (response.getContent() != null) { - responseStream = response.getContent(); - } - - // Create the Stetho proxy inputStream, when Parse read this stream, it will proxy the - // response body to Stetho reporter. - responseStream = stethoEventReporter.interpretResponseStream( - requestId, - response.getContentType(), - response.getAllHeaders().get("Content-Encoding"), - responseStream, - new DefaultResponseHandler(stethoEventReporter, requestId) - ); - if (responseStream != null) { - response = new ParseHttpResponse.Builder(response) - .setContent(responseStream) - .build(); - } - } - return response; - } -} diff --git a/Parse/src/test/java/com/parse/ParseLogInterceptorTest.java b/Parse/src/test/java/com/parse/ParseLogInterceptorTest.java deleted file mode 100644 index 0b117d630..000000000 --- a/Parse/src/test/java/com/parse/ParseLogInterceptorTest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (c) 2015-present, Parse, LLC. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -package com.parse; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricGradleTestRunner; -import org.robolectric.annotation.Config; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.zip.GZIPOutputStream; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -// For GZIPOutputStream -@RunWith(RobolectricGradleTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 21) -public class ParseLogInterceptorTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void testInterceptNotGZIPResponse() throws Exception { - ParseLogInterceptor interceptor = new ParseLogInterceptor(); - - final String content = "content"; - final Semaphore done = new Semaphore(0); - interceptor.setLogger(new ParseLogInterceptor.Logger() { - @Override - public void write(String str) { - } - - @Override - public void writeLine(String name, String value) { - if ("Body".equals(name)) { - assertEquals(content, value); - done.release(); - } - } - }); - - ParseHttpResponse response = interceptor.intercept(new ParseNetworkInterceptor.Chain() { - @Override - public ParseHttpRequest getRequest() { - // We do not need request for this test so we simply return an empty request - return new ParseHttpRequest.Builder().setMethod(ParseHttpRequest.Method.GET).build(); - } - - @Override - public ParseHttpResponse proceed(ParseHttpRequest request) throws IOException { - ParseHttpResponse response = new ParseHttpResponse.Builder() - .setContentType("application/json") - .setContent(new ByteArrayInputStream(content.getBytes())) - .build(); - return response; - } - }); - - // Make sure the content we get from response is correct - ByteArrayOutputStream output = new ByteArrayOutputStream(); - ParseIOUtils.copy(response.getContent(), output); - assertEquals(content, new String(output.toByteArray())); - // Make sure we log the right content - assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); - } - - @Test - public void testInterceptGZIPResponse() throws Exception { - ParseLogInterceptor interceptor = new ParseLogInterceptor(); - - final String content = "content"; - final ByteArrayOutputStream gzipByteOutput = new ByteArrayOutputStream(); - GZIPOutputStream gzipOutput = new GZIPOutputStream(gzipByteOutput); - gzipOutput.write(content.getBytes()); - gzipOutput.close(); - - final Semaphore done = new Semaphore(0); - interceptor.setLogger(new ParseLogInterceptor.Logger() { - @Override - public void write(String str) { - } - - @Override - public void writeLine(String name, String value) { - if ("Body".equals(name)) { - assertEquals(content, value); - done.release(); - } - } - }); - - ParseHttpResponse response = interceptor.intercept(new ParseNetworkInterceptor.Chain() { - @Override - public ParseHttpRequest getRequest() { - // We do not need request for this test so we simply return an empty request - return new ParseHttpRequest.Builder().setMethod(ParseHttpRequest.Method.GET).build(); - } - - @Override - public ParseHttpResponse proceed(ParseHttpRequest request) throws IOException { - ParseHttpResponse response = new ParseHttpResponse.Builder() - .addHeader("Content-Encoding", "gzip") - .setContentType("application/json") - .setContent(new ByteArrayInputStream(gzipByteOutput.toByteArray())) - .build(); - return response; - } - }); - - // Make sure the content we get from response is the gzip content - ByteArrayOutputStream output = new ByteArrayOutputStream(); - ParseIOUtils.copy(response.getContent(), output); - assertArrayEquals(gzipByteOutput.toByteArray(), output.toByteArray()); - // Make sure we log the ungzip content - assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); - } - - @Test - public void testInterceptResponseWithException() throws Exception { - ParseLogInterceptor interceptor = new ParseLogInterceptor(); - - final String errorMessage = "error"; - final Semaphore done = new Semaphore(0); - interceptor.setLogger(new ParseLogInterceptor.Logger() { - @Override - public void write(String str) { - } - - @Override - public void writeLine(String name, String value) { - if ("Error".equals(name)) { - assertEquals(errorMessage, value); - done.release(); - } - } - }); - - // Make sure the exception we get is correct - thrown.expect(IOException.class); - thrown.expectMessage(errorMessage); - - interceptor.intercept(new ParseNetworkInterceptor.Chain() { - @Override - public ParseHttpRequest getRequest() { - // We do not need request for this test so we simply return an empty request - return new ParseHttpRequest.Builder().setMethod(ParseHttpRequest.Method.GET).build(); - } - - @Override - public ParseHttpResponse proceed(ParseHttpRequest request) throws IOException { - throw new IOException(errorMessage); - } - }); - - // Make sure we log the right content - assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); - } -}