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