com.strandls.alchemy.rest.client.AlchemyRestClientFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.strandls.alchemy.rest.client.AlchemyRestClientFactory.java

Source

/*
 * Copyright (C) 2015 Strand Life Sciences.
 *
 * 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.strandls.alchemy.rest.client;

import java.io.File;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import javassist.Modifier;
import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.ws.rs.CookieParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.glassfish.jersey.media.multipart.ContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.glassfish.jersey.media.multipart.MultiPart;
import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
import org.objenesis.ObjenesisStd;

import com.strandls.alchemy.rest.client.exception.ResponseToThrowableMapper;
import com.strandls.alchemy.rest.client.request.RequestBuilderFilter;

/**
 * Factory for jersey based proxy clients.
 *
 * @author Ashish Shinde
 *
 */
@Singleton
@Slf4j
public class AlchemyRestClientFactory {
    /**
     * Handles rest method invocation for a single rest service.
     *
     * @author Ashish Shinde
     *
     */
    @RequiredArgsConstructor
    private static class RestMethodInvocationHandler implements MethodHandler {
        /**
         * The base URI.
         */
        private final String baseUri;

        /**
         * Jax rs client provider.
         */
        private final Provider<Client> clientProvider;

        /**
         * Rest interface metadata.
         */
        private final RestInterfaceMetadata restInterfaceMetadata;

        /**
         * Maps server side errors to local errors.
         */
        private final ResponseToThrowableMapper responseToThrowableMapper;

        /**
         * The request builder filter.
         */
        private final RequestBuilderFilter builderFilter;

        /**
         * Create the path for the rest method.
         *
         * @param methodMetaData
         *            the method meta data.
         * @param arguments
         *            the method arguments, used to add matrix and path
         *            parameters to the generated path.
         * @return the absolute remote rest path for this method invocation.
         */
        private String getPath(final RestMethodMetadata methodMetaData, final Object[] arguments) {
            final UriBuilder uriBuilder = UriBuilder.fromPath(baseUri);

            if (!StringUtils.isBlank(restInterfaceMetadata.getPath())) {
                uriBuilder.path(restInterfaceMetadata.getPath());
            }

            if (!StringUtils.isBlank(methodMetaData.getPath())) {
                uriBuilder.path(methodMetaData.getPath());
            }

            // add matrix parameters to the path
            final Annotation[][] parameterAnnotations = methodMetaData.getParameterAnnotations();
            final Map<String, Object> pathParamsMap = new LinkedHashMap<String, Object>();
            for (int i = 0; i < parameterAnnotations.length && i < arguments.length; i++) {
                final Annotation[] annotations = parameterAnnotations[i];

                final Object argument = arguments[i];
                for (final Annotation annotation : annotations) {
                    if (annotation instanceof MatrixParam) {
                        final String name = ((MatrixParam) annotation).value();
                        Object[] values = new Object[] {};
                        if (argument != null && argument.getClass().isArray()) {
                            values = (Object[]) argument;
                        } else if (argument instanceof Collection) {
                            @SuppressWarnings("unchecked")
                            final Collection<Object> collection = (Collection<Object>) argument;
                            values = collection.toArray();
                        } else {
                            values = new Object[] { argument };
                        }
                        uriBuilder.matrixParam(name, values);
                    } else if (annotation instanceof PathParam) {
                        pathParamsMap.put(((PathParam) annotation).value(), argument);
                    }
                }
            }

            // add path params to the path
            return uriBuilder.buildFromMap(pathParamsMap).toString();
        }

