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-util-se Show documentation
Show all versions of abacus-util-se Show documentation
A general programming library in Java/Android. It's easy to learn and simple to use with concise and powerful APIs.
/*
* 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.invoke.MethodHandle;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.HttpURLConnection;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
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.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.ParserUtil;
import com.landawn.abacus.parser.ParserUtil.EntityInfo;
import com.landawn.abacus.parser.SerializationConfig;
import com.landawn.abacus.type.Type;
import com.landawn.abacus.util.AndroidUtil;
import com.landawn.abacus.util.Array;
import com.landawn.abacus.util.Charsets;
import com.landawn.abacus.util.ClassUtil;
import com.landawn.abacus.util.IOUtil;
import com.landawn.abacus.util.N;
import com.landawn.abacus.util.NamingPolicy;
import com.landawn.abacus.util.Objectory;
import com.landawn.abacus.util.Result;
import com.landawn.abacus.util.StringUtil;
import com.landawn.abacus.util.Throwables;
import com.landawn.abacus.util.Tuple;
import com.landawn.abacus.util.Tuple.Tuple2;
import com.landawn.abacus.util.URLEncodedUtil;
import com.landawn.abacus.util.WD;
import com.landawn.abacus.util.function.Predicate;
/**
* 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.
*
* @author Haiyang Li
* @since 0.8
*/
public final class HttpProxy {
/** The Constant logger. */
private static final Logger logger = LoggerFactory.getLogger(HttpProxy.class);
/** The Constant DEFAULT_MAX_CONNECTION. */
private static final int DEFAULT_MAX_CONNECTION = AbstractHttpClient.DEFAULT_MAX_CONNECTION;
/** The Constant DEFAULT_CONNECTION_TIMEOUT. */
private static final int DEFAULT_CONNECTION_TIMEOUT = AbstractHttpClient.DEFAULT_CONNECTION_TIMEOUT;
/** The Constant DEFAULT_READ_TIMEOUT. */
private static final int DEFAULT_READ_TIMEOUT = AbstractHttpClient.DEFAULT_READ_TIMEOUT;
/** The Constant PARAM. */
// Upper and lower characters, digits, underscores, and hyphens, starting with a character.
private static final String PARAM = "[a-zA-Z][a-zA-Z0-9_-]*";
/** The Constant PARAM_NAME_REGEX. */
private static final Pattern PARAM_NAME_REGEX = Pattern.compile(PARAM);
/** The Constant PARAM_URL_REGEX. */
private static final Pattern PARAM_URL_REGEX = Pattern.compile("\\{(" + PARAM + ")\\}");
/**
* Creates the client proxy.
*
* @param
* @param interfaceClass
* @param baseUrl
* @return
*/
public static T createClientProxy(final Class interfaceClass, final String baseUrl) {
return createClientProxy(interfaceClass, baseUrl, null);
}
/**
* Creates the client proxy.
*
* @param
* @param interfaceClass
* @param baseUrl
* @param config
* @return
*/
public static T createClientProxy(final Class interfaceClass, final String baseUrl, final Config config) {
final WebService wsAnno = interfaceClass.getAnnotation(WebService.class);
final int maxConnection = wsAnno != null && wsAnno.maxConnection() > 0 ? wsAnno.maxConnection() : DEFAULT_MAX_CONNECTION;
final long connectionTimeout = wsAnno != null && wsAnno.connectionTimeout() > 0 ? wsAnno.connectionTimeout() : DEFAULT_CONNECTION_TIMEOUT;
final long readTimeout = wsAnno != null && wsAnno.readTimeout() > 0 ? wsAnno.readTimeout() : DEFAULT_READ_TIMEOUT;
return createClientProxy(interfaceClass, baseUrl, maxConnection, connectionTimeout, readTimeout, config);
}
/**
* Creates the client proxy.
*
* @param
* @param interfaceClass
* @param baseUrl
* @param maxConnection
* @param connectionTimeout
* @param readTimeout
* @return
*/
public static T createClientProxy(final Class interfaceClass, final String baseUrl, final int maxConnection, final long connectionTimeout,
final long readTimeout) {
return createClientProxy(interfaceClass, baseUrl, maxConnection, connectionTimeout, readTimeout, null);
}
/**
*
* @param
* @param interfaceClass
* @param baseUrl
* @param maxConnection
* @param connectionTimeout
* @param readTimeout
* @param config
* @return
*/
public static T createClientProxy(final Class interfaceClass, final String baseUrl, final int maxConnection, final long connectionTimeout,
final long readTimeout, final Config config) {
return createClientProxy(interfaceClass, null, baseUrl, maxConnection, connectionTimeout, readTimeout, config);
}
/**
* Creates the client proxy.
*
* @param
* @param interfaceClass
* @param contentformat add for test only
* @param baseUrl
* @param maxConnection
* @param connectionTimeout
* @param readTimeout
* @param config
* @return
*/
static T createClientProxy(final Class interfaceClass, final ContentFormat contentformat, final String baseUrl, final int maxConnection,
final long connectionTimeout, final long readTimeout, final Config config) {
N.checkArgNotNull(interfaceClass, "interfaceClass");
N.checkArgPositive(maxConnection == 0 ? DEFAULT_MAX_CONNECTION : maxConnection, "maxConnection");
N.checkArgPositive(connectionTimeout == 0 ? DEFAULT_CONNECTION_TIMEOUT : connectionTimeout, "connectionTimeout");
N.checkArgPositive(readTimeout == 0 ? DEFAULT_READ_TIMEOUT : readTimeout, "readTimeout");
final Annotation[] interfaceAnnos = interfaceClass.getAnnotations();
final WebService wsAnno = interfaceClass.getAnnotation(WebService.class);
final String finalBaseUrl = N.isNullOrEmpty(baseUrl) && wsAnno != null ? wsAnno.baseUrl() : baseUrl;
N.checkArgNotNull(finalBaseUrl, "baseUrl");
InvocationHandler h = new InvocationHandler() {
private final Logger _logger = LoggerFactory.getLogger(interfaceClass);
private final String _baseUrl = composeUrl(finalBaseUrl, HttpUtil.getHttpPath(interfaceAnnos));
private final int _maxConnection = maxConnection == 0 ? DEFAULT_MAX_CONNECTION : maxConnection;
private final long _connectionTimeout = connectionTimeout == 0 ? DEFAULT_CONNECTION_TIMEOUT : connectionTimeout;
private final long _readTimeout = readTimeout == 0 ? DEFAULT_READ_TIMEOUT : readTimeout;
private final ContentFormat _contentFormat = contentformat == null || contentformat == ContentFormat.NONE
? HttpUtil.getContentFormat(interfaceAnnos, ContentFormat.JSON)
: contentformat;
private final ContentFormat _acceptFormat = HttpUtil.getAcceptFormat(interfaceAnnos, ContentFormat.JSON);
private final String _contentCharset = wsAnno == null ? null : wsAnno.contentCharset();
private final String _acceptCharset = wsAnno == null ? null : wsAnno.accepCharset();
private final int _maxRetryTimes = wsAnno == null ? 0 : wsAnno.maxRetryTimes();
private final long _retryInterval = wsAnno == null ? 0 : wsAnno.retryInterval();
private final Map _httpHeaders = HttpUtil.getHttpHeaders(interfaceAnnos);
private final Config _config = config == null ? new Config() : N.copy(config);
private boolean _hasFutureReturnType = false;
{
final Set declaredMethods = N.asLinkedHashSet(interfaceClass.getDeclaredMethods());
for (Class> superClass : interfaceClass.getInterfaces()) {
declaredMethods.addAll(Arrays.asList(superClass.getDeclaredMethods()));
}
final Set declaredMethodNames = new HashSet<>(declaredMethods.size());
for (Method method : declaredMethods) {
declaredMethodNames.add(method.getName());
}
if (_config.parser == null) {
_config.setParser(HttpUtil.getParser(_contentFormat));
}
if (config != null && config.getRequestSettings() != null) {
_config.setRequestSettings(config.getRequestSettings().copy());
} else {
_config.setRequestSettings(HttpSettings.create());
}
// 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()) {
if (!declaredMethodNames.contains(entry.getKey())) {
throw new IllegalArgumentException("No method found by name: " + entry.getKey() + " for OperationConfig");
}
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();
}
if (operationConfig.getRequestSettings() == null) {
if (_config.getRequestSettings() != null) {
operationConfig.setRequestSettings(_config.getRequestSettings().copy());
} else {
operationConfig.setRequestSettings(HttpSettings.create());
}
}
_config.methodConfigs.put(method, operationConfig);
operationConfig.requestEntityName = StringUtil.capitalize(methodName) + "Request";
operationConfig.responseEntityName = StringUtil.capitalize(methodName) + "Response";
final Annotation[] methdAnnos = method.getAnnotations();
final WebMethod wmAnnno = method.getAnnotation(WebMethod.class);
operationConfig.httpMethod = HttpUtil.getHttpMethod(method);
operationConfig.path = HttpUtil.getHttpPath(method);
operationConfig.requestUrl = composeUrl(_baseUrl, operationConfig.path);
operationConfig.connectionTimeout = wmAnnno == null || wmAnnno.connectionTimeout() <= 0 ? _connectionTimeout : wmAnnno.connectionTimeout();
operationConfig.readTimeout = wmAnnno == null || wmAnnno.readTimeout() <= 0 ? _readTimeout : wmAnnno.readTimeout();
operationConfig.maxRetryTimes = wmAnnno == null || wmAnnno.maxRetryTimes() < 0 ? _maxRetryTimes : wmAnnno.maxRetryTimes();
operationConfig.retryInterval = wmAnnno == null || wmAnnno.retryInterval() < 0 ? _retryInterval : wmAnnno.retryInterval();
operationConfig.contentFormat = HttpUtil.getContentFormat(methdAnnos, _contentFormat);
operationConfig.acceptFormat = HttpUtil.getAcceptFormat(methdAnnos, _acceptFormat);
operationConfig.contentCharset = wmAnnno == null || N.isNullOrEmpty(wmAnnno.contentCharset()) ? _contentCharset : wmAnnno.contentCharset();
operationConfig.acceptCharset = wmAnnno == null || N.isNullOrEmpty(wmAnnno.acceptCharset()) ? _acceptCharset : wmAnnno.acceptCharset();
if (N.notNullOrEmpty(_httpHeaders)) {
operationConfig.getRequestSettings().headers(_httpHeaders);
}
if (wmAnnno != null && N.notNullOrEmpty(wmAnnno.headers())) {
operationConfig.getRequestSettings().headers(N. asMap((Object[]) wmAnnno.headers()));
}
operationConfig.getRequestSettings().setContentFormat(operationConfig.contentFormat);
String contentType = HttpUtil.getContentType(operationConfig.requestSettings);
if (N.isNullOrEmpty(contentType)) {
if (operationConfig.contentFormat != null && operationConfig.contentFormat != ContentFormat.NONE) {
contentType = operationConfig.contentFormat.contentType();
operationConfig.requestSettings.header(HttpHeaders.Names.CONTENT_TYPE, contentType);
}
}
if (N.notNullOrEmpty(operationConfig.contentCharset)) {
if (N.isNullOrEmpty(contentType)) {
operationConfig.requestSettings.header(HttpHeaders.Names.CONTENT_TYPE, "charset=" + operationConfig.contentCharset);
} else if (StringUtil.indexOfIgnoreCase(contentType, "charset=") < 0) {
operationConfig.requestSettings.header(HttpHeaders.Names.CONTENT_TYPE, contentType + "; charset=" + operationConfig.contentCharset);
}
}
if (N.isNullOrEmpty(HttpUtil.getContentEncoding(operationConfig.requestSettings))) {
if (operationConfig.contentFormat != null && operationConfig.contentFormat != ContentFormat.NONE) {
operationConfig.requestSettings.header(HttpHeaders.Names.CONTENT_ENCODING, operationConfig.contentFormat.contentEncoding());
}
}
if (N.isNullOrEmpty(HttpUtil.getAccept(operationConfig.requestSettings))) {
if (operationConfig.acceptFormat != null && operationConfig.acceptFormat != ContentFormat.NONE) {
operationConfig.requestSettings.header(HttpHeaders.Names.ACCEPT, operationConfig.acceptFormat.contentType());
}
}
if (N.isNullOrEmpty(HttpUtil.getAcceptEncoding(operationConfig.requestSettings))) {
if (operationConfig.acceptFormat != null && operationConfig.acceptFormat != ContentFormat.NONE) {
operationConfig.requestSettings.header(HttpHeaders.Names.ACCEPT_ENCODING, operationConfig.acceptFormat.contentEncoding());
}
}
if (N.isNullOrEmpty(HttpUtil.getAcceptCharset(operationConfig.requestSettings))) {
if (N.notNullOrEmpty(operationConfig.acceptCharset)) {
operationConfig.requestSettings.header(HttpHeaders.Names.ACCEPT_CHARSET, operationConfig.acceptCharset);
}
}
operationConfig.parser = operationConfig.contentFormat == _contentFormat ? _config.parser
: HttpUtil.getParser(operationConfig.contentFormat);
if (operationConfig.parser.getClass().equals(_config.parser.getClass())) {
operationConfig.sc = _config.sc;
operationConfig.dc = _config.dc;
} else {
operationConfig.sc = null;
operationConfig.dc = null;
}
{
operationConfig.paramTypes = new Type[parameterCount];
operationConfig.fieldParams = new Field[parameterCount];
operationConfig.fieldNameSet = new HashSet<>(parameterCount);
operationConfig.pathParams = new Tuple2[parameterCount];
operationConfig.pathParamNameSet = new HashSet<>(parameterCount);
operationConfig.queryParams = new String[parameterCount];
operationConfig.queryParamNameSet = new HashSet<>(parameterCount);
operationConfig.pathAndQueryParamNameSet = new HashSet<>(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.fieldParams[i] = (Field) parameterAnnotation;
if (operationConfig.paramNameTypeMap.put(operationConfig.fieldParams[i].value(),
Tuple.of(i, operationConfig.paramTypes[i])) != null) {
throw new IllegalArgumentException("Duplicated field parameter names: " + operationConfig.fieldParams[i].value()
+ " in method: " + method.getName());
}
operationConfig.fieldNameSet.add(operationConfig.fieldParams[i].value());
} else if (parameterAnnotation.annotationType() == PathParam.class) {
final PathParam pathParam = (PathParam) parameterAnnotation;
operationConfig.validatePathName(pathParam.value());
operationConfig.pathParams[i] = Tuple.of(pathParam.value(), pathParam.encode());
if (operationConfig.paramNameTypeMap.put(operationConfig.pathParams[i]._1,
Tuple.of(i, operationConfig.paramTypes[i])) != null) {
throw new IllegalArgumentException(
"Duplicated path parameter names: " + operationConfig.pathParams[i]._1 + " in method: " + method.getName());
}
operationConfig.pathParamNameSet.add(operationConfig.pathParams[i]._1);
operationConfig.pathAndQueryParamNameSet.add(operationConfig.pathParams[i]._1);
} else if (parameterAnnotation.annotationType() == QueryParam.class) {
final QueryParam queryParam = (QueryParam) parameterAnnotation;
operationConfig.queryParams[i] = queryParam.value();
if (operationConfig.paramNameTypeMap.put(operationConfig.queryParams[i],
Tuple.of(i, operationConfig.paramTypes[i])) != null) {
throw new IllegalArgumentException(
"Duplicated query parameter names: " + operationConfig.queryParams[i] + " in method: " + method.getName());
}
operationConfig.queryParamNameSet.add(operationConfig.queryParams[i]);
operationConfig.pathAndQueryParamNameSet.add(operationConfig.queryParams[i]);
} else {
try {
if (parameterAnnotation.annotationType() == javax.ws.rs.PathParam.class) {
final javax.ws.rs.PathParam pathParam = (javax.ws.rs.PathParam) parameterAnnotation;
operationConfig.validatePathName(pathParam.value());
operationConfig.pathParams[i] = Tuple.of(pathParam.value(), true);
if (operationConfig.paramNameTypeMap.put(operationConfig.pathParams[i]._1,
Tuple.of(i, operationConfig.paramTypes[i])) != null) {
throw new IllegalArgumentException("Duplicated path parameter names: " + operationConfig.pathParams[i]._1
+ " in method: " + method.getName());
}
operationConfig.pathParamNameSet.add(operationConfig.pathParams[i]._1);
operationConfig.pathAndQueryParamNameSet.add(operationConfig.pathParams[i]._1);
}
} catch (Throwable e) {
// ignore
}
try {
if (parameterAnnotation.annotationType() == javax.ws.rs.QueryParam.class) {
final javax.ws.rs.QueryParam queryParam = (javax.ws.rs.QueryParam) parameterAnnotation;
operationConfig.queryParams[i] = queryParam.value();
if (operationConfig.paramNameTypeMap.put(operationConfig.queryParams[i],
Tuple.of(i, operationConfig.paramTypes[i])) != null) {
throw new IllegalArgumentException("Duplicated query parameter names: " + operationConfig.queryParams[i]
+ " in method: " + method.getName());
}
operationConfig.queryParamNameSet.add(operationConfig.queryParams[i]);
operationConfig.pathAndQueryParamNameSet.add(operationConfig.queryParams[i]);
}
} catch (Throwable e) {
// ignore
}
}
if (operationConfig.fieldParams[i] == null && operationConfig.pathParams[i] == null && operationConfig.queryParams[i] == null) {
throw new IllegalArgumentException("Parameter is not named by annoation @Field/@FieldParam/@QueryParam at position: " + i
+ " in method: " + method.getName());
}
}
}
operationConfig.urlPartsSplittedByParaNames = operationConfig.requestUrl.split(PARAM_URL_REGEX.pattern());
if (operationConfig.urlPartsSplittedByParaNames.length > 1) {
final List patterns = new ArrayList<>(operationConfig.urlPartsSplittedByParaNames.length);
final Matcher m = PARAM_URL_REGEX.matcher(operationConfig.requestUrl);
while (m.find()) {
patterns.add(m.group(1));
}
operationConfig.urlParamNames = patterns.toArray(new String[patterns.size()]);
} else {
operationConfig.urlParamNames = N.EMPTY_STRING_ARRAY;
}
operationConfig.urlParamNameSet = N.asSet(operationConfig.urlParamNames);
if (N.notNullOrEmpty(operationConfig.paramNameTypeMap)) {
final List diff = N.symmetricDifference(operationConfig.urlParamNameSet, operationConfig.pathParamNameSet);
if (N.notNullOrEmpty(diff)) {
throw new IllegalArgumentException("Path parameters: " + diff + " are not configured in path: " + operationConfig.path
+ " in method: " + method.getName());
}
}
}
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(_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);
}
}
operationConfig.returnType = N.typeOf(ClassUtil.formatParameterizedTypeName(method.getGenericReturnType().toString()));
operationConfig.concreteReturnType = Future.class.isAssignableFrom(method.getReturnType())
? (Type
© 2015 - 2025 Weber Informatics LLC | Privacy Policy