001    package 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    
022    import java.io.ByteArrayInputStream;
023    import java.io.ByteArrayOutputStream;
024    import java.io.File;
025    import java.io.IOException;
026    import java.io.StringReader;
027    import java.io.UnsupportedEncodingException;
028    import java.net.MalformedURLException;
029    import java.net.URL;
030    import java.net.URLClassLoader;
031    import java.util.ArrayList;
032    import java.util.HashMap;
033    import java.util.LinkedList;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.Stack;
037    import java.util.regex.Matcher;
038    import java.util.regex.Pattern;
039    
040    import javax.swing.text.MutableAttributeSet;
041    import javax.swing.text.html.HTML;
042    import javax.swing.text.html.HTMLEditorKit;
043    import javax.swing.text.html.parser.ParserDelegator;
044    
045    import org.apache.maven.artifact.DependencyResolutionRequiredException;
046    import org.apache.maven.model.Dependency;
047    import org.apache.maven.plugin.descriptor.MojoDescriptor;
048    import org.apache.maven.plugin.descriptor.PluginDescriptor;
049    import org.apache.maven.project.MavenProject;
050    import org.apache.maven.reporting.MavenReport;
051    import org.codehaus.plexus.component.repository.ComponentDependency;
052    import org.codehaus.plexus.util.StringUtils;
053    import org.codehaus.plexus.util.xml.XMLWriter;
054    import org.w3c.tidy.Tidy;
055    
056    /**
057     * Convenience methods to play with Maven plugins.
058     *
059     * @author jdcasey
060     * @version $Id: GeneratorUtils.java 1342928 2012-05-26 17:12:27Z hboutemy $
061     */
062    public 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                public 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            public 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    }