/*
* Copyright (c) 2014-present, Facebook, Inc.
* 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.facebook.stetho.inspector.network;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import android.content.Context;
import android.util.Base64;
import android.util.Base64OutputStream;
import com.facebook.stetho.common.ExceptionUtil;
import com.facebook.stetho.common.LogRedirector;
import com.facebook.stetho.common.Util;
/**
* Manages temporary files created by {@link ChromeHttpFlowObserver} to serve request bodies.
*/
public class ResponseBodyFileManager {
private static final String TAG = "ResponseBodyFileManager";
private static final String FILENAME_PREFIX = "network-response-body-";
private static final int PRETTY_PRINT_TIMEOUT_SEC = 10;
private final Context mContext;
private final Map<String, AsyncPrettyPrinter> mRequestIdMap = Collections.synchronizedMap(
new HashMap<String, AsyncPrettyPrinter>());
public ResponseBodyFileManager(Context context) {
mContext = context;
}
public void cleanupFiles() {
for (File file : mContext.getFilesDir().listFiles()) {
if (file.getName().startsWith(FILENAME_PREFIX)) {
if (!file.delete()) {
LogRedirector.w(TAG, "Failed to delete " + file.getAbsolutePath());
}
}
}
LogRedirector.i(TAG, "Cleaned up temporary network files.");
}
public ResponseBodyData readFile(String requestId) throws IOException {
InputStream in = mContext.openFileInput(getFilename(requestId));
try {
int firstByte = in.read();
if (firstByte == -1) {
throw new EOFException("Failed to read base64Encode byte");
}
ResponseBodyData bodyData = new ResponseBodyData();
bodyData.base64Encoded = firstByte != 0;
AsyncPrettyPrinter asyncPrettyPrinter = mRequestIdMap.get(requestId);
if (asyncPrettyPrinter != null) {
// TODO: this line blocks for up to 10 seconds and create problems as described
// in issue #243 allow asynchronous dispatch for MethodDispatcher
bodyData.data = prettyPrintContentWithTimeOut(asyncPrettyPrinter, in);
} else {
bodyData.data = Util.readAsUTF8(in);
}
return bodyData;
} finally {
in.close();
}
}
private String prettyPrintContentWithTimeOut(
AsyncPrettyPrinter asyncPrettyPrinter,
InputStream in) throws IOException {
AsyncPrettyPrintingCallable prettyPrintingCallable = new AsyncPrettyPrintingCallable(
in,
asyncPrettyPrinter);
ExecutorService executorService = AsyncPrettyPrinterExecutorHolder.getExecutorService();
if (executorService == null) {
//last peer is unregistered...
return null;
}
Future<String> future = executorService.submit(prettyPrintingCallable);
try {
return Util.getUninterruptibly(future, PRETTY_PRINT_TIMEOUT_SEC, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
return "Time out after " + PRETTY_PRINT_TIMEOUT_SEC +
" seconds of attempting to pretty print\n" + Util.readAsUTF8(in);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
ExceptionUtil.propagateIfInstanceOf(cause, IOException.class);
throw ExceptionUtil.propagate(cause);
}
}
public OutputStream openResponseBodyFile(String requestId, boolean base64Encode)
throws IOException {
OutputStream out = mContext.openFileOutput(getFilename(requestId), Context.MODE_PRIVATE);
out.write(base64Encode ? 1 : 0);
if (base64Encode) {
return new Base64OutputStream(out, Base64.DEFAULT);
} else {
return out;
}
}
private static String getFilename(String requestId) {
return FILENAME_PREFIX + requestId;
}
/**
* Associates an asynchronous pretty printer with a response request id
* The pretty printer will be used to pretty print the response body that has
* the particular request id
*
* @param requestId Unique identifier for the response
* as per {@link NetworkEventReporter.InspectorResponse#requestId()}
* @param asyncPrettyPrinter Asynchronous Pretty Printer to pretty print the response body
*/
public void associateAsyncPrettyPrinterWithId(
String requestId,
AsyncPrettyPrinter asyncPrettyPrinter) {
if (mRequestIdMap.put(requestId, asyncPrettyPrinter) != null) {
throw new IllegalArgumentException("cannot associate different " +
"pretty printers with the same request id: "+requestId);
}
}
private class AsyncPrettyPrintingCallable implements Callable<String> {
private final InputStream mInputStream;
private final AsyncPrettyPrinter mAsyncPrettyPrinter;
public AsyncPrettyPrintingCallable(
InputStream in,
AsyncPrettyPrinter asyncPrettyPrinter) {
mInputStream = in;
mAsyncPrettyPrinter = asyncPrettyPrinter;
}
@Override
public String call() throws IOException {
return prettyPrintContent(mInputStream, mAsyncPrettyPrinter);
}
private String prettyPrintContent(InputStream in, AsyncPrettyPrinter asyncPrettyPrinter)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(out);
asyncPrettyPrinter.printTo(writer, in);
writer.flush();
return out.toString("UTF-8");
}
}
}