com.strandls.alchemy.rest.client.stubgenerator.ServiceStubGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.strandls.alchemy.rest.client.stubgenerator.ServiceStubGenerator.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.stubgenerator;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;

import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

import lombok.RequiredArgsConstructor;

import org.apache.commons.lang3.StringUtils;

import com.strandls.alchemy.rest.client.NotRestInterfaceException;
import com.strandls.alchemy.rest.client.RestInterfaceAnalyzer;
import com.strandls.alchemy.rest.client.RestInterfaceMetadata;
import com.strandls.alchemy.rest.client.RestMethodMetadata;
import com.sun.codemodel.CodeWriter;
import com.sun.codemodel.JAnnotatable;
import com.sun.codemodel.JAnnotationArrayMember;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JDocComment;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JPrimitiveType;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;

/**
 * Generate a stub interface for a restful service. All the service class and
 * method jaxrs {@link Annotation}s are preserved.
 *
 * @author Ashish Shinde
 *
 */
@RequiredArgsConstructor(onConstructor = @_(@Inject))
@Singleton
public class ServiceStubGenerator {
    /**
     * Annotation value parameter name.
     */
    private static final String ANNOTATION_VALUE_PARAM_NAME = "value";

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

    /**
     * Add annotation to the annotatable.
     *
     * @param jAnnotatable
     *            the annotatable.
     * @param annotation
     *            the annotation class.
     */
    private void addAnnotation(final JAnnotatable jAnnotatable, final Class<? extends Annotation> annotation) {
        jAnnotatable.annotate(annotation);
    }

    /**
     * Add a list valued annotation the annotatable.
     *
     * @param jAnnotatable
     *            the annotatable
     * @param annotation
     *            the annotation
     * @param param
     *            the name of the parameter
     * @param values
     *            the values.
     */
    private void addListAnnotation(final JAnnotatable jAnnotatable, final Class<? extends Annotation> annotation,
            final String param, final Collection<String> values) {
        if (!values.isEmpty()) {
            final JAnnotationArrayMember annotationValues = jAnnotatable.annotate(annotation).paramArray(param);
            for (final String value : values) {
                annotationValues.param(value);
            }
        }
    }

    /**
     * Add a rest method to the parent class.
     *
     * @param jCodeModel
     *            the code model.
     * @param jParentClass
     *            the parent class.
     * @param method
     *            the method.
     * @param methodMetaData
     *            the method metadata.
     */
    private void addMethod(final JCodeModel jCodeModel, final JDefinedClass jParentClass, final Method method,
            final RestMethodMetadata methodMetaData) {
        final String mehtodName = method.getName();

        final JType result = typeToJType(method.getReturnType(), method.getGenericReturnType(), jCodeModel);

        final JMethod jMethod = jParentClass.method(JMod.PUBLIC, result, mehtodName);

        @SuppressWarnings("unchecked")
        final Class<? extends Throwable>[] exceptionTypes = (Class<? extends Throwable>[]) method
                .getExceptionTypes();

        for (final Class<? extends Throwable> exceptionCType : exceptionTypes) {
            jMethod._throws(exceptionCType);
        }

        addSingleValueAnnotation(jMethod, Path.class, ANNOTATION_VALUE_PARAM_NAME, methodMetaData.getPath());

        addListAnnotation(jMethod, Produces.class, ANNOTATION_VALUE_PARAM_NAME, methodMetaData.getProduced());
        addListAnnotation(jMethod, Consumes.class, ANNOTATION_VALUE_PARAM_NAME, methodMetaData.getConsumed());

        final String httpMethod = methodMetaData.getHttpMethod();
        Class<? extends Annotation> httpMethodAnnotation = null;
        if (HttpMethod.GET.equals(httpMethod)) {
            httpMethodAnnotation = GET.class;
        } else if (HttpMethod.PUT.equals(httpMethod)) {
            httpMethodAnnotation = PUT.class;
        } else if (HttpMethod.POST.equals(httpMethod)) {
            httpMethodAnnotation = POST.class;
        } else if (HttpMethod.DELETE.equals(httpMethod)) {
            httpMethodAnnotation = DELETE.class;
        }

        addAnnotation(jMethod, httpMethodAnnotation);

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

        final Type[] argumentTypes = method.getGenericParameterTypes();
        final Class<?>[] argumentClasses = method.getParameterTypes();
        for (int i = 0; i < argumentTypes.length; i++) {
            final JType jType = typeToJType(argumentClasses[i], argumentTypes[i], jCodeModel);
            // we have lost the actual names, use generic arg names
            final String name = "arg" + i;
            final JVar param = jMethod.param(jType, name);
            if (parameterAnnotations.length > i) {
                for (final Annotation annotation : parameterAnnotations[i]) {
                    final JAnnotationUse jAnnotation = param.annotate(annotation.annotationType());
                    final String value = getValue(annotation);
                    if (value != null) {
                        jAnnotation.param(ANNOTATION_VALUE_PARAM_NAME, value);
                    }
                }
            }
        }
    }

