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.net.MalformedURLException;
033import java.net.URL;
034import java.net.URLClassLoader;
035import java.nio.charset.StandardCharsets;
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
057/**
058 * Convenience methods to play with Maven plugins.
059 *
060 * @author jdcasey
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        List<ComponentDependency> deps = pluginDescriptor.getDependencies();
078        for ( ComponentDependency dep : deps )
079        {
080            w.startElement( "dependency" );
081
082            element( w, "groupId", dep.getGroupId() );
083
084            element( w, "artifactId", dep.getArtifactId() );
085
086            element( w, "type", dep.getType() );
087
088            element( w, "version", dep.getVersion() );
089
090            w.endElement();
091        }
092
093        w.endElement();
094    }
095
096    /**
097     * @param w not null writer
098     * @param name  not null
099     * @param value could be null
100     */
101    public static void element( XMLWriter w, String name, String value )
102    {
103        w.startElement( name );
104
105        if ( value == null )
106        {
107            value = "";
108        }
109
110        w.writeText( value );
111
112        w.endElement();
113    }
114
115    /**
116     * @param artifacts not null collection of <code>Artifact</code>
117     * @return list of component dependencies, without in provided scope
118     */
119    public static List<ComponentDependency> toComponentDependencies( Collection<Artifact> artifacts )
120    {
121        List<ComponentDependency> componentDeps = new LinkedList<>();
122
123        for ( Artifact artifact : artifacts )
124        {
125            if ( Artifact.SCOPE_PROVIDED.equals( artifact.getScope() ) )
126            {
127                continue;
128            }
129
130            ComponentDependency cd = new ComponentDependency();
131
132            cd.setArtifactId( artifact.getArtifactId() );
133            cd.setGroupId( artifact.getGroupId() );
134            cd.setVersion( artifact.getVersion() );
135            cd.setType( artifact.getType() );
136
137            componentDeps.add( cd );
138        }
139
140        return componentDeps;
141    }
142
143    /**
144     * Returns a literal replacement <code>String</code> for the specified <code>String</code>. This method
145     * produces a <code>String</code> that will work as a literal replacement <code>s</code> in the
146     * <code>appendReplacement</code> method of the {@link Matcher} class. The <code>String</code> produced will
147     * match the sequence of characters in <code>s</code> treated as a literal sequence. Slashes ('\') and dollar
148     * signs ('$') will be given no special meaning. TODO: copied from Matcher class of Java 1.5, remove once target
149     * platform can be upgraded
150     *
151     * @see <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Matcher.html">java.util.regex.Matcher</a>
152     * @param s The string to be literalized
153     * @return A literal string replacement
154     */
155    private static String quoteReplacement( String s )
156    {
157        if ( ( s.indexOf( '\\' ) == -1 ) && ( s.indexOf( '$' ) == -1 ) )
158        {
159            return s;
160        }
161
162        StringBuilder sb = new StringBuilder();
163        for ( int i = 0; i < s.length(); i++ )
164        {
165            char c = s.charAt( i );
166            if ( c == '\\' )
167            {
168                sb.append( '\\' );
169                sb.append( '\\' );
170            }
171            else if ( c == '$' )
172            {
173                sb.append( '\\' );
174                sb.append( '$' );
175            }
176            else
177            {
178                sb.append( c );
179            }
180        }
181
182        return sb.toString();
183    }
184
185    /**
186     * Decodes javadoc inline tags into equivalent HTML tags. For instance, the inline tag "{@code <A&B>}" should be
187     * rendered as "<code>&lt;A&amp;B&gt;</code>".
188     *
189     * @param description The javadoc description to decode, may be <code>null</code>.
190     * @return The decoded description, never <code>null</code>.
191     * @deprecated Only used for non java extractor
192     */
193    @Deprecated
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     * @deprecated Redundant for java extractor
265     */
266    @Deprecated
267    public static String makeHtmlValid( String description )
268    {
269        
270        if ( StringUtils.isEmpty( description ) )
271        {
272            return "";
273        }
274
275        String commentCleaned = decodeJavadocTags( description );
276
277        // Using jTidy to clean comment
278        Tidy tidy = new Tidy();
279        tidy.setDocType( "loose" );
280        tidy.setXHTML( true );
281        tidy.setXmlOut( true );
282        tidy.setInputEncoding( "UTF-8" );
283        tidy.setOutputEncoding( "UTF-8" );
284        tidy.setMakeClean( true );
285        tidy.setNumEntities( true );
286        tidy.setQuoteNbsp( false );
287        tidy.setQuiet( true );
288        tidy.setShowWarnings( true );
289        
290        ByteArrayOutputStream out = new ByteArrayOutputStream( commentCleaned.length() + 256 );
291        tidy.parse( new ByteArrayInputStream( commentCleaned.getBytes( StandardCharsets.UTF_8 ) ), out );
292        commentCleaned = new String( out.toByteArray(), StandardCharsets.UTF_8 );
293
294        if ( StringUtils.isEmpty( commentCleaned ) )
295        {
296            return "";
297        }
298
299        // strip the header/body stuff
300        String ls = System.getProperty( "line.separator" );
301        int startPos = commentCleaned.indexOf( "<body>" + ls ) + 6 + ls.length();
302        int endPos = commentCleaned.indexOf( ls + "</body>" );
303        commentCleaned = commentCleaned.substring( startPos, endPos );
304
305        return commentCleaned;
306    }
307
308    /**
309     * Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain
310     * as much of the text formatting as possible by means of the following transformations:
311     * <ul>
312     * <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and
313     * finally the item contents. Each tab denotes an increase of indentation.</li>
314     * <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline
315     * (U+000A) to denote a mandatory line break.</li>
316     * <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized
317     * to a single space. The resulting space denotes a possible point for line wrapping.</li>
318     * <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li>
319     * </ul>
320     *
321     * @param html The HTML fragment to convert to plain text, may be <code>null</code>.
322     * @return A string with HTML tags converted into pure text, never <code>null</code>.
323     * @since 2.4.3
324     * @deprecated Replaced by {@link HtmlToPlainTextConverter}
325     */
326    @Deprecated
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<>();
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        @Override
413        public void handleSimpleTag( HTML.Tag t, MutableAttributeSet a, int pos )
414        {
415            simpleTag = true;
416            if ( body && HTML.Tag.BR.equals( t ) )
417            {
418                newline( false );
419            }
420        }
421
422        /** {@inheritDoc} */
423        @Override
424        public void handleStartTag( HTML.Tag t, MutableAttributeSet a, int pos )
425        {
426            simpleTag = false;
427            if ( body && ( t.breaksFlow() || t.isBlock() ) )
428            {
429                newline( true );
430            }
431            if ( HTML.Tag.OL.equals( t ) )
432            {
433                numbering.push( new Counter() );
434            }
435            else if ( HTML.Tag.UL.equals( t ) )
436            {
437                numbering.push( null );
438            }
439            else if ( HTML.Tag.LI.equals( t ) )
440            {
441                Counter counter = numbering.peek();
442                if ( counter == null )
443                {
444                    text( "-\t" );
445                }
446                else
447                {
448                    text( ++counter.value + ".\t" );
449                }
450                depth++;
451            }
452            else if ( HTML.Tag.DD.equals( t ) )
453            {
454                depth++;
455            }
456            else if ( t.isPreformatted() )
457            {
458                preformatted++;
459            }
460            else if ( HTML.Tag.BODY.equals( t ) )
461            {
462                body = true;
463            }
464        }
465
466        /** {@inheritDoc} */
467        @Override
468        public void handleEndTag( HTML.Tag t, int pos )
469        {
470            if ( HTML.Tag.OL.equals( t ) || HTML.Tag.UL.equals( t ) )
471            {
472                numbering.pop();
473            }
474            else if ( HTML.Tag.LI.equals( t ) || HTML.Tag.DD.equals( t ) )
475            {
476                depth--;
477            }
478            else if ( t.isPreformatted() )
479            {
480                preformatted--;
481            }
482            else if ( HTML.Tag.BODY.equals( t ) )
483            {
484                body = false;
485            }
486            if ( body && ( t.breaksFlow() || t.isBlock() ) && !HTML.Tag.LI.equals( t ) )
487            {
488                if ( ( HTML.Tag.P.equals( t ) || HTML.Tag.PRE.equals( t ) || HTML.Tag.OL.equals( t )
489                    || HTML.Tag.UL.equals( t ) || HTML.Tag.DL.equals( t ) )
490                    && numbering.isEmpty() )
491                {
492                    pendingNewline = false;
493                    newline( pendingNewline );
494                }
495                else
496                {
497                    newline( true );
498                }
499            }
500        }
501
502        /** {@inheritDoc} */
503        @Override
504        public void handleText( char[] data, int pos )
505        {
506            /*
507             * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by
508             * the text event ">..." so we need to watch out for the closing angle bracket.
509             */
510            int offset = 0;
511            if ( simpleTag && data[0] == '>' )
512            {
513                simpleTag = false;
514                for ( ++offset; offset < data.length && data[offset] <= ' '; )
515                {
516                    offset++;
517                }
518            }
519            if ( offset < data.length )
520            {
521                String text = new String( data, offset, data.length - offset );
522                text( text );
523            }
524        }
525
526        /** {@inheritDoc} */
527        @Override
528        public void flush()
529        {
530            flushPendingNewline();
531        }
532
533        /**
534         * Writes a line break to the plain text output.
535         *
536         * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are
537         *            always written to the output whereas consecutive implicit line breaks are merged into a single
538         *            line break.
539         */
540        private void newline( boolean implicit )
541        {
542            if ( implicit )
543            {
544                pendingNewline = true;
545            }
546            else
547            {
548                flushPendingNewline();
549                sb.append( '\n' );
550            }
551        }
552
553        /**
554         * Flushes a pending newline (if any).
555         */
556        private void flushPendingNewline()
557        {
558            if ( pendingNewline )
559            {
560                pendingNewline = false;
561                if ( sb.length() > 0 )
562                {
563                    sb.append( '\n' );
564                }
565            }
566        }
567
568        /**
569         * Writes the specified character data to the plain text output. If the last output was a line break, the
570         * character data will automatically be prefixed with the current indent.
571         *
572         * @param data The character data, must not be <code>null</code>.
573         */
574        private void text( String data )
575        {
576            flushPendingNewline();
577            if ( sb.length() <= 0 || sb.charAt( sb.length() - 1 ) == '\n' )
578            {
579                for ( int i = 0; i < depth; i++ )
580                {
581                    sb.append( '\t' );
582                }
583            }
584            String text;
585            if ( preformatted > 0 )
586            {
587                text = data;
588            }
589            else
590            {
591                text = data.replace( '\n', ' ' );
592            }
593            sb.append( text );
594        }
595    }
596
597    /**
598     * Find the best package name, based on the number of hits of actual Mojo classes.
599     *
600     * @param pluginDescriptor not null
601     * @return the best name of the package for the generated mojo
602     */
603    public static String discoverPackageName( PluginDescriptor pluginDescriptor )
604    {
605        Map<String, Integer> packageNames = new HashMap<>();
606
607        List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos();
608        if ( mojoDescriptors == null )
609        {
610            return "";
611        }
612        for ( MojoDescriptor descriptor : mojoDescriptors )
613        {
614
615            String impl = descriptor.getImplementation();
616            if ( StringUtils.equals( descriptor.getGoal(), "help" ) && StringUtils.equals( "HelpMojo", impl ) )
617            {
618                continue;
619            }
620            if ( impl.lastIndexOf( '.' ) != -1 )
621            {
622                String name = impl.substring( 0, impl.lastIndexOf( '.' ) );
623                if ( packageNames.get( name ) != null )
624                {
625                    int next = ( packageNames.get( name ) ).intValue() + 1;
626                    packageNames.put( name,  Integer.valueOf( next ) );
627                }
628                else
629                {
630                    packageNames.put( name, Integer.valueOf( 1 ) );
631                }
632            }
633            else
634            {
635                packageNames.put( "", Integer.valueOf( 1 ) );
636            }
637        }
638
639        String packageName = "";
640        int max = 0;
641        for ( Map.Entry<String, Integer> entry : packageNames.entrySet() )
642        {
643            int value = entry.getValue().intValue();
644            if ( value > max )
645            {
646                max = value;
647                packageName = entry.getKey();
648            }
649        }
650
651        return packageName;
652    }
653
654    /**
655     * @param impl a Mojo implementation, not null
656     * @param project a MavenProject instance, could be null
657     * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>,
658     * <code>false</code> otherwise.
659     * @throws IllegalArgumentException if any
660     */
661    @SuppressWarnings( "unchecked" )
662    public static boolean isMavenReport( String impl, MavenProject project )
663        throws IllegalArgumentException
664    {
665        if ( impl == null )
666        {
667            throw new IllegalArgumentException( "mojo implementation should be declared" );
668        }
669
670        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
671        if ( project != null )
672        {
673            List<String> classPathStrings;
674            try
675            {
676                classPathStrings = project.getCompileClasspathElements();
677                if ( project.getExecutionProject() != null )
678                {
679                    classPathStrings.addAll( project.getExecutionProject().getCompileClasspathElements() );
680                }
681            }
682            catch ( DependencyResolutionRequiredException e )
683            {
684                throw new IllegalArgumentException( e );
685            }
686
687            List<URL> urls = new ArrayList<>( classPathStrings.size() );
688            for ( String classPathString : classPathStrings )
689            {
690                try
691                {
692                    urls.add( new File( classPathString ).toURL() );
693                }
694                catch ( MalformedURLException e )
695                {
696                    throw new IllegalArgumentException( e );
697                }
698            }
699
700            classLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), classLoader );
701        }
702
703        try
704        {
705            Class<?> clazz = Class.forName( impl, false, classLoader );
706
707            return MavenReport.class.isAssignableFrom( clazz );
708        }
709        catch ( ClassNotFoundException e )
710        {
711            return false;
712        }
713    }
714
715}