        /*
         * (non-Javadoc)
         * @see javassist.util.proxy.MethodHandler#invoke(java.lang.Object,
         * java.lang.reflect.Method, java.lang.reflect.Method,
         * java.lang.Object[])
         */
        @Override
        public Object invoke(final Object self, final Method thisMethod, final Method proceed,
                final Object[] arguments) throws Throwable {

            final RestMethodMetadata methodMetaData = restInterfaceMetadata.getMethodMetaData().get(thisMethod);

            if (methodMetaData == null) {
                throw new NotRestMethodException(thisMethod);
            }

            final String path = getPath(methodMetaData, arguments);
            log.debug("Invoking rest service at {}", path);

            final Client client = clientProvider.get();

            WebTarget webTarget = client.target(path);
            Entity<?> entity = null;

            final Annotation[][] parameterAnnotations = methodMetaData.getParameterAnnotations();

            final String bodyParameterMediaType = methodMetaData.getConsumed().isEmpty() ? null
                    : methodMetaData.getConsumed().get(0);

            // set query params
            for (int i = 0; i < arguments.length; i++) {
                final Object argument = arguments[i];

                if (parameterAnnotations.length <= i) {
                    // body parameter without annotation
                    entity = toEntity(argument, bodyParameterMediaType);
                    continue;
                }

                final Annotation[] annotations = parameterAnnotations[i];

                if (annotations == null || annotations.length == 0) {
                    // body parameter without annotation
                    entity = toEntity(argument, bodyParameterMediaType);
                    continue;
                }

                for (final Annotation annotation : annotations) {
                    if (annotation instanceof QueryParam) {
                        final String key = ((QueryParam) annotation).value();
                        final String value = ObjectUtils.toString(argument);
                        webTarget = webTarget.queryParam(key, value);
                    }
                }

            }

            // create the request builder
            Builder webRequestBuilder = webTarget.request();
            webRequestBuilder.accept(methodMetaData.getProduced().toArray(new String[0]));
            webRequestBuilder = webRequestBuilder.accept(methodMetaData.getConsumed().toArray(new String[0]));

            if (builderFilter != null) {
                builderFilter.apply(webRequestBuilder);
            }

            // for form params
            Form formParams = null;
            // process cookie and header params
            for (int i = 0; i < arguments.length; i++) {
                if (i >= parameterAnnotations.length) {
                    continue;
                }

                final Annotation[] annotations = parameterAnnotations[i];
                final Object argument = arguments[i];

                for (final Annotation annotation : annotations) {
                    if (annotation instanceof CookieParam) {
                        // add cookie to the request
                        Cookie cookie = null;
                        if (argument instanceof Cookie) {
                            cookie = (Cookie) argument;
                        } else {
                            cookie = new Cookie(((CookieParam) annotation).value(), ObjectUtils.toString(argument));
                        }
                        webRequestBuilder = webRequestBuilder.cookie(cookie);
                    } else if (annotation instanceof HeaderParam) {
                        // add header param
                        final String key = ((HeaderParam) annotation).value();
                        final String value = ObjectUtils.toString(argument);
                        webRequestBuilder = webRequestBuilder.header(key, value);
                    } else if (annotation instanceof FormParam) {
                        if (formParams == null) {
                            formParams = new javax.ws.rs.core.Form();
                        }
                        formParams.param(((FormParam) annotation).value(), ObjectUtils.toString(argument));
                    }
                }
            }

            if (formParams != null) {
                // cannot have form parameters and body parameters without
                // annotation
                assert entity == null;

                // for form parameters the method should be post
                assert HttpMethod.POST.equals(methodMetaData.getHttpMethod());

                entity = toEntity(formParams, MediaType.APPLICATION_FORM_URLENCODED);
            }

            final FormDataMultiPart formDataMultiPart = processFormDataParams(arguments, parameterAnnotations);

            if (formDataMultiPart != null) {
                // Cannot have form parameters and body parameters without
                // annotation
                assert entity == null;

                // for form parameters the method should be post
                assert HttpMethod.POST.equals(methodMetaData.getHttpMethod());

                entity = toEntity(formDataMultiPart, formDataMultiPart.getMediaType().toString());
            }

            // Get the return type.
            @SuppressWarnings("rawtypes")
            final GenericType<?> returnType = new GenericType(thisMethod.getGenericReturnType()) {
            };

            try {
                Object retval = null;
                final String httpMethod = methodMetaData.getHttpMethod();
                if (HttpMethod.GET.equals(httpMethod)) {
                    retval = webRequestBuilder.get(returnType);
                } else if (HttpMethod.PUT.equals(httpMethod)) {
                    retval = webRequestBuilder.put(entity, returnType);
                } else if (HttpMethod.POST.equals(httpMethod)) {
                    retval = webRequestBuilder.post(entity, returnType);
                } else if (HttpMethod.DELETE.equals(httpMethod)) {
                    retval = webRequestBuilder.delete(returnType);
                }
                return retval;
            } catch (final InternalServerErrorException e) {
                throw responseToThrowableMapper.apply(e.getResponse());
            }

        }

