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.File;
029import java.io.IOException;
030import java.io.StringReader;
031import java.net.MalformedURLException;
032import java.net.URL;
033import java.net.URLClassLoader;
034import java.nio.charset.StandardCharsets;
035import java.util.ArrayList;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Map;
041import java.util.Stack;
042import java.util.regex.Matcher;
043import java.util.regex.Pattern;
044
045import org.apache.maven.artifact.Artifact;
046import org.apache.maven.artifact.DependencyResolutionRequiredException;
047import org.apache.maven.plugin.descriptor.MojoDescriptor;
048import org.apache.maven.plugin.descriptor.PluginDescriptor;
049import org.apache.maven.project.MavenProject;
050import org.apache.maven.reporting.MavenReport;
051import org.codehaus.plexus.component.repository.ComponentDependency;
052import org.codehaus.plexus.util.StringUtils;
053import org.codehaus.plexus.util.xml.XMLWriter;
054import org.w3c.tidy.Tidy;
055
056/**
057 * Convenience methods to play with Maven plugins.
058 *
059 * @author jdcasey
060 */
061public final class GeneratorUtils {
062    private GeneratorUtils() {
063        // nop
064    }
065
066    /**
067     * @param w not null writer
068     * @param pluginDescriptor not null
069     */
070    public static void writeDependencies(XMLWriter w, PluginDescriptor pluginDescriptor) {
071        w.startElement("dependencies");
072
073        List<ComponentDependency> deps = pluginDescriptor.getDependencies();
074        for (ComponentDependency dep : deps) {
075            w.startElement("dependency");
076
077            element(w, "groupId", dep.getGroupId());
078
079            element(w, "artifactId", dep.getArtifactId());
080
081            element(w, "type", dep.getType());
082
083            element(w, "version", dep.getVersion());
084
085            w.endElement();
086        }
087
088        w.endElement();
089    }
090
091    /**
092     * @param w not null writer
093     * @param name  not null
094     * @param value could be null
095     */
096    public static void element(XMLWriter w, String name, String value) {
097        w.startElement(name);
098
099        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}