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