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