View Javadoc
1   package org.apache.maven.plugins.help;
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.FileInputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.StringWriter;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Properties;
31  import java.util.TreeMap;
32  import java.util.jar.JarEntry;
33  import java.util.jar.JarInputStream;
34  
35  import org.apache.commons.lang3.ClassUtils;
36  import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
37  import org.apache.maven.model.Dependency;
38  import org.apache.maven.model.Model;
39  import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
40  import org.apache.maven.plugin.MojoExecution;
41  import org.apache.maven.plugin.MojoExecutionException;
42  import org.apache.maven.plugin.MojoFailureException;
43  import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
44  import org.apache.maven.plugin.descriptor.MojoDescriptor;
45  import org.apache.maven.plugins.annotations.Component;
46  import org.apache.maven.plugins.annotations.Mojo;
47  import org.apache.maven.plugins.annotations.Parameter;
48  import org.apache.maven.project.DefaultProjectBuildingRequest;
49  import org.apache.maven.project.MavenProject;
50  import org.apache.maven.project.ProjectBuildingException;
51  import org.apache.maven.project.ProjectBuildingRequest;
52  import org.apache.maven.settings.Settings;
53  import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer;
54  import org.apache.maven.shared.transfer.artifact.ArtifactCoordinate;
55  import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException;
56  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
57  import org.codehaus.plexus.components.interactivity.InputHandler;
58  import org.codehaus.plexus.util.IOUtil;
59  import org.codehaus.plexus.util.StringUtils;
60  
61  import com.thoughtworks.xstream.XStream;
62  import com.thoughtworks.xstream.converters.MarshallingContext;
63  import com.thoughtworks.xstream.converters.collections.PropertiesConverter;
64  import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
65  
66  /**
67   * Evaluates Maven expressions given by the user in an interactive mode.
68   *
69   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
70   * @since 2.1
71   */
72  @Mojo( name = "evaluate", requiresProject = false )
73  public class EvaluateMojo
74      extends AbstractHelpMojo
75  {
76      // ----------------------------------------------------------------------
77      // Mojo components
78      // ----------------------------------------------------------------------
79  
80      /**
81       * Input handler, needed for command line handling.
82       */
83      @Component
84      private InputHandler inputHandler;
85  
86      /**
87       * Component used to get mojo descriptors.
88       */
89      @Component
90      private MojoDescriptorCreator mojoDescriptorCreator;
91  
92      // ----------------------------------------------------------------------
93      // Mojo parameters
94      // ----------------------------------------------------------------------
95      
96      // we need to hide the 'output' defined in AbstractHelpMojo to have a correct "since".
97      /**
98       * Optional parameter to write the output of this help in a given file, instead of writing to the console.
99       * This parameter will be ignored if no <code>expression</code> is specified.
100      * <br/>
101      * <b>Note</b>: Could be a relative path.
102      * 
103      * @since 3.0.0
104      */
105     @Parameter( property = "output" )
106     private File output;
107 
108     /**
109      * This options gives the option to output information in cases where the output has been suppressed by using
110      * <code>-q</code> (quiet option) in Maven. This is useful if you like to use
111      * <code>maven-help-plugin:evaluate</code> in a script call (for example in bash) like this:
112      * 
113      * <pre>
114      * RESULT=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
115      * echo $RESULT
116      * </pre>
117      * 
118      * This will only printout the information which has been requested by <code>expression</code> to
119      * <code>stdout</code>.
120      * 
121      * @since 3.1.0
122      */
123     @Parameter( property = "forceStdout", defaultValue = "false" )
124     private boolean forceStdout;
125 
126     /**
127      * An artifact for evaluating Maven expressions. <br/>
128      * <b>Note</b>: Should respect the Maven format, i.e. <code>groupId:artifactId[:version]</code>. The latest version
129      * of the artifact will be used when no version is specified.
130      */
131     @Parameter( property = "artifact" )
132     private String artifact;
133 
134     /**
135      * An expression to evaluate instead of prompting. Note that this <i>must not</i> include the surrounding ${...}.
136      */
137     @Parameter( property = "expression" )
138     private String expression;
139 
140     /**
141      * Maven project built from the given {@link #artifact}. Otherwise, the current Maven project or the super pom.
142      */
143     @Parameter( defaultValue = "${project}", readonly = true, required = true )
144     private MavenProject project;
145 
146     /**
147      * The system settings for Maven.
148      */
149     @Parameter( defaultValue = "${settings}", readonly = true, required = true )
150     private Settings settings;
151 
152     // ----------------------------------------------------------------------
153     // Instance variables
154     // ----------------------------------------------------------------------
155 
156     /** lazy loading evaluator variable */
157     private PluginParameterExpressionEvaluator evaluator;
158 
159     /** lazy loading xstream variable */
160     private XStream xstream;
161 
162     // ----------------------------------------------------------------------
163     // Public methods
164     // ----------------------------------------------------------------------
165 
166     /** {@inheritDoc} */
167     public void execute()
168         throws MojoExecutionException, MojoFailureException
169     {
170         if ( expression == null && !settings.isInteractiveMode() )
171         {
172 
173             getLog().error( "Maven is configured to NOT interact with the user for input. "
174                             + "This Mojo requires that 'interactiveMode' in your settings file is flag to 'true'." );
175             return;
176         }
177 
178         validateParameters();
179 
180         if ( StringUtils.isNotEmpty( artifact ) )
181         {
182             project = getMavenProject( artifact );
183         }
184 
185         if ( expression == null )
186         {
187             if ( output != null )
188             {
189                 getLog().warn( "When prompting for input, the result will be written to the console, "
190                     + "ignoring 'output'." );
191             }
192             while ( true )
193             {
194                 getLog().info( "Enter the Maven expression i.e. ${project.groupId} or 0 to exit?:" );
195 
196                 try
197                 {
198                     String userExpression = inputHandler.readLine();
199                     if ( userExpression == null || userExpression.toLowerCase( Locale.ENGLISH ).equals( "0" ) )
200                     {
201                         break;
202                     }
203 
204                     handleResponse( userExpression, null );
205                 }
206                 catch ( IOException e )
207                 {
208                     throw new MojoExecutionException( "Unable to read from standard input.", e );
209                 }
210             }
211         }
212         else
213         {
214             handleResponse( "${" + expression + "}", output );
215         }
216     }
217 
218     // ----------------------------------------------------------------------
219     // Private methods
220     // ----------------------------------------------------------------------
221 
222     /**
223      * Validate Mojo parameters.
224      */
225     private void validateParameters()
226     {
227         if ( artifact == null )
228         {
229             // using project if found or super-pom
230             getLog().info( "No artifact parameter specified, using '" + project.getId() + "' as project." );
231         }
232     }
233 
234     /**
235      * @return a lazy loading evaluator object.
236      * @throws MojoExecutionException if any
237      * @throws MojoFailureException if any reflection exceptions occur or missing components.
238      */
239     private PluginParameterExpressionEvaluator getEvaluator()
240         throws MojoExecutionException, MojoFailureException
241     {
242         if ( evaluator == null )
243         {
244             MojoDescriptor mojoDescriptor;
245             try
246             {
247                 mojoDescriptor = mojoDescriptorCreator.getMojoDescriptor( "help:evaluate", session, project );
248             }
249             catch ( Exception e )
250             {
251                 throw new MojoFailureException( "Failure while evaluating.", e );
252             }
253             MojoExecution mojoExecution = new MojoExecution( mojoDescriptor );
254 
255             MavenProject currentProject = session.getCurrentProject();
256             // Maven 3: PluginParameterExpressionEvaluator gets the current project from the session:
257             // synchronize in case another thread wants to fetch the real current project in between
258             synchronized ( session )
259             {
260                 session.setCurrentProject( project );
261                 evaluator = new PluginParameterExpressionEvaluator( session, mojoExecution );
262                 session.setCurrentProject( currentProject );
263             }
264         }
265 
266         return evaluator;
267     }
268 
269     /**
270      * @param expr the user expression asked.
271      * @param output the file where to write the result, or <code>null</code> to print in standard output.
272      * @throws MojoExecutionException if any
273      * @throws MojoFailureException if any reflection exceptions occur or missing components.
274      */
275     private void handleResponse( String expr, File output )
276         throws MojoExecutionException, MojoFailureException
277     {
278         StringBuilder response = new StringBuilder();
279 
280         Object obj;
281         try
282         {
283             obj = getEvaluator().evaluate( expr );
284         }
285         catch ( ExpressionEvaluationException e )
286         {
287             throw new MojoExecutionException( "Error when evaluating the Maven expression", e );
288         }
289 
290         if ( obj != null && expr.equals( obj.toString() ) )
291         {
292             getLog().warn( "The Maven expression was invalid. Please use a valid expression." );
293             return;
294         }
295 
296         // handle null
297         if ( obj == null )
298         {
299             response.append( "null object or invalid expression" );
300         }
301         // handle primitives objects
302         else if ( obj instanceof String )
303         {
304             response.append( obj.toString() );
305         }
306         else if ( obj instanceof Boolean )
307         {
308             response.append( obj.toString() );
309         }
310         else if ( obj instanceof Byte )
311         {
312             response.append( obj.toString() );
313         }
314         else if ( obj instanceof Character )
315         {
316             response.append( obj.toString() );
317         }
318         else if ( obj instanceof Double )
319         {
320             response.append( obj.toString() );
321         }
322         else if ( obj instanceof Float )
323         {
324             response.append( obj.toString() );
325         }
326         else if ( obj instanceof Integer )
327         {
328             response.append( obj.toString() );
329         }
330         else if ( obj instanceof Long )
331         {
332             response.append( obj.toString() );
333         }
334         else if ( obj instanceof Short )
335         {
336             response.append( obj.toString() );
337         }
338         // handle specific objects
339         else if ( obj instanceof File )
340         {
341             File f = (File) obj;
342             response.append( f.getAbsolutePath() );
343         }
344         // handle Maven pom object
345         else if ( obj instanceof MavenProject )
346         {
347             MavenProject projectAsked = (MavenProject) obj;
348             StringWriter sWriter = new StringWriter();
349             MavenXpp3Writer pomWriter = new MavenXpp3Writer();
350             try
351             {
352                 pomWriter.write( sWriter, projectAsked.getModel() );
353             }
354             catch ( IOException e )
355             {
356                 throw new MojoExecutionException( "Error when writing pom", e );
357             }
358 
359             response.append( sWriter.toString() );
360         }
361         // handle Maven Settings object
362         else if ( obj instanceof Settings )
363         {
364             Settings settingsAsked = (Settings) obj;
365             StringWriter sWriter = new StringWriter();
366             SettingsXpp3Writer settingsWriter = new SettingsXpp3Writer();
367             try
368             {
369                 settingsWriter.write( sWriter, settingsAsked );
370             }
371             catch ( IOException e )
372             {
373                 throw new MojoExecutionException( "Error when writing settings", e );
374             }
375 
376             response.append( sWriter.toString() );
377         }
378         else
379         {
380             // others Maven objects
381             response.append( toXML( expr, obj ) );
382         }
383 
384         if ( output != null )
385         {
386             try
387             {
388                 writeFile( output, response );
389             }
390             catch ( IOException e )
391             {
392                 throw new MojoExecutionException( "Cannot write evaluation of expression to output: " + output, e );
393             }
394             getLog().info( "Result of evaluation written to: " + output );
395         }
396         else
397         {
398             if ( getLog().isInfoEnabled() )
399             {
400                 getLog().info( LS + response.toString() );
401             }
402             else
403             {
404                 if ( forceStdout )
405                 {
406                     System.out.print( response.toString() );
407                     System.out.flush();
408                 }
409             }
410         }
411     }
412 
413     /**
414      * @param expr the user expression.
415      * @param obj a not null.
416      * @return the XML for the given object.
417      */
418     private String toXML( String expr, Object obj )
419     {
420         XStream currentXStream = getXStream();
421 
422         // beautify list
423         if ( obj instanceof List )
424         {
425             List<?> list = (List<?>) obj;
426             if ( list.size() > 0 )
427             {
428                 Object elt = list.iterator().next();
429 
430                 String name = StringUtils.lowercaseFirstLetter( elt.getClass().getSimpleName() );
431                 currentXStream.alias( pluralize( name ), List.class );
432             }
433             else
434             {
435                 // try to detect the alias from question
436                 if ( expr.indexOf( '.' ) != -1 )
437                 {
438                     String name = expr.substring( expr.indexOf( '.' ) + 1, expr.indexOf( '}' ) );
439                     currentXStream.alias( name, List.class );
440                 }
441             }
442         }
443 
444         return currentXStream.toXML( obj );
445     }
446 
447     /**
448      * @return lazy loading xstream object.
449      */
450     private XStream getXStream()
451     {
452         if ( xstream == null )
453         {
454             xstream = new XStream();
455             addAlias( xstream );
456 
457             // handle Properties a la Maven
458             xstream.registerConverter( new PropertiesConverter()
459             {
460                 /** {@inheritDoc} */
461                 public boolean canConvert( @SuppressWarnings( "rawtypes" ) Class type )
462                 {
463                     return Properties.class == type;
464                 }
465 
466                 /** {@inheritDoc} */
467                 public void marshal( Object source, HierarchicalStreamWriter writer, MarshallingContext context )
468                 {
469                     Properties properties = (Properties) source;
470                     Map<?, ?> map = new TreeMap<Object, Object>( properties ); // sort
471                     for ( Map.Entry<?, ?> entry : map.entrySet() )
472                     {
473                         writer.startNode( entry.getKey().toString() );
474                         writer.setValue( entry.getValue().toString() );
475                         writer.endNode();
476                     }
477                 }
478             } );
479         }
480 
481         return xstream;
482     }
483 
484     /**
485      * @param xstreamObject not null
486      */
487     private void addAlias( XStream xstreamObject )
488     {
489         try
490         {
491             addAlias( xstreamObject, getMavenModelJarFile(), "org.apache.maven.model" );
492             addAlias( xstreamObject, getMavenSettingsJarFile(), "org.apache.maven.settings" );
493         }
494         catch ( MojoExecutionException e )
495         {
496             if ( getLog().isDebugEnabled() )
497             {
498                 getLog().debug( "MojoExecutionException: " + e.getMessage(), e );
499             }
500         }
501         catch ( ArtifactResolverException e )
502         {
503             if ( getLog().isDebugEnabled() )
504             {
505                 getLog().debug( "ArtifactResolverException: " + e.getMessage(), e );
506             }
507         }
508         catch ( ProjectBuildingException e )
509         {
510             if ( getLog().isDebugEnabled() )
511             {
512                 getLog().debug( "ProjectBuildingException: " + e.getMessage(), e );
513             }
514         }
515 
516         // TODO need to handle specific Maven objects like DefaultArtifact?
517     }
518 
519     /**
520      * @param xstreamObject not null
521      * @param jarFile not null
522      * @param packageFilter a package name to filter.
523      */
524     private void addAlias( XStream xstreamObject, File jarFile, String packageFilter )
525     {
526         JarInputStream jarStream = null;
527         try
528         {
529             jarStream = new JarInputStream( new FileInputStream( jarFile ) );
530             for ( JarEntry jarEntry = jarStream.getNextJarEntry(); jarEntry != null;
531                   jarEntry = jarStream.getNextJarEntry() )
532             {
533                 if ( jarEntry.getName().toLowerCase( Locale.ENGLISH ).endsWith( ".class" ) )
534                 {
535                     String name = jarEntry.getName().substring( 0, jarEntry.getName().indexOf( "." ) );
536                     name = name.replaceAll( "/", "\\." );
537 
538                     if ( name.contains( packageFilter ) )
539                     {
540                         try
541                         {
542                             Class<?> clazz = ClassUtils.getClass( name );
543                             String alias = StringUtils.lowercaseFirstLetter( clazz.getSimpleName() );
544                             xstreamObject.alias( alias, clazz );
545                             if ( !clazz.equals( Model.class ) )
546                             {
547                                 xstreamObject.omitField( clazz, "modelEncoding" ); // unnecessary field
548                             }
549                         }
550                         catch ( ClassNotFoundException e )
551                         {
552                             getLog().error( e );
553                         }
554                     }
555                 }
556 
557                 jarStream.closeEntry();
558             }
559 
560             jarStream.close();
561             jarStream = null;
562         }
563         catch ( IOException e )
564         {
565             if ( getLog().isDebugEnabled() )
566             {
567                 getLog().debug( "IOException: " + e.getMessage(), e );
568             }
569         }
570         finally
571         {
572             IOUtil.close( jarStream );
573         }
574     }
575 
576     /**
577      * @return the <code>org.apache.maven:maven-model</code> artifact jar file in the local repository.
578      * @throws MojoExecutionException if any
579      * @throws ProjectBuildingException if any
580      * @throws ArtifactResolverException if any
581      */
582     private File getMavenModelJarFile()
583         throws MojoExecutionException, ProjectBuildingException, ArtifactResolverException
584     {
585         return getArtifactFile( true );
586     }
587 
588     /**
589      * @return the <code>org.apache.maven:maven-settings</code> artifact jar file in the local repository.
590      * @throws MojoExecutionException if any
591      * @throws ProjectBuildingException if any
592      * @throws ArtifactResolverException if any
593      */
594     private File getMavenSettingsJarFile()
595         throws MojoExecutionException, ProjectBuildingException, ArtifactResolverException
596     {
597         return getArtifactFile( false );
598     }
599 
600     /**
601      * @param isPom <code>true</code> to lookup the <code>maven-model</code> artifact jar, <code>false</code> to lookup
602      *            the <code>maven-settings</code> artifact jar.
603      * @return the <code>org.apache.maven:maven-model|maven-settings</code> artifact jar file for this current
604      *         HelpPlugin pom.
605      * @throws MojoExecutionException if any
606      * @throws ProjectBuildingException if any
607      * @throws ArtifactResolverException if any
608      */
609     private File getArtifactFile( boolean isPom )
610         throws MojoExecutionException, ProjectBuildingException, ArtifactResolverException
611     {
612         List<Dependency> dependencies = getHelpPluginPom().getDependencies();
613         for ( Dependency depependency : dependencies )
614         {
615             if ( !( depependency.getGroupId().equals( "org.apache.maven" ) ) )
616             {
617                 continue;
618             }
619 
620             if ( isPom )
621             {
622                 if ( !( depependency.getArtifactId().equals( "maven-model" ) ) )
623                 {
624                     continue;
625                 }
626             }
627             else
628             {
629                 if ( !( depependency.getArtifactId().equals( "maven-settings" ) ) )
630                 {
631                     continue;
632                 }
633             }
634 
635             ArtifactCoordinate coordinate =
636                 getArtifactCoordinate( depependency.getGroupId(), depependency.getArtifactId(),
637                                        depependency.getVersion(), "jar" );
638             ProjectBuildingRequest pbr = new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() );
639             pbr.setRemoteRepositories( remoteRepositories );
640             return artifactResolver.resolveArtifact( pbr, coordinate ).getArtifact().getFile();
641         }
642 
643         throw new MojoExecutionException( "Unable to find the 'org.apache.maven:"
644             + ( isPom ? "maven-model" : "maven-settings" ) + "' artifact" );
645     }
646 
647     /**
648      * @return the Maven POM for the current help plugin
649      * @throws MojoExecutionException if any
650      * @throws ProjectBuildingException if any
651      */
652     private MavenProject getHelpPluginPom()
653         throws MojoExecutionException, ProjectBuildingException
654     {
655         String resource = "META-INF/maven/org.apache.maven.plugins/maven-help-plugin/pom.properties";
656 
657         InputStream resourceAsStream = EvaluateMojo.class.getClassLoader().getResourceAsStream( resource );
658         if ( resourceAsStream == null )
659         {
660             throw new MojoExecutionException( "The help plugin artifact was not found." );
661         }
662         Properties properties = new Properties();
663         try
664         {
665             properties.load( resourceAsStream );
666         }
667         catch ( IOException e )
668         {
669             if ( getLog().isDebugEnabled() )
670             {
671                 getLog().debug( "IOException: " + e.getMessage(), e );
672             }
673         }
674         finally
675         {
676             IOUtil.close( resourceAsStream );
677         }
678 
679         String artifactString =
680             properties.getProperty( "groupId", "unknown" ) + ":"
681                 + properties.getProperty( "artifactId", "unknown" ) + ":"
682                 + properties.getProperty( "version", "unknown" );
683 
684         return getMavenProject( artifactString );
685     }
686 
687     /**
688      * @param name not null
689      * @return the plural of the name
690      */
691     private static String pluralize( String name )
692     {
693         if ( StringUtils.isEmpty( name ) )
694         {
695             throw new IllegalArgumentException( "name is required" );
696         }
697 
698         if ( name.endsWith( "y" ) )
699         {
700             return name.substring( 0, name.length() - 1 ) + "ies";
701         }
702         else if ( name.endsWith( "s" ) )
703         {
704             return name;
705         }
706         else
707         {
708             return name + "s";
709         }
710     }
711 }