        /**
         * Process form data params.
         *
         * @param arguments
         *            the function call arguments.
         * @param parameterAnnotations
         *            function parameter annotations.
         * @return form data multipart object if the funtion contains form data
         *         elements.
         */
        private FormDataMultiPart processFormDataParams(final Object[] arguments,
                final Annotation[][] parameterAnnotations) {
            FormDataMultiPart formDataMultiPart = null;
            // map from param name to content disposition
            final Map<String, ContentDisposition> contentDispositions = new HashMap<String, ContentDisposition>();

            // map from param name to input streams
            final Map<String, InputStream> inputstreams = new HashMap<String, InputStream>();
            for (int i = 0; i < arguments.length; i++) {
                if (i >= parameterAnnotations.length) {
                    continue;
                }

                final Annotation[] annotations = parameterAnnotations[i];
                for (final Annotation annotation : annotations) {
                    if (annotation instanceof FormDataParam) {
                        if (formDataMultiPart == null) {
                            formDataMultiPart = new FormDataMultiPart();
                        }

                        final Object argument = arguments[i];
                        final String paramName = ((FormDataParam) annotation).value();
                        if (argument instanceof File) {
                            formDataMultiPart.bodyPart(new FileDataBodyPart(paramName, (File) argument));
                        } else if (argument instanceof InputStream) {
                            inputstreams.put(paramName, (InputStream) argument);
                        } else if (argument instanceof FormDataContentDisposition) {
                            contentDispositions.put(paramName, (ContentDisposition) argument);
                        } else {
                            formDataMultiPart.field(paramName, ObjectUtils.toString(argument));
                        }

                    }
                }
            }

            if (formDataMultiPart != null && !inputstreams.isEmpty()) {
                // we have input streams that may have content dispositions
                for (final Entry<String, InputStream> streamEntry : inputstreams.entrySet()) {
                    final String paramName = streamEntry.getKey();
                    if (contentDispositions.containsKey(paramName)) {
                        // we have a content disposition for this input stream
                        final ContentDisposition contentDisposition = contentDispositions.get(paramName);
                        final StreamDataBodyPart streamBodyPart = new StreamDataBodyPart(paramName,
                                streamEntry.getValue(), contentDisposition.getFileName());
                        formDataMultiPart.bodyPart(streamBodyPart);
                    } else {
                        final StreamDataBodyPart streamBodyPart = new StreamDataBodyPart(paramName,
                                streamEntry.getValue(), paramName);
                        formDataMultiPart.bodyPart(streamBodyPart);
                    }
                }
            }

            return formDataMultiPart;
        }

        /**
         * Converts an object to an entity.
         *
         * @param object
         *            the object.
         * @param bodyParameterMediaType
         *            the media type. If <code>null</code>
         *            {@link MediaType#MEDIA_TYPE_WILDCARD} will be used.
         * @return converted entity.
         */
        private Entity<Object> toEntity(final Object object, String bodyParameterMediaType) {

            if (object instanceof MultiPart) {
                bodyParameterMediaType = ((MultiPart) object).getMediaType().toString();
            }

            return !StringUtils.isBlank(bodyParameterMediaType) ? Entity.entity(object, bodyParameterMediaType)
                    : Entity.entity(object, MediaType.MEDIA_TYPE_WILDCARD);
        }
    }

    /**
     * The base uri named param name.
     */
    public static final String BASE_URI_NAMED_PARAM = "com.strandls.alchemy.rest.client.AlchemyRestClientFactory.baseURI";

    /**
     * The base URI.
     */
    @NonNull
    private final String baseUri;

    /**
     * Jax rs client provider.
     */
    @NonNull
    private final Provider<Client> clientProvider;

    /**
     * The rest interface analyzer.
     */
    private final RestInterfaceAnalyzer interfaceAnalyzer;

    /**
     * Object instantiator.
     */
    private final ObjenesisStd objenesis;

    /**
     * Maps {@link Response} to a {@link Throwable} object for server side
     * exceptions.
     */
    private final ResponseToThrowableMapper responseToThrowableMapper;

    /**
     * The request biulder filter.
     */
    private final RequestBuilderFilter builderFilter;

    /**
     * Creates the new factory.
     *
     * @param baseUri
     *            the base URI for the rest service.
     * @param clientProvider
     *            the {@link Client} provider.
     * @param interfaceAnalyzer
     *            the interface analyzer.
     */
    @Inject
    public AlchemyRestClientFactory(@Named(BASE_URI_NAMED_PARAM) final String baseUri,
            final Provider<Client> clientProvider, final RestInterfaceAnalyzer interfaceAnalyzer,
            final ResponseToThrowableMapper responseToThrowableMapper, final RequestBuilderFilter builderFilter) {
        this.baseUri = baseUri;
        this.clientProvider = clientProvider;
        this.interfaceAnalyzer = interfaceAnalyzer;
        this.objenesis = new ObjenesisStd();
        this.responseToThrowableMapper = responseToThrowableMapper;
        this.builderFilter = builderFilter;
    }

    /**
     * Get an instance of a rest proxy instance for the service class.
     *
     * @param serviceClass
     *            the service class.
     * @return the proxy implementation that invokes the remote service.
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public <T> T getInstance(@NonNull final Class<T> serviceClass) throws Exception {
        final ProxyFactory factory = new ProxyFactory();
        if (serviceClass.isInterface()) {
            factory.setInterfaces(new Class[] { serviceClass });
        } else {
            factory.setSuperclass(serviceClass);
        }
        factory.setFilter(new MethodFilter() {
            @Override
            public boolean isHandled(final Method method) {
                return Modifier.isPublic(method.getModifiers());
            }
        });

        final Class<?> klass = factory.createClass();
        final Object instance = objenesis.getInstantiatorOf(klass).newInstance();
        ((ProxyObject) instance).setHandler(new RestMethodInvocationHandler(baseUri, clientProvider,
                interfaceAnalyzer.analyze(serviceClass), responseToThrowableMapper, builderFilter));
        return (T) instance;
    }
}