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