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