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