package com.gustz.dove.cli.api.comm.service.impl;
import java.io.BufferedInputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import com.gustz.dove.cli.api.service.conf.AsyncHttpCliConf;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.sinovatech.rd.wcsb.cli.api.service.AsyncHttpCliService;
import com.gustz.dove.cli.api.service.conf.BaseWcsbConf;
import com.sinovatech.rd.wcsb.cli.api.service.dict.RspCodeDict;
import com.sinovatech.rd.wcsb.cli.api.service.vo.Attachment;
import com.sinovatech.rd.wcsb.cli.api.service.vo.UploadFileForm;
/**
*
* TODO: Async HTTP client service impl
*
* @author ZHENFENG ZHANG
* @since [ Aug 9, 2015 ]
*/
@Service
public class AsyncHttpCliServiceImpl implements AsyncHttpCliService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private BaseWcsbConf baseWcsbConf;
@Autowired
private AsyncHttpCliConf asyncHttpCliConf;
private CloseableHttpAsyncClient asyncHttpClient;
private Charset charset;
private RequestConfig requestConfig;
private IdlePoolingNConnEvictor connEvictor;
/**
* Init config
*/
@PostConstruct
private void init() {
try {
// set config
charset = Charset.forName(baseWcsbConf.getDefaultCharset());
int connTimeoutMs = asyncHttpCliConf.getConnTimeoutMs();
int soTimeoutMs = asyncHttpCliConf.getSoTimeoutMs();
int maxTotal = asyncHttpCliConf.getMaxTotal();
int maxPerRoute = asyncHttpCliConf.getMaxPerRoute();
int connRequestTimeoutMs = asyncHttpCliConf.getConnRequestTimeoutMs();
//
// Create I/O reactor configuration
IOReactorConfig ioReactorConfig = IOReactorConfig.custom() //
.setIoThreadCount(Runtime.getRuntime().availableProcessors()) //
.setConnectTimeout(connTimeoutMs) //
.setSoTimeout(soTimeoutMs) //
.build();
// Create a custom I/O reactort
ConnectingIOReactor ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
// Create a connection manager with custom configuration.
PoolingNHttpClientConnectionManager connMgr = new PoolingNHttpClientConnectionManager(ioReactor);
connMgr.setMaxTotal(maxTotal); // max number of connections allowed
connMgr.setDefaultMaxPerRoute(maxPerRoute); // max number of connections allowed per route
//
asyncHttpClient = HttpAsyncClientBuilder.create().setConnectionManager(connMgr).build();
//
requestConfig = RequestConfig.custom() //
.setSocketTimeout(soTimeoutMs) //
.setConnectTimeout(connTimeoutMs) //
.setConnectionRequestTimeout(connRequestTimeoutMs) //
.build();
//
connEvictor = new IdlePoolingNConnEvictor(connMgr);
connEvictor.start();
} catch (Exception e) {
throw new Error("Init async HttpClient is fail. \n", e);
}
}
@PreDestroy
private void dostory() {
try {
if (connEvictor != null) {
connEvictor.shutdown();
}
if (asyncHttpClient != null) {
asyncHttpClient.close();
}
} catch (Exception e) {
throw new Error("Dostory async HttpClient is fail. \n", e);
}
}
/**
*
* Get request
*
* @param url
* @return
* @throws Exception
*/
@Override
public String get(String url) throws Exception {
//
return get(charset, url, null);
}
/**
*
* Get request
*
* @param charset
* @param url
* @param headerMap
* @return
* @throws Exception
*/
@Override
public String get(Charset charset, String url, Map<String, String> headerMap) throws Exception {
HttpGet httpGet = new HttpGet(url);
ContentTypeEnum contentType = null;
if (headerMap != null && !headerMap.isEmpty()) {
for (Map.Entry<String, String> _entry : headerMap.entrySet()) {
httpGet.setHeader(_entry.getKey(), _entry.getValue());
}
} else {
contentType = ContentTypeEnum.HTML;
}
return executeToStr(charset, httpGet, contentType);
}
/**
*
* Get request
*
* @param url
* @param paramMap
* @return
* @throws Exception
*/
@Override
public String get(String url, Map<String, String> paramMap) throws Exception {
return get(charset, url, paramMap, null);
}
/**
*
* Get request
*
* @param charset
* @param url
* @param paramMap
* @param headerMap
* @return
* @throws Exception
*/
@Override
public String get(Charset charset, String url, Map<String, String> paramMap, Map<String, String> headerMap) throws Exception {
if (paramMap != null && !paramMap.isEmpty()) {
StringBuffer sbf = new StringBuffer(url);
List<NameValuePair> ps = new ArrayList<NameValuePair>();
for (Map.Entry<String, String> _entry : paramMap.entrySet()) {
ps.add(new BasicNameValuePair(_entry.getKey(), _entry.getValue()));
}
sbf.append("?");
sbf.append(URLEncodedUtils.format(ps, charset));
url = sbf.toString();
}
//
ContentTypeEnum contentType = null;
HttpGet httpGet = new HttpGet(url);
if (headerMap != null && !headerMap.isEmpty()) {
for (Map.Entry<String, String> _entry : headerMap.entrySet()) {
httpGet.setHeader(_entry.getKey(), _entry.getValue());
}
} else {
contentType = ContentTypeEnum.HTML;
}
return executeToStr(charset, httpGet, contentType);
}
/**
* Post request
*
* @param url
* @param param
* @return
* @throws Exception
*/
@Override
public String post(String url, String param) throws Exception {
//
return post(charset, url, param, null);
}
/**
* Post request
*
* @param charset
* @param url
* @param param
* @param headerMap
* @return
* @throws Exception
*/
@Override
public String post(Charset charset, String url, String param, Map<String, String> headerMap) throws Exception {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(param, charset));
//
ContentTypeEnum contentType = null;
if (headerMap != null && !headerMap.isEmpty()) {
for (Map.Entry<String, String> _entry : headerMap.entrySet()) {
httpPost.setHeader(_entry.getKey(), _entry.getValue());
}
} else {
contentType = ContentTypeEnum.HTML;
}
//
return executeToStr(charset, httpPost, contentType);
}
/**
* Post request
*
* @param url
* @param paramMap
* @return
* @throws Exception
*/
@Override
public String post(String url, Map<String, String> paramMap) throws Exception {
//
return post(charset, url, paramMap, null);
}
/**
* Post request
*
* @param charset
* @param url
* @param paramMap
* @param headerMap
* @return
* @throws Exception
*/
@Override
public String post(Charset charset, String url, Map<String, String> paramMap, Map<String, String> headerMap) throws Exception {
AbstractHttpEntity httpEntity = null;
if (paramMap != null && !paramMap.isEmpty()) {
List<NameValuePair> formParams = new ArrayList<NameValuePair>();
for (Map.Entry<String, String> _entry : paramMap.entrySet()) {
formParams.add(new BasicNameValuePair(_entry.getKey(), _entry.getValue()));
}
httpEntity = new UrlEncodedFormEntity(formParams, charset);
}
//
HttpPost httpPost = new HttpPost(url);
if (httpEntity != null) {
httpPost.setEntity(httpEntity);
}
ContentTypeEnum contentType = null;
if (headerMap != null && !headerMap.isEmpty()) {
for (Map.Entry<String, String> _entry : headerMap.entrySet()) {
httpPost.setHeader(_entry.getKey(), _entry.getValue());
}
} else {
contentType = ContentTypeEnum.HTML;
}
//
return executeToStr(charset, httpPost, contentType);
}
/**
* Executing and to string
*
* @param charset
* @param requestBase
* @param contentType
* @return
* @throws Exception
*/
private String executeToStr(Charset charset, HttpRequestBase requestBase, ContentTypeEnum contentType) throws Exception {
return EntityUtils.toString(this.execute(charset, requestBase, contentType).getEntity(), charset);
}
/**
* Executing
*
* @param charset
* @param requestBase
* @param contentType
* @return
* @throws Exception
*/
private HttpResponse execute(Charset charset, HttpRequestBase requestBase, ContentTypeEnum contentType) throws Exception {
HttpResponse response = null;
try {
requestBase.setConfig(requestConfig);
this.setDyncHeader(charset, requestBase, contentType);
//
asyncHttpClient.start();
logger.info("Executing request line: {}", requestBase.getRequestLine());
//
Future<HttpResponse> future = asyncHttpClient.execute(requestBase, null);
//
response = future.get();
logger.info("Response status line: {}", response.getStatusLine());
assertStatus(response);
return response;
} finally {
requestBase.releaseConnection();
}
}
/**
* Post request for SOAP XML
*
* @param url
* @param tns
* @param methodName
* @param xml
* @return
* @throws Exception
*/
@Override
public String postSoapXml(String url, String tns, String methodName, String xml) throws Exception {
// SOAP XML
xml = getRequestSoapXml(tns, methodName, StringEscapeUtils.escapeXml10(xml));
//
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(xml, charset));
//
String rspMsg = executeToStr(charset, httpPost, ContentTypeEnum.SOAP_XML);
// format response message
if (rspMsg != null && !rspMsg.isEmpty()) {
if (rspMsg.indexOf("<return>") > 0) { // CXF
rspMsg = rspMsg.substring(rspMsg.indexOf("<return>") + 8, rspMsg.lastIndexOf("</return>"));
} else if (rspMsg.indexOf(":out>") > 0) { //XFIRE
rspMsg = rspMsg.substring(rspMsg.indexOf(":out>") + 5, rspMsg.lastIndexOf(":out>") - 5);
}
}
return StringEscapeUtils.unescapeXml(rspMsg);
}
/**
* Upload file of post request
*
* @param url
* @param fileForms
* @return
* @throws Exception
*/
@Override
public String uploadFile(String url, UploadFileForm[] fileForms) throws Exception {
CloseableHttpClient httpclient = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost(url);
try {
httpPost.setHeader("connection", "Keep-Alive");
// multi file
MultipartEntityBuilder builder = MultipartEntityBuilder.create() //
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
//
for (int i = 0; i < fileForms.length; i++) {
UploadFileForm form = fileForms[i];
// file
builder.addBinaryBody(form.getFileKey(), form.getFileValue());
// text
Map<String, String> _map = form.getTextBody();
if (_map == null || _map.isEmpty()) {
continue;
}
for (Map.Entry<String, String> _entry : _map.entrySet()) {
builder.addTextBody(_entry.getKey(), _entry.getValue(), ContentType.TEXT_PLAIN.withCharset(charset));
}
}
httpPost.setEntity(builder.build());
//
HttpResponse rsp = httpclient.execute(httpPost);
//
return EntityUtils.toString(rsp.getEntity(), charset);
} finally {
httpPost.releaseConnection();
httpclient.close();
}
}
/**
* Download file of get request
*
* @param url
* @return
* @throws Exception
*/
@Override
public Attachment downloadFile(String url) throws Exception {
Attachment atta = new Attachment();
HttpGet httpGet = new HttpGet(url);
// do
HttpResponse rsp = this.execute(charset, httpGet, ContentTypeEnum.HTML);
HttpEntity httpEntity = rsp.getEntity();
if (httpEntity.getContentType().getValue().equalsIgnoreCase("text/plain")) { // error file
atta.setRspCode(RspCodeDict.A0006);
} else { // ok
String fullName = null;
String relName = null;
String suffix = null;
Header dispHeader = rsp.getFirstHeader("Content-disposition");
if (dispHeader != null) {
String disp = dispHeader.getValue();
//
fullName = disp.substring(disp.indexOf("filename=\"") + 10, disp.length() - 1);
relName = fullName.substring(0, fullName.lastIndexOf("."));
suffix = fullName.substring(relName.length() + 1);
}
// set
atta.setFullName(fullName);
atta.setFileName(relName);
atta.setSuffix(suffix);
atta.setContentLength(httpEntity.getContentLength());
atta.setContentType(httpEntity.getContentType().getValue());
atta.setFileStream(new BufferedInputStream(httpEntity.getContent()));
}
return atta;
}
/**
* Get request SOAP XML
*
* @param tns
* @param methodName
* @param params
* @return
*/
private String getRequestSoapXml(String tns, String methodName, String... params) {
StringBuilder sbd = new StringBuilder();
sbd.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
sbd.append("<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
sbd.append(" xmlns:sam=\"").append(((tns != null && !tns.endsWith("/")) ? tns + "/" : tns)).append("\" "); // targetNamespace
sbd.append(" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"");
sbd.append(" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">");
sbd.append("<soap:Header/>");
sbd.append("<soap:Body>");
sbd.append("<sam:").append(methodName).append(">"); // method name
// arguments begin
for (int i = 0, len = params.length; i < len; i++) {
sbd.append("<arg").append(i).append(">").append(params[i]).append("</arg").append(i).append(">");
}
// arguments end
sbd.append("</sam:").append(methodName).append(">");
sbd.append("</soap:Body>");
sbd.append("</soap:Envelope>");
return sbd.toString();
}
private void assertStatus(HttpResponse response) {
if (response == null) {
throw new IllegalArgumentException("HttpResponse is null.");
}
if (response.getStatusLine() == null) {
throw new IllegalArgumentException("HttpResponse status is null.");
}
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
throw new IllegalStateException("Http response is fail.status code is " + statusCode);
}
}
private void setDyncHeader(Charset charset, HttpRequestBase requestBase, ContentTypeEnum contentType) {
requestBase.setHeader(HTTP.CONTENT_ENCODING, charset.toString());
if (contentType != null) {
requestBase.setHeader(HttpHeaders.ACCEPT, contentType.getValue());
requestBase.setHeader(HTTP.CONTENT_TYPE, contentType.getValue() + ";charset=" + charset);
}
}
private enum ContentTypeEnum {
/**
* SOAP XML content type
*/
SOAP_XML("application/soap+xml"),
HTML("text/html");
private final String value;
private ContentTypeEnum(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
public static final class IdlePoolingNConnEvictor {
private final ThreadFactory threadFactory;
private final Thread thread;
private final long sleepTimeMs;
private final long maxIdleTimeMs;
private volatile boolean shutdown;
public IdlePoolingNConnEvictor(final PoolingNHttpClientConnectionManager connMgr, final ThreadFactory threadFactory,
final long sleepTime, final TimeUnit sleepTimeUnit, final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
if (connMgr == null) {
throw new IllegalArgumentException("PoolingNHttpClient connection manager may not be null");
}
//
this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
this.thread = this.threadFactory.newThread(new Runnable() {
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(sleepTimeMs);
connMgr.closeExpiredConnections();
if (maxIdleTimeMs > 0) {
connMgr.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
}
}
}
} catch (Exception e) {
throw new Error("Connection evictor", e);
}
}
});
}
public IdlePoolingNConnEvictor(final PoolingNHttpClientConnectionManager connMgr, final long sleepTime,
final TimeUnit sleepTimeUnit, final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this(connMgr, null, sleepTime, sleepTimeUnit, maxIdleTime, maxIdleTimeUnit);
}
public IdlePoolingNConnEvictor(final PoolingNHttpClientConnectionManager connMgr, final long maxIdleTime,
final TimeUnit maxIdleTimeUnit) {
this(connMgr, null, maxIdleTime > 0 ? maxIdleTime : 5, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
maxIdleTime, maxIdleTimeUnit);
}
public IdlePoolingNConnEvictor(final PoolingNHttpClientConnectionManager connMgr) {
this(connMgr, 5L, TimeUnit.SECONDS);
}
public void start() {
thread.start();
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
public boolean isRunning() {
return thread.isAlive();
}
public void awaitTermination(final long time, final TimeUnit tunit) throws InterruptedException {
thread.join((tunit != null ? tunit : TimeUnit.MILLISECONDS).toMillis(time));
}
static class DefaultThreadFactory implements ThreadFactory {
@Override
public Thread newThread(final Runnable r) {
final Thread t = new Thread(r, "Connection evictor");
t.setDaemon(true);
return t;
}
};
}
}