View Javadoc
1   package org.codehaus.mojo.gwt;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedWriter;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.OutputStreamWriter;
26  import java.io.PrintWriter;
27  import java.net.MalformedURLException;
28  import java.net.URL;
29  import java.net.URLClassLoader;
30  import java.nio.charset.Charset;
31  import java.text.MessageFormat;
32  import java.util.Collection;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Map;
36  
37  import org.apache.maven.artifact.Artifact;
38  import org.apache.maven.plugin.MojoExecutionException;
39  import org.apache.maven.plugins.annotations.Component;
40  import org.apache.maven.plugins.annotations.LifecyclePhase;
41  import org.apache.maven.plugins.annotations.Mojo;
42  import org.apache.maven.plugins.annotations.Parameter;
43  import org.apache.maven.plugins.annotations.ResolutionScope;
44  import org.codehaus.plexus.util.Scanner;
45  import org.sonatype.plexus.build.incremental.BuildContext;
46  
47  import com.thoughtworks.qdox.JavaDocBuilder;
48  import com.thoughtworks.qdox.model.Annotation;
49  import com.thoughtworks.qdox.model.JavaClass;
50  import com.thoughtworks.qdox.model.JavaMethod;
51  import com.thoughtworks.qdox.model.JavaParameter;
52  import com.thoughtworks.qdox.model.Type;
53  
54  /**
55   * Goal which generate Async interface.
56   * 
57   * @author <a href="mailto:nicolas@apache.org">Nicolas De Loof</a>
58   * @version $Id$
59   */
60  @Mojo(name = "generateAsync", defaultPhase = LifecyclePhase.GENERATE_SOURCES, requiresDependencyResolution = ResolutionScope.COMPILE,
61        threadSafe = true)
62  public class GenerateAsyncMojo
63      extends AbstractGwtMojo
64  {
65      private static final String REMOTE_SERVICE_INTERFACE = "com.google.gwt.user.client.rpc.RemoteService";
66  
67      private final static Map<String, String> WRAPPERS = new HashMap<String, String>();
68      static
69      {
70          WRAPPERS.put( "boolean", Boolean.class.getName() );
71          WRAPPERS.put( "byte", Byte.class.getName() );
72          WRAPPERS.put( "char", Character.class.getName() );
73          WRAPPERS.put( "short", Short.class.getName() );
74          WRAPPERS.put( "int", Integer.class.getName() );
75          WRAPPERS.put( "long", Long.class.getName() );
76          WRAPPERS.put( "float", Float.class.getName() );
77          WRAPPERS.put( "double", Double.class.getName() );
78      }
79  
80      /**
81       * Pattern for GWT service interface
82       */
83      @Parameter(defaultValue = "**/*Service.java")
84      private String servicePattern;
85  
86      /**
87       * Return a com.google.gwt.http.client.Request on async interface to allow cancellation.
88       */
89      @Parameter(defaultValue = "false")
90      private boolean returnRequest;
91  
92      /**
93       * A (MessageFormat) Pattern to get the GWT-RPC servlet URL based on service interface name. For example to
94       * "{0}.rpc" if you want to map GWT-RPC calls to "*.rpc" in web.xml, for example when using Spring dispatch servlet
95       * to handle RPC requests.
96       */
97      @Parameter(defaultValue = "{0}", property = "gwt.rpcPattern")
98      private String rpcPattern;
99  
100     /**
101      * Stop the build on error
102      */
103     @Parameter(defaultValue = "true", property = "maven.gwt.failOnError")
104     private boolean failOnError;
105 
106     /**
107      * Pattern for GWT service interface
108      */
109     @Parameter(defaultValue = "false", property = "generateAsync.force")
110     private boolean force;
111 
112     @Parameter(property = "project.build.sourceEncoding")
113     private String encoding;
114 
115     @Component
116     private BuildContext buildContext;
117 
118     @Override
119     protected boolean isGenerator()
120     {
121         return true;
122     }
123 
124     public void execute()
125         throws MojoExecutionException
126     {
127         if ( "pom".equals( getProject().getPackaging() ) )
128         {
129             getLog().info( "GWT generateAsync is skipped" );
130             return;
131         }
132 
133         setupGenerateDirectory();
134 
135         if ( encoding == null )
136         {
137             getLog().warn( "Encoding is not set, your build will be platform dependent" );
138             encoding = Charset.defaultCharset().name();
139         }
140 
141         JavaDocBuilder builder = createJavaDocBuilder();
142 
143         List<String> sourceRoots = getProject().getCompileSourceRoots();
144         for ( String sourceRoot : sourceRoots )
145         {
146             try
147             {
148                 scanAndGenerateAsync( new File( sourceRoot ), builder );
149             }
150             catch ( Throwable e )
151             {
152                 getLog().error( "Failed to generate Async interface", e );
153                 if ( failOnError )
154                 {
155                     throw new MojoExecutionException( "Failed to generate Async interface", e );
156                 }
157             }
158         }
159     }
160 
161     /**
162      * @param sourceRoot the base directory to scan for RPC services
163      * @return true if some file have been generated
164      * @throws Exception generation failure
165      */
166     private boolean scanAndGenerateAsync( File sourceRoot, JavaDocBuilder builder )
167         throws Exception
168     {
169         Scanner scanner = buildContext.newScanner( sourceRoot );
170         scanner.setIncludes( new String[] { servicePattern } );
171         scanner.scan();
172         String[] sources = scanner.getIncludedFiles();
173         if ( sources.length == 0 )
174         {
175             return false;
176         }
177         boolean fileGenerated = false;
178         for ( String source : sources )
179         {
180             File sourceFile = new File( sourceRoot, source );
181             File targetFile = getTargetFile( source );
182             if ( !force && buildContext.isUptodate( targetFile, sourceFile ) )
183             {
184                 getLog().debug( targetFile.getAbsolutePath() + " is up to date. Generation skipped" );
185                 // up to date, but still need to report generated-sources directory as sourceRoot
186                 fileGenerated = true;
187                 continue;
188             }
189 
190             String className = getTopLevelClassName( source );
191             JavaClass clazz = builder.getClassByName( className );
192             if ( isEligibleForGeneration( clazz ) )
193             {
194                 getLog().debug( "Generating async interface for service " + className );
195                 targetFile.getParentFile().mkdirs();
196                 generateAsync( clazz, targetFile );
197                 fileGenerated = true;
198             }
199         }
200         return fileGenerated;
201     }
202 
203     private File getTargetFile( String source )
204     {
205         String targetFileName = source.substring( 0, source.length() - 5 ) + "Async.java";
206         File targetFile = new File( getGenerateDirectory(), targetFileName );
207         return targetFile;
208     }
209 
210     /**
211      * @param clazz the RPC service java class
212      * @param targetFile RemoteAsync file to generate
213      * @throws Exception generation failure
214      */
215     private void generateAsync( JavaClass clazz, File targetFile )
216         throws IOException
217     {
218         PrintWriter writer = new PrintWriter( new BufferedWriter(
219             new OutputStreamWriter( buildContext.newFileOutputStream( targetFile ), encoding ) ) );
220 
221         boolean hasRemoteServiceRelativePath = hasRemoteServiceRelativePath(clazz);
222 
223         String className = clazz.getName();
224         if ( clazz.getPackage() != null )
225         {
226             writer.println( "package " + clazz.getPackageName() + ";" );
227             writer.println();
228         }
229         writer.println( "import com.google.gwt.core.client.GWT;" );
230         writer.println( "import com.google.gwt.user.client.rpc.AsyncCallback;" );
231 
232         if (!hasRemoteServiceRelativePath)
233         {
234             writer.println( "import com.google.gwt.user.client.rpc.ServiceDefTarget;" );
235         }
236 
237         writer.println();
238         writer.println( "public interface " + className + "Async" );
239         writer.println( "{" );
240 
241         JavaMethod[] methods = clazz.getMethods( true );
242         for ( JavaMethod method : methods )
243         {
244             boolean deprecated = isDeprecated( method );
245 
246             writer.println( "" );
247             writer.println( "    /**" );
248             writer.println( "     * GWT-RPC service  asynchronous (client-side) interface" );
249             writer.println( "     * @see " + clazz.getFullyQualifiedName() );
250             if ( deprecated )
251                 writer.println( "     * @deprecated" );
252             writer.println( "     */" );
253             if ( deprecated )
254                 writer.println( "    @Deprecated" );
255             if ( returnRequest )
256             {
257                 writer.print( "    com.google.gwt.http.client.Request " + method.getName() + "( " );
258             }
259             else
260             {
261                 writer.print( "    void " + method.getName() + "( " );
262             }
263             JavaParameter[] params = method.getParameters();
264             for ( int j = 0; j < params.length; j++ )
265             {
266                 JavaParameter param = params[j];
267                 if ( j > 0 )
268                 {
269                     writer.print( ", " );
270                 }
271 
272                 writer.print( method.getParameterTypes( true )[j].getGenericValue() );
273                 if ( param.getType().getDimensions() != method.getParameterTypes( true )[j].getDimensions() )
274                 {
275                     for ( int dimensions = 0; dimensions < param.getType().getDimensions(); dimensions++ )
276                     {
277                         writer.print( "[]" );
278                     }
279                 }
280                 writer.print( " " + param.getName() );
281             }
282             if ( params.length > 0 )
283             {
284                 writer.print( ", " );
285             }
286 
287             if ( method.getReturnType().isVoid() )
288             {
289                 writer.println( "AsyncCallback<Void> callback );" );
290             }
291             else if ( method.getReturnType().isPrimitive() )
292             {
293                 String primitive = method.getReturnType().getGenericValue();
294                 writer.println( "AsyncCallback<" + WRAPPERS.get( primitive ) + "> callback );" );
295             }
296             else
297             {
298                 Type returnType = method.getReturnType( true );
299                 String type = returnType.getGenericValue();
300 
301                 if ( method.getReturnType().getDimensions() != method.getReturnType( true ).getDimensions() )
302                 {
303                     for ( int dimensions = 0; dimensions < method.getReturnType().getDimensions(); dimensions++ )
304                     {
305                         type += "[]";
306                     }
307                 }
308                 writer.println( "AsyncCallback<" + type + "> callback );" );
309             }
310             writer.println();
311         }
312 
313         writer.println();
314         writer.println( "    /**" );
315         writer.println( "     * Utility class to get the RPC Async interface from client-side code" );
316         writer.println( "     */" );
317         writer.println( "    public static final class Util " );
318         writer.println( "    { " );
319         writer.println( "        private static " + className + "Async instance;" );
320         writer.println();
321         writer.println( "        public static final " + className + "Async getInstance()" );
322         writer.println( "        {" );
323         writer.println( "            if ( instance == null )" );
324         writer.println( "            {" );
325         writer.println( "                instance = (" + className + "Async) GWT.create( " + className + ".class );" );
326         if ( !hasRemoteServiceRelativePath )
327         {
328             String uri = MessageFormat.format( rpcPattern, className );
329             writer.println( "                ServiceDefTarget target = (ServiceDefTarget) instance;" );
330             writer.println( "                target.setServiceEntryPoint( GWT.getModuleBaseURL() + \"" + uri + "\" );" );
331         }
332         writer.println( "            }" );
333         writer.println( "            return instance;" );
334         writer.println( "        }" );
335         writer.println( "" );
336         writer.println( "        private Util()" );
337         writer.println( "        {" );
338         writer.println( "            // Utility class should not be instantiated" );
339         writer.println( "        }" );
340         writer.println( "    }" );
341 
342         writer.println( "}" );
343         writer.close();
344     }
345 
346     private boolean isEligibleForGeneration( JavaClass javaClass )
347     {
348         return javaClass.isInterface() && javaClass.isPublic() && javaClass.isA( REMOTE_SERVICE_INTERFACE );
349     }
350 
351     private JavaDocBuilder createJavaDocBuilder()
352         throws MojoExecutionException
353     {
354         JavaDocBuilder builder = new JavaDocBuilder();
355         builder.setEncoding( encoding );
356         builder.getClassLibrary().addClassLoader( getProjectClassLoader() );
357         for ( String sourceRoot : getProject().getCompileSourceRoots() )
358         {
359             builder.getClassLibrary().addSourceFolder( new File( sourceRoot ) );
360         }
361         return builder;
362     }
363 
364     private String getTopLevelClassName( String sourceFile )
365     {
366         String className = sourceFile.substring( 0, sourceFile.length() - 5 ); // strip ".java"
367         return className.replace( File.separatorChar, '.' );
368     }
369 
370     /**
371      * Determine if a client service method is deprecated.
372      * 
373      * @see MGWT-352
374      */
375     private boolean isDeprecated( JavaMethod method )
376     {
377         if ( method == null )
378             return false;
379 
380         for ( Annotation annotation : method.getAnnotations() )
381         {
382             if ( "java.lang.Deprecated".equals( annotation.getType().getFullyQualifiedName() ) )
383             {
384                 return true;
385             }
386         }
387 
388         return method.getTagByName( "deprecated" ) != null;
389     }
390 
391     private boolean hasRemoteServiceRelativePath(final JavaClass clazz)
392     {
393         if ( clazz != null && clazz.getAnnotations() != null )
394         {
395             for ( Annotation annotation : clazz.getAnnotations() )
396             {
397                 getLog().debug( "annotation found on service interface " + annotation );
398                 if ( annotation.getType().getValue().equals( "com.google.gwt.user.client.rpc.RemoteServiceRelativePath" ) )
399                 {
400                     getLog().debug( "@RemoteServiceRelativePath annotation found on service interface" );
401                     return true;
402                 }
403             }
404         }
405 
406         return false;
407     }
408 
409     private ClassLoader getProjectClassLoader() throws MojoExecutionException
410     {
411         Collection<File> classpath = getClasspath( Artifact.SCOPE_COMPILE );
412         URL[] urls = new URL[classpath.size()];
413         try
414         {
415             int i = 0;
416             for ( File classpathFile : classpath )
417             {
418                 urls[i] = classpathFile.toURI().toURL();
419                 i++;
420             }
421         }
422         catch ( MalformedURLException e )
423         {
424             throw new MojoExecutionException( e.getMessage(), e );
425         }
426         return new URLClassLoader( urls, ClassLoader.getSystemClassLoader() );
427     }
428 }