Java tutorial
/* * Copyright (C) 2014 Andrei Varabyeu * * 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.github.avarabyeu.restendpoint.http; import com.github.avarabyeu.restendpoint.http.exception.RestEndpointIOException; import com.github.avarabyeu.restendpoint.http.exception.SerializerException; import com.github.avarabyeu.restendpoint.serializer.Serializer; import com.github.avarabyeu.restendpoint.serializer.VoidSerializer; import com.github.avarabyeu.wills.Will; import com.github.avarabyeu.wills.Wills; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.net.MediaType; import com.google.common.util.concurrent.SettableFuture; import org.apache.http.Header; import org.apache.http.HeaderElement; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPatch; 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.utils.URIBuilder; import org.apache.http.concurrent.FutureCallback; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.entity.mime.content.ByteArrayBody; import org.apache.http.entity.mime.content.StringBody; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.nio.entity.NByteArrayEntity; import org.apache.http.util.EntityUtils; import javax.annotation.Nonnull; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Type; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import java.util.concurrent.TimeoutException; /** * {@link RestEndpoint} implementation. Uses * Apache HTTP Components {@link org.apache.http.client.HttpClient} as default * http client implementation * * @author Andrei Varabyeu */ public class HttpClientRestEndpoint implements RestEndpoint, Closeable { /** * Serializer for converting HTTP messages */ private List<Serializer> serializers; /** * Base Endpoint URL */ private String baseUrl; /** * Error Handler for HttpResponses */ private ErrorHandler<HttpUriRequest, HttpResponse> errorHandler; /** * HTTP Client */ private CloseableHttpAsyncClient httpClient; /** * Default constructor. * * @param httpClient Apache Async Http Client * @param serializers Serializer for converting HTTP messages. Shouldn't be null * @param errorHandler Error handler for HTTP messages */ public HttpClientRestEndpoint(CloseableHttpAsyncClient httpClient, List<Serializer> serializers, ErrorHandler<HttpUriRequest, HttpResponse> errorHandler) { this(httpClient, serializers, errorHandler, null); } /** * Default constructor. * * @param httpClient Apache Async Http Client * @param serializers Serializer for converting HTTP messages. Shouldn't be null * @param errorHandler Error handler for HTTP messages * @param baseUrl REST WebService Base URL */ public HttpClientRestEndpoint(CloseableHttpAsyncClient httpClient, List<Serializer> serializers, ErrorHandler<HttpUriRequest, HttpResponse> errorHandler, String baseUrl) { Preconditions.checkArgument(null != serializers && !serializers.isEmpty(), "There is no any serializer provided"); //noinspection ConstantConditions this.serializers = ImmutableList.<Serializer>builder().addAll(serializers).add(new VoidSerializer()) .build(); if (!Strings.isNullOrEmpty(baseUrl)) { Preconditions.checkArgument(IOUtils.isValidUrl(baseUrl), "'%s' is not valid URL", baseUrl); } this.baseUrl = baseUrl; this.errorHandler = errorHandler == null ? new DefaultErrorHandler() : errorHandler; this.httpClient = httpClient; if (!httpClient.isRunning()) { httpClient.start(); } } /* * (non-Javadoc) * * @see RestEndpoint#post(java.lang .String, * java.lang.Object, java.lang.Class) */ @Override public final <RQ, RS> Will<Response<RS>> post(String resource, RQ rq, Class<RS> clazz) throws RestEndpointIOException { HttpPost post = new HttpPost(spliceUrl(resource)); Serializer serializer = getSupportedSerializer(rq); ByteArrayEntity httpEntity = new ByteArrayEntity(serializer.serialize(rq), ContentType.create(serializer.getMimeType())); post.setEntity(httpEntity); return executeInternal(post, new ClassConverterCallback<RS>(serializers, clazz)); } /* * (non-Javadoc) * * @see RestEndpoint#postFor(java.lang .String, * java.lang.Object, java.lang.Class) */ @Override public final <RQ, RS> Will<RS> postFor(String resource, RQ rq, Class<RS> clazz) throws RestEndpointIOException { return post(resource, rq, clazz).map(new BodyTransformer<RS>()); } /* * (non-Javadoc) * * @see RestEndpoint#post(java.lang.String, * java.lang.Object, java.lang.reflect.Type) */ @Override public final <RQ, RS> Will<Response<RS>> post(String resource, RQ rq, Type type) throws RestEndpointIOException { HttpPost post = new HttpPost(spliceUrl(resource)); Serializer serializer = getSupportedSerializer(rq); ByteArrayEntity httpEntity = new ByteArrayEntity(serializer.serialize(rq), ContentType.create(serializer.getMimeType())); post.setEntity(httpEntity); return executeInternal(post, new TypeConverterCallback<RS>(serializers, type)); } @Override public final <RQ, RS> Will<RS> postFor(String resource, RQ rq, Type type) throws RestEndpointIOException { Will<Response<RS>> post = post(resource, rq, type); return post.map(new BodyTransformer<RS>()); } /* * (non-Javadoc) * * @see RestEndpoint#post(java.lang .String, * MultiPartRequest, java.lang.Class) */ @Override public final <RS> Will<Response<RS>> post(String resource, MultiPartRequest request, Class<RS> clazz) throws RestEndpointIOException { HttpPost post = new HttpPost(spliceUrl(resource)); try { MultipartEntityBuilder builder = MultipartEntityBuilder.create(); for (MultiPartRequest.MultiPartSerialized<?> serializedPart : request.getSerializedRQs()) { Serializer serializer = getSupportedSerializer(serializedPart); builder.addPart(serializedPart.getPartName(), new StringBody(new String(serializer.serialize(serializedPart.getRequest())), ContentType.parse(serializer.getMimeType()))); } for (MultiPartRequest.MultiPartBinary partBinaty : request.getBinaryRQs()) { builder.addPart(partBinaty.getPartName(), new ByteArrayBody(partBinaty.getData().read(), ContentType.parse(partBinaty.getContentType()), partBinaty.getFilename())); } /* Here is some dirty hack to avoid problem with MultipartEntity and asynchronous http client * Details can be found here: http://comments.gmane.org/gmane.comp.apache.httpclient.user/2426 * * The main idea is to replace MultipartEntity with NByteArrayEntity once first * doesn't support #getContent method * which is required for async client implementation. So, we are copying response * body as byte array to NByteArrayEntity to * leave it unmodified. * * Alse we need to add boundary value to content type header. Details are here: * http://en.wikipedia.org/wiki/Delimiter#Content_boundary * MultipartEntity generates correct header by yourself, but we need to put it * manually once we replaced entity type to NByteArrayEntity */ String boundary = "-------------" + UUID.randomUUID().toString(); builder.setBoundary(boundary); ByteArrayOutputStream baos = new ByteArrayOutputStream(); builder.build().writeTo(baos); post.setEntity(new NByteArrayEntity(baos.toByteArray(), ContentType.MULTIPART_FORM_DATA)); post.setHeader("Content-Type", "multipart/form-data;boundary=" + boundary); } catch (Exception e) { throw new RestEndpointIOException("Unable to build post multipart request", e); } return executeInternal(post, new ClassConverterCallback<RS>(serializers, clazz)); } @Override public final <RS> Will<RS> postFor(String resource, MultiPartRequest request, Class<RS> clazz) throws RestEndpointIOException { return post(resource, request, clazz).map(new BodyTransformer<RS>()); } /* * (non-Javadoc) * * @see RestEndpoint#put(java.lang .String, * java.lang.Object, java.lang.Class) */ @Override public final <RQ, RS> Will<Response<RS>> put(String resource, RQ rq, Class<RS> clazz) throws RestEndpointIOException { HttpPut put = new HttpPut(spliceUrl(resource)); Serializer serializer = getSupportedSerializer(rq); ByteArrayEntity httpEntity = new ByteArrayEntity(serializer.serialize(rq), ContentType.create(serializer.getMimeType())); put.setEntity(httpEntity); return executeInternal(put, new ClassConverterCallback<RS>(serializers, clazz)); } @Override public final <RQ, RS> Will<RS> putFor(String resource, RQ rq, Class<RS> clazz) throws RestEndpointIOException { return put(resource, rq, clazz).map(new BodyTransformer<RS>()); } /* * (non-Javadoc) * * @see RestEndpoint#put(java.lang.String, * java.lang.Object, java.lang.reflect.Type) */ @Override public final <RQ, RS> Will<Response<RS>> put(String resource, RQ rq, Type type) throws RestEndpointIOException { HttpPut put = new HttpPut(spliceUrl(resource)); Serializer serializer = getSupportedSerializer(rq); ByteArrayEntity httpEntity = new ByteArrayEntity(serializer.serialize(rq), ContentType.create(serializer.getMimeType())); put.setEntity(httpEntity); return executeInternal(put, new TypeConverterCallback<RS>(serializers, type)); } @Override public final <RQ, RS> Will<RS> putFor(String resource, RQ rq, Type type) throws RestEndpointIOException { Will<Response<RS>> rs = put(resource, rq, type); return rs.map(new BodyTransformer<RS>()); } /* * (non-Javadoc) * * @see RestEndpoint#delete(java. * lang.String, java.lang.Class) */ @Override public final <RS> Will<Response<RS>> delete(String resource, Class<RS> clazz) throws RestEndpointIOException { HttpDelete delete = new HttpDelete(spliceUrl(resource)); return executeInternal(delete, new ClassConverterCallback<RS>(serializers, clazz)); } @Override public final <RS> Will<RS> deleteFor(String resource, Class<RS> clazz) throws RestEndpointIOException { return delete(resource, clazz).map(new BodyTransformer<RS>()); } /* * (non-Javadoc) * * @see RestEndpoint#get(java.lang .String, * java.lang.Class) */ @Override public final <RS> Will<Response<RS>> get(String resource, Class<RS> clazz) throws RestEndpointIOException { HttpGet get = new HttpGet(spliceUrl(resource)); return executeInternal(get, new ClassConverterCallback<RS>(serializers, clazz)); } @Override public final <RS> Will<RS> getFor(String resource, Class<RS> clazz) throws RestEndpointIOException { return get(resource, clazz).map(new BodyTransformer<RS>()); } @Override public final <RS> Will<Response<RS>> get(String resource, Type type) throws RestEndpointIOException { HttpGet get = new HttpGet(spliceUrl(resource)); return executeInternal(get, new TypeConverterCallback<RS>(serializers, type)); } @Override public final <RS> Will<RS> getFor(String resource, Type type) throws RestEndpointIOException { Will<Response<RS>> rs = get(resource, type); return rs.map(new BodyTransformer<RS>()); } @Override public final <RS> Will<Response<RS>> get(String resource, Map<String, String> parameters, Class<RS> clazz) throws RestEndpointIOException { HttpGet get = new HttpGet(spliceUrl(resource, parameters)); return executeInternal(get, new ClassConverterCallback<RS>(serializers, clazz)); } @Override public final <RS> Will<RS> getFor(String resource, Map<String, String> parameters, Class<RS> clazz) throws RestEndpointIOException { return get(resource, parameters, clazz).map(new BodyTransformer<RS>()); } @Override public final <RS> Will<Response<RS>> get(String resource, Map<String, String> parameters, Type type) throws RestEndpointIOException { HttpGet get = new HttpGet(spliceUrl(resource, parameters)); return executeInternal(get, new TypeConverterCallback<RS>(serializers, type)); } @Override public final <RS> Will<RS> getFor(String resource, Map<String, String> parameters, Type type) throws RestEndpointIOException { Will<Response<RS>> rs = get(resource, parameters, type); return rs.map(new BodyTransformer<RS>()); } /** * Executes request command * * @param command REST request representation * @return Future wrapper of REST response * @throws RestEndpointIOException In case of error * @see com.github.avarabyeu.wills.Will */ @Override public final <RQ, RS> Will<Response<RS>> executeRequest(RestCommand<RQ, RS> command) throws RestEndpointIOException { URI uri = spliceUrl(command.getUri()); HttpUriRequest rq; Serializer serializer; switch (command.getHttpMethod()) { case GET: rq = new HttpGet(uri); break; case POST: serializer = getSupportedSerializer(command.getRequest()); rq = new HttpPost(uri); ((HttpPost) rq).setEntity(new ByteArrayEntity(serializer.serialize(command.getRequest()), ContentType.create(serializer.getMimeType()))); break; case PUT: serializer = getSupportedSerializer(command.getRequest()); rq = new HttpPut(uri); ((HttpPut) rq).setEntity(new ByteArrayEntity(serializer.serialize(command.getRequest()), ContentType.create(serializer.getMimeType()))); break; case DELETE: rq = new HttpDelete(uri); break; case PATCH: serializer = getSupportedSerializer(command.getRequest()); rq = new HttpPatch(uri); ((HttpPatch) rq).setEntity(new ByteArrayEntity(serializer.serialize(command.getRequest()), ContentType.create(serializer.getMimeType()))); break; default: throw new IllegalArgumentException("Method '" + command.getHttpMethod() + "' is unsupported"); } return executeInternal(rq, new TypeConverterCallback<RS>(serializers, command.getResponseType())); } /** * Splice base URL and URL of resource * * @param resource REST Resource Path * @return Absolute URL to the REST Resource including server and port * @throws RestEndpointIOException If URL is incorrect */ private URI spliceUrl(String resource) throws RestEndpointIOException { try { return Strings.isNullOrEmpty(baseUrl) ? new URI(resource) : new URI(baseUrl.concat(resource)); } catch (URISyntaxException e) { throw new RestEndpointIOException( "Unable to builder URL with base url '" + baseUrl + "' and resouce '" + resource + "'", e); } } /** * Splice base URL and URL of resource * * @param resource REST Resource Path * @param parameters Map of query parameters * @return Absolute URL to the REST Resource including server and port * @throws RestEndpointIOException In case of incorrect URL format */ final URI spliceUrl(String resource, Map<String, String> parameters) throws RestEndpointIOException { try { URIBuilder builder; if (!Strings.isNullOrEmpty(baseUrl)) { builder = new URIBuilder(baseUrl); builder.setPath(builder.getPath() + resource); } else { builder = new URIBuilder(resource); } for (Entry<String, String> parameter : parameters.entrySet()) { builder.addParameter(parameter.getKey(), parameter.getValue()); } return builder.build(); } catch (URISyntaxException e) { throw new RestEndpointIOException( "Unable to builder URL with base url '" + baseUrl + "' and resouce '" + resource + "'", e); } } /** * Finds supported serializer for this type of object * * @param o Object to be serialized * @return Serializer * @throws SerializerException if serializer not found */ private Serializer getSupportedSerializer(Object o) throws SerializerException { for (Serializer s : serializers) { if (s.canWrite(o)) { return s; } } throw new SerializerException("Unable to find serializer for object with type '" + o.getClass() + "'"); } /** * Executes {@link org.apache.http.client.methods.HttpUriRequest} * * @param rq - Request * @param callback - Callback to be applied on response * @param <RS> type of response * @return - Serialized Response Body * @throws RestEndpointIOException IO exception */ private <RS> Will<Response<RS>> executeInternal(final HttpUriRequest rq, final HttpEntityCallback<RS> callback) throws RestEndpointIOException { final SettableFuture<Response<RS>> future = SettableFuture.create(); httpClient.execute(rq, new FutureCallback<org.apache.http.HttpResponse>() { @Override public void completed(final org.apache.http.HttpResponse response) { try { if (errorHandler.hasError(response)) { errorHandler.handle(rq, response); } HttpEntity entity = response.getEntity(); Header[] allHeaders = response.getAllHeaders(); ImmutableMultimap.Builder<String, String> headersBuilder = ImmutableMultimap.builder(); for (Header header : allHeaders) { for (HeaderElement element : header.getElements()) { headersBuilder.put(header.getName(), null == element.getValue() ? "" : element.getValue()); } } Response<RS> rs = new Response<RS>(rq.getURI().toASCIIString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), headersBuilder.build(), callback.callback(entity)); future.set(rs); } catch (SerializerException e) { future.setException(e); } catch (IOException e) { future.setException(new RestEndpointIOException("Unable to execute request", e)); } catch (Exception e) { future.setException(e); } } @Override public void failed(final Exception ex) { future.setException(new RestEndpointIOException("Unable to execute request", ex)); } @Override public void cancelled() { final TimeoutException timeoutException = new TimeoutException(); future.setException(timeoutException); } }); return Wills.forListenableFuture(future); } @Override public final void close() throws IOException { httpClient.close(); } private static abstract class HttpEntityCallback<RS> { protected List<Serializer> serializers; /** * Response callback * * @param serializers Serializers list */ public HttpEntityCallback(List<Serializer> serializers) { this.serializers = serializers; } /** * Performs callback on http entity * * @param entity HttpEntity * @return Serialized RS body * @throws IOException In case of IO error */ abstract public RS callback(HttpEntity entity) throws IOException; } private static class TypeConverterCallback<RS> extends HttpEntityCallback<RS> { private Type type; /** * Callback based on Type * * @param serializers List of serializers * @param type Type of object */ public TypeConverterCallback(List<Serializer> serializers, Type type) { super(serializers); this.type = type; } @Override public RS callback(HttpEntity entity) throws IOException { return getSupported(null == entity.getContentType() ? MediaType.ANY_TYPE : MediaType.parse(entity.getContentType().getValue()), type) .deserialize(EntityUtils.toByteArray(entity), type); } /** * Finds supported serializer * * @param contentType ContentType * @param resultType Result object Type * @return Found Serializer * @throws SerializerException If not serializer found */ protected Serializer getSupported(MediaType contentType, Type resultType) throws SerializerException { for (Serializer s : serializers) { if (s.canRead(contentType, resultType)) { return s; } } throw new SerializerException( "Conversion media type '" + contentType + "' to type '" + resultType + "' is not supported"); } } private static class ClassConverterCallback<RS> extends HttpEntityCallback<RS> { private Class<RS> clazz; /** * Callback based on Type * * @param serializers List of serializers * @param clazz Type of object */ public ClassConverterCallback(List<Serializer> serializers, Class<RS> clazz) { super(serializers); this.clazz = clazz; } @Override public RS callback(HttpEntity entity) throws IOException { return getSupported(null == entity.getContentType() ? MediaType.ANY_TYPE : MediaType.parse(entity.getContentType().getValue()), clazz) .deserialize(EntityUtils.toByteArray(entity), clazz); } /** * Finds supported serializer * * @param contentType ContentType * @param resultType Result object Type * @return Found Serializer * @throws SerializerException If not serializer found */ private Serializer getSupported(MediaType contentType, Class<?> resultType) throws SerializerException { for (Serializer s : serializers) { if (s.canRead(contentType, resultType)) { return s; } } throw new SerializerException( "Conversion media type '" + contentType + "' to type '" + resultType + "' is not supported"); } } /** * Transforms response object to Body * * @param <T> Type of body */ public static final class BodyTransformer<T> implements Function<Response<T>, T> { @Nonnull @Override public T apply(@Nonnull Response<T> input) { return input.getBody(); } } }