/*
* Copyright (C) 2013 litesuits.com
*
* 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.litesuits.http;
import android.content.Context;
import android.util.Log;
import com.litesuits.http.data.NameValuePair;
import com.litesuits.http.data.StatisticsInfo;
import com.litesuits.http.exception.*;
import com.litesuits.http.listener.GlobalHttpListener;
import com.litesuits.http.listener.HttpListener;
import com.litesuits.http.listener.StatisticsListener;
import com.litesuits.http.log.HttpLog;
import com.litesuits.http.network.Network;
import com.litesuits.http.parser.DataParser;
import com.litesuits.http.request.AbstractRequest;
import com.litesuits.http.request.JsonRequest;
import com.litesuits.http.request.StringRequest;
import com.litesuits.http.request.param.CacheMode;
import com.litesuits.http.request.param.HttpMethods;
import com.litesuits.http.request.param.HttpRichParamModel;
import com.litesuits.http.response.InternalResponse;
import com.litesuits.http.response.Response;
import com.litesuits.http.utils.HttpUtil;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicLong;
// _oo0oo_
// o8888888o
// 88" . "88
// (| -_- |)
// 0\ = /0
// ___/'---'\___
// .' \\\| |// '.
// / \\\||| : |||// \\
// / _ ||||| -:- |||||- \\
// | | \\\\ - /// | |
// | \_| ''\---/'' |_/ |
// \ .-\__ '-' __/-. /
// ___'. .' /--.--\ '. .'___
// ."" '< '.___\_<|>_/___.' >' "".
// | | : '- \'.;'\ _ /';.'/ - ' : | |
// \ \ '_. \_ __\ /__ _/ .-' / /
// ====='-.____'.___ \_____/___.-'____.-'=====
// '=---='
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 佛祖保佑 永无BUG 镇类之宝
//
//
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// LiteHttp 1.0 Features
// * 1. 单线程:基于当前线程高效率运作。
// * 2. 轻量级:微小的内存开销与Jar包体积,仅约 86K 。
// * 3. 全支持:GET, POST, PUT, DELETE, HEAD, TRACE, OPTIONS, PATCH.
// * 4. 全自动:一行代码将请求Java Model 转化为 Http Parameter,结果Json String 转化为 Java Model 。
// * 5. 易拓展:自定义 DataParser,将网络数据流自由转化为你想要的任意数据类型。
// * 6. 基于接口:架构灵活,轻松替换网络连接方式的核心实现方式,以及 Json 序列化库。
// * 7. 文件上传:支持单个、多个大文件上传。
// * 8. 文件下载:支持文件、Bimtap下载及其进度通知。
// * 9. 网络禁用:快速禁用一种、多种网络环境,比如禁用 2G,3G 。
// * 10. 数据统计:链接、读取时长统计,以及流量统计。
// * 11. 异常体系:统一的异常处理体系,简明清晰地抛出可再细分的三大类异常:客户端、网络、服务器异常。
// * 12. GZIP压缩:Request, Response 自动 GZIP 压缩节省流量。
// * 13. 自动重试:结合探测异常类型和当前网络状况,智能执行重试策略。
// * 14. 自动重定向:基于 30X 状态的重试,且可设置最大次数防止过度跳转。
// * 15. 自带简单异步执行器,方便开发者实现异步请求方案。
//
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// LiteHttp 2.0 Features
// * 1. 可配置:更多更灵活的配置选择项,多达 23+ 项。
// * 2. 多态化:更加直观的API,输入和输出更加明确。
// * 3. 强并发:智能高效的并发调度,有效控制核心并发与队列控制策略。
// * 4. 注解化:信息配置约定更多样,如果你喜欢,可以注解 API、Method、ID、TAG、CacheMode 等参数。
// * 5. 多层缓存:内存命中更高效!支持多样的缓存模式,支持设置缓存有效期。
// * 6. 完善回调:自由设置回调当前或UI线程,自由开启上传、下载进度通知。
// * 7. 完善构建:提供 jar 包支持,后边支持 gradle 和 maven 。
// 2.2.0 版本:
// 1. 修复某些情况下参数无法拼接到URI的bug。
// 2. http参数类增加通过注解指定Key,避免成员变量出现java关键词,同时增加动态URL构建;
// 3. 每一个request可以直接接受注解参数和内部构建参数。
/**
* A simple, intelligent and flexible HTTP client for Android.
* With LiteHttp you can make HTTP request with only one line of code!
* It supports get, post, put, delete, head, trace, options and patch request types.
* LiteHttp could convert a java model to the parameter of http request and
* rander the response JSON as a java model intelligently.
* And you can extend the abstract class {@link DataParser} to parse inputstream to any you want.
* </p>
* <p/>
* need permission:
* </br>
* <uses-permission android:name="android.permission.INTERNET"/>
* <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
* </br>
* <p/>
* if set cache directory on SD card, we will need this permisson:
* </br>
* <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
*
* @author MaTianyu
* 2014-1-1下午9:53:30
*/
public class LiteHttp {
private static final String TAG = LiteHttp.class.getSimpleName();
protected HttpConfig config;
protected final Object lock = new Object();
protected StatisticsInfo statisticsInfo = new StatisticsInfo();
protected AtomicLong memCachedSize = new AtomicLong();
protected ConcurrentHashMap<String, HttpCache> memCache = new ConcurrentHashMap<String, HttpCache>();
public static HttpConfig build(Context context) {
return new HttpConfig(context);
}
protected LiteHttp(HttpConfig config) {
initConfig(config);
}
/* ____________________________ may be overridden by sub class ____________________________*/
protected void initConfig(HttpConfig config) {
this.config = config;
Log.d(TAG, config.toString());
}
public final HttpConfig getConfig() {
return config;
}
public final Context getAppContext() {
return config.context;
}
public final StatisticsInfo getStatisticsInfo() {
return statisticsInfo;
}
public <T> void executeAsync(final AbstractRequest<T> request) {
config.smartExecutor.execute(new Runnable() {
@Override
public void run() {
execute(request);
}
});
}
public <T> Response<T> execute(HttpRichParamModel<T> model) {
return execute(model.buildRequest());
}
public <T> JsonRequest<T> executeAsync(HttpRichParamModel<T> model) {
JsonRequest<T> request = model.buildRequest();
executeAsync(request);
return request;
}
public <T> T perform(AbstractRequest<T> request) {
return execute(request).getResult();
}
public <T> T perform(HttpRichParamModel<T> model) {
return execute(model.buildRequest()).getResult();
}
public <T> FutureTask<T> performAsync(final AbstractRequest<T> request) {
FutureTask<T> futureTask = new FutureTask<T>(new Callable<T>() {
@Override
public T call() throws Exception {
return execute(request).getResult();
}
});
config.smartExecutor.execute(futureTask);
return futureTask;
}
public <T> Response<T> executeOrThrow(AbstractRequest<T> request) throws HttpException {
final Response<T> response = execute(request);
HttpException e = response.getException();
if (e != null) {
throw e;
}
return response;
}
public <T> T performOrThrow(AbstractRequest<T> request) throws HttpException {
return executeOrThrow(request).getResult();
}
public String get(String uri) {
return perform(new StringRequest(uri).setMethod(HttpMethods.Get));
}
public <T> T get(String uri, Class<T> claxx) {
return perform(new JsonRequest<T>(uri, claxx).setMethod(HttpMethods.Get));
}
public <T> T get(AbstractRequest<T> request) {
return perform(request.setMethod(HttpMethods.Get));
}
public <T> T put(AbstractRequest<T> request) {
return perform(request.setMethod(HttpMethods.Put));
}
public <T> T post(AbstractRequest<T> request) {
return perform(request.setMethod(HttpMethods.Post));
}
public <T> T delete(AbstractRequest<T> request) {
return perform(request.setMethod(HttpMethods.Delete));
}
public <T> ArrayList<NameValuePair> head(AbstractRequest<T> request) {
return execute(request.setMethod(HttpMethods.Head)).getHeaders();
}
public <T> Response<T> execute(AbstractRequest<T> request) {
final InternalResponse<T> response = handleRequest(request);
HttpException httpException = null;
final HttpListener<T> listener = request.getHttpListener();
final GlobalHttpListener globalListener = request.getGlobalHttpListener();
try {
if (HttpLog.isPrint) {
Thread t = Thread.currentThread();
HttpLog.i(TAG,
"lite http request: " + request.createFullUri()
+ " , tag: " + request.getTag()
+ " , method: " + request.getMethod()
+ " , cache mode: " + request.getCacheMode()
+ " , thread ID: " + t.getId()
+ " , thread name: " + t.getName());
}
if (globalListener != null) {
globalListener.notifyCallStart(request);
}
if (listener != null) {
listener.notifyCallStart(request);
}
if (request.getCacheMode() == CacheMode.CacheOnly) {
tryHitCache(response);
return response;
} else if (request.getCacheMode() == CacheMode.CacheFirst && tryHitCache(response)) {
return response;
} else {
tryToConnectNetwork(request, response);
}
} catch (HttpException e) {
e.printStackTrace();
httpException = e;
response.setException(httpException);
} catch (Throwable e) {
e.printStackTrace();
httpException = new HttpClientException(e);
response.setException(httpException);
} finally {
try {
if (HttpLog.isPrint) {
Thread t = Thread.currentThread();
HttpLog.i(TAG, "lite http response: " + request.getUri()
+ " , tag: " + request.getTag()
+ " , method: " + request.getMethod()
+ " , cache mode: " + request.getCacheMode()
+ " , thread ID: " + t.getId()
+ " , thread name: " + t.getName());
}
if (listener != null) {
if (request.isCancelledOrInterrupted()) {
listener.notifyCallCancel(response.getResult(), response);
} else if (httpException != null) {
listener.notifyCallFailure(httpException, response);
} else {
listener.notifyCallSuccess(response.getResult(), response);
}
listener.notifyCallEnd(response);
}
if (globalListener != null) {
if (request.isCancelledOrInterrupted()) {
globalListener.notifyCallCancel(response.getResult(), response);
} else if (httpException != null) {
globalListener.notifyCallFailure(httpException, response);
} else {
globalListener.notifyCallSuccess(response.getResult(), response);
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
return response;
}
/**
* if some of request params is null or 0, set global default value to it.
*/
protected <T> InternalResponse<T> handleRequest(AbstractRequest<T> request) {
if (config.commonHeaders != null) {
request.addHeader(config.commonHeaders);
}
if (request.getCacheMode() == null) {
request.setCacheMode(config.defaultCacheMode);
}
if (request.getCacheDir() == null) {
request.setCacheDir(config.defaultCacheDir);
}
if (request.getCacheExpireMillis() < 0) {
request.setCacheExpireMillis(config.defaultCacheExpireMillis);
}
if (request.getCharSet() == null) {
request.setCharSet(config.defaultCharSet);
}
if (request.getMethod() == null) {
request.setMethod(config.defaultHttpMethod);
}
if (request.getMaxRedirectTimes() < 0) {
request.setMaxRedirectTimes(config.defaultMaxRedirectTimes);
}
if (request.getMaxRetryTimes() < 0) {
request.setMaxRetryTimes(config.defaultMaxRetryTimes);
}
if (request.getSocketTimeout() < 0) {
request.setSocketTimeout(config.socketTimeout);
}
if (request.getConnectTimeout() < 0) {
request.setConnectTimeout(config.connectTimeout);
}
if (request.getQueryBuilder() == null) {
request.setQueryBuilder(config.defaultModelQueryBuilder);
}
if (config.globalHttpListener != null) {
request.setGlobalHttpListener(config.globalHttpListener);
}
if (request.getBaseUrl() == null) {
request.setBaseUrl(config.baseUrl);
}
return new InternalResponse<T>(request);
}
/**
* try to detect the network
* <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
*/
protected void tryToDetectNetwork() throws HttpClientException, HttpNetException {
if (config.detectNetwork || config.disableNetworkFlags > 0) {
if (config.context == null) {
throw new HttpClientException(ClientException.ContextNeeded);
}
try {
Network.NetType type = null;
if (config.detectNetwork) {
type = Network.getConnectedType(config.context);
if (type == Network.NetType.None) {
throw new HttpNetException(NetException.NetworkNotAvilable);
}
}
if (config.disableNetworkFlags > 0) {
if (type == null) {
type = Network.getConnectedType(config.context);
}
if (type != null) {
if (config.isDisableAllNetwork() || config.isDisableNetwork(type.value)) {
throw new HttpNetException(NetException.NetworkDisabled);
}
} else {
HttpLog.e(TAG, "DisableNetwork but cant get network type !!!");
}
}
} catch (SecurityException e) {
throw new HttpClientException(e, ClientException.PermissionDenied);
}
}
}
/**
* connect to network
*/
protected <T> boolean tryToConnectNetwork(AbstractRequest<T> request, InternalResponse<T> response)
throws HttpNetException, HttpClientException, HttpServerException, IOException, InterruptedException {
StatisticsListener statistic = null;
if (config.doStatistics) {
statistic = new StatisticsListener(response, statisticsInfo);
response.setStatistics(statistic);
}
if (statistic != null) {
statistic.onStart(request);
}
try {
if (!request.isCancelledOrInterrupted()) {
tryToDetectNetwork();
if (HttpLog.isPrint) {
HttpLog.v(TAG, "lite http connect network... url: "
+ request.getUri() + " tag: " + request.getTag());
}
connectWithRetries(request, response);
tryToKeepCacheInMemory(response);
return true;
}
} finally {
if (statistic != null) {
statistic.onEnd(response);
}
if (request.getCacheMode() == CacheMode.NetFirst
&& !response.isResultOk()
&& !request.isCancelledOrInterrupted()) {
tryHitCache(response);
}
}
return false;
}
/**
* try to connect and read data.
*/
public <T> void connectWithRetries(AbstractRequest<T> request, InternalResponse response)
throws HttpClientException, HttpNetException, HttpServerException, InterruptedException {
int times = 0, maxRetryTimes = request.getMaxRetryTimes();
boolean retry = true;
IOException cause = null;
while (retry) {
try {
cause = null;
retry = false;
if (request.isCancelledOrInterrupted()) {
return;
}
// connect and parse data
config.httpClient.connect(request, response);
} catch (HttpServerException | HttpNetException e) {
throw e;
} catch (SecurityException e) {
throw new HttpClientException(e, ClientException.PermissionDenied);
} catch (IOException e) {
cause = e;
} catch (Exception e) {
throw new HttpClientException(e);
}
if (cause != null) {
if (request.isCancelledOrInterrupted()) {
return;
}
times++;
retry = config.retryHandler.retryRequest(cause, times, maxRetryTimes, config.getContext());
if (retry) {
response.setRetryTimes(times);
if (HttpLog.isPrint) {
HttpLog.i(TAG, "LiteHttp retry request: " + request.getUri());
}
if (request.getHttpListener() != null) {
request.getHttpListener().notifyCallRetry(request, maxRetryTimes, times);
}
}
}
}
if (cause != null) {
throw new HttpNetException(cause);
}
}
/**
* Multi-Level cache design.
* <ul>
* <li>Memory</li>
* <li>Disk</li>
* <li>Network</li>
* </ul>
*/
@SuppressWarnings("unchecked")
protected <T> boolean tryHitCache(InternalResponse<T> response) throws IOException {
AbstractRequest<T> request = response.getRequest();
String key = request.getCacheKey();
long expire = request.getCacheExpireMillis();
boolean isMemCacheSupport = request.getDataParser().isMemCacheSupport();
// 1. try to hit memory cache
if (isMemCacheSupport) {
HttpCache<T> cache = (HttpCache<T>) memCache.get(key);
if (cache != null) {
if (expire < 0 || expire > getCurrentTimeMillis() - cache.time) {
// memory hit!
request.getDataParser().readFromMemoryCache(cache.data);
response.setCacheHit(true);
if (HttpLog.isPrint) {
HttpLog.i(TAG, "lite-http mem cache hit! "
+ " url:" + request.getUri()
+ " tag:" + request.getTag()
+ " key:" + key
+ " cache time:" + HttpUtil.formatDate(cache.time)
+ " expire: " + expire);
}
return true;
}
}
}
if (request.isCancelledOrInterrupted()) {
return false;
}
// 2. try to hit disk cache
File file = request.getCachedFile();
if (file.exists()) {
if (expire < 0 || expire > getCurrentTimeMillis() - file.lastModified()) {
// disk hit!
request.getDataParser().readFromDiskCache(file);
response.setCacheHit(true);
if (HttpLog.isPrint) {
HttpLog.i(TAG, "lite-http disk cache hit! "
+ " url:" + request.getUri()
+ " tag:" + request.getTag()
+ " key:" + key
+ " cache time:" + HttpUtil.formatDate(file.lastModified())
+ " expire: " + expire);
}
return true;
}
}
return false;
}
/**
* try to save data into cache
*/
protected <T> boolean tryToKeepCacheInMemory(InternalResponse<T> response) {
AbstractRequest<T> request = response.getRequest();
if (request.isCachedModel()) {
DataParser<T> dataParser = request.getDataParser();
if (HttpLog.isPrint) {
HttpLog.v(TAG, "lite http try to keep cache.. maximum cache len: " + config.maxMemCacheBytesSize
+ " now cache len: " + memCachedSize.get()
+ " wanna put len: " + dataParser.getReadedLength()
+ " url: " + request.getUri()
+ " tag: " + request.getTag());
}
if (dataParser.isMemCacheSupport()) {
if (memCachedSize.get() + dataParser.getReadedLength() > config.maxMemCacheBytesSize) {
clearMemCache();
if (HttpLog.isPrint) {
HttpLog.i(TAG, "lite http cache full ______________ and clear all mem cache success");
}
}
if (dataParser.getReadedLength() < config.maxMemCacheBytesSize) {
HttpCache<T> cache = new HttpCache<T>();
cache.time = getCurrentTimeMillis();
cache.key = request.getCacheKey();
cache.charSet = response.getCharSet();
cache.data = dataParser.getData();
cache.length = dataParser.getReadedLength();
synchronized (lock) {
memCache.put(cache.key, cache);
memCachedSize.addAndGet(cache.length);
if (HttpLog.isPrint) {
HttpLog.v(TAG, "lite http keep mem cache success, "
+ " url: " + request.getUri()
+ " tag: " + request.getTag()
+ " key: " + cache.key
+ " len: " + cache.length);
}
}
}
return true;
}
}
return false;
}
public final long cleanCacheForRequest(AbstractRequest request) {
long len = 0;
if (request.getCacheDir() == null) {
request.setCacheDir(config.defaultCacheDir);
}
synchronized (lock) {
if (memCache.get(request.getCacheKey()) != null) {
HttpCache cache = memCache.remove(request.getCacheKey());
len = cache.length;
memCachedSize.addAndGet(-len);
}
}
File file = request.getCachedFile();
if (file != null) {
len = file.length();
file.delete();
}
return len;
}
public final long clearMemCache() {
long len;
synchronized (lock) {
len = memCachedSize.get();
memCache.clear();
memCachedSize.set(0);
}
return len;
}
public final boolean deleteCachedFile(String cacehKey) {
File file = new File(config.defaultCacheDir, cacehKey);
return file.delete();
}
public final long deleteCachedFiles() {
File file = new File(config.defaultCacheDir);
long len = 0;
if (file.isDirectory()) {
File[] fs = file.listFiles();
if (fs != null) {
for (File f : fs) {
len += f.length();
f.delete();
}
}
}
return len;
}
protected long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
}