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