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;
024
025import java.io.ByteArrayInputStream;
026import java.io.ByteArrayOutputStream;
027import java.nio.charset.StandardCharsets;
028import java.util.Collection;
029import java.util.HashMap;
030import java.util.LinkedList;
031import java.util.List;
032import java.util.Map;
033import java.util.Objects;
034import java.util.Stack;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037
038import org.apache.maven.artifact.Artifact;
039import org.apache.maven.plugin.descriptor.MojoDescriptor;
040import org.apache.maven.plugin.descriptor.PluginDescriptor;
041import org.apache.maven.project.MavenProject;
042import org.apache.maven.tools.plugin.util.PluginUtils;
043import org.codehaus.plexus.component.repository.ComponentDependency;
044import org.codehaus.plexus.util.StringUtils;
045import org.codehaus.plexus.util.xml.XMLWriter;
046import org.w3c.tidy.Tidy;
047
048/**
049 * Convenience methods to play with Maven plugins.
050 *
051 * @author jdcasey
052 */
053public final class GeneratorUtils {
054    private GeneratorUtils() {
055        // nop
056    }
057
058    /**
059     * @param w not null writer
060     * @param pluginDescriptor not null
061     */
062    public static void writeDependencies(XMLWriter w, PluginDescriptor pluginDescriptor) {
063        w.startElement("dependencies");
064
065        List<ComponentDependency> deps = pluginDescriptor.getDependencies();
066        for (ComponentDependency dep : deps) {
067            w.startElement("dependency");
068
069            element(w, "groupId", dep.getGroupId());
070
071            element(w, "artifactId", dep.getArtifactId());
072
073            element(w, "type", dep.getType());
074
075            element(w, "version", dep.getVersion());
076
077            w.endElement();
078        }
079
080        w.endElement();
081    }
082
083    /**
084     * @param w not null writer
085     * @param name  not null
086     * @param value could be null
087     */
088    public static void element(XMLWriter w, String name, String value) {
089        w.startElement(name);
090
091        if (value == null) {
092            value = "";
093        }
094
095        w.writeText(value);
096
097        w.endElement();
098    }
099
100    /**
101     * @param artifacts not null collection of <code>Artifact</code>
102     * @return list of component dependencies, without in provided scope
103     */
104    public static List<ComponentDependency> toComponentDependencies(Collection<Artifact> artifacts) {
105        List<ComponentDependency> componentDeps = new LinkedList<>();
106
107        for (Artifact artifact : artifacts) {
108            if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) {
109                continue;
110            }
111
112            ComponentDependency cd = new ComponentDependency();
113
114            cd.setArtifactId(artifact.getArtifactId());
115            cd.setGroupId(artifact.getGroupId());
116            cd.setVersion(artifact.getVersion());
117            cd.setType(artifact.getType());
118
119            componentDeps.add(cd);
120        }
121
122        return componentDeps;
123    }
124
125    /**
126     * Returns a literal replacement <code>String</code> for the specified <code>String</code>. This method
127     * produces a <code>String</code> that will work as a literal replacement <code>s</code> in the
128     * <code>appendReplacement</code> method of the {@link Matcher} class. The <code>String</code> produced will
129     * match the sequence of characters in <code>s</code> treated as a literal sequence. Slashes ('\') and dollar
130     * signs ('$') will be given no special meaning. TODO: copied from Matcher class of Java 1.5, remove once target
131     * platform can be upgraded
132     *
133     * @see <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Matcher.html">java.util.regex.Matcher</a>
134     * @param s The string to be literalized
135     * @return A literal string replacement
136     */
137    private static String quoteReplacement(String s) {
138        if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1)) {
139            return s;
140        }
141
142        StringBuilder sb = new StringBuilder();
143        for (int i = 0; i < s.length(); i++) {
144            char c = s.charAt(i);
145            if (c == '\\') {
146                sb.append('\\');
147                sb.append('\\');
148            } else if (c == '$') {
149                sb.append('\\');
150                sb.append('$');
151            } else {
152                sb.append(c);
153            }
154        }
155
156        return sb.toString();
157    }
158
159    /**
160     * Decodes javadoc inline tags into equivalent HTML tags. For instance, the inline tag "{@code <A&B>}" should be
161     * rendered as "<code>&lt;A&amp;B&gt;</code>".
162     *
163     * @param description The javadoc description to decode, may be <code>null</code>.
164     * @return The decoded description, never <code>null</code>.
165     * @deprecated Only used for non java extractor
166     */
167    @Deprecated
168    static String decodeJavadocTags(String description) {
169        if (description == null || description.isEmpty()) {
170            return "";
171        }
172
173        StringBuffer decoded = new StringBuffer(description.length() + 1024);
174
175        Matcher matcher = Pattern.compile("\\{@(\\w+)\\s*([^\\}]*)\\}").matcher(description);
176        while (matcher.find()) {
177            String tag = matcher.group(1);
178            String text = matcher.group(2);
179            text = text.replace("&", "&amp;");
180            text = text.replace("<", "&lt;");
181            text = text.replace(">", "&gt;");
182            if ("code".equals(tag)) {
183                text = "<code>" + text + "</code>";
184            } else if ("link".equals(tag) || "linkplain".equals(tag) || "value".equals(tag)) {
185                String pattern = "(([^#\\.\\s]+\\.)*([^#\\.\\s]+))?" + "(#([^\\(\\s]*)(\\([^\\)]*\\))?\\s*(\\S.*)?)?";
186                final int label = 7;
187                final int clazz = 3;
188                final int member = 5;
189                final int args = 6;
190                Matcher link = Pattern.compile(pattern).matcher(text);
191                if (link.matches()) {
192                    text = link.group(label);
193                    if (text == null || text.isEmpty()) {
194                        text = link.group(clazz);
195                        if (text == null || text.isEmpty()) {
196                            text = "";
197                        }
198                        if (StringUtils.isNotEmpty(link.group(member))) {
199                            if (text != null && !text.isEmpty()) {
200                                text += '.';
201                            }
202                            text += link.group(member);
203                            if (StringUtils.isNotEmpty(link.group(args))) {
204                                text += "()";
205                            }
206                        }
207                    }
208                }
209                if (!"linkplain".equals(tag)) {
210                    text = "<code>" + text + "</code>";
211                }
212            }
213            matcher.appendReplacement(decoded, (text != null) ? quoteReplacement(text) : "");
214        }
215        matcher.appendTail(decoded);
216
217        return decoded.toString();
218    }
219
220    /**
221     * Fixes some javadoc comment to become a valid XHTML snippet.
222     *
223     * @param description Javadoc description with HTML tags, may be <code>null</code>.
224     * @return The description with valid XHTML tags, never <code>null</code>.
225     * @deprecated Redundant for java extractor
226     */
227    @Deprecated
228    public static String makeHtmlValid(String description) {
229
230        if (description == null || description.isEmpty()) {
231            return "";
232        }
233
234        String commentCleaned = decodeJavadocTags(description);
235
236        // Using jTidy to clean comment
237        Tidy tidy = new Tidy();
238        tidy.setDocType("loose");
239        tidy.setXHTML(true);
240        tidy.setXmlOut(true);
241        tidy.setInputEncoding("UTF-8");
242        tidy.setOutputEncoding("UTF-8");
243        tidy.setMakeClean(true);
244        tidy.setNumEntities(true);
245        tidy.setQuoteNbsp(false);
246        tidy.setQuiet(true);
247        tidy.setShowWarnings(true);
248
249        ByteArrayOutputStream out = new ByteArrayOutputStream(commentCleaned.length() + 256);
250        tidy.parse(new ByteArrayInputStream(commentCleaned.getBytes(StandardCharsets.UTF_8)), out);
251        commentCleaned = new String(out.toByteArray(), StandardCharsets.UTF_8);
252
253        if (commentCleaned == null || commentCleaned.isEmpty()) {
254            return "";
255        }
256
257        // strip the header/body stuff
258        String ls = System.getProperty("line.separator");
259        int startPos = commentCleaned.indexOf("<body>" + ls) + 6 + ls.length();
260        int endPos = commentCleaned.indexOf(ls + "</body>");
261        commentCleaned = commentCleaned.substring(startPos, endPos);
262
263        return commentCleaned;
264    }
265
266    /**
267     * ParserCallback implementation.
268     */
269    private static class MojoParserCallback extends HTMLEditorKit.ParserCallback {
270        /**
271         * Holds the index of the current item in a numbered list.
272         */
273        class Counter {
274            int value;
275        }
276
277        /**
278         * A flag whether the parser is currently in the body element.
279         */
280        private boolean body;
281
282        /**
283         * A flag whether the parser is currently processing preformatted text, actually a counter to track nesting.
284         */
285        private int preformatted;
286
287        /**
288         * The current indentation depth for the output.
289         */
290        private int depth;
291
292        /**
293         * A stack of {@link Counter} objects corresponding to the nesting of (un-)ordered lists. A
294         * <code>null</code> element denotes an unordered list.
295         */
296        private Stack<Counter> numbering = new Stack<>();
297
298        /**
299         * A flag whether an implicit line break is pending in the output buffer. This flag is used to postpone the
300         * output of implicit line breaks until we are sure that are not to be merged with other implicit line
301         * breaks.
302         */
303        private boolean pendingNewline;
304
305        /**
306         * A flag whether we have just parsed a simple tag.
307         */
308        private boolean simpleTag;
309
310        /**
311         * The current buffer.
312         */
313        private final StringBuilder sb;
314
315        /**
316         * @param sb not null
317         */
318        MojoParserCallback(StringBuilder sb) {
319            this.sb = sb;
320        }
321
322        /** {@inheritDoc} */
323        @Override
324        public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
325            simpleTag = true;
326            if (body && HTML.Tag.BR.equals(t)) {
327                newline(false);
328            }
329        }
330
331        /** {@inheritDoc} */
332        @Override
333        public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
334            simpleTag = false;
335            if (body && (t.breaksFlow() || t.isBlock())) {
336                newline(true);
337            }
338            if (HTML.Tag.OL.equals(t)) {
339                numbering.push(new Counter());
340            } else if (HTML.Tag.UL.equals(t)) {
341                numbering.push(null);
342            } else if (HTML.Tag.LI.equals(t)) {
343                Counter counter = numbering.peek();
344                if (counter == null) {
345                    text("-\t");
346                } else {
347                    text(++counter.value + ".\t");
348                }
349                depth++;
350            } else if (HTML.Tag.DD.equals(t)) {
351                depth++;
352            } else if (t.isPreformatted()) {
353                preformatted++;
354            } else if (HTML.Tag.BODY.equals(t)) {
355                body = true;
356            }
357        }
358
359        /** {@inheritDoc} */
360        @Override
361        public void handleEndTag(HTML.Tag t, int pos) {
362            if (HTML.Tag.OL.equals(t) || HTML.Tag.UL.equals(t)) {
363                numbering.pop();
364            } else if (HTML.Tag.LI.equals(t) || HTML.Tag.DD.equals(t)) {
365                depth--;
366            } else if (t.isPreformatted()) {
367                preformatted--;
368            } else if (HTML.Tag.BODY.equals(t)) {
369                body = false;
370            }
371            if (body && (t.breaksFlow() || t.isBlock()) && !HTML.Tag.LI.equals(t)) {
372                if ((HTML.Tag.P.equals(t)
373                                || HTML.Tag.PRE.equals(t)
374                                || HTML.Tag.OL.equals(t)
375                                || HTML.Tag.UL.equals(t)
376                                || HTML.Tag.DL.equals(t))
377                        && numbering.isEmpty()) {
378                    pendingNewline = false;
379                    newline(pendingNewline);
380                } else {
381                    newline(true);
382                }
383            }
384        }
385
386        /** {@inheritDoc} */
387        @Override
388        public void handleText(char[] data, int pos) {
389            /*
390             * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by
391             * the text event ">..." so we need to watch out for the closing angle bracket.
392             */
393            int offset = 0;
394            if (simpleTag && data[0] == '>') {
395                simpleTag = false;
396                for (++offset; offset < data.length && data[offset] <= ' '; ) {
397                    offset++;
398                }
399            }
400            if (offset < data.length) {
401                String text = new String(data, offset, data.length - offset);
402                text(text);
403            }
404        }
405
406        /** {@inheritDoc} */
407        @Override
408        public void flush() {
409            flushPendingNewline();
410        }
411
412        /**
413         * Writes a line break to the plain text output.
414         *
415         * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are
416         *            always written to the output whereas consecutive implicit line breaks are merged into a single
417         *            line break.
418         */
419        private void newline(boolean implicit) {
420            if (implicit) {
421                pendingNewline = true;
422            } else {
423                flushPendingNewline();
424                sb.append('\n');
425            }
426        }
427
428        /**
429         * Flushes a pending newline (if any).
430         */
431        private void flushPendingNewline() {
432            if (pendingNewline) {
433                pendingNewline = false;
434                if (sb.length() > 0) {
435                    sb.append('\n');
436                }
437            }
438        }
439
440        /**
441         * Writes the specified character data to the plain text output. If the last output was a line break, the
442         * character data will automatically be prefixed with the current indent.
443         *
444         * @param data The character data, must not be <code>null</code>.
445         */
446        private void text(String data) {
447            flushPendingNewline();
448            if (sb.length() <= 0 || sb.charAt(sb.length() - 1) == '\n') {
449                for (int i = 0; i < depth; i++) {
450                    sb.append('\t');
451                }
452            }
453            String text;
454            if (preformatted > 0) {
455                text = data;
456            } else {
457                text = data.replace('\n', ' ');
458            }
459            sb.append(text);
460        }
461    }
462
463    /**
464     * Find the best package name, based on the number of hits of actual Mojo classes.
465     *
466     * @param pluginDescriptor not null
467     * @return the best name of the package for the generated mojo
468     */
469    public static String discoverPackageName(PluginDescriptor pluginDescriptor) {
470        Map<String, Integer> packageNames = new HashMap<>();
471
472        List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos();
473        if (mojoDescriptors == null) {
474            return "";
475        }
476        for (MojoDescriptor descriptor : mojoDescriptors) {
477
478            String impl = descriptor.getImplementation();
479            if (Objects.equals(descriptor.getGoal(), "help") && Objects.equals("HelpMojo", impl)) {
480                continue;
481            }
482            if (impl.lastIndexOf('.') != -1) {
483                String name = impl.substring(0, impl.lastIndexOf('.'));
484                if (packageNames.get(name) != null) {
485                    int next = (packageNames.get(name)).intValue() + 1;
486                    packageNames.put(name, Integer.valueOf(next));
487                } else {
488                    packageNames.put(name, Integer.valueOf(1));
489                }
490            } else {
491                packageNames.put("", Integer.valueOf(1));
492            }
493        }
494
495        String packageName = "";
496        int max = 0;
497        for (Map.Entry<String, Integer> entry : packageNames.entrySet()) {
498            int value = entry.getValue().intValue();
499            if (value > max) {
500                max = value;
501                packageName = entry.getKey();
502            }
503        }
504
505        return packageName;
506    }
507
508    /**
509     * @param impl a Mojo implementation, not null
510     * @param project a MavenProject instance, could be null
511     * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>,
512     * <code>false</code> otherwise.
513     * @throws IllegalArgumentException if any
514     * @deprecated Use {@link PluginUtils#isMavenReport(String, MavenProject)} instead.
515     */
516    @Deprecated
517    public static boolean isMavenReport(String impl, MavenProject project) throws IllegalArgumentException {
518        return PluginUtils.isMavenReport(impl, project);
519    }
520}