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