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