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.artifact.ArtifactCoordinate;
55  import org.apache.maven.shared.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                 }
408             }
409         }
410     }
411 
412     /**
413      * @param expr the user expression.
414      * @param obj a not null.
415      * @return the XML for the given object.
416      */
417     private String toXML( String expr, Object obj )
418     {
419         XStream currentXStream = getXStream();
420 
421         // beautify list
422         if ( obj instanceof List )
423         {
424             List<?> list = (List<?>) obj;
425             if ( list.size() > 0 )
426             {
427                 Object elt = list.iterator().next();
428 
429                 String name = StringUtils.lowercaseFirstLetter( elt.getClass().getSimpleName() );
430                 currentXStream.alias( pluralize( name ), List.class );
431             }
432             else
433             {
434                 // try to detect the alias from question
435                 if ( expr.indexOf( '.' ) != -1 )
436                 {
437                     String name = expr.substring( expr.indexOf( '.' ) + 1, expr.indexOf( '}' ) );
438                     currentXStream.alias( name, List.class );
439                 }
440             }
441         }
442 
443         return currentXStream.toXML( obj );
444     }
445 
446     /**
447      * @return lazy loading xstream object.
448      */
449     private XStream getXStream()
450     {
451         if ( xstream == null )
452         {
453             xstream = new XStream();
454             addAlias( xstream );
455 
456             // handle Properties a la Maven
457             xstream.registerConverter( new PropertiesConverter()
458             {
459                 /** {@inheritDoc} */
460                 public boolean canConvert( @SuppressWarnings( "rawtypes" ) Class type )
461                 {
462                     return Properties.class == type;
463                 }
464 
465                 /** {@inheritDoc} */
466                 public void marshal( Object source, HierarchicalStreamWriter writer, MarshallingContext context )
467                 {
468                     Properties properties = (Properties) source;
469                     Map<?, ?> map = new TreeMap<Object, Object>( properties ); // sort
470                     for ( Map.Entry<?, ?> entry : map.entrySet() )
471                     {
472                         writer.startNode( entry.getKey().toString() );
473                         writer.setValue( entry.getValue().toString() );
474                         writer.endNode();
475                     }
476                 }
477             } );
478         }
479 
480         return xstream;
481     }
482 
483     /**
484      * @param xstreamObject not null
485      */
486     private void addAlias( XStream xstreamObject )
487     {
488         try
489         {
490             addAlias( xstreamObject, getMavenModelJarFile(), "org.apache.maven.model" );
491             addAlias( xstreamObject, getMavenSettingsJarFile(), "org.apache.maven.settings" );
492         }
493         catch ( MojoExecutionException e )
494         {
495             if ( getLog().isDebugEnabled() )
496             {
497                 getLog().debug( "MojoExecutionException: " + e.getMessage(), e );
498             }
499         }
500         catch ( ArtifactResolverException e )
501         {
502             if ( getLog().isDebugEnabled() )
503             {
504                 getLog().debug( "ArtifactResolverException: " + e.getMessage(), e );
505             }
506         }
507         catch ( ProjectBuildingException e )
508         {
509             if ( getLog().isDebugEnabled() )
510             {
511                 getLog().debug( "ProjectBuildingException: " + e.getMessage(), e );
512             }
513         }
514 
515         // TODO need to handle specific Maven objects like DefaultArtifact?
516     }
517 
518     /**
519      * @param xstreamObject not null
520      * @param jarFile not null
521      * @param packageFilter a package name to filter.
522      */
523     private void addAlias( XStream xstreamObject, File jarFile, String packageFilter )
524     {
525         JarInputStream jarStream = null;
526         try
527         {
528             jarStream = new JarInputStream( new FileInputStream( jarFile ) );
529             for ( JarEntry jarEntry = jarStream.getNextJarEntry(); jarEntry != null;
530                   jarEntry = jarStream.getNextJarEntry() )
531             {
532                 if ( jarEntry.getName().toLowerCase( Locale.ENGLISH ).endsWith( ".class" ) )
533                 {
534                     String name = jarEntry.getName().substring( 0, jarEntry.getName().indexOf( "." ) );
535                     name = name.replaceAll( "/", "\\." );
536 
537                     if ( name.contains( packageFilter ) )
538                     {
539                         try
540                         {
541                             Class<?> clazz = ClassUtils.getClass( name );
542                             String alias = StringUtils.lowercaseFirstLetter( clazz.getSimpleName() );
543                             xstreamObject.alias( alias, clazz );
544                             if ( !clazz.equals( Model.class ) )
545                             {
546                                 xstreamObject.omitField( clazz, "modelEncoding" ); // unnecessary field
547                             }
548                         }
549                         catch ( ClassNotFoundException e )
550                         {
551                             getLog().error( e );
552                         }
553                     }
554                 }
555 
556                 jarStream.closeEntry();
557             }
558 
559             jarStream.close();
560             jarStream = null;
561         }
562         catch ( IOException e )
563         {
564             if ( getLog().isDebugEnabled() )
565             {
566                 getLog().debug( "IOException: " + e.getMessage(), e );
567             }
568         }
569         finally
570         {
571             IOUtil.close( jarStream );
572         }
573     }
574 
575     /**
576      * @return the <code>org.apache.maven:maven-model</code> artifact jar file in the local repository.
577      * @throws MojoExecutionException if any
578      * @throws ProjectBuildingException if any
579      * @throws ArtifactResolverException if any
580      */
581     private File getMavenModelJarFile()
582         throws MojoExecutionException, ProjectBuildingException, ArtifactResolverException
583     {
584         return getArtifactFile( true );
585     }
586 
587     /**
588      * @return the <code>org.apache.maven:maven-settings</code> artifact jar file in the local repository.
589      * @throws MojoExecutionException if any
590      * @throws ProjectBuildingException if any
591      * @throws ArtifactResolverException if any
592      */
593     private File getMavenSettingsJarFile()
594         throws MojoExecutionException, ProjectBuildingException, ArtifactResolverException
595     {
596         return getArtifactFile( false );
597     }
598 
599     /**
600      * @param isPom <code>true</code> to lookup the <code>maven-model</code> artifact jar, <code>false</code> to lookup
601      *            the <code>maven-settings</code> artifact jar.
602      * @return the <code>org.apache.maven:maven-model|maven-settings</code> artifact jar file for this current
603      *         HelpPlugin pom.
604      * @throws MojoExecutionException if any
605      * @throws ProjectBuildingException if any
606      * @throws ArtifactResolverException if any
607      */
608     private File getArtifactFile( boolean isPom )
609         throws MojoExecutionException, ProjectBuildingException, ArtifactResolverException
610     {
611         List<Dependency> dependencies = getHelpPluginPom().getDependencies();
612         for ( Dependency depependency : dependencies )
613         {
614             if ( !( depependency.getGroupId().equals( "org.apache.maven" ) ) )
615             {
616                 continue;
617             }
618 
619             if ( isPom )
620             {
621                 if ( !( depependency.getArtifactId().equals( "maven-model" ) ) )
622                 {
623                     continue;
624                 }
625             }
626             else
627             {
628                 if ( !( depependency.getArtifactId().equals( "maven-settings" ) ) )
629                 {
630                     continue;
631                 }
632             }
633 
634             ArtifactCoordinate coordinate =
635                 getArtifactCoordinate( depependency.getGroupId(), depependency.getArtifactId(),
636                                        depependency.getVersion(), "jar" );
637             ProjectBuildingRequest pbr = new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() );
638             pbr.setRemoteRepositories( remoteRepositories );
639             return artifactResolver.resolveArtifact( pbr, coordinate ).getArtifact().getFile();
640         }
641 
642         throw new MojoExecutionException( "Unable to find the 'org.apache.maven:"
643             + ( isPom ? "maven-model" : "maven-settings" ) + "' artifact" );
644     }
645 
646     /**
647      * @return the Maven POM for the current help plugin
648      * @throws MojoExecutionException if any
649      * @throws ProjectBuildingException if any
650      */
651     private MavenProject getHelpPluginPom()
652         throws MojoExecutionException, ProjectBuildingException
653     {
654         String resource = "META-INF/maven/org.apache.maven.plugins/maven-help-plugin/pom.properties";
655 
656         InputStream resourceAsStream = EvaluateMojo.class.getClassLoader().getResourceAsStream( resource );
657         if ( resourceAsStream == null )
658         {
659             throw new MojoExecutionException( "The help plugin artifact was not found." );
660         }
661         Properties properties = new Properties();
662         try
663         {
664             properties.load( resourceAsStream );
665         }
666         catch ( IOException e )
667         {
668             if ( getLog().isDebugEnabled() )
669             {
670                 getLog().debug( "IOException: " + e.getMessage(), e );
671             }
672         }
673         finally
674         {
675             IOUtil.close( resourceAsStream );
676         }
677 
678         String artifactString =
679             properties.getProperty( "groupId", "unknown" ) + ":"
680                 + properties.getProperty( "artifactId", "unknown" ) + ":"
681                 + properties.getProperty( "version", "unknown" );
682 
683         return getMavenProject( artifactString );
684     }
685 
686     /**
687      * @param name not null
688      * @return the plural of the name
689      */
690     private static String pluralize( String name )
691     {
692         if ( StringUtils.isEmpty( name ) )
693         {
694             throw new IllegalArgumentException( "name is required" );
695         }
696 
697         if ( name.endsWith( "y" ) )
698         {
699             return name.substring( 0, name.length() - 1 ) + "ies";
700         }
701         else if ( name.endsWith( "s" ) )
702         {
703             return name;
704         }
705         else
706         {
707             return name + "s";
708         }
709     }
710 }