View Javadoc
1   package org.apache.maven.tools.plugin.generator;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import javax.swing.text.MutableAttributeSet;
23  import javax.swing.text.html.HTML;
24  import javax.swing.text.html.HTMLEditorKit;
25  import javax.swing.text.html.parser.ParserDelegator;
26  
27  import java.io.ByteArrayInputStream;
28  import java.io.ByteArrayOutputStream;
29  import java.io.File;
30  import java.io.IOException;
31  import java.io.StringReader;
32  import java.net.MalformedURLException;
33  import java.net.URL;
34  import java.net.URLClassLoader;
35  import java.nio.charset.StandardCharsets;
36  import java.util.ArrayList;
37  import java.util.Collection;
38  import java.util.HashMap;
39  import java.util.LinkedList;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Stack;
43  import java.util.regex.Matcher;
44  import java.util.regex.Pattern;
45  
46  import org.apache.maven.artifact.Artifact;
47  import org.apache.maven.artifact.DependencyResolutionRequiredException;
48  import org.apache.maven.plugin.descriptor.MojoDescriptor;
49  import org.apache.maven.plugin.descriptor.PluginDescriptor;
50  import org.apache.maven.project.MavenProject;
51  import org.apache.maven.reporting.MavenReport;
52  import org.codehaus.plexus.component.repository.ComponentDependency;
53  import org.codehaus.plexus.util.StringUtils;
54  import org.codehaus.plexus.util.xml.XMLWriter;
55  import org.w3c.tidy.Tidy;
56  
57  /**
58   * Convenience methods to play with Maven plugins.
59   *
60   * @author jdcasey
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          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     /**
116      * @param artifacts not null collection of <code>Artifact</code>
117      * @return list of component dependencies, without in provided scope
118      */
119     public static List<ComponentDependency> toComponentDependencies( Collection<Artifact> artifacts )
120     {
121         List<ComponentDependency> componentDeps = new LinkedList<>();
122 
123         for ( Artifact artifact : artifacts )
124         {
125             if ( Artifact.SCOPE_PROVIDED.equals( artifact.getScope() ) )
126             {
127                 continue;
128             }
129 
130             ComponentDependency cd = new ComponentDependency();
131 
132             cd.setArtifactId( artifact.getArtifactId() );
133             cd.setGroupId( artifact.getGroupId() );
134             cd.setVersion( artifact.getVersion() );
135             cd.setType( artifact.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      * @deprecated Only used for non java extractor
192      */
193     @Deprecated
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      * @deprecated Redundant for java extractor
265      */
266     @Deprecated
267     public static String makeHtmlValid( String description )
268     {
269         
270         if ( StringUtils.isEmpty( description ) )
271         {
272             return "";
273         }
274 
275         String commentCleaned = decodeJavadocTags( description );
276 
277         // Using jTidy to clean comment
278         Tidy tidy = new Tidy();
279         tidy.setDocType( "loose" );
280         tidy.setXHTML( true );
281         tidy.setXmlOut( true );
282         tidy.setInputEncoding( "UTF-8" );
283         tidy.setOutputEncoding( "UTF-8" );
284         tidy.setMakeClean( true );
285         tidy.setNumEntities( true );
286         tidy.setQuoteNbsp( false );
287         tidy.setQuiet( true );
288         tidy.setShowWarnings( true );
289         
290         ByteArrayOutputStream out = new ByteArrayOutputStream( commentCleaned.length() + 256 );
291         tidy.parse( new ByteArrayInputStream( commentCleaned.getBytes( StandardCharsets.UTF_8 ) ), out );
292         commentCleaned = new String( out.toByteArray(), StandardCharsets.UTF_8 );
293 
294         if ( StringUtils.isEmpty( commentCleaned ) )
295         {
296             return "";
297         }
298 
299         // strip the header/body stuff
300         String ls = System.getProperty( "line.separator" );
301         int startPos = commentCleaned.indexOf( "<body>" + ls ) + 6 + ls.length();
302         int endPos = commentCleaned.indexOf( ls + "</body>" );
303         commentCleaned = commentCleaned.substring( startPos, endPos );
304 
305         return commentCleaned;
306     }
307 
308     /**
309      * Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain
310      * as much of the text formatting as possible by means of the following transformations:
311      * <ul>
312      * <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and
313      * finally the item contents. Each tab denotes an increase of indentation.</li>
314      * <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline
315      * (U+000A) to denote a mandatory line break.</li>
316      * <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized
317      * to a single space. The resulting space denotes a possible point for line wrapping.</li>
318      * <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li>
319      * </ul>
320      *
321      * @param html The HTML fragment to convert to plain text, may be <code>null</code>.
322      * @return A string with HTML tags converted into pure text, never <code>null</code>.
323      * @since 2.4.3
324      * @deprecated Replaced by {@link HtmlToPlainTextConverter}
325      */
326     @Deprecated
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<>();
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         @Override
413         public void handleSimpleTag( HTML.Tag t, MutableAttributeSet a, int pos )
414         {
415             simpleTag = true;
416             if ( body && HTML.Tag.BR.equals( t ) )
417             {
418                 newline( false );
419             }
420         }
421 
422         /** {@inheritDoc} */
423         @Override
424         public void handleStartTag( HTML.Tag t, MutableAttributeSet a, int pos )
425         {
426             simpleTag = false;
427             if ( body && ( t.breaksFlow() || t.isBlock() ) )
428             {
429                 newline( true );
430             }
431             if ( HTML.Tag.OL.equals( t ) )
432             {
433                 numbering.push( new Counter() );
434             }
435             else if ( HTML.Tag.UL.equals( t ) )
436             {
437                 numbering.push( null );
438             }
439             else if ( HTML.Tag.LI.equals( t ) )
440             {
441                 Counter counter = numbering.peek();
442                 if ( counter == null )
443                 {
444                     text( "-\t" );
445                 }
446                 else
447                 {
448                     text( ++counter.value + ".\t" );
449                 }
450                 depth++;
451             }
452             else if ( HTML.Tag.DD.equals( t ) )
453             {
454                 depth++;
455             }
456             else if ( t.isPreformatted() )
457             {
458                 preformatted++;
459             }
460             else if ( HTML.Tag.BODY.equals( t ) )
461             {
462                 body = true;
463             }
464         }
465 
466         /** {@inheritDoc} */
467         @Override
468         public void handleEndTag( HTML.Tag t, int pos )
469         {
470             if ( HTML.Tag.OL.equals( t ) || HTML.Tag.UL.equals( t ) )
471             {
472                 numbering.pop();
473             }
474             else if ( HTML.Tag.LI.equals( t ) || HTML.Tag.DD.equals( t ) )
475             {
476                 depth--;
477             }
478             else if ( t.isPreformatted() )
479             {
480                 preformatted--;
481             }
482             else if ( HTML.Tag.BODY.equals( t ) )
483             {
484                 body = false;
485             }
486             if ( body && ( t.breaksFlow() || t.isBlock() ) && !HTML.Tag.LI.equals( t ) )
487             {
488                 if ( ( HTML.Tag.P.equals( t ) || HTML.Tag.PRE.equals( t ) || HTML.Tag.OL.equals( t )
489                     || HTML.Tag.UL.equals( t ) || HTML.Tag.DL.equals( t ) )
490                     && numbering.isEmpty() )
491                 {
492                     pendingNewline = false;
493                     newline( pendingNewline );
494                 }
495                 else
496                 {
497                     newline( true );
498                 }
499             }
500         }
501 
502         /** {@inheritDoc} */
503         @Override
504         public void handleText( char[] data, int pos )
505         {
506             /*
507              * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by
508              * the text event ">..." so we need to watch out for the closing angle bracket.
509              */
510             int offset = 0;
511             if ( simpleTag && data[0] == '>' )
512             {
513                 simpleTag = false;
514                 for ( ++offset; offset < data.length && data[offset] <= ' '; )
515                 {
516                     offset++;
517                 }
518             }
519             if ( offset < data.length )
520             {
521                 String text = new String( data, offset, data.length - offset );
522                 text( text );
523             }
524         }
525 
526         /** {@inheritDoc} */
527         @Override
528         public void flush()
529         {
530             flushPendingNewline();
531         }
532 
533         /**
534          * Writes a line break to the plain text output.
535          *
536          * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are
537          *            always written to the output whereas consecutive implicit line breaks are merged into a single
538          *            line break.
539          */
540         private void newline( boolean implicit )
541         {
542             if ( implicit )
543             {
544                 pendingNewline = true;
545             }
546             else
547             {
548                 flushPendingNewline();
549                 sb.append( '\n' );
550             }
551         }
552 
553         /**
554          * Flushes a pending newline (if any).
555          */
556         private void flushPendingNewline()
557         {
558             if ( pendingNewline )
559             {
560                 pendingNewline = false;
561                 if ( sb.length() > 0 )
562                 {
563                     sb.append( '\n' );
564                 }
565             }
566         }
567 
568         /**
569          * Writes the specified character data to the plain text output. If the last output was a line break, the
570          * character data will automatically be prefixed with the current indent.
571          *
572          * @param data The character data, must not be <code>null</code>.
573          */
574         private void text( String data )
575         {
576             flushPendingNewline();
577             if ( sb.length() <= 0 || sb.charAt( sb.length() - 1 ) == '\n' )
578             {
579                 for ( int i = 0; i < depth; i++ )
580                 {
581                     sb.append( '\t' );
582                 }
583             }
584             String text;
585             if ( preformatted > 0 )
586             {
587                 text = data;
588             }
589             else
590             {
591                 text = data.replace( '\n', ' ' );
592             }
593             sb.append( text );
594         }
595     }
596 
597     /**
598      * Find the best package name, based on the number of hits of actual Mojo classes.
599      *
600      * @param pluginDescriptor not null
601      * @return the best name of the package for the generated mojo
602      */
603     public static String discoverPackageName( PluginDescriptor pluginDescriptor )
604     {
605         Map<String, Integer> packageNames = new HashMap<>();
606 
607         List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos();
608         if ( mojoDescriptors == null )
609         {
610             return "";
611         }
612         for ( MojoDescriptor descriptor : mojoDescriptors )
613         {
614 
615             String impl = descriptor.getImplementation();
616             if ( StringUtils.equals( descriptor.getGoal(), "help" ) && StringUtils.equals( "HelpMojo", impl ) )
617             {
618                 continue;
619             }
620             if ( impl.lastIndexOf( '.' ) != -1 )
621             {
622                 String name = impl.substring( 0, impl.lastIndexOf( '.' ) );
623                 if ( packageNames.get( name ) != null )
624                 {
625                     int next = ( packageNames.get( name ) ).intValue() + 1;
626                     packageNames.put( name,  Integer.valueOf( next ) );
627                 }
628                 else
629                 {
630                     packageNames.put( name, Integer.valueOf( 1 ) );
631                 }
632             }
633             else
634             {
635                 packageNames.put( "", Integer.valueOf( 1 ) );
636             }
637         }
638 
639         String packageName = "";
640         int max = 0;
641         for ( Map.Entry<String, Integer> entry : packageNames.entrySet() )
642         {
643             int value = entry.getValue().intValue();
644             if ( value > max )
645             {
646                 max = value;
647                 packageName = entry.getKey();
648             }
649         }
650 
651         return packageName;
652     }
653 
654     /**
655      * @param impl a Mojo implementation, not null
656      * @param project a MavenProject instance, could be null
657      * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>,
658      * <code>false</code> otherwise.
659      * @throws IllegalArgumentException if any
660      */
661     @SuppressWarnings( "unchecked" )
662     public static boolean isMavenReport( String impl, MavenProject project )
663         throws IllegalArgumentException
664     {
665         if ( impl == null )
666         {
667             throw new IllegalArgumentException( "mojo implementation should be declared" );
668         }
669 
670         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
671         if ( project != null )
672         {
673             List<String> classPathStrings;
674             try
675             {
676                 classPathStrings = project.getCompileClasspathElements();
677                 if ( project.getExecutionProject() != null )
678                 {
679                     classPathStrings.addAll( project.getExecutionProject().getCompileClasspathElements() );
680                 }
681             }
682             catch ( DependencyResolutionRequiredException e )
683             {
684                 throw new IllegalArgumentException( e );
685             }
686 
687             List<URL> urls = new ArrayList<>( classPathStrings.size() );
688             for ( String classPathString : classPathStrings )
689             {
690                 try
691                 {
692                     urls.add( new File( classPathString ).toURL() );
693                 }
694                 catch ( MalformedURLException e )
695                 {
696                     throw new IllegalArgumentException( e );
697                 }
698             }
699 
700             classLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), classLoader );
701         }
702 
703         try
704         {
705             Class<?> clazz = Class.forName( impl, false, classLoader );
706 
707             return MavenReport.class.isAssignableFrom( clazz );
708         }
709         catch ( ClassNotFoundException e )
710         {
711             return false;
712         }
713     }
714 
715 }