001package org.apache.maven.tools.plugin.generator;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import javax.swing.text.MutableAttributeSet;
023import javax.swing.text.html.HTML;
024import javax.swing.text.html.HTMLEditorKit;
025import javax.swing.text.html.parser.ParserDelegator;
026
027import java.io.ByteArrayInputStream;
028import java.io.ByteArrayOutputStream;
029import java.io.File;
030import java.io.IOException;
031import java.io.StringReader;
032import java.io.UnsupportedEncodingException;
033import java.net.MalformedURLException;
034import java.net.URL;
035import java.net.URLClassLoader;
036import java.util.ArrayList;
037import java.util.Collection;
038import java.util.HashMap;
039import java.util.LinkedList;
040import java.util.List;
041import java.util.Map;
042import java.util.Stack;
043import java.util.regex.Matcher;
044import java.util.regex.Pattern;
045
046import org.apache.maven.artifact.Artifact;
047import org.apache.maven.artifact.DependencyResolutionRequiredException;
048import org.apache.maven.plugin.descriptor.MojoDescriptor;
049import org.apache.maven.plugin.descriptor.PluginDescriptor;
050import org.apache.maven.project.MavenProject;
051import org.apache.maven.reporting.MavenReport;
052import org.codehaus.plexus.component.repository.ComponentDependency;
053import org.codehaus.plexus.util.StringUtils;
054import org.codehaus.plexus.util.xml.XMLWriter;
055import org.w3c.tidy.Tidy;
056
057import static java.nio.charset.StandardCharsets.UTF_8;
058
059/**
060 * Convenience methods to play with Maven plugins.
061 *
062 * @author jdcasey
063 */
064public final class GeneratorUtils
065{
066    private GeneratorUtils()
067    {
068        // nop
069    }
070
071    /**
072     * @param w not null writer
073     * @param pluginDescriptor not null
074     */
075    public static void writeDependencies( XMLWriter w, PluginDescriptor pluginDescriptor )
076    {
077        w.startElement( "dependencies" );
078
079        @SuppressWarnings( "unchecked" )
080        List<ComponentDependency> deps = pluginDescriptor.getDependencies();
081        for ( ComponentDependency dep : deps )
082        {
083            w.startElement( "dependency" );
084
085            element( w, "groupId", dep.getGroupId() );
086
087            element( w, "artifactId", dep.getArtifactId() );
088
089            element( w, "type", dep.getType() );
090
091            element( w, "version", dep.getVersion() );
092
093            w.endElement();
094        }
095
096        w.endElement();
097    }
098
099    /**
100     * @param w not null writer
101     * @param name  not null
102     * @param value could be null
103     */
104    public static void element( XMLWriter w, String name, String value )
105    {
106        w.startElement( name );
107
108        if ( value == null )
109        {
110            value = "";
111        }
112
113        w.writeText( value );
114
115        w.endElement();
116    }
117
118    public static void element( XMLWriter w, String name, String value, boolean asText )
119    {
120        element( w, name, asText ? GeneratorUtils.toText( value ) : value );
121    }
122    
123    /**
124     * @param artifacts not null collection of <code>Artifact</code>
125     * @return list of component dependencies, without in provided scope
126     */
127    public static List<ComponentDependency> toComponentDependencies( Collection<Artifact> artifacts )
128    {
129        List<ComponentDependency> componentDeps = new LinkedList<>();
130
131        for ( Artifact artifact : artifacts )
132        {
133            if ( Artifact.SCOPE_PROVIDED.equals( artifact.getScope() ) )
134            {
135                continue;
136            }
137
138            ComponentDependency cd = new ComponentDependency();
139
140            cd.setArtifactId( artifact.getArtifactId() );
141            cd.setGroupId( artifact.getGroupId() );
142            cd.setVersion( artifact.getVersion() );
143            cd.setType( artifact.getType() );
144
145            componentDeps.add( cd );
146        }
147
148        return componentDeps;
149    }
150
151    /**
152     * Returns a literal replacement <code>String</code> for the specified <code>String</code>. This method
153     * produces a <code>String</code> that will work as a literal replacement <code>s</code> in the
154     * <code>appendReplacement</code> method of the {@link Matcher} class. The <code>String</code> produced will
155     * match the sequence of characters in <code>s</code> treated as a literal sequence. Slashes ('\') and dollar
156     * signs ('$') will be given no special meaning. TODO: copied from Matcher class of Java 1.5, remove once target
157     * platform can be upgraded
158     *
159     * @see <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Matcher.html">java.util.regex.Matcher</a>
160     * @param s The string to be literalized
161     * @return A literal string replacement
162     */
163    private static String quoteReplacement( String s )
164    {
165        if ( ( s.indexOf( '\\' ) == -1 ) && ( s.indexOf( '$' ) == -1 ) )
166        {
167            return s;
168        }
169
170        StringBuilder sb = new StringBuilder();
171        for ( int i = 0; i < s.length(); i++ )
172        {
173            char c = s.charAt( i );
174            if ( c == '\\' )
175            {
176                sb.append( '\\' );
177                sb.append( '\\' );
178            }
179            else if ( c == '$' )
180            {
181                sb.append( '\\' );
182                sb.append( '$' );
183            }
184            else
185            {
186                sb.append( c );
187            }
188        }
189
190        return sb.toString();
191    }
192
193    /**
194     * Decodes javadoc inline tags into equivalent HTML tags. For instance, the inline tag "{@code <A&B>}" should be
195     * rendered as "<code>&lt;A&amp;B&gt;</code>".
196     *
197     * @param description The javadoc description to decode, may be <code>null</code>.
198     * @return The decoded description, never <code>null</code>.
199     */
200    static String decodeJavadocTags( String description )
201    {
202        if ( StringUtils.isEmpty( description ) )
203        {
204            return "";
205        }
206
207        StringBuffer decoded = new StringBuffer( description.length() + 1024 );
208
209        Matcher matcher = Pattern.compile( "\\{@(\\w+)\\s*([^\\}]*)\\}" ).matcher( description );
210        while ( matcher.find() )
211        {
212            String tag = matcher.group( 1 );
213            String text = matcher.group( 2 );
214            text = StringUtils.replace( text, "&", "&amp;" );
215            text = StringUtils.replace( text, "<", "&lt;" );
216            text = StringUtils.replace( text, ">", "&gt;" );
217            if ( "code".equals( tag ) )
218            {
219                text = "<code>" + text + "</code>";
220            }
221            else if ( "link".equals( tag ) || "linkplain".equals( tag ) || "value".equals( tag ) )
222            {
223                String pattern = "(([^#\\.\\s]+\\.)*([^#\\.\\s]+))?" + "(#([^\\(\\s]*)(\\([^\\)]*\\))?\\s*(\\S.*)?)?";
224                final int label = 7;
225                final int clazz = 3;
226                final int member = 5;
227                final int args = 6;
228                Matcher link = Pattern.compile( pattern ).matcher( text );
229                if ( link.matches() )
230                {
231                    text = link.group( label );
232                    if ( StringUtils.isEmpty( text ) )
233                    {
234                        text = link.group( clazz );
235                        if ( StringUtils.isEmpty( text ) )
236                        {
237                            text = "";
238                        }
239                        if ( StringUtils.isNotEmpty( link.group( member ) ) )
240                        {
241                            if ( StringUtils.isNotEmpty( text ) )
242                            {
243                                text += '.';
244                            }
245                            text += link.group( member );
246                            if ( StringUtils.isNotEmpty( link.group( args ) ) )
247                            {
248                                text += "()";
249                            }
250                        }
251                    }
252                }
253                if ( !"linkplain".equals( tag ) )
254                {
255                    text = "<code>" + text + "</code>";
256                }
257            }
258            matcher.appendReplacement( decoded, ( text != null ) ? quoteReplacement( text ) : "" );
259        }
260        matcher.appendTail( decoded );
261
262        return decoded.toString();
263    }
264
265    /**
266     * Fixes some javadoc comment to become a valid XHTML snippet.
267     *
268     * @param description Javadoc description with HTML tags, may be <code>null</code>.
269     * @return The description with valid XHTML tags, never <code>null</code>.
270     */
271    public static String makeHtmlValid( String description )
272    {
273        if ( StringUtils.isEmpty( description ) )
274        {
275            return "";
276        }
277
278        String commentCleaned = decodeJavadocTags( description );
279
280        // Using jTidy to clean comment
281        Tidy tidy = new Tidy();
282        tidy.setDocType( "loose" );
283        tidy.setXHTML( true );
284        tidy.setXmlOut( true );
285        tidy.setInputEncoding( "UTF-8" );
286        tidy.setOutputEncoding( "UTF-8" );
287        tidy.setMakeClean( true );
288        tidy.setNumEntities( true );
289        tidy.setQuoteNbsp( false );
290        tidy.setQuiet( true );
291        tidy.setShowWarnings( false );
292        try
293        {
294            ByteArrayOutputStream out = new ByteArrayOutputStream( commentCleaned.length() + 256 );
295            tidy.parse( new ByteArrayInputStream( commentCleaned.getBytes( UTF_8 ) ), out );
296            commentCleaned = out.toString( "UTF-8" );
297        }
298        catch ( UnsupportedEncodingException e )
299        {
300            // cannot happen as every JVM must support UTF-8, see also class javadoc for java.nio.charset.Charset
301        }
302
303        if ( StringUtils.isEmpty( commentCleaned ) )
304        {
305            return "";
306        }
307
308        // strip the header/body stuff
309        String ls = System.getProperty( "line.separator" );
310        int startPos = commentCleaned.indexOf( "<body>" + ls ) + 6 + ls.length();
311        int endPos = commentCleaned.indexOf( ls + "</body>" );
312        commentCleaned = commentCleaned.substring( startPos, endPos );
313
314        return commentCleaned;
315    }
316
317    /**
318     * Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain
319     * as much of the text formatting as possible by means of the following transformations:
320     * <ul>
321     * <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and
322     * finally the item contents. Each tab denotes an increase of indentation.</li>
323     * <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline
324     * (U+000A) to denote a mandatory line break.</li>
325     * <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized
326     * to a single space. The resulting space denotes a possible point for line wrapping.</li>
327     * <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li>
328     * </ul>
329     *
330     * @param html The HTML fragment to convert to plain text, may be <code>null</code>.
331     * @return A string with HTML tags converted into pure text, never <code>null</code>.
332     * @since 2.4.3
333     */
334    public static String toText( String html )
335    {
336        if ( StringUtils.isEmpty( html ) )
337        {
338            return "";
339        }
340
341        final StringBuilder sb = new StringBuilder();
342
343        HTMLEditorKit.Parser parser = new ParserDelegator();
344        HTMLEditorKit.ParserCallback htmlCallback = new MojoParserCallback( sb );
345
346        try
347        {
348            parser.parse( new StringReader( makeHtmlValid( html ) ), htmlCallback, true );
349        }
350        catch ( IOException e )
351        {
352            throw new RuntimeException( e );
353        }
354
355        return sb.toString().replace( '\"', '\'' ); // for CDATA
356    }
357
358    /**
359     * ParserCallback implementation.
360     */
361    private static class MojoParserCallback
362        extends HTMLEditorKit.ParserCallback
363    {
364        /**
365         * Holds the index of the current item in a numbered list.
366         */
367        class Counter
368        {
369            int value;
370        }
371
372        /**
373         * A flag whether the parser is currently in the body element.
374         */
375        private boolean body;
376
377        /**
378         * A flag whether the parser is currently processing preformatted text, actually a counter to track nesting.
379         */
380        private int preformatted;
381
382        /**
383         * The current indentation depth for the output.
384         */
385        private int depth;
386
387        /**
388         * A stack of {@link Counter} objects corresponding to the nesting of (un-)ordered lists. A
389         * <code>null</code> element denotes an unordered list.
390         */
391        private Stack<Counter> numbering = new Stack<>();
392
393        /**
394         * A flag whether an implicit line break is pending in the output buffer. This flag is used to postpone the
395         * output of implicit line breaks until we are sure that are not to be merged with other implicit line
396         * breaks.
397         */
398        private boolean pendingNewline;
399
400        /**
401         * A flag whether we have just parsed a simple tag.
402         */
403        private boolean simpleTag;
404
405        /**
406         * The current buffer.
407         */
408        private final StringBuilder sb;
409
410        /**
411         * @param sb not null
412         */
413        MojoParserCallback( StringBuilder sb )
414        {
415            this.sb = sb;
416        }
417
418        /** {@inheritDoc} */
419        @Override
420        public void handleSimpleTag( HTML.Tag t, MutableAttributeSet a, int pos )
421        {
422            simpleTag = true;
423            if ( body && HTML.Tag.BR.equals( t ) )
424            {
425                newline( false );
426            }
427        }
428
429        /** {@inheritDoc} */
430        @Override
431        public void handleStartTag( HTML.Tag t, MutableAttributeSet a, int pos )
432        {
433            simpleTag = false;
434            if ( body && ( t.breaksFlow() || t.isBlock() ) )
435            {
436                newline( true );
437            }
438            if ( HTML.Tag.OL.equals( t ) )
439            {
440                numbering.push( new Counter() );
441            }
442            else if ( HTML.Tag.UL.equals( t ) )
443            {
444                numbering.push( null );
445            }
446            else if ( HTML.Tag.LI.equals( t ) )
447            {
448                Counter counter = numbering.peek();
449                if ( counter == null )
450                {
451                    text( "-\t" );
452                }
453                else
454                {
455                    text( ++counter.value + ".\t" );
456                }
457                depth++;
458            }
459            else if ( HTML.Tag.DD.equals( t ) )
460            {
461                depth++;
462            }
463            else if ( t.isPreformatted() )
464            {
465                preformatted++;
466            }
467            else if ( HTML.Tag.BODY.equals( t ) )
468            {
469                body = true;
470            }
471        }
472
473        /** {@inheritDoc} */
474        @Override
475        public void handleEndTag( HTML.Tag t, int pos )
476        {
477            if ( HTML.Tag.OL.equals( t ) || HTML.Tag.UL.equals( t ) )
478            {
479                numbering.pop();
480            }
481            else if ( HTML.Tag.LI.equals( t ) || HTML.Tag.DD.equals( t ) )
482            {
483                depth--;
484            }
485            else if ( t.isPreformatted() )
486            {
487                preformatted--;
488            }
489            else if ( HTML.Tag.BODY.equals( t ) )
490            {
491                body = false;
492            }
493            if ( body && ( t.breaksFlow() || t.isBlock() ) && !HTML.Tag.LI.equals( t ) )
494            {
495                if ( ( HTML.Tag.P.equals( t ) || HTML.Tag.PRE.equals( t ) || HTML.Tag.OL.equals( t )
496                    || HTML.Tag.UL.equals( t ) || HTML.Tag.DL.equals( t ) )
497                    && numbering.isEmpty() )
498                {
499                    pendingNewline = false;
500                    newline( pendingNewline );
501                }
502                else
503                {
504                    newline( true );
505                }
506            }
507        }
508
509        /** {@inheritDoc} */
510        @Override
511        public void handleText( char[] data, int pos )
512        {
513            /*
514             * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by
515             * the text event ">..." so we need to watch out for the closing angle bracket.
516             */
517            int offset = 0;
518            if ( simpleTag && data[0] == '>' )
519            {
520                simpleTag = false;
521                for ( ++offset; offset < data.length && data[offset] <= ' '; )
522                {
523                    offset++;
524                }
525            }
526            if ( offset < data.length )
527            {
528                String text = new String( data, offset, data.length - offset );
529                text( text );
530            }
531        }
532
533        /** {@inheritDoc} */
534        @Override
535        public void flush()
536        {
537            flushPendingNewline();
538        }
539
540        /**
541         * Writes a line break to the plain text output.
542         *
543         * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are
544         *            always written to the output whereas consecutive implicit line breaks are merged into a single
545         *            line break.
546         */
547        private void newline( boolean implicit )
548        {
549            if ( implicit )
550            {
551                pendingNewline = true;
552            }
553            else
554            {
555                flushPendingNewline();
556                sb.append( '\n' );
557            }
558        }
559
560        /**
561         * Flushes a pending newline (if any).
562         */
563        private void flushPendingNewline()
564        {
565            if ( pendingNewline )
566            {
567                pendingNewline = false;
568                if ( sb.length() > 0 )
569                {
570                    sb.append( '\n' );
571                }
572            }
573        }
574
575        /**
576         * Writes the specified character data to the plain text output. If the last output was a line break, the
577         * character data will automatically be prefixed with the current indent.
578         *
579         * @param data The character data, must not be <code>null</code>.
580         */
581        private void text( String data )
582        {
583            flushPendingNewline();
584            if ( sb.length() <= 0 || sb.charAt( sb.length() - 1 ) == '\n' )
585            {
586                for ( int i = 0; i < depth; i++ )
587                {
588                    sb.append( '\t' );
589                }
590            }
591            String text;
592            if ( preformatted > 0 )
593            {
594                text = data;
595            }
596            else
597            {
598                text = data.replace( '\n', ' ' );
599            }
600            sb.append( text );
601        }
602    }
603
604    /**
605     * Find the best package name, based on the number of hits of actual Mojo classes.
606     *
607     * @param pluginDescriptor not null
608     * @return the best name of the package for the generated mojo
609     */
610    public static String discoverPackageName( PluginDescriptor pluginDescriptor )
611    {
612        Map<String, Integer> packageNames = new HashMap<>();
613
614        List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos();
615        if ( mojoDescriptors == null )
616        {
617            return "";
618        }
619        for ( MojoDescriptor descriptor : mojoDescriptors )
620        {
621
622            String impl = descriptor.getImplementation();
623            if ( StringUtils.equals( descriptor.getGoal(), "help" ) && StringUtils.equals( "HelpMojo", impl ) )
624            {
625                continue;
626            }
627            if ( impl.lastIndexOf( '.' ) != -1 )
628            {
629                String name = impl.substring( 0, impl.lastIndexOf( '.' ) );
630                if ( packageNames.get( name ) != null )
631                {
632                    int next = ( packageNames.get( name ) ).intValue() + 1;
633                    packageNames.put( name,  Integer.valueOf( next ) );
634                }
635                else
636                {
637                    packageNames.put( name, Integer.valueOf( 1 ) );
638                }
639            }
640            else
641            {
642                packageNames.put( "", Integer.valueOf( 1 ) );
643            }
644        }
645
646        String packageName = "";
647        int max = 0;
648        for ( Map.Entry<String, Integer> entry : packageNames.entrySet() )
649        {
650            int value = entry.getValue().intValue();
651            if ( value > max )
652            {
653                max = value;
654                packageName = entry.getKey();
655            }
656        }
657
658        return packageName;
659    }
660
661    /**
662     * @param impl a Mojo implementation, not null
663     * @param project a MavenProject instance, could be null
664     * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>,
665     * <code>false</code> otherwise.
666     * @throws IllegalArgumentException if any
667     */
668    @SuppressWarnings( "unchecked" )
669    public static boolean isMavenReport( String impl, MavenProject project )
670        throws IllegalArgumentException
671    {
672        if ( impl == null )
673        {
674            throw new IllegalArgumentException( "mojo implementation should be declared" );
675        }
676
677        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
678        if ( project != null )
679        {
680            List<String> classPathStrings;
681            try
682            {
683                classPathStrings = project.getCompileClasspathElements();
684                if ( project.getExecutionProject() != null )
685                {
686                    classPathStrings.addAll( project.getExecutionProject().getCompileClasspathElements() );
687                }
688            }
689            catch ( DependencyResolutionRequiredException e )
690            {
691                throw new IllegalArgumentException( e );
692            }
693
694            List<URL> urls = new ArrayList<>( classPathStrings.size() );
695            for ( String classPathString : classPathStrings )
696            {
697                try
698                {
699                    urls.add( new File( classPathString ).toURL() );
700                }
701                catch ( MalformedURLException e )
702                {
703                    throw new IllegalArgumentException( e );
704                }
705            }
706
707            classLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), classLoader );
708        }
709
710        try
711        {
712            Class<?> clazz = Class.forName( impl, false, classLoader );
713
714            return MavenReport.class.isAssignableFrom( clazz );
715        }
716        catch ( ClassNotFoundException e )
717        {
718            return false;
719        }
720    }
721
722}