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