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