android-templates/src/main/java/ru/touchin/templates/requests/HttpRequest.java

240 lines
7.9 KiB
Java

/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* 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 ru.touchin.templates.requests;
import android.support.annotation.NonNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.log.LcLevel;
/**
* Created by Gavriil Sitnikov on 13/11/2015.
* Base class that is requesting data via HTTP using OkHttp library as executor and RxJava as interface.
* Also it is parsing responded data to specific type.
*
* @param <T> Type of parsed object.
*/
public abstract class HttpRequest<T> {
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@NonNull
private static Charset getCharset(@NonNull final ResponseBody responseBody) {
final MediaType contentType = responseBody.contentType();
return contentType == null ? DEFAULT_CHARSET : contentType.charset(DEFAULT_CHARSET);
}
@NonNull
private static String requestBodyToString(@NonNull final Request request) throws IOException {
final RequestBody body = request.newBuilder().build().body();
if (body == null) {
return "";
}
final Buffer buffer = new Buffer();
body.writeTo(buffer);
return buffer.readUtf8();
}
@NonNull
private final Class<T> responseResultType;
protected HttpRequest(@NonNull final Class<T> responseResultType) {
this.responseResultType = responseResultType;
}
/**
* Class of object to be parsed from response.
*
* @return Type of object.
*/
@NonNull
public Class<T> getResponseResultType() {
return responseResultType;
}
/**
* Base URL of request.
*
* @return URL.
*/
@NonNull
protected abstract String baseUrl();
/**
* Creates {@link OkHttpClient} object.
* Could be override if you want to specify client.
*
* @return Exemplar of {@link OkHttpClient}.
*/
@NonNull
protected OkHttpClient createHttpClient() {
return new OkHttpClient();
}
/**
* Creates Request builder.
* Could be override if you want to specify request building.
*
* @return Request builder;
* @throws IOException Exception during request creation.
*/
@NonNull
protected Request.Builder createHttpRequest() throws IOException {
final HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl()).newBuilder();
final Map<String, String> headers = new HashMap<>();
onConfigureHeaders(headers);
onConfigureUrlParameters(urlBuilder);
return new Request.Builder().url(urlBuilder.build()).headers(Headers.of(headers));
}
/**
* Method to specify headers.
* Override it to add new headers etc.
*
* @param headers Headers to be used in request.
*/
protected void onConfigureHeaders(@NonNull final Map<String, String> headers) {
// to be overridden. default does nothing
}
/**
* Method to specify URL parameters.
* Override it to add new URL parameters etc.
*
* @param urlBuilder URL builder to be used to build URL of request.
*/
protected void onConfigureUrlParameters(@NonNull final HttpUrl.Builder urlBuilder) {
// to be overridden. default does nothing
}
/**
* Parses responded data to specific typed object.
*
* @param responseResultType Type of object to be parsed;
* @param charset Charset of responded data;
* @param inputStream Responded data;
* @return Parsed object;
* @throws IOException Exception during parsing.
*/
@NonNull
protected abstract T parse(@NonNull final Class<T> responseResultType, @NonNull final Charset charset, @NonNull final InputStream inputStream)
throws IOException;
@SuppressWarnings({"unchecked", "PMD.NPathComplexity"})
//TODO: NPathComplexity
@NonNull
private T executeSyncInternal(@NonNull final RequestController requestController) throws IOException {
final boolean shouldLog = Lc.getLogProcessor().getMinLogLevel().lessThan(LcLevel.INFO);
if (shouldLog) {
Lc.d("Url requested: %s\n%s", requestController.request.url(), requestBodyToString(requestController.request));
}
final Response response = requestController.call.execute();
final ResponseBody responseBody = response.body();
final Charset charset = getCharset(responseBody);
final byte[] bytes = shouldLog ? response.body().bytes() : null;
if (shouldLog) {
Lc.d("Response for: %s has code %s and content: %s", requestController.request.url(), response.code(),
new String(bytes, charset));
}
if (getResponseResultType().equals(Response.class)) {
return handleResponse((T) response);
}
final T result;
try {
result = parse(responseResultType, charset, bytes == null ? response.body().byteStream() : new ByteArrayInputStream(bytes));
} catch (final RuntimeException throwable) {
Lc.assertion("Runtime exception during response parsing " + requestController.request.url());
throw new IOException(throwable);
}
return handleResponse(result);
}
/**
* Synchronously executes request.
*
* @return Parsed result of request;
* @throws IOException Exception during request.
*/
@NonNull
public T executeSync() throws IOException {
return executeSyncInternal(new RequestController());
}
/**
* Asynchronously executes request. Basically emits only one item as a result of request.
* Could emit {@link IOException} as error.
*/
@NonNull
public Observable<T> execute() {
return Observable
.fromCallable(RequestController::new)
.switchMap(requestController -> Observable
.fromCallable(() -> executeSyncInternal(requestController))
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.doOnDispose(requestController.call::cancel));
}
/**
* Override this method to handle response (change or replace or throw exception if it's invalid or whatever).
*
* @param response Response to handle;
* @return Handled response;
* @throws IOException Exception during handle.
*/
@NonNull
protected T handleResponse(@NonNull final T response) throws IOException {
return response;
}
private class RequestController {
@NonNull
private final Request request;
@NonNull
private final Call call;
public RequestController() throws IOException {
this.request = createHttpRequest().build();
this.call = createHttpClient().newCall(this.request);
}
}
}