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