/*
* Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.scvngr.levelup.core.net;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.scvngr.levelup.core.annotation.LevelUpApi;
import com.scvngr.levelup.core.annotation.LevelUpApi.Contract;
import com.scvngr.levelup.core.annotation.VisibleForTesting;
import com.scvngr.levelup.core.annotation.VisibleForTesting.Visibility;
import com.scvngr.levelup.core.util.LogManager;
import com.scvngr.levelup.core.util.NullUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* An {@link AbstractResponse} that uses a buffered interface where the response data is buffered in
* memory.
*/
@LevelUpApi(contract = Contract.DRAFT)
public class BufferedResponse extends AbstractResponse<String> implements Parcelable {
/**
* Creator for parceling.
*/
public static final Creator<BufferedResponse> CREATOR = new Creator<BufferedResponse>() {
@Override
public BufferedResponse[] newArray(final int size) {
return new BufferedResponse[size];
}
@Override
public BufferedResponse createFromParcel(final Parcel in) {
return new BufferedResponse(in);
}
};
/**
* The maximum download size allowed.
* <p>
* This prevents the server from crashing the app by returning an excessively large response.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */static final int MAX_DATA_SIZE_BYTES = 1024 * 450; // 450kb
/**
* Size of the in memory buffer when reading the input stream.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */static final int READ_BUFFER_SIZE_BYTES = 4096;
/**
* Data represented in the response.
*/
@NonNull
private final String mData;
/**
* Error representing and error that occurred during the reading of the response.
*/
@Nullable
private final Exception mReadError;
/**
* @param data the string data (typically JSON) from the response from the server.
*/
public BufferedResponse(@NonNull final String data) {
mData = data;
mReadError = null;
}
/**
* Constructor for parceling.
*
* @param in the parcel to read from.
*/
public BufferedResponse(@NonNull final Parcel in) {
super(in.readInt(), (Exception) in.readSerializable());
mData = in.readString();
mReadError = (Exception) in.readSerializable();
}
/**
* @param data the input stream from the response.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */BufferedResponse(@NonNull final InputStream data) {
Exception error = null;
StringBuilder builder = null;
try {
builder = readStream(data);
} catch (final IOException e) {
builder = new StringBuilder();
error = e;
}
mData = builder.toString();
mReadError = error;
}
/**
* Constructor to use to build the LevelUpResponse for testing.
*
* @param data the string content of the response.
* @param statusCode HTTP status code.
* @param headers HTTP headers.
* @param error error from response or null if there was none.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */BufferedResponse(@NonNull final String data, final int statusCode,
@Nullable final Map<String, List<String>> headers, @Nullable final Exception error) {
super(statusCode, headers, error);
mReadError = null;
mData = data;
}
/**
* Constructor to use to build the LevelUpResponse from a {@link StreamingResponse}. Reads the
* data from the response and calls {@link StreamingResponse#close} after.
*
* @param response the {@link StreamingResponse} to convert to an LevelUpResponse.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */BufferedResponse(@NonNull final StreamingResponse response) {
super(response.getHttpStatusCode(), response.getHttpHeaders(), response.getError());
Exception error = null;
StringBuilder builder = null;
try {
final InputStream data = response.getData();
if (null != data) {
builder = readStream(data);
}
} catch (final IOException e) {
error = e;
}
mData = (null == builder) ? "" : NullUtils.nonNullContract(builder.toString());
mReadError = error;
response.close();
}
@Override
@Nullable
public String getData() {
return mData;
}
/**
* Get the error that occurred during the sending of the request OR during the reading of the
* response.
*
* @return the error that occurred.
*/
@Override
@Nullable
public Exception getError() {
Exception error = mReadError;
if (null == error) {
error = super.getError();
}
return error;
}
/**
* Reads the contents of the input stream passed into a {@link StringBuilder}.
*
* @param data the {@link InputStream} containing the response data.
* @return a {@link StringBuilder} containing the data read.
* @throws IOException if an error occurs during the reading of the stream.
*/
@NonNull
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */static StringBuilder readStream(@NonNull final InputStream data)
throws IOException {
final StringBuilder builder = new StringBuilder();
final BufferedReader reader = new BufferedReader(new InputStreamReader(data, "utf-8"));
final char[] chars = new char[READ_BUFFER_SIZE_BYTES];
int size = 0;
try {
while (true) {
final int read = reader.read(chars);
if (-1 == read) {
break;
}
size += read;
if (MAX_DATA_SIZE_BYTES < size) {
throw new ResponseTooLargeException();
}
// Only append the number of characters read (no extra junk)
builder.append(chars, 0, read);
}
} finally {
reader.close();
}
LogManager.v("Response is %s", builder);
return builder;
}
@Override
public String toString() {
return String.format(Locale.US,
"BufferedResponse [mData=%s, AbstractResponse=%s]", mData, super.toString());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeInt(getHttpStatusCode());
dest.writeSerializable(super.getError());
dest.writeString(mData);
dest.writeSerializable(mReadError);
}
/**
* Exception subclass that gets thrown if the response from the server is larger than
* {@link BufferedResponse#MAX_DATA_SIZE_BYTES}.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */static class ResponseTooLargeException extends IOException {
/**
* Implements the {@link java.io.Serializable} interface.
*/
private static final long serialVersionUID = 2498672579135573453L;
}
}