View Javadoc
1   package org.codehaus.mojo.gwt.shell;
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.File;
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.regex.Pattern;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.MojoFailureException;
34  import org.apache.maven.plugins.annotations.Component;
35  import org.apache.maven.plugins.annotations.Execute;
36  import org.apache.maven.plugins.annotations.LifecyclePhase;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.plugins.annotations.ResolutionScope;
40  import org.apache.maven.project.MavenProject;
41  import org.codehaus.mojo.gwt.utils.GwtModuleReaderException;
42  import org.codehaus.plexus.archiver.ArchiverException;
43  import org.codehaus.plexus.archiver.UnArchiver;
44  import org.codehaus.plexus.archiver.manager.ArchiverManager;
45  import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
46  import org.codehaus.plexus.util.FileUtils;
47  import org.codehaus.plexus.util.DirectoryScanner;
48  
49  /**
50   * Runs the project in the GWT (Classic or Super) Dev Mode for development.
51   *
52   * @author ccollins
53   * @author cooper
54   * @version $Id$
55   */
56  @Mojo(name = "run", requiresDirectInvocation = true, requiresDependencyResolution = ResolutionScope.TEST)
57  @Execute(phase = LifecyclePhase.PROCESS_CLASSES, goal = "war:exploded")
58  public class RunMojo
59      extends AbstractGwtWebMojo
60  {
61      /**
62       * Location of the hosted-mode web application structure.
63       */
64      @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}")
65      // Parameter shared with EclipseMojo
66      private File hostedWebapp;
67  
68      /**
69       * The MavenProject executed by the "compile" phase
70       */
71      @Parameter(defaultValue = "${executedProject}")
72      private MavenProject executedProject;
73  
74      /**
75       * URL that should be automatically opened in the GWT shell. For example com.myapp.gwt.Module/Module.html.
76       * <p>
77       * When the host page is outside the module "public" folder (for example, at webapp root), the module MUST be
78       * specified (using a single &lt;module&gt; in configuration or by setting <code>-Dgwt.module=..</code>) and the
79       * runTarget parameter can only contain the host page URI.
80       * <p>
81       * When the GWT module host page is part of the module "public" folder, the runTarget MAY define the full GWT module
82       * path (<code>com.myapp.gwt.Module/Module.html</code>) that will be automatically converted according to the
83       * <code>rename-to</code> directive into <code>renamed/Module.html</code>.
84       */
85      @Parameter(property = "runTarget", required = true)
86      private String runTarget;
87  
88      /**
89       * Forked process execution timeOut (in seconds). Primary used for integration-testing.
90       */
91      @Parameter
92      @SuppressWarnings("unused")
93      private int runTimeOut;
94  
95      /**
96       * Runs the embedded GWT server on the specified port.
97       */
98      @Parameter(defaultValue = "8888", property = "gwt.port")
99      private int port;
100 
101     /**
102      * Runs the code server on the specified port.
103      */
104     @Parameter(defaultValue = "9997", property = "gwt.codeServerPort")
105     private int codeServerPort;
106 
107     /**
108      * Location of the compiled classes.
109      */
110     @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
111     private File buildOutputDirectory;
112 
113     /**
114      * Prevents the embedded GWT Tomcat server from running (even if a port is specified).
115      * <p>
116      * Can be set from command line using '-Dgwt.noserver=...'
117      */
118     @Parameter(defaultValue = "false", property = "gwt.noserver")
119     private boolean noServer;
120 
121     /**
122      * Specifies a different embedded web server to run (must implement ServletContainerLauncher)
123      */
124     @Parameter(property = "gwt.server")
125     private String server;
126 
127     /**
128      * List of System properties to pass when running the hosted mode.
129      *
130      * @since 1.2
131      */
132     @Parameter
133     private Map<String, String> systemProperties;
134     
135     /**
136      * Copies the contents of warSourceDirectory to hostedWebapp.
137      * <p>
138      * Can be set from command line using '-Dgwt.copyWebapp=...'
139      * </p>
140      *
141      * @since 2.1.0-1
142      */
143     @Parameter(defaultValue = "false", property = "gwt.copyWebapp")
144     private boolean copyWebapp;
145 
146     /**
147      * set the appengine sdk to use
148      * <p>
149      * Artifact will be downloaded with groupId : {@link #appEngineGroupId} 
150      * and artifactId {@link #appEngineArtifactId}
151      * <p>
152      *
153      * @since 2.1.0-1
154      */
155     @Parameter(defaultValue = "1.3.8", property = "gwt.appEngineVersion")
156     private String appEngineVersion;
157 
158     /**
159      * <p>
160      * List of {@link Pattern} jars to exclude from the classPath when running
161      * dev mode
162      * </p>
163      * 
164      * @since 2.1.0-1
165      */
166     @Parameter
167     private List<String> runClasspathExcludes;
168 
169     /**
170      * <p>
171      * Location to find appengine sdk or to unzip downloaded one see {@link #appEngineVersion}
172      * </p>
173      *
174      * @since 2.1.0-1
175      */
176     @Parameter(defaultValue = "${project.build.directory}/appengine-sdk/", property = "gwt.appEngineHome")
177     private File appEngineHome;
178 
179     /**
180      * groupId to download appengine sdk from maven repo
181      *
182      * @since 2.1.0-1
183      */
184     @Parameter(defaultValue = "com.google.appengine", property = "gwt.appEngineGroupId")
185     private String appEngineGroupId;
186 
187     /**
188      * groupId to download appengine sdk from maven repo
189      * 
190      * @since 2.1.0-1
191      */
192     @Parameter(defaultValue = "appengine-java-sdk", property = "gwt.appEngineArtifactId")
193     private String appEngineArtifactId;
194 
195     /**
196      * To look up Archiver/UnArchiver implementations
197      * @since 2.1.0-1
198      */
199     @Component
200     protected ArchiverManager archiverManager;
201 
202      /**
203      * Set GWT shell bindAddress.
204      * <p>
205      * Can be set from command line using '-Dgwt.bindAddress=...'
206      * @since 2.1.0-1
207      */
208     @Parameter(property = "gwt.bindAddress")
209     private String bindAddress;
210 
211     /**
212      * EXPERIMENTAL: Cache results of generators with stable output.
213      * 
214      * @since 2.6.0-rc1
215      */
216     @Parameter(defaultValue = "true", property = "gwt.cacheGeneratorResults")
217     private boolean cacheGeneratorResults;
218 
219     /**
220      * The compiler's working directory for internal use (must be writeable; defaults to a system temp dir)
221      *
222      * @since 2.6.0-rc1
223      */
224     @Parameter
225     private File workDir;
226 
227     /**
228      * Logs to a file in the given directory, as well as graphically
229      * 
230      * @since 2.6.0-rc1
231      */
232     @Parameter
233     private File logDir;
234 
235     /**
236      * Specifies Java source level.
237      * <p>
238      * The default value depends on the JVM used to launch Maven.
239      *
240      * @since 2.6.0-rc1
241      */
242     @Parameter(defaultValue = "auto", property = "maven.compiler.source")
243     private String sourceLevel;
244 
245     /**
246      * Runs Super Dev Mode instead of classic Development Mode.
247      * 
248      * @since 2.7.0-rc1
249      */
250     @Parameter(defaultValue = "true", property = "gwt.superDevMode")
251     private boolean superDevMode;
252 
253     /**
254      * Compiles faster by reusing data from the previous compile.
255      * 
256      * @since 2.7.0-rc1
257      */
258     @Parameter(alias = "compilePerFile", defaultValue = "true", property = "gwt.compiler.incremental")
259     private boolean incremental;
260 
261     /**
262      * Generate exports for JsInterop purposes.
263      *
264      * @since 2.8.0-rc1
265      */
266     @Parameter(alias = "generateJsInteropExports", defaultValue = "false", property = "gwt.compiler.generateJsInteropExports")
267     private boolean generateJsInteropExports;
268 
269     /**
270      * EXPERIMENTAL: Emit extra information allow chrome dev tools to display Java identifiers in many places instead of JavaScript functions.
271      * <p>
272      * Value can be one of NONE, ONLY_METHOD_NAME, ABBREVIATED or FULL.
273      * 
274      * @since 2.7.0-rc1
275      */
276     @Parameter(defaultValue = "NONE", property = "gwt.compiler.methodNameDisplayMode")
277     private String methodNameDisplayMode;
278 
279     /**
280      * @return the startup URL to open in hosted browser (gwt 1.6+)
281      */
282     public String getStartupUrl()
283         throws MojoExecutionException
284     {
285         if ( noServer )
286         {
287             return runTarget;
288         }
289 
290         int dash = runTarget.indexOf( '/' );
291         if ( dash > 0 )
292         {
293             String prefix = runTarget.substring( 0, dash );
294             // runTarget includes the GWT module full path.
295             // Lets retrieve the GWT module and apply the rename-to directive
296             String[] modules = getModules();
297             for ( String module : modules )
298             {
299                 if ( prefix.equals( module ) )
300                 {
301                     try
302                     {
303                         return readModule( module ).getPath() + '/' + runTarget.substring( dash + 1 );
304                     }
305                     catch ( GwtModuleReaderException e )
306                     {
307                         throw new MojoExecutionException( e.getMessage(), e );
308                     }
309                 }
310             }
311         }
312         return runTarget;
313     }
314 
315     public void doExecute( )
316         throws MojoExecutionException, MojoFailureException
317     {
318         JavaCommand cmd = createJavaCommand()
319             .setMainClass( "com.google.gwt.dev.DevMode" );
320 
321         if ( gwtSdkFirstInClasspath )
322         {
323             cmd.addToClasspath( getGwtUserJar() )
324                 .addToClasspath( getGwtDevJar() );
325         }
326 
327         cmd.addToClasspath( getClasspath( Artifact.SCOPE_RUNTIME ) );
328         addCompileSourceArtifacts( cmd );
329         addArgumentDeploy(cmd);
330         addArgumentGen( cmd );
331         addPersistentUnitCache(cmd);
332 
333         if ( !gwtSdkFirstInClasspath )
334         {
335             cmd.addToClasspath( getGwtUserJar() )
336                 .addToClasspath( getGwtDevJar() );
337         }
338 
339         cmd.arg( "-war", hostedWebapp.getAbsolutePath() )
340             .arg( "-logLevel", getLogLevel() )
341             .arg( "-port", Integer.toString( getPort() ) )
342             .arg( "-codeServerPort" , Integer.toString( codeServerPort ))
343             .arg( "-startupUrl", getStartupUrl() )
344             .arg( noServer, "-nostartServer" )
345             .arg( !cacheGeneratorResults, "-XnocacheGeneratorResults" )
346             .arg( !superDevMode, "-nosuperDevMode" )
347             .arg( !incremental, "-noincremental" )
348             .arg( generateJsInteropExports, "-generateJsInteropExports" )
349             .arg( "-sourceLevel", sourceLevel );
350 
351         if ( style != null && style.length() > 0 )
352         {
353             cmd.arg( "-style", style );
354         }
355 
356         if ( methodNameDisplayMode != null && methodNameDisplayMode.length() > 0 && !methodNameDisplayMode.equals( "NONE" ))
357         {
358             cmd.arg( "-XmethodNameDisplayMode", methodNameDisplayMode );
359         }
360 
361         if ( workDir != null )
362         {
363             cmd.arg( "-workDir", workDir.getAbsolutePath() );
364         }
365         if ( logDir != null )
366         {
367             cmd.arg( "-logdir", logDir.getAbsolutePath() );
368         }
369 
370         if ( server != null )
371         {
372             cmd.arg( "-server", server );
373         }
374 
375         if ( systemProperties != null && !systemProperties.isEmpty() )
376         {
377             for ( String key : systemProperties.keySet() )
378             {
379                 String value = systemProperties.get( key );
380                 if ( value != null )
381                 {
382                     getLog().debug( " " + key + "=" + value );
383                     cmd.systemProperty( key, value );
384                 }
385                 else
386                 {
387                     getLog().debug( "skip sysProps " + key + " with empty value" );
388                 }
389             }
390         }
391 
392         if ( bindAddress != null && bindAddress.length() > 0 )
393         {
394             cmd.arg( "-bindAddress" ).arg( bindAddress );
395         }
396 
397         if ( modulePathPrefix != null && !modulePathPrefix.isEmpty() )
398         {
399             cmd.arg( "-modulePathPrefix" ).arg( modulePathPrefix );
400         }
401 
402         if ( !noServer )
403         {
404             setupExplodedWar();
405         }
406         else
407         {
408             getLog().info( "noServer is set! Skipping exploding war file..." );
409         }
410 
411         for ( String module : getModules() )
412         {
413             cmd.arg( module );
414         }
415 
416         try
417         {
418             cmd.execute();
419         }
420         catch ( JavaCommandException e )
421         {
422             throw new MojoExecutionException( e.getMessage(), e );
423         }
424     }
425 
426     @Override
427     protected void postProcessClassPath( Collection<File> classPath )
428     {
429         boolean isAppEngine = "com.google.appengine.tools.development.gwt.AppEngineLauncher".equals( server );
430         List<Pattern> patternsToExclude = new ArrayList<Pattern>();
431         if ( runClasspathExcludes != null && !runClasspathExcludes.isEmpty() )
432         {
433             for ( String runClasspathExclude : runClasspathExcludes )
434             {
435                 patternsToExclude.add( Pattern.compile( runClasspathExclude ) );
436             }
437         }
438         Iterator<File> it = classPath.iterator();
439         while ( it.hasNext() )
440         {
441             String name = it.next().getName();
442             if ( !patternsToExclude.isEmpty() )
443             {
444                 for ( Pattern pattern : patternsToExclude )
445                 {
446                     if ( pattern.matcher( name ).find() )
447                     {
448                         getLog().info( "remove jar " + name + " from system classpath" );
449                         it.remove();
450                         continue;
451                     }
452                 }
453             }
454 
455         }
456         // TODO refactor this a little 
457         if ( isAppEngine )
458         {
459             File appEngineToolsApi = new File( appEngineHome, "/lib/appengine-tools-api.jar" );
460             File appEngineLocalRuntime = new File( appEngineHome, "/lib/impl/appengine-local-runtime.jar" );
461             File appEngineAgent = new File( appEngineHome, "/lib/agent/appengine-agent.jar" );
462             if ( appEngineHome.exists() && appEngineToolsApi.exists() && appEngineLocalRuntime.exists()
463                 && appEngineAgent.exists() )
464             {
465                 classPath.add( appEngineToolsApi );
466                 classPath.add( appEngineLocalRuntime );
467                 classPath.add( appEngineAgent );
468             }
469             else
470             {
471                 try
472                 {
473                     if ( !appEngineHome.exists() )
474                     {
475                         appEngineHome.mkdirs();
476                         // force addition of appengine SDK in a exploded SDK repository location
477                         Artifact appEngineSdk =
478                             resolve( appEngineGroupId, appEngineArtifactId, appEngineVersion, "zip", "" );
479                         // sdk extraction
480                         UnArchiver unArchiver = archiverManager.getUnArchiver( appEngineSdk.getFile() );
481                         unArchiver.setSourceFile( appEngineSdk.getFile() );
482                         unArchiver.setDestDirectory( appEngineHome );
483                         getLog().info( "extract appengine " + appEngineVersion + " sdk to " + appEngineHome.getPath() );
484                         unArchiver.extract();
485                     }
486                     else
487                     {
488                         getLog().info( "use existing appengine sdk from " + appEngineHome.getPath() );
489                     }
490                     appEngineToolsApi =
491                         new File( appEngineHome, "appengine-java-sdk-" + appEngineVersion
492                             + "/lib/appengine-tools-api.jar" );
493                     if ( !appEngineToolsApi.exists() )
494                     {
495                         throw new RuntimeException( appEngineToolsApi.getPath() + " not exists" );
496                     }
497                     classPath.add( appEngineToolsApi );
498                     getLog().debug( "add " + appEngineToolsApi.getPath() + " to the classpath" );
499 
500                     appEngineLocalRuntime =
501                         new File( appEngineHome, "appengine-java-sdk-" + appEngineVersion
502                             + "/lib/impl/appengine-local-runtime.jar" );
503                     if ( !appEngineLocalRuntime.exists() )
504                     {
505                         throw new RuntimeException( appEngineLocalRuntime.getPath() + " not exists" );
506                     }
507                     classPath.add( appEngineLocalRuntime );
508                     getLog().debug( "add " + appEngineLocalRuntime.getPath() + " to the classpath" );
509 
510                     appEngineAgent =
511                         new File( appEngineHome, "appengine-java-sdk-" + appEngineVersion
512                             + "/lib/agent/appengine-agent.jar" );
513                     classPath.add( appEngineAgent );
514                     getLog().debug( "add " + appEngineAgent.getPath() + " to the classpath" );
515                 }
516                 catch ( MojoExecutionException e )
517                 {
518                     // FIXME add throws MojoExecutionException in postProcessClassPath
519                     throw new RuntimeException( e.getMessage(), e );
520                 }
521                 catch ( ArchiverException e )
522                 {
523                     // FIXME add throws MojoExecutionException in postProcessClassPath
524                     throw new RuntimeException( e.getMessage(), e );
525                 }
526                 catch ( NoSuchArchiverException e )
527                 {
528                     // FIXME add throws MojoExecutionException in postProcessClassPath
529                     throw new RuntimeException( e.getMessage(), e );
530                 }
531             }
532         }
533     }  
534 
535     /**
536      * Copied a directory structure with deafault exclusions (.svn, CVS, etc)
537      *
538      * @param sourceDir The source directory to copy, must not be <code>null</code>.
539      * @param destDir The target directory to copy to, must not be <code>null</code>.
540      * @throws java.io.IOException If the directory structure could not be copied.
541      */
542     private void copyDirectoryStructureIfModified(File sourceDir, File destDir)
543             throws IOException {
544         
545         DirectoryScanner scanner = new DirectoryScanner();
546         scanner.setBasedir( sourceDir );
547         scanner.addDefaultExcludes();
548         scanner.scan();
549 
550         /*
551          * NOTE: Make sure the destination directory is always there (even if empty) to support POM-less ITs.
552          */
553         destDir.mkdirs();
554         String[] includedDirs = scanner.getIncludedDirectories();
555         for ( int i = 0; i < includedDirs.length; ++i ) {
556             File clonedDir = new File( destDir, includedDirs[i] );
557             clonedDir.mkdirs();
558         }
559 
560         String[] includedFiles = scanner.getIncludedFiles();
561         for ( int i = 0; i < includedFiles.length; ++i ) {
562             File sourceFile = new File(sourceDir, includedFiles[i]);
563             File destFile = new File(destDir, includedFiles[i]);
564             FileUtils.copyFileIfModified(sourceFile, destFile);
565         }
566     }
567     
568     private void setupExplodedWar()
569         throws MojoExecutionException
570     {
571         getLog().info( "create exploded Jetty webapp in " + hostedWebapp );
572 
573         if ( copyWebapp && !warSourceDirectory.getAbsolutePath().equals( hostedWebapp.getAbsolutePath() ) )
574         {
575             try
576             {
577                 // can't use FileUtils.copyDirectoryStructureIfModified because it does not 
578                 // excludes the DEFAULTEXCLUDES
579                 copyDirectoryStructureIfModified(warSourceDirectory, hostedWebapp);
580             }
581             catch ( IOException e )
582             {
583                 throw new MojoExecutionException( "Failed to copy warSourceDirectory to " + hostedWebapp, e );
584             }
585         }        
586         
587         File classes = new File( hostedWebapp, "WEB-INF/classes" );
588         classes.mkdirs();
589 
590         if ( !buildOutputDirectory.getAbsolutePath().equals( classes.getAbsolutePath() ) )
591         {
592             getLog().warn( "Your POM <build><outputdirectory> does not match your "
593                                 + "hosted webapp WEB-INF/classes folder for GWT Hosted browser to see your classes." );
594             try
595             {
596                 FileUtils.copyDirectoryStructure( buildOutputDirectory, classes );
597             }
598             catch ( IOException e )
599             {
600                 throw new MojoExecutionException( "Failed to copy classes to " + classes , e );
601             }
602         }
603 
604         File lib = new File( hostedWebapp, "WEB-INF/lib" );
605         lib.mkdirs();
606 
607         Collection<Artifact> artifacts = getProjectRuntimeArtifacts();
608         for ( Artifact artifact : artifacts )
609         {
610             try
611             {
612                 // Using m2eclipse with "resolve workspace dependencies" the artifact is the buildOutputDirectory
613                 if ( ! artifact.getFile().isDirectory() )
614                 {
615                     FileUtils.copyFileToDirectory( artifact.getFile(), lib );
616                 }
617             }
618             catch ( IOException e )
619             {
620                 throw new MojoExecutionException( "Failed to copy runtime dependency " + artifact, e );
621             }
622         }
623     }
624 
625     public int getPort()
626     {
627         return this.port;
628     }
629 
630     /**
631      * @param runTimeOut the runTimeOut to set
632      */
633     public void setRunTimeOut( int runTimeOut )
634     {
635         setTimeOut( runTimeOut );
636     }
637 
638     public void setExecutedProject( MavenProject executedProject )
639     {
640         this.executedProject = executedProject;
641     }
642 
643     @Override
644     public MavenProject getProject()
645     {
646         return executedProject;
647     }
648 }