package org.codehaus.mojo.gwt; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.MojoExecutionException; import org.codehaus.plexus.util.DirectoryScanner; import com.thoughtworks.qdox.JavaDocBuilder; import com.thoughtworks.qdox.model.Annotation; import com.thoughtworks.qdox.model.JavaClass; import com.thoughtworks.qdox.model.JavaMethod; import com.thoughtworks.qdox.model.JavaParameter; import com.thoughtworks.qdox.model.Type; /** * Goal which generate Asyn interface. * * @goal generateAsync * @phase generate-sources * @requiresDependencyResolution compile * @author <a href="mailto:nicolas@apache.org">Nicolas De Loof</a> * @version $Id$ */ public class GenerateAsyncMojo extends AbstractGwtMojo { private static final String REMOTE_SERVICE_INTERFACE = "com.google.gwt.user.client.rpc.RemoteService"; private final static Map<String, String> WRAPPERS = new HashMap<String, String>(); static { WRAPPERS.put( "boolean", Boolean.class.getName() ); WRAPPERS.put( "byte", Byte.class.getName() ); WRAPPERS.put( "char", Character.class.getName() ); WRAPPERS.put( "short", Short.class.getName() ); WRAPPERS.put( "int", Integer.class.getName() ); WRAPPERS.put( "long", Long.class.getName() ); WRAPPERS.put( "float", Float.class.getName() ); WRAPPERS.put( "double", Double.class.getName() ); } /** * Pattern for GWT service interface * * @parameter default-value="**\/*Service.java" */ private String servicePattern; /** * Return a com.google.gwt.http.client.Request on async interface to allow cancellation. * * @parameter default-value="false" */ private boolean returnRequest; /** * A (MessageFormat) Pattern to get the GWT-RPC servlet URL based on service interface name. For example to * "{0}.rpc" if you want to map GWT-RPC calls to "*.rpc" in web.xml, for example when using Spring dispatch servlet * to handle RPC requests. * * @parameter default-value="{0}" expression="${gwt.rpcPattern}" */ private String rpcPattern; /** * Stop the build on error * * @parameter default-value="true" expression="${maven.gwt.failOnError}" */ private boolean failOnError; /** * Pattern for GWT service interface * * @parameter default-value="false" expression="${generateAsync.force}" */ private boolean force; /** * @parameter expression="${project.build.sourceEncoding}" */ private String encoding; /** * {@inheritDoc} */ @SuppressWarnings( "unchecked" ) public void execute() throws MojoExecutionException { getLog().debug( "GenerateAsyncMojo#execute()" ); if ( encoding == null ) { getLog().warn( "Encoding is not set, your build will be platform dependent" ); encoding = Charset.defaultCharset().name(); } JavaDocBuilder builder = createJavaDocBuilder(); List<String> sourceRoots = getProject().getCompileSourceRoots(); boolean generated = false; for ( String sourceRoot : sourceRoots ) { try { generated |= scanAndGenerateAsync( new File( sourceRoot ), builder ); } catch ( Throwable e ) { getLog().error( "Failed to generate Async interface", e ); if ( failOnError ) { throw new MojoExecutionException( "Failed to generate Async interface", e ); } } } if ( generated ) { getLog().debug( "add compile source root " + getGenerateDirectory() ); addCompileSourceRoot( getGenerateDirectory() ); } } /** * @param sourceRoot the base directory to scan for RPC services * @return true if some file have been generated * @throws Exception generation failure */ private boolean scanAndGenerateAsync( File sourceRoot, JavaDocBuilder builder ) throws Exception { DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir( sourceRoot ); scanner.setIncludes( new String[] { servicePattern } ); scanner.scan(); String[] sources = scanner.getIncludedFiles(); if ( sources.length == 0 ) { return false; } boolean fileGenerated = false; for ( String source : sources ) { File sourceFile = new File( sourceRoot, source ); File targetFile = getTargetFile( source ); if ( isUpToDate( sourceFile, targetFile ) ) { getLog().debug( targetFile.getAbsolutePath() + " is up to date. Generation skipped" ); // up to date, but still need to report generated-sources directory as sourceRoot fileGenerated = true; continue; } String className = getTopLevelClassName( source ); JavaClass clazz = builder.getClassByName( className ); if ( isEligibleForGeneration( clazz ) ) { targetFile.getParentFile().mkdirs(); generateAsync( clazz, targetFile ); fileGenerated = true; } } return fileGenerated; } private boolean isUpToDate( File sourceFile, File targetFile ) { return !force && targetFile.exists() && targetFile.lastModified() > sourceFile.lastModified(); } private File getTargetFile( String source ) { String targetFileName = source.substring( 0, source.length() - 5 ) + "Async.java"; File targetFile = new File( getGenerateDirectory(), targetFileName ); return targetFile; } /** * @param clazz the RPC service java class * @param targetFile RemoteAsync file to generate * @throws Exception generation failure */ private void generateAsync( JavaClass clazz, File targetFile ) throws IOException { PrintWriter writer = new PrintWriter( targetFile, encoding ); String className = clazz.getName(); if ( clazz.getPackage() != null ) { writer.println( "package " + clazz.getPackageName() + ";" ); writer.println(); } writer.println( "import com.google.gwt.core.client.GWT;" ); writer.println( "import com.google.gwt.user.client.rpc.AsyncCallback;" ); writer.println( "import com.google.gwt.user.client.rpc.ServiceDefTarget;" ); writer.println(); writer.println( "public interface " + className + "Async" ); writer.println( "{" ); JavaMethod[] methods = clazz.getMethods( true ); for ( JavaMethod method : methods ) { writer.println( "" ); writer.println( " /**" ); writer.println( " * GWT-RPC service asynchronous (client-side) interface" ); writer.println( " * @see " + clazz.getFullyQualifiedName() ); writer.println( " */" ); if ( returnRequest ) { writer.print( " com.google.gwt.http.client.Request " + method.getName() + "( " ); } else { writer.print( " void " + method.getName() + "( " ); } JavaParameter[] params = method.getParameters(); for ( int j = 0; j < params.length; j++ ) { JavaParameter param = params[j]; if ( j > 0 ) { writer.print( ", " ); } writer.print( method.getParameterTypes( true )[j].getGenericValue() ); if ( param.getType().getDimensions() != method.getParameterTypes( true )[j].getDimensions() ) { for ( int dimensions = 0 ; dimensions < param.getType().getDimensions(); dimensions++ ) { writer.print( "[]" ); } } writer.print( " " + param.getName() ); } if ( params.length > 0 ) { writer.print( ", " ); } if ( method.getReturnType().isVoid() ) { writer.println( "AsyncCallback<Void> callback );" ); } else if ( method.getReturnType().isPrimitive() ) { String primitive = method.getReturnType().getGenericValue(); writer.println( "AsyncCallback<" + WRAPPERS.get( primitive ) + "> callback );" ); } else { Type returnType = method.getReturnType( true ); String type = returnType.getGenericValue(); if ( method.getReturnType().getDimensions() != method.getReturnType( true ).getDimensions() ) { for ( int dimensions = 0; dimensions < method.getReturnType().getDimensions(); dimensions++ ) { type += "[]"; } } writer.println( "AsyncCallback<" + type + "> callback );" ); } writer.println(); } writer.println(); String uri = MessageFormat.format( rpcPattern, className ); if ( clazz.getAnnotations() != null ) { for ( Annotation annotation : clazz.getAnnotations() ) { getLog().debug( "annotation found on service interface " + annotation ); if ( annotation.getType().getValue().equals( "com.google.gwt.user.client.rpc.RemoteServiceRelativePath" ) ) { uri = annotation.getNamedParameter( "value" ).toString(); // remove quotes uri = uri.substring( 1, uri.length() - 1 ); getLog().debug( "@RemoteServiceRelativePath annotation found on service interface " + uri ); } } } writer.println( " /**" ); writer.println( " * Utility class to get the RPC Async interface from client-side code" ); writer.println( " */" ); writer.println( " public static final class Util " ); writer.println( " { " ); writer.println( " private static " + className + "Async instance;" ); writer.println(); writer.println( " public static final " + className + "Async getInstance()" ); writer.println( " {" ); writer.println( " if ( instance == null )" ); writer.println( " {" ); writer.println( " instance = (" + className + "Async) GWT.create( " + className + ".class );" ); writer.println( " ServiceDefTarget target = (ServiceDefTarget) instance;" ); writer.println( " target.setServiceEntryPoint( GWT.getModuleBaseURL() + \"" + uri + "\" );" ); writer.println( " }" ); writer.println( " return instance;" ); writer.println( " }" ); writer.println( "" ); writer.println( " private Util()" ); writer.println( " {" ); writer.println( " // Utility class should not be instanciated" ); writer.println( " }" ); writer.println( " }" ); writer.println( "}" ); writer.close(); } private boolean isEligibleForGeneration( JavaClass javaClass ) { return javaClass.isInterface() && javaClass.isPublic() && javaClass.isA( REMOTE_SERVICE_INTERFACE ); } @SuppressWarnings("unchecked") private JavaDocBuilder createJavaDocBuilder() throws MojoExecutionException { try { JavaDocBuilder builder = new JavaDocBuilder(); builder.setEncoding( encoding ); builder.getClassLibrary().addClassLoader( getProjectClassLoader() ); for ( String sourceRoot : ( List < String > ) getProject().getCompileSourceRoots() ) { builder.getClassLibrary().addSourceFolder( new File( sourceRoot ) ); } return builder; } catch ( MalformedURLException e ) { throw new MojoExecutionException( "Failed to resolve project classpath", e ); } catch ( DependencyResolutionRequiredException e ) { throw new MojoExecutionException( "Failed to resolve project classpath", e ); } } private String getTopLevelClassName( String sourceFile ) { String className = sourceFile.substring( 0, sourceFile.length() - 5 ); // strip ".java" return className.replace( File.separatorChar, '.' ); } /** * @return the project classloader * @throws DependencyResolutionRequiredException failed to resolve project dependencies * @throws MalformedURLException configuration issue ? */ protected ClassLoader getProjectClassLoader() throws DependencyResolutionRequiredException, MalformedURLException { getLog().debug( "AbstractMojo#getProjectClassLoader()" ); List<?> compile = getProject().getCompileClasspathElements(); URL[] urls = new URL[compile.size()]; int i = 0; for ( Object object : compile ) { if ( object instanceof Artifact ) { urls[i] = ( (Artifact) object ).getFile().toURI().toURL(); } else { urls[i] = new File( (String) object ).toURI().toURL(); } i++; } return new URLClassLoader( urls, ClassLoader.getSystemClassLoader() ); } }