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