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.tools.plugin.generator;
20  
21  import javax.swing.text.MutableAttributeSet;
22  import javax.swing.text.html.HTML;
23  import javax.swing.text.html.HTMLEditorKit;
24  import javax.swing.text.html.parser.ParserDelegator;
25  
26  import java.io.ByteArrayInputStream;
27  import java.io.ByteArrayOutputStream;
28  import java.io.File;
29  import java.io.IOException;
30  import java.io.StringReader;
31  import java.net.MalformedURLException;
32  import java.net.URL;
33  import java.net.URLClassLoader;
34  import java.nio.charset.StandardCharsets;
35  import java.util.ArrayList;
36  import java.util.Collection;
37  import java.util.HashMap;
38  import java.util.LinkedList;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Stack;
42  import java.util.regex.Matcher;
43  import java.util.regex.Pattern;
44  
45  import org.apache.maven.artifact.Artifact;
46  import org.apache.maven.artifact.DependencyResolutionRequiredException;
47  import org.apache.maven.plugin.descriptor.MojoDescriptor;
48  import org.apache.maven.plugin.descriptor.PluginDescriptor;
49  import org.apache.maven.project.MavenProject;
50  import org.apache.maven.reporting.MavenReport;
51  import org.codehaus.plexus.component.repository.ComponentDependency;
52  import org.codehaus.plexus.util.StringUtils;
53  import org.codehaus.plexus.util.xml.XMLWriter;
54  import org.w3c.tidy.Tidy;
55  
56  /**
57   * Convenience methods to play with Maven plugins.
58   *
59   * @author jdcasey
60   */
61  public final class GeneratorUtils {
62      private GeneratorUtils() {
63          // nop
64      }
65  
66      /**
67       * @param w not null writer
68       * @param pluginDescriptor not null
69       */
70      public static void writeDependencies(XMLWriter w, PluginDescriptor pluginDescriptor) {
71          w.startElement("dependencies");
72  
73          List<ComponentDependency> deps = pluginDescriptor.getDependencies();
74          for (ComponentDependency dep : deps) {
75              w.startElement("dependency");
76  
77              element(w, "groupId", dep.getGroupId());
78  
79              element(w, "artifactId", dep.getArtifactId());
80  
81              element(w, "type", dep.getType());
82  
83              element(w, "version", dep.getVersion());
84  
85              w.endElement();
86          }
87  
88          w.endElement();
89      }
90  
91      /**
92       * @param w not null writer
93       * @param name  not null
94       * @param value could be null
95       */
96      public static void element(XMLWriter w, String name, String value) {
97          w.startElement(name);
98  
99          if (value == null) {
100             value = "";
101         }
102 
103         w.writeText(value);
104 
105         w.endElement();
106     }
107 
108     /**
109      * @param artifacts not null collection of <code>Artifact</code>
110      * @return list of component dependencies, without in provided scope
111      */
112     public static List<ComponentDependency> toComponentDependencies(Collection<Artifact> artifacts) {
113         List<ComponentDependency> componentDeps = new LinkedList<>();
114 
115         for (Artifact artifact : artifacts) {
116             if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) {
117                 continue;
118             }
119 
120             ComponentDependency cd = new ComponentDependency();
121 
122             cd.setArtifactId(artifact.getArtifactId());
123             cd.setGroupId(artifact.getGroupId());
124             cd.setVersion(artifact.getVersion());
125             cd.setType(artifact.getType());
126 
127             componentDeps.add(cd);
128         }
129 
130         return componentDeps;
131     }
132 
133     /**
134      * Returns a literal replacement <code>String</code> for the specified <code>String</code>. This method
135      * produces a <code>String</code> that will work as a literal replacement <code>s</code> in the
136      * <code>appendReplacement</code> method of the {@link Matcher} class. The <code>String</code> produced will
137      * match the sequence of characters in <code>s</code> treated as a literal sequence. Slashes ('\') and dollar
138      * signs ('$') will be given no special meaning. TODO: copied from Matcher class of Java 1.5, remove once target
139      * platform can be upgraded
140      *
141      * @see <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Matcher.html">java.util.regex.Matcher</a>
142      * @param s The string to be literalized
143      * @return A literal string replacement
144      */
145     private static String quoteReplacement(String s) {
146         if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1)) {
147             return s;
148         }
149 
150         StringBuilder sb = new StringBuilder();
151         for (int i = 0; i < s.length(); i++) {
152             char c = s.charAt(i);
153             if (c == '\\') {
154                 sb.append('\\');
155                 sb.append('\\');
156             } else if (c == '$') {
157                 sb.append('\\');
158                 sb.append('$');
159             } else {
160                 sb.append(c);
161             }
162         }
163 
164         return sb.toString();
165     }
166 
167     /**
168      * Decodes javadoc inline tags into equivalent HTML tags. For instance, the inline tag "{@code <A&B>}" should be
169      * rendered as "<code>&lt;A&amp;B&gt;</code>".
170      *
171      * @param description The javadoc description to decode, may be <code>null</code>.
172      * @return The decoded description, never <code>null</code>.
173      * @deprecated Only used for non java extractor
174      */
175     @Deprecated
176     static String decodeJavadocTags(String description) {
177         if (StringUtils.isEmpty(description)) {
178             return "";
179         }
180 
181         StringBuffer decoded = new StringBuffer(description.length() + 1024);
182 
183         Matcher matcher = Pattern.compile("\\{@(\\w+)\\s*([^\\}]*)\\}").matcher(description);
184         while (matcher.find()) {
185             String tag = matcher.group(1);
186             String text = matcher.group(2);
187             text = StringUtils.replace(text, "&", "&amp;");
188             text = StringUtils.replace(text, "<", "&lt;");
189             text = StringUtils.replace(text, ">", "&gt;");
190             if ("code".equals(tag)) {
191                 text = "<code>" + text + "</code>";
192             } else if ("link".equals(tag) || "linkplain".equals(tag) || "value".equals(tag)) {
193                 String pattern = "(([^#\\.\\s]+\\.)*([^#\\.\\s]+))?" + "(#([^\\(\\s]*)(\\([^\\)]*\\))?\\s*(\\S.*)?)?";
194                 final int label = 7;
195                 final int clazz = 3;
196                 final int member = 5;
197                 final int args = 6;
198                 Matcher link = Pattern.compile(pattern).matcher(text);
199                 if (link.matches()) {
200                     text = link.group(label);
201                     if (StringUtils.isEmpty(text)) {
202                         text = link.group(clazz);
203                         if (StringUtils.isEmpty(text)) {
204                             text = "";
205                         }
206                         if (StringUtils.isNotEmpty(link.group(member))) {
207                             if (StringUtils.isNotEmpty(text)) {
208                                 text += '.';
209                             }
210                             text += link.group(member);
211                             if (StringUtils.isNotEmpty(link.group(args))) {
212                                 text += "()";
213                             }
214                         }
215                     }
216                 }
217                 if (!"linkplain".equals(tag)) {
218                     text = "<code>" + text + "</code>";
219                 }
220             }
221             matcher.appendReplacement(decoded, (text != null) ? quoteReplacement(text) : "");
222         }
223         matcher.appendTail(decoded);
224 
225         return decoded.toString();
226     }
227 
228     /**
229      * Fixes some javadoc comment to become a valid XHTML snippet.
230      *
231      * @param description Javadoc description with HTML tags, may be <code>null</code>.
232      * @return The description with valid XHTML tags, never <code>null</code>.
233      * @deprecated Redundant for java extractor
234      */
235     @Deprecated
236     public static String makeHtmlValid(String description) {
237 
238         if (StringUtils.isEmpty(description)) {
239             return "";
240         }
241 
242         String commentCleaned = decodeJavadocTags(description);
243 
244         // Using jTidy to clean comment
245         Tidy tidy = new Tidy();
246         tidy.setDocType("loose");
247         tidy.setXHTML(true);
248         tidy.setXmlOut(true);
249         tidy.setInputEncoding("UTF-8");
250         tidy.setOutputEncoding("UTF-8");
251         tidy.setMakeClean(true);
252         tidy.setNumEntities(true);
253         tidy.setQuoteNbsp(false);
254         tidy.setQuiet(true);
255         tidy.setShowWarnings(true);
256 
257         ByteArrayOutputStream out = new ByteArrayOutputStream(commentCleaned.length() + 256);
258         tidy.parse(new ByteArrayInputStream(commentCleaned.getBytes(StandardCharsets.UTF_8)), out);
259         commentCleaned = new String(out.toByteArray(), StandardCharsets.UTF_8);
260 
261         if (StringUtils.isEmpty(commentCleaned)) {
262             return "";
263         }
264 
265         // strip the header/body stuff
266         String ls = System.getProperty("line.separator");
267         int startPos = commentCleaned.indexOf("<body>" + ls) + 6 + ls.length();
268         int endPos = commentCleaned.indexOf(ls + "</body>");
269         commentCleaned = commentCleaned.substring(startPos, endPos);
270 
271         return commentCleaned;
272     }
273 
274     /**
275      * Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain
276      * as much of the text formatting as possible by means of the following transformations:
277      * <ul>
278      * <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and
279      * finally the item contents. Each tab denotes an increase of indentation.</li>
280      * <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline
281      * (U+000A) to denote a mandatory line break.</li>
282      * <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized
283      * to a single space. The resulting space denotes a possible point for line wrapping.</li>
284      * <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li>
285      * </ul>
286      *
287      * @param html The HTML fragment to convert to plain text, may be <code>null</code>.
288      * @return A string with HTML tags converted into pure text, never <code>null</code>.
289      * @since 2.4.3
290      * @deprecated Replaced by {@link HtmlToPlainTextConverter}
291      */
292     @Deprecated
293     public static String toText(String html) {
294         if (StringUtils.isEmpty(html)) {
295             return "";
296         }
297 
298         final StringBuilder sb = new StringBuilder();
299 
300         HTMLEditorKit.Parser parser = new ParserDelegator();
301         HTMLEditorKit.ParserCallback htmlCallback = new MojoParserCallback(sb);
302 
303         try {
304             parser.parse(new StringReader(makeHtmlValid(html)), htmlCallback, true);
305         } catch (IOException e) {
306             throw new RuntimeException(e);
307         }
308 
309         return sb.toString().replace('\"', '\''); // for CDATA
310     }
311 
312     /**
313      * ParserCallback implementation.
314      */
315     private static class MojoParserCallback extends HTMLEditorKit.ParserCallback {
316         /**
317          * Holds the index of the current item in a numbered list.
318          */
319         class Counter {
320             int value;
321         }
322 
323         /**
324          * A flag whether the parser is currently in the body element.
325          */
326         private boolean body;
327 
328         /**
329          * A flag whether the parser is currently processing preformatted text, actually a counter to track nesting.
330          */
331         private int preformatted;
332 
333         /**
334          * The current indentation depth for the output.
335          */
336         private int depth;
337 
338         /**
339          * A stack of {@link Counter} objects corresponding to the nesting of (un-)ordered lists. A
340          * <code>null</code> element denotes an unordered list.
341          */
342         private Stack<Counter> numbering = new Stack<>();
343 
344         /**
345          * A flag whether an implicit line break is pending in the output buffer. This flag is used to postpone the
346          * output of implicit line breaks until we are sure that are not to be merged with other implicit line
347          * breaks.
348          */
349         private boolean pendingNewline;
350 
351         /**
352          * A flag whether we have just parsed a simple tag.
353          */
354         private boolean simpleTag;
355 
356         /**
357          * The current buffer.
358          */
359         private final StringBuilder sb;
360 
361         /**
362          * @param sb not null
363          */
364         MojoParserCallback(StringBuilder sb) {
365             this.sb = sb;
366         }
367 
368         /** {@inheritDoc} */
369         @Override
370         public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
371             simpleTag = true;
372             if (body && HTML.Tag.BR.equals(t)) {
373                 newline(false);
374             }
375         }
376 
377         /** {@inheritDoc} */
378         @Override
379         public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
380             simpleTag = false;
381             if (body && (t.breaksFlow() || t.isBlock())) {
382                 newline(true);
383             }
384             if (HTML.Tag.OL.equals(t)) {
385                 numbering.push(new Counter());
386             } else if (HTML.Tag.UL.equals(t)) {
387                 numbering.push(null);
388             } else if (HTML.Tag.LI.equals(t)) {
389                 Counter counter = numbering.peek();
390                 if (counter == null) {
391                     text("-\t");
392                 } else {
393                     text(++counter.value + ".\t");
394                 }
395                 depth++;
396             } else if (HTML.Tag.DD.equals(t)) {
397                 depth++;
398             } else if (t.isPreformatted()) {
399                 preformatted++;
400             } else if (HTML.Tag.BODY.equals(t)) {
401                 body = true;
402             }
403         }
404 
405         /** {@inheritDoc} */
406         @Override
407         public void handleEndTag(HTML.Tag t, int pos) {
408             if (HTML.Tag.OL.equals(t) || HTML.Tag.UL.equals(t)) {
409                 numbering.pop();
410             } else if (HTML.Tag.LI.equals(t) || HTML.Tag.DD.equals(t)) {
411                 depth--;
412             } else if (t.isPreformatted()) {
413                 preformatted--;
414             } else if (HTML.Tag.BODY.equals(t)) {
415                 body = false;
416             }
417             if (body && (t.breaksFlow() || t.isBlock()) && !HTML.Tag.LI.equals(t)) {
418                 if ((HTML.Tag.P.equals(t)
419                                 || HTML.Tag.PRE.equals(t)
420                                 || HTML.Tag.OL.equals(t)
421                                 || HTML.Tag.UL.equals(t)
422                                 || HTML.Tag.DL.equals(t))
423                         && numbering.isEmpty()) {
424                     pendingNewline = false;
425                     newline(pendingNewline);
426                 } else {
427                     newline(true);
428                 }
429             }
430         }
431 
432         /** {@inheritDoc} */
433         @Override
434         public void handleText(char[] data, int pos) {
435             /*
436              * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by
437              * the text event ">..." so we need to watch out for the closing angle bracket.
438              */
439             int offset = 0;
440             if (simpleTag && data[0] == '>') {
441                 simpleTag = false;
442                 for (++offset; offset < data.length && data[offset] <= ' '; ) {
443                     offset++;
444                 }
445             }
446             if (offset < data.length) {
447                 String text = new String(data, offset, data.length - offset);
448                 text(text);
449             }
450         }
451 
452         /** {@inheritDoc} */
453         @Override
454         public void flush() {
455             flushPendingNewline();
456         }
457 
458         /**
459          * Writes a line break to the plain text output.
460          *
461          * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are
462          *            always written to the output whereas consecutive implicit line breaks are merged into a single
463          *            line break.
464          */
465         private void newline(boolean implicit) {
466             if (implicit) {
467                 pendingNewline = true;
468             } else {
469                 flushPendingNewline();
470                 sb.append('\n');
471             }
472         }
473 
474         /**
475          * Flushes a pending newline (if any).
476          */
477         private void flushPendingNewline() {
478             if (pendingNewline) {
479                 pendingNewline = false;
480                 if (sb.length() > 0) {
481                     sb.append('\n');
482                 }
483             }
484         }
485 
486         /**
487          * Writes the specified character data to the plain text output. If the last output was a line break, the
488          * character data will automatically be prefixed with the current indent.
489          *
490          * @param data The character data, must not be <code>null</code>.
491          */
492         private void text(String data) {
493             flushPendingNewline();
494             if (sb.length() <= 0 || sb.charAt(sb.length() - 1) == '\n') {
495                 for (int i = 0; i < depth; i++) {
496                     sb.append('\t');
497                 }
498             }
499             String text;
500             if (preformatted > 0) {
501                 text = data;
502             } else {
503                 text = data.replace('\n', ' ');
504             }
505             sb.append(text);
506         }
507     }
508 
509     /**
510      * Find the best package name, based on the number of hits of actual Mojo classes.
511      *
512      * @param pluginDescriptor not null
513      * @return the best name of the package for the generated mojo
514      */
515     public static String discoverPackageName(PluginDescriptor pluginDescriptor) {
516         Map<String, Integer> packageNames = new HashMap<>();
517 
518         List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos();
519         if (mojoDescriptors == null) {
520             return "";
521         }
522         for (MojoDescriptor descriptor : mojoDescriptors) {
523 
524             String impl = descriptor.getImplementation();
525             if (StringUtils.equals(descriptor.getGoal(), "help") && StringUtils.equals("HelpMojo", impl)) {
526                 continue;
527             }
528             if (impl.lastIndexOf('.') != -1) {
529                 String name = impl.substring(0, impl.lastIndexOf('.'));
530                 if (packageNames.get(name) != null) {
531                     int next = (packageNames.get(name)).intValue() + 1;
532                     packageNames.put(name, Integer.valueOf(next));
533                 } else {
534                     packageNames.put(name, Integer.valueOf(1));
535                 }
536             } else {
537                 packageNames.put("", Integer.valueOf(1));
538             }
539         }
540 
541         String packageName = "";
542         int max = 0;
543         for (Map.Entry<String, Integer> entry : packageNames.entrySet()) {
544             int value = entry.getValue().intValue();
545             if (value > max) {
546                 max = value;
547                 packageName = entry.getKey();
548             }
549         }
550 
551         return packageName;
552     }
553 
554     /**
555      * @param impl a Mojo implementation, not null
556      * @param project a MavenProject instance, could be null
557      * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>,
558      * <code>false</code> otherwise.
559      * @throws IllegalArgumentException if any
560      */
561     @SuppressWarnings("unchecked")
562     public static boolean isMavenReport(String impl, MavenProject project) throws IllegalArgumentException {
563         if (impl == null) {
564             throw new IllegalArgumentException("mojo implementation should be declared");
565         }
566 
567         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
568         if (project != null) {
569             List<String> classPathStrings;
570             try {
571                 classPathStrings = project.getCompileClasspathElements();
572                 if (project.getExecutionProject() != null) {
573                     classPathStrings.addAll(project.getExecutionProject().getCompileClasspathElements());
574                 }
575             } catch (DependencyResolutionRequiredException e) {
576                 throw new IllegalArgumentException(e);
577             }
578 
579             List<URL> urls = new ArrayList<>(classPathStrings.size());
580             for (String classPathString : classPathStrings) {
581                 try {
582                     urls.add(new File(classPathString).toURL());
583                 } catch (MalformedURLException e) {
584                     throw new IllegalArgumentException(e);
585                 }
586             }
587 
588             classLoader = new URLClassLoader(urls.toArray(new URL[urls.size()]), classLoader);
589         }
590 
591         try {
592             Class<?> clazz = Class.forName(impl, false, classLoader);
593 
594             return MavenReport.class.isAssignableFrom(clazz);
595         } catch (ClassNotFoundException e) {
596             return false;
597         }
598     }
599 }