/*
* 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;
}
}