/* * Copyright 2015 Nicolas Morel * * 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.nmorel.gwtjackson.rest.processor; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import java.io.IOException; import java.io.Writer; import java.lang.annotation.Annotation; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.github.nmorel.gwtjackson.client.ObjectMapper; import com.github.nmorel.gwtjackson.client.ObjectReader; import com.github.nmorel.gwtjackson.client.ObjectWriter; import com.github.nmorel.gwtjackson.rest.api.RestCallback; import com.github.nmorel.gwtjackson.rest.api.RestRequestBuilder; import com.google.gwt.core.client.GWT; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.RequestBuilder; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; /** * Processor handling type annotated with {@link GenRestBuilder}. */ public class GenRestBuilderProcessor extends AbstractProcessor { private Filer filer; private Messager messager; private Options options; @Override public Set<String> getSupportedOptions() { return Options.getOptionsName(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotations = new LinkedHashSet<String>(); annotations.add( GenRestBuilder.class.getCanonicalName() ); return annotations; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public synchronized void init( ProcessingEnvironment processingEnv ) { super.init( processingEnv ); filer = processingEnv.getFiler(); messager = processingEnv.getMessager(); options = new Options( processingEnv.getOptions() ); } @Override public boolean process( Set<? extends TypeElement> annotations, RoundEnvironment roundEnv ) { for ( Element element : roundEnv.getElementsAnnotatedWith( GenRestBuilder.class ) ) { if ( !isAnnotatedWith( element, Path.class ) ) { error( element, "Only classes and interfaces annotated with @%s are supported", Path.class.getCanonicalName() ); continue; } RestService service = new RestService( options, element ); // For each methods in error, we log the message if ( !service.getMethodsInError().isEmpty() ) { for ( Entry<ExecutableElement, Exception> entry : service.getMethodsInError().entrySet() ) { try { throw entry.getValue(); } catch ( MoreThanOneBodyParamException e ) { warn( entry.getKey(), "Cannot have more than one body parameter" ); } catch ( MissingGenResponseClassTypeException e ) { note( entry.getKey(), "Methods with return type javax.ws.rs.core.Response can be annotated with @%s to define an other type.", GenResponseClassType.class.getCanonicalName() ); } catch ( Exception e ) { error( entry.getKey(), "Unexpected error: " + e.getMessage() ); } } } TypeSpec type = generateBuilder( service ); try { JavaFileObject jfo = filer.createSourceFile( service.getBuilderQualifiedClassName() ); JavaFile file = JavaFile.builder( service.getPackageName(), type ).build(); Writer writer = jfo.openWriter(); file.writeTo( writer ); writer.close(); } catch ( IOException e ) { error( null, e.getMessage() ); return true; // Exit processing } } return true; } /** * Generate the rest service builder * * @param restService The rest service */ private TypeSpec generateBuilder( RestService restService ) { TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( restService.getBuilderSimpleClassName() ) .addModifiers( Modifier.PUBLIC, Modifier.FINAL ) .addJavadoc( "Generated REST service builder for {@link $L}.\n", restService.getTypeElement().getQualifiedName() ) .addMethod( MethodSpec.constructorBuilder().addModifiers( Modifier.PRIVATE ).build() ); Map<TypeMirror, MethodSpec> mapperGetters = buildMappers( typeBuilder, restService ); for ( RestServiceMethod method : restService.getMethods() ) { buildMethod( typeBuilder, mapperGetters, method ); } return typeBuilder.build(); } private Map<TypeMirror, MethodSpec> buildMappers( TypeSpec.Builder typeBuilder, RestService restService ) { Set<TypeMirror> readers = new LinkedHashSet<TypeMirror>( restService.getReturnTypes() ); readers.removeAll( restService.getBodyTypes() ); Set<TypeMirror> writers = new LinkedHashSet<TypeMirror>( restService.getBodyTypes() ); writers.removeAll( restService.getReturnTypes() ); Set<TypeMirror> mappers = new LinkedHashSet<TypeMirror>( restService.getBodyTypes() ); mappers.retainAll( restService.getReturnTypes() ); Map<TypeMirror, MethodSpec> result = new LinkedHashMap<TypeMirror, MethodSpec>(); result.putAll( buildMappers( restService.getPackageName(), restService .getBuilderSimpleClassName(), typeBuilder, readers, ObjectReader.class ) ); result.putAll( buildMappers( restService.getPackageName(), restService .getBuilderSimpleClassName(), typeBuilder, writers, ObjectWriter.class ) ); result.putAll( buildMappers( restService.getPackageName(), restService .getBuilderSimpleClassName(), typeBuilder, mappers, ObjectMapper.class ) ); return result; } private Map<TypeMirror, MethodSpec> buildMappers( String packageName, String className, TypeSpec.Builder typeBuilder, Set<TypeMirror> types, Class clazz ) { int i = 1; Map<TypeMirror, MethodSpec> result = new HashMap<TypeMirror, MethodSpec>(); for ( TypeMirror type : types ) { String mapperName = clazz.getSimpleName() + i++; TypeName mapperType = ClassName.get( packageName, className, mapperName ); TypeSpec innerMapper = TypeSpec.interfaceBuilder( mapperName ) .addModifiers( Modifier.STATIC ) .addSuperinterface( ParameterizedTypeName.get( ClassName.get( clazz ), ClassName.get( type ) ) ) .build(); typeBuilder.addType( innerMapper ); FieldSpec mapperField = FieldSpec .builder( mapperType, mapperName.toLowerCase() ) .addModifiers( Modifier.PRIVATE, Modifier.STATIC ) .build(); typeBuilder.addField( mapperField ); MethodSpec mapperGetter = MethodSpec.methodBuilder( "get" + mapperName ) .addModifiers( Modifier.PRIVATE, Modifier.STATIC ) .returns( mapperType ) .beginControlFlow( "if ($N == null)", mapperField ) .addStatement( "$N = $T.create($T.class)", mapperField, GWT.class, mapperType ) .endControlFlow() .addStatement( "return $N", mapperField ) .build(); typeBuilder.addMethod( mapperGetter ); result.put( type, mapperGetter ); } return result; } private void buildMethod( TypeSpec.Builder typeBuilder, Map<TypeMirror, MethodSpec> mapperGetters, RestServiceMethod method ) { String methodName = method.getMethod().getSimpleName().toString(); AnnotationMirror httpMethodAnnotation = method.getHttpMethodAnnotation(); TypeMirror returnType = method.getReturnType(); MethodSpec returnTypeReaderGetter = mapperGetters.get( returnType ); TypeName returnTypeName; if ( null == returnTypeReaderGetter ) { returnTypeName = ClassName.get( Void.class ); } else { returnTypeName = TypeName.get( returnType ); } VariableElement bodyVariable = method.getBodyParamVariable(); MethodSpec bodyTypeWriterGetter; TypeName bodyTypeName; if ( null != bodyVariable ) { bodyTypeWriterGetter = mapperGetters.get( bodyVariable.asType() ); bodyTypeName = TypeName.get( bodyVariable.asType() ); } else { bodyTypeWriterGetter = null; bodyTypeName = ClassName.get( Void.class ); } TypeName restType = ParameterizedTypeName.get( ClassName.get( RestRequestBuilder.class ), bodyTypeName, returnTypeName ); MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder( methodName ) .addModifiers( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ) .returns( restType ); MethodSpec.Builder methodWithCallbackSpecBuilder = MethodSpec.methodBuilder( methodName ) .addModifiers( Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL ) .returns( Request.class ); CodeBlock.Builder initRestBuilder = CodeBlock.builder() .add( "new $T()", restType ) .indent() .add( "\n.method($T.$L)", RequestBuilder.class, httpMethodAnnotation.getAnnotationType().asElement().getSimpleName() ) .add( "\n.url($S)", method.getUrl() ); if ( null != bodyVariable ) { initRestBuilder.add( "\n.body($L)", bodyVariable.getSimpleName() ); } if ( null != bodyTypeWriterGetter ) { initRestBuilder.add( "\n.bodyConverter($N())", bodyTypeWriterGetter ); } StringBuilder callParamBuilder = new StringBuilder(); for ( VariableElement variable : method.getMethod().getParameters() ) { if ( isAnnotatedWith( variable, Context.class ) ) { continue; } ParameterSpec parameterSpec = ParameterSpec.builder( ClassName.get( variable.asType() ), variable.getSimpleName() .toString(), Modifier.FINAL ).build(); methodSpecBuilder.addParameter( parameterSpec ); methodWithCallbackSpecBuilder.addParameter( parameterSpec ); if ( callParamBuilder.length() > 0 ) { callParamBuilder.append( ", " ); } callParamBuilder.append( variable.getSimpleName().toString() ); if ( isAnnotatedWith( variable, PathParam.class ) ) { PathParam pathParamAnnotation = variable.getAnnotation( PathParam.class ); initRestBuilder.add( "\n.addPathParam($S, $L)", pathParamAnnotation.value(), variable.getSimpleName() ); } else if ( isAnnotatedWith( variable, QueryParam.class ) ) { QueryParam queryParamAnnotation = variable.getAnnotation( QueryParam.class ); initRestBuilder.add( "\n.addQueryParam($S, $L)", queryParamAnnotation.value(), variable.getSimpleName() ); } } if ( null != returnTypeReaderGetter ) { initRestBuilder.add( "\n.responseConverter($N())", returnTypeReaderGetter ); } initRestBuilder.unindent(); methodSpecBuilder.addStatement( "return $L", initRestBuilder.build() ); MethodSpec methodSpec = methodSpecBuilder.build(); typeBuilder.addMethod( methodSpec ); methodWithCallbackSpecBuilder.addParameter( ParameterizedTypeName .get( ClassName.get( RestCallback.class ), returnTypeName ), "_callback_" ); methodWithCallbackSpecBuilder.addStatement( "return $L", CodeBlock.builder() .add( "$L($L)", methodName, callParamBuilder ) .indent() .add( "\n.callback(_callback_)" ) .add( "\n.send()" ) .unindent() .build() ); typeBuilder.addMethod( methodWithCallbackSpecBuilder.build() ); } private boolean isAnnotatedWith( Element element, Class<? extends Annotation> clazz ) { return element.getAnnotation( clazz ) != null; } /** * Prints a note message * * @param e The element which has caused the error. Can be null * @param msg The error message * @param args if the error message contains %s, %d etc. placeholders this arguments will be used * to replace them */ public void note( Element e, String msg, Object... args ) { messager.printMessage( Diagnostic.Kind.NOTE, String.format( msg, args ), e ); } /** * Prints a warning message * * @param e The element which has caused the error. Can be null * @param msg The error message * @param args if the error message contains %s, %d etc. placeholders this arguments will be used * to replace them */ public void warn( Element e, String msg, Object... args ) { messager.printMessage( Diagnostic.Kind.WARNING, String.format( msg, args ), e ); } /** * Prints an error message * * @param e The element which has caused the error. Can be null * @param msg The error message * @param args if the error message contains %s, %d etc. placeholders this arguments will be used * to replace them */ public void error( Element e, String msg, Object... args ) { messager.printMessage( Diagnostic.Kind.ERROR, String.format( msg, args ), e ); } }