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