    /**
     * Add a single valued annotation to the annotatable.
     *
     * @param jAnnotatable
     *            the annotatable.
     * @param annotation
     *            the annotation class.
     * @param param
     *            the name of the param
     * @param value
     *            the value of the param
     */
    private void addSingleValueAnnotation(final JAnnotatable jAnnotatable,
            final Class<? extends Annotation> annotation, final String param, final String value) {
        if (!StringUtils.isBlank(value)) {
            jAnnotatable.annotate(annotation).param(param, value);
        }
    }

    /**
     * Generate a stub interface for a rest web service implemented by the input
     * service class.
     *
     * <p>
     * The code writer is not close to allow for appends to same code writer.
     * The caller should close the code writer.
     * </p>
     *
     * @param serviceClass
     *            the input rest service class.
     * @param destinationInterfaceName
     *            the name of the destination interface
     * @param destinationPackage
     *            the destination package name.
     * @param codeWriter
     *            the writer to output the source to.
     * @throws NotRestInterfaceException
     *             if the service class is not a rest service.
     * @throws Exception
     *             if code generation fails.
     */
    public void generateStubInterface(final Class<?> serviceClass, final String destinationInterfaceName,
            final String destinationPackage, final CodeWriter codeWriter)
            throws NotRestInterfaceException, Exception {
        final RestInterfaceMetadata metaData = interfaceAnalyzer.analyze(serviceClass);

        final JCodeModel jCodeModel = new JCodeModel();
        final JPackage jPackage = jCodeModel._package(destinationPackage);
        final JDefinedClass jInterface = jPackage._interface(destinationInterfaceName);
        final String path = metaData.getPath();
        addSingleValueAnnotation(jInterface, Path.class, ANNOTATION_VALUE_PARAM_NAME, path);

        // add consumes annotation
        addListAnnotation(jInterface, Consumes.class, ANNOTATION_VALUE_PARAM_NAME, metaData.getConsumed());

        // add produces annotation
        addListAnnotation(jInterface, Produces.class, ANNOTATION_VALUE_PARAM_NAME, metaData.getProduced());
        final JDocComment jDocComment = jInterface.javadoc();
        jDocComment
                .add(String.format("Client side stub interface for {@link %s}.", serviceClass.getCanonicalName()));

        final List<Entry<Method, RestMethodMetadata>> methodEntries = new ArrayList<>(
                metaData.getMethodMetaData().entrySet());

        // sort methods alphabetically to give consistent order.
        Collections.sort(methodEntries, new Comparator<Entry<Method, RestMethodMetadata>>() {

            @Override
            public int compare(final Entry<Method, RestMethodMetadata> o1,
                    final Entry<Method, RestMethodMetadata> o2) {
                return o1.getKey().toGenericString().compareTo(o2.getKey().toGenericString());
            }
        });

        // generate methods.
        for (final Entry<Method, RestMethodMetadata> methodEntry : methodEntries) {
            final Method method = methodEntry.getKey();
            final RestMethodMetadata methodMetaData = methodEntry.getValue();
            addMethod(jCodeModel, jInterface, method, methodMetaData);

        }

        jCodeModel.build(codeWriter);
    }

    /**
     * Return {@link #ANNOTATION_VALUE_PARAM_NAME} for given annotation, if that
     * is a field.
     *
     * @param annotation
     *            the annotation
     * @return the string value if it exists, else null.
     */
    private String getValue(final Annotation annotation) {
        final Class<? extends Annotation> klass = annotation.getClass();
        Method method;
        try {
            method = klass.getDeclaredMethod(ANNOTATION_VALUE_PARAM_NAME);
        } catch (NoSuchMethodException | SecurityException e) {
            return null;
        }
        try {
            return (String) method.invoke(annotation);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            return null;
        }
    }

    /**
     * Convert a generic {@link Type} to {@link JType}.
     *
     * @param rawType
     *            the raw type
     * @param type
     *            the generic type
     * @param jCodeModel
     *            the code model
     *
     * @return converted {@link JType}.
     */
    private JType typeToJType(final Class<?> rawType, final Type type, final JCodeModel jCodeModel) {
        final JType jType = jCodeModel._ref(rawType);
        if (jType instanceof JPrimitiveType) {
            return jType;
        }
        JClass result = (JClass) jType;
        if (type instanceof ParameterizedType) {
            for (final Type typeArgument : ((ParameterizedType) type).getActualTypeArguments()) {
                if (typeArgument instanceof WildcardType) {
                    result = result.narrow(jCodeModel.wildcard());
                } else if (typeArgument instanceof Class) {
                    result = result.narrow(jCodeModel._ref((Class<?>) typeArgument));
                }
            }
        }
        return result;
    }
}