
org.brutusin.rpc.client.http.HttpEndpoint Maven / Gradle / Ivy
/*
* Copyright 2016 Ignacio del Valle Alles [email protected].
*
* 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 org.brutusin.rpc.client.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.impl.client.cache.CacheConfig;
import org.apache.http.impl.client.cache.CachingHttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.brutusin.commons.io.MetaDataInputStream;
import org.brutusin.commons.utils.Miscellaneous;
import org.brutusin.json.ParseException;
import org.brutusin.json.spi.JsonCodec;
import org.brutusin.json.spi.JsonNode;
import org.brutusin.rpc.RpcErrorCode;
import org.brutusin.rpc.RpcRequest;
import org.brutusin.rpc.RpcResponse;
import org.brutusin.rpc.client.ProgressCallback;
import org.brutusin.rpc.http.CachingInfo;
import org.brutusin.rpc.http.HttpServiceItem;
/**
*
* @author Ignacio del Valle Alles [email protected]
*/
public class HttpEndpoint {
private static final String JSON_CONTENT_TYPE = "application/json";
private static final Logger LOGGER = Logger.getLogger(HttpEndpoint.class.getName());
private final ThreadLocal contexts = new ThreadLocal();
private volatile boolean loaded;
private final URI endpoint;
private enum HttpMethod {
GET, POST, PUT
}
private final CloseableHttpClient httpClient;
private final HttpClientContextFactory clientContextFactory;
private Map services;
private Thread pingThread;
public HttpEndpoint(URI endpoint) {
this(endpoint, (Config) null, null);
}
public HttpEndpoint(URI endpoint, HttpClientContextFactory clientContextFactory) {
this(endpoint, (Config) null, clientContextFactory);
}
public HttpEndpoint(URI endpoint, Config cfg, HttpClientContextFactory clientContextFactory) {
if (endpoint == null) {
throw new IllegalArgumentException("Endpoint is required");
}
if (cfg == null) {
cfg = new ConfigurationBuilder().build();
}
CacheConfig cacheConfig = CacheConfig.custom()
.setMaxCacheEntries(cfg.getMaxCacheEntries())
.setMaxObjectSize(cfg.getMaxCacheObjectSize())
.build();
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(1000 * cfg.getConnectTimeOutSeconds())
.setSocketTimeout(1000 * cfg.getSocketTimeOutSeconds())
.build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(cfg.getMaxConections());
this.endpoint = endpoint;
this.httpClient = CachingHttpClients.custom()
.setCacheConfig(cacheConfig)
.setDefaultRequestConfig(requestConfig)
.setRetryHandler(new StandardHttpRequestRetryHandler())
.setConnectionManager(cm)
.build();
this.clientContextFactory = clientContextFactory;
initPingThread(cfg.getPingSeconds());
}
public HttpEndpoint(URI endpoint, CloseableHttpClient httpClient) throws IOException {
this(endpoint, httpClient, null);
}
public HttpEndpoint(URI endpoint, CloseableHttpClient httpClient, HttpClientContextFactory clientContextFactory) throws IOException {
if (endpoint == null) {
throw new IllegalArgumentException("Endpoint is required");
}
if (httpClient == null) {
throw new IllegalArgumentException("HttpClient is required");
}
this.endpoint = endpoint;
this.httpClient = httpClient;
this.clientContextFactory = clientContextFactory;
initPingThread(new ConfigurationBuilder().build().getPingSeconds());
}
private void initPingThread(final int pingSeconds) {
this.pingThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
try {
Thread.sleep(1000 * pingSeconds);
try {
CloseableHttpResponse resp = doExec("rpc.http.ping", null, HttpMethod.GET, null);
resp.close();
} catch (ConnectException ex) {
LOGGER.log(Level.SEVERE, ex.getMessage() + " (" + HttpEndpoint.this.endpoint + ")");
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, ex.getMessage() + " (" + HttpEndpoint.this.endpoint + ")", ex);
}
} catch (InterruptedException ie) {
break;
}
}
}
};
this.pingThread.setDaemon(true);
this.pingThread.start();
}
private void loadServices() throws IOException {
CloseableHttpResponse servicesResp = doExec("rpc.http.services", null, HttpMethod.GET, null);
if (servicesResp.getStatusLine().getStatusCode() != 200) {
throw new RuntimeException("Server returned status code " + servicesResp.getStatusLine().getStatusCode());
}
if (!servicesResp.getEntity().getContentType().getValue().contains(JSON_CONTENT_TYPE)) {
throw new RuntimeException("Server returned response of type " + servicesResp.getEntity().getContentType().getValue() + ":\n" + Miscellaneous.toString(servicesResp.getEntity().getContent(), "UTF-8"));
}
HashMap serviceMap = new HashMap();
try {
JsonNode node = JsonCodec.getInstance().parse(Miscellaneous.toString(servicesResp.getEntity().getContent(), "UTF-8")).get("result");
for (int i = 0; i < node.getSize(); i++) {
JsonNode service = node.get(i);
HttpServiceItem si = JsonCodec.getInstance().load(service, HttpServiceItem.class);
serviceMap.put(si.getId(), si);
}
this.services = Collections.unmodifiableMap(serviceMap);
} catch (ParseException ex) {
throw new RuntimeException(ex);
}
}
public Map getServices() {
if (!loaded) {
synchronized (this) {
if (!loaded) {
try {
loadServices();
} catch (IOException iOException) {
throw new RuntimeException(iOException);
}
loaded = true;
}
}
}
return services;
}
private static Map sortFiles(final Map files) {
if (files == null) {
return null;
}
Comparator comparator = new Comparator() {
public int compare(String s1, String s2) {
if (s1.equals(s2)) {
return 0;
}
InputStream f1 = files.get(s1);
InputStream f2 = files.get(s2);
if (f1 == null || !(f1 instanceof MetaDataInputStream) || ((MetaDataInputStream) f1).getLength() == null) {
return -1;
}
if (f2 == null || !(f2 instanceof MetaDataInputStream) || ((MetaDataInputStream) f2).getLength() == null) {
return 1;
}
return Long.compare(((MetaDataInputStream) f1).getLength(), ((MetaDataInputStream) f2).getLength());
}
};
Map ret = new TreeMap(comparator);
ret.putAll(files);
return ret;
}
private static String getAttachmentFileName(Header contentDiposition) {
if (contentDiposition == null) {
return null;
}
String value = contentDiposition.getValue();
Pattern pat = Pattern.compile(".*filename[^;=\n]*=((['\"]).*?\\2|[^;\n]*)");
Matcher matcher = pat.matcher(value);
if (matcher.matches()) {
return matcher.group(1);
}
return null;
}
private static HttpMethod getMethod(HttpServiceItem si) {
if (si == null) {
return null;
}
if (si.isSafe()) {
return HttpMethod.GET;
} else {
if (si.isIdempotent()) {
return HttpMethod.PUT;
} else {
return HttpMethod.POST;
}
}
}
/**
*
* @param serviceId
* @param input supports inputstreams
* @param progressCallback
* @return
* @throws IOException
*/
public final HttpResponse exec(final String serviceId, final JsonNode input, final ProgressCallback progressCallback) throws IOException {
if (!loaded) {
synchronized (this) {
if (!loaded) {
loadServices();
loaded = true;
}
}
}
final HttpMethod method = getMethod(services.get(serviceId));
if (method == null) {
throw new IllegalArgumentException("Invalid service id " + serviceId);
}
HttpResponse ret = new HttpResponse();
try {
CloseableHttpResponse resp = doExec(serviceId, input, method, progressCallback);
if (resp.getEntity() == null) {
throw new RuntimeException(resp.getStatusLine().toString());
}
Header cacheControl = resp.getFirstHeader("Cache-Control");
if (cacheControl != null) {
HeaderElement[] elements = cacheControl.getElements();
if (elements != null) {
for (HeaderElement element : elements) {
if (element.getName().equals("no-cache")) {
break;
}
if (ret.getCachingInfo() == null) {
ret.setCachingInfo(new CachingInfo(0, true, false));
}
if (element.getName().equals("max-age")) {
ret.getCachingInfo().setMaxAge(Integer.valueOf(element.getValue()));
}
if (element.getName().equals("public")) {
ret.getCachingInfo().setShared(true);
}
if (element.getName().equals("private")) {
ret.getCachingInfo().setShared(false);
}
if (element.getName().equals("no-store")) {
ret.getCachingInfo().setStore(false);
}
}
}
}
if (!resp.getEntity().getContentType().getValue().startsWith(JSON_CONTENT_TYPE)) {
MetaDataInputStream is = new MetaDataInputStream(resp.getEntity().getContent(), getAttachmentFileName(resp.getFirstHeader("Content-Disposition")), resp.getEntity().getContentType().getValue(), resp.getEntity().getContentLength(), null);
ret.setInputStream(is);
} else {
JsonNode responseNode = JsonCodec.getInstance().parse(Miscellaneous.toString(resp.getEntity().getContent(), "UTF-8"));
RpcResponse rpcResponse = new RpcResponse();
if (responseNode.get("error") != null) {
rpcResponse.setError(JsonCodec.getInstance().load(responseNode.get("error"), RpcResponse.Error.class));
}
rpcResponse.setResult(responseNode.get("result"));
ret.setRpcResponse(rpcResponse);
}
} catch (ConnectException ex) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError(new RpcResponse.Error(RpcErrorCode.connectionError, ex.getMessage()));
ret.setRpcResponse(rpcResponse);
} catch (Throwable t) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError(new RpcResponse.Error(RpcErrorCode.internalError, t.getMessage()));
ret.setRpcResponse(rpcResponse);
}
return ret;
}
private CloseableHttpResponse doExec(String serviceId, JsonNode input, HttpMethod httpMethod, final ProgressCallback progressCallback) throws IOException {
RpcRequest request = new RpcRequest();
request.setJsonrpc("2.0");
request.setMethod(serviceId);
request.setParams(input);
final String payload = JsonCodec.getInstance().transform(request);
final HttpUriRequest req;
if (httpMethod == HttpMethod.GET) {
String urlparam = URLEncoder.encode(payload, "UTF-8");
req = new HttpGet(this.endpoint + "?jsonrpc=" + urlparam);
} else {
HttpEntityEnclosingRequestBase reqBase;
if (httpMethod == HttpMethod.POST) {
reqBase = new HttpPost(this.endpoint);
} else if (httpMethod == HttpMethod.PUT) {
reqBase = new HttpPut(this.endpoint);
} else {
throw new AssertionError();
}
req = reqBase;
HttpEntity entity;
Map files = JsonCodec.getInstance().getStreams(input);
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.setMode(HttpMultipartMode.STRICT);
builder.addPart("jsonrpc", new StringBody(payload, ContentType.APPLICATION_JSON));
if (files != null && !files.isEmpty()) {
files = sortFiles(files);
for (Map.Entry entrySet : files.entrySet()) {
String key = entrySet.getKey();
InputStream is = entrySet.getValue();
if (is instanceof MetaDataInputStream) {
MetaDataInputStream mis = (MetaDataInputStream) is;
builder.addPart(key, new InputStreamBody(mis, mis.getName()));
} else {
builder.addPart(key, new InputStreamBody(is, key));
}
}
}
entity = builder.build();
if (progressCallback != null) {
entity = new ProgressHttpEntityWrapper(entity, progressCallback);
}
reqBase.setEntity(entity);
}
HttpClientContext context = contexts.get();
if (this.clientContextFactory != null && context == null) {
context = clientContextFactory.create();
contexts.set(context);
}
return this.httpClient.execute(req, context);
}
public URI getEndpoint() {
return endpoint;
}
public void close() throws IOException {
this.pingThread.interrupt();
this.httpClient.close();
}
public static void main(String[] args) throws Exception {
HttpClientContextFactory ctxFact = new HttpClientContextFactory() {
public HttpClientContext create() {
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope("localhost", 8080, AuthScope.ANY_REALM, "basic"),
new UsernamePasswordCredentials("user", "password"));
HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);
return context;
}
};
HttpEndpoint endpoint = new HttpEndpoint(new URI("http://localhost:8080/rpc/http"), ctxFact);
HttpResponse resp = endpoint.exec("rpc.http.version", null, null);
if (resp.isIsBinary()) {
System.out.println("binary");
System.out.println(resp.getInputStream().getName());
} else {
System.out.println(resp.getRpcResponse().getResult());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy