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