com.landawn.abacus.http.HttpProxy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of abacus-android Show documentation
Show all versions of abacus-android Show documentation
A general and simple library for Android
/*
* Copyright (C) 2015 HaiYang Li
*
* 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.landawn.abacus.http;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.landawn.abacus.core.MapEntity;
import com.landawn.abacus.exception.AbacusException;
import com.landawn.abacus.exception.UncheckedIOException;
import com.landawn.abacus.logging.Logger;
import com.landawn.abacus.logging.LoggerFactory;
import com.landawn.abacus.parser.DeserializationConfig;
import com.landawn.abacus.parser.Parser;
import com.landawn.abacus.parser.SerializationConfig;
import com.landawn.abacus.type.Type;
import com.landawn.abacus.util.AsyncExecutor;
import com.landawn.abacus.util.ClassUtil;
import com.landawn.abacus.util.IOUtil;
import com.landawn.abacus.util.Maps;
import com.landawn.abacus.util.N;
import com.landawn.abacus.util.NamingPolicy;
import com.landawn.abacus.util.StringUtil;
import com.landawn.abacus.util.function.Function;
/**
* The client and server communicate by xml/json(may compressed by lz4/snappy/gzip)
* through http. There are two ways to send the request: 1, Send the request
* with the url. The target web method is identified by request type.
* 2, Send the request with the url+'/'+operationName. The target web method is
* identified by operation name in the url.
*
* @since 0.8
*
* @author Haiyang Li
*/
public final class HttpProxy {
private static final int DEFAULT_MAX_CONNECTION = AbstractHttpClient.DEFAULT_MAX_CONNECTION;
private static final int DEFAULT_CONNECTION_TIMEOUT = AbstractHttpClient.DEFAULT_CONNECTION_TIMEOUT;
private static final int DEFAULT_READ_TIMEOUT = AbstractHttpClient.DEFAULT_READ_TIMEOUT;
// Upper and lower characters, digits, underscores, and hyphens, starting with a character.
private static final String PARAM = "[a-zA-Z][a-zA-Z0-9_-]*";
private static final Pattern PARAM_NAME_REGEX = Pattern.compile(PARAM);
private static final Pattern PARAM_URL_REGEX = Pattern.compile("\\{(" + PARAM + ")\\}");
public static T createClientProxy(final Class interfaceClass, final ContentFormat contentFormat, final String url) {
return createClientProxy(interfaceClass, contentFormat, url, DEFAULT_MAX_CONNECTION, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_READ_TIMEOUT);
}
public static T createClientProxy(final Class interfaceClass, final ContentFormat contentFormat, final String url, final Config config) {
return createClientProxy(interfaceClass, contentFormat, url, DEFAULT_MAX_CONNECTION, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_READ_TIMEOUT, config);
}
public static T createClientProxy(final Class interfaceClass, final ContentFormat contentFormat, final String url, final int maxConnection) {
return createClientProxy(interfaceClass, contentFormat, url, maxConnection, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_READ_TIMEOUT);
}
public static T createClientProxy(final Class interfaceClass, final ContentFormat contentFormat, final String url, final int maxConnection,
final long connTimeout, final long readTimeout) {
return createClientProxy(interfaceClass, contentFormat, url, maxConnection, connTimeout, readTimeout, null);
}
public static T createClientProxy(final Class interfaceClass, final ContentFormat contentFormat, final String url, final int maxConnection,
final long connTimeout, final long readTimeout, final Config config) {
if (contentFormat == null || contentFormat == ContentFormat.NONE) {
throw new IllegalArgumentException("Content format can't be null or NONE");
}
InvocationHandler h = new InvocationHandler() {
private final Logger _logger = LoggerFactory.getLogger(interfaceClass);
private final ContentFormat _contentFormat = contentFormat;
private final String _url = url;
private final int _maxConnection = maxConnection;
private final long _connTimeout = connTimeout;
private final long _readTimeout = readTimeout;
private final Config _config = config == null ? new Config() : N.copy(config);
{
final Set declaredMethods = N.asLinkedHashSet(interfaceClass.getDeclaredMethods());
for (Class> superClass : interfaceClass.getInterfaces()) {
declaredMethods.addAll(Arrays.asList(superClass.getDeclaredMethods()));
}
if (_config.parser == null) {
_config.setParser(HTTP.getParser(_contentFormat));
}
// set operation configuration.
final Map newOperationConfigs = new HashMap<>(N.initHashCapacity(declaredMethods.size()));
if (config != null && N.notNullOrEmpty(config.operationConfigs)) {
for (Map.Entry entry : config.operationConfigs.entrySet()) {
OperationConfig copy = entry.getValue() == null ? new OperationConfig() : N.copy(entry.getValue());
if (entry.getValue() != null && entry.getValue().getRequestSettings() != null) {
copy.setRequestSettings(entry.getValue().getRequestSettings().copy());
}
newOperationConfigs.put(entry.getKey(), copy);
}
}
_config.setOperationConfigs(newOperationConfigs);
for (Method method : declaredMethods) {
final String methodName = method.getName();
final Class>[] parameterTypes = method.getParameterTypes();
final int parameterCount = parameterTypes.length;
OperationConfig operationConfig = _config.operationConfigs.get(methodName);
if (operationConfig == null) {
operationConfig = new OperationConfig();
_config.operationConfigs.put(methodName, operationConfig);
}
operationConfig.requestEntityName = StringUtil.capitalize(methodName) + "Request";
operationConfig.responseEntityName = StringUtil.capitalize(methodName) + "Response";
RestMethod methodInfo = null;
for (Annotation methodAnnotation : method.getAnnotations()) {
Class extends Annotation> annotationType = methodAnnotation.annotationType();
for (Annotation innerAnnotation : annotationType.getAnnotations()) {
if (RestMethod.class == innerAnnotation.annotationType()) {
methodInfo = (RestMethod) innerAnnotation;
break;
}
}
if (methodInfo != null) {
if (N.isNullOrEmpty(operationConfig.getRequestUrl())) {
try {
String path = (String) annotationType.getMethod("value").invoke(methodAnnotation);
if (N.notNullOrEmpty(path)) {
operationConfig.setRequestUrl(path);
}
} catch (Exception e) {
throw new AbacusException("Failed to extract String 'value' from @%s annotation:" + annotationType.getSimpleName());
}
}
if (operationConfig.getHttpMethod() == null) {
operationConfig.setHttpMethod(HttpMethod.valueOf(methodInfo.value()));
}
break;
}
}
if (N.isNullOrEmpty(operationConfig.paramNameTypeMap)) {
operationConfig.paramTypes = new Type[parameterCount];
operationConfig.paramFields = new Field[parameterCount];
operationConfig.paramPaths = new Path[parameterCount];
operationConfig.paramNameTypeMap = new HashMap<>();
final Annotation[][] parameterAnnotationArrays = method.getParameterAnnotations();
for (int i = 0; i < parameterCount; i++) {
operationConfig.paramTypes[i] = N.typeOf(parameterTypes[i]);
for (Annotation parameterAnnotation : parameterAnnotationArrays[i]) {
if (parameterAnnotation.annotationType() == Field.class) {
operationConfig.paramFields[i] = (Field) parameterAnnotation;
if (operationConfig.paramNameTypeMap.put(operationConfig.paramFields[i].value(), operationConfig.paramTypes[i]) != null) {
throw new AbacusException("Duplicated parameter names: " + operationConfig.paramFields[i].value());
}
} else if (parameterAnnotation.annotationType() == Path.class) {
operationConfig.validatePathName(((Path) parameterAnnotation).value());
operationConfig.paramPaths[i] = (Path) parameterAnnotation;
if (operationConfig.paramNameTypeMap.put(operationConfig.paramPaths[i].value(), operationConfig.paramTypes[i]) != null) {
throw new AbacusException("Duplicated parameter names: " + operationConfig.paramPaths[i].value());
}
}
}
}
}
if (operationConfig.httpMethod == null) {
operationConfig.httpMethod = HttpMethod.POST;
} else if (!(operationConfig.httpMethod == HttpMethod.GET || operationConfig.httpMethod == HttpMethod.POST
|| operationConfig.httpMethod == HttpMethod.PUT || operationConfig.httpMethod == HttpMethod.DELETE)) {
throw new IllegalArgumentException("Unsupported http method: " + operationConfig.httpMethod);
}
if (parameterCount > 1 && operationConfig.paramNameTypeMap.isEmpty()) {
throw new IllegalArgumentException("Unsupported web service method: " + method.getName()
+ ". Only one parameter or multi parameters with Field/Path annotaions are supported");
}
if (N.notNullOrEmpty(operationConfig.requestUrl)) {
if (StringUtil.startsWithIgnoreCase(operationConfig.requestUrl, "http:")
|| StringUtil.startsWithIgnoreCase(operationConfig.requestUrl, "https:")) {
// no action took
} else {
if (_url.endsWith("/") || _url.endsWith("\\")) {
if (operationConfig.requestUrl.startsWith("/") || operationConfig.requestUrl.startsWith("\\")) {
operationConfig.requestUrl = _url + operationConfig.requestUrl.substring(1);
} else {
operationConfig.requestUrl = _url + operationConfig.requestUrl;
}
} else {
if (operationConfig.requestUrl.startsWith("/") || operationConfig.requestUrl.startsWith("\\")) {
operationConfig.requestUrl = _url + operationConfig.requestUrl;
} else {
operationConfig.requestUrl = _url + "/" + operationConfig.requestUrl;
}
}
}
} else if (_config.requestByOperatioName) {
String operationNameUrl = null;
if (_config.requestUrlNamingPolicy == NamingPolicy.LOWER_CASE_WITH_UNDERSCORE) {
operationNameUrl = ClassUtil.toLowerCaseWithUnderscore(methodName);
} else if (_config.requestUrlNamingPolicy == NamingPolicy.UPPER_CASE_WITH_UNDERSCORE) {
operationNameUrl = ClassUtil.toUpperCaseWithUnderscore(methodName);
} else {
operationNameUrl = methodName;
}
if (_url.endsWith("/") || _url.endsWith("\\")) {
operationConfig.requestUrl = _url + operationNameUrl;
} else {
operationConfig.requestUrl = _url + "/" + operationNameUrl;
}
} else {
operationConfig.requestUrl = _url;
}
if ((N.notNullOrEmpty(_config.getEncryptionUserName()) || N.notNullOrEmpty(_config.getEncryptionPassword()))
&& (N.isNullOrEmpty(operationConfig.getEncryptionUserName()) && N.isNullOrEmpty(operationConfig.getEncryptionPassword()))) {
if (N.isNullOrEmpty(operationConfig.getEncryptionUserName())) {
operationConfig.setEncryptionUserName(_config.getEncryptionUserName());
}
if (N.isNullOrEmpty(operationConfig.getEncryptionPassword())) {
operationConfig.setEncryptionPassword(_config.getEncryptionPassword());
}
if (operationConfig.getEncryptionMessage() == null) {
operationConfig.setEncryptionMessage(_config.getEncryptionMessage());
}
if (operationConfig.getEncryptionMessage() == null) {
operationConfig.setEncryptionMessage(MessageEncryption.NONE);
}
}
}
if (config != null && config.getRequestSettings() != null) {
_config.setRequestSettings(config.getRequestSettings().copy());
}
}
private final AtomicInteger sharedActiveConnectionCounter = new AtomicInteger(0);
private final Map _httpClientPool = new HashMap<>(N.initHashCapacity(_config.operationConfigs.size()));
private final HttpClient _httpClient = HttpClient.create(_url, _maxConnection, _connTimeout, _readTimeout, _config.getRequestSettings(),
sharedActiveConnectionCounter);
private final AsyncExecutor _asyncExecutor = _config.executedByThreadPool
? (_config.asyncExecutor == null ? new AsyncExecutor() : _config.asyncExecutor) : null;
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
final String methodName = method.getName();
final OperationConfig operationConfig = _config.operationConfigs.get(methodName);
if (operationConfig.getRetryTimes() > 0) {
try {
return invoke(method, args);
} catch (Exception e) {
_logger.error("Failed to call: " + method.getName(), e);
final int retryTimes = operationConfig.getRetryTimes();
final long retryInterval = operationConfig.getRetryInterval();
final Function ifRetry = operationConfig.getIfRetry();
int retriedTimes = 0;
Throwable throwable = e;
while (retriedTimes++ < retryTimes && (ifRetry == null || ifRetry.apply(e).booleanValue())) {
try {
if (retryInterval > 0) {
N.sleep(retryInterval);
}
return invoke(method, args);
} catch (Exception e2) {
throwable = e2;
}
}
throw N.toRuntimeException(throwable);
}
} else {
return invoke(method, args);
}
}
private Object invoke(final Method method, final Object[] args) throws InterruptedException, ExecutionException {
if (_config.executedByThreadPool) {
final Callable