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