001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.tools.plugin.generator;
020
021import javax.swing.text.MutableAttributeSet;
022import javax.swing.text.html.HTML;
023import javax.swing.text.html.HTMLEditorKit;
024import javax.swing.text.html.parser.ParserDelegator;
025
026import java.io.ByteArrayInputStream;
027import java.io.ByteArrayOutputStream;
028import java.io.IOException;
029import java.io.StringReader;
030import java.nio.charset.StandardCharsets;
031import java.util.Collection;
032import java.util.HashMap;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Map;
036import java.util.Objects;
037import java.util.Stack;
038import java.util.regex.Matcher;
039import java.util.regex.Pattern;
040
041import org.apache.maven.artifact.Artifact;
042import org.apache.maven.plugin.descriptor.MojoDescriptor;
043import org.apache.maven.plugin.descriptor.PluginDescriptor;
044import org.apache.maven.project.MavenProject;
045import org.apache.maven.tools.plugin.util.PluginUtils;
046import org.codehaus.plexus.component.repository.ComponentDependency;
047import org.codehaus.plexus.util.StringUtils;
048import org.codehaus.plexus.util.xml.XMLWriter;
049import org.w3c.tidy.Tidy;
050
051/**
052 * Convenience methods to play with Maven plugins.
053 *
054 * @author jdcasey
055 */
056public final class GeneratorUtils {
057    private GeneratorUtils() {
058        // nop
059    }
060
061    /**
062     * @param w not null writer
063     * @param pluginDescriptor not null
064     */
065    public static void writeDependencies(XMLWriter w, PluginDescriptor pluginDescriptor) {
066        w.startElement("dependencies");
067
068        List<ComponentDependency> deps = pluginDescriptor.getDependencies();
069        for (ComponentDependency dep : deps) {
070            w.startElement("dependency");
071
072            element(w, "groupId", dep.getGroupId());
073
074            element(w, "artifactId", dep.getArtifactId());
075
076            element(w, "type", dep.getType());
077
078            element(w, "version", dep.getVersion());
079
080            w.endElement();
081        }
082
083        w.endElement();
084    }
085
086    /**
087     * @param w not null writer
088     * @param name  not null
089     * @param value could be null
090     */
091    public static void element(XMLWriter w, String name, String value) {
092        w.startElement(name);
093
094        if (value == null) {
095            value = "";
096        }
097
098        w.writeText(value);
099
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}