View Javadoc

1   package org.apache.maven.reporting;
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 org.apache.commons.validator.EmailValidator;
23  import org.apache.commons.validator.UrlValidator;
24  
25  import org.apache.maven.doxia.sink.Sink;
26  import org.apache.maven.doxia.util.HtmlTools;
27  
28  import org.codehaus.plexus.util.StringUtils;
29  
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Properties;
36  
37  /**
38   * An abstract class to manage report generation, with many helper methods to ease the job: you just need to
39   * implement getTitle() and renderBody().
40   *
41   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
42   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
43   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
44   * @version $Id: AbstractMavenReportRenderer.java 1164753 2011-09-02 22:50:23Z hboutemy $
45   * @since 2.0
46   * @TODO Later it may be appropriate to create something like a VelocityMavenReportRenderer
47   * that could take a velocity template and pipe that through Doxia rather than coding them
48   * up like this.
49   * @see #getTitle()
50   * @see #renderBody()
51   */
52  public abstract class AbstractMavenReportRenderer
53      implements MavenReportRenderer
54  {
55      /** The current sink to use */
56      protected Sink sink;
57  
58      /** The current section number */
59      private int section;
60  
61      /**
62       * Default constructor.
63       *
64       * @param sink the sink to use.
65       */
66      public AbstractMavenReportRenderer( Sink sink )
67      {
68          this.sink = sink;
69      }
70  
71      /** {@inheritDoc} */
72      public void render()
73      {
74          sink.head();
75  
76          sink.title();
77          text( getTitle() );
78          sink.title_();
79  
80          sink.head_();
81  
82          sink.body();
83          renderBody();
84          sink.body_();
85  
86          sink.flush();
87  
88          sink.close();
89      }
90  
91      // ----------------------------------------------------------------------
92      // Section handler
93      // ----------------------------------------------------------------------
94  
95      /**
96       * Convenience method to wrap section creation in the current sink. An anchor will be add for the name.
97       *
98       * @param name the name of this section, could be null.
99       * @see #text(String)
100      * @see Sink#section1()
101      * @see Sink#sectionTitle1()
102      * @see Sink#sectionTitle1_()
103      * @see Sink#section2()
104      * @see Sink#sectionTitle2()
105      * @see Sink#sectionTitle2_()
106      * @see Sink#section3()
107      * @see Sink#sectionTitle3()
108      * @see Sink#sectionTitle3_()
109      * @see Sink#section4()
110      * @see Sink#sectionTitle4()
111      * @see Sink#sectionTitle4_()
112      * @see Sink#section5()
113      * @see Sink#sectionTitle5()
114      * @see Sink#sectionTitle5_()
115      */
116     protected void startSection( String name )
117     {
118         section = section + 1;
119 
120         switch ( section )
121         {
122             case 1:
123                 sink.section1();
124                 sink.sectionTitle1();
125                 break;
126             case 2:
127                 sink.section2();
128                 sink.sectionTitle2();
129                 break;
130             case 3:
131                 sink.section3();
132                 sink.sectionTitle3();
133                 break;
134             case 4:
135                 sink.section4();
136                 sink.sectionTitle4();
137                 break;
138             case 5:
139                 sink.section5();
140                 sink.sectionTitle5();
141                 break;
142 
143             default:
144                 // TODO: warning - just don't start a section
145                 break;
146         }
147 
148         text( name );
149 
150         switch ( section )
151         {
152             case 1:
153                 sink.sectionTitle1_();
154                 break;
155             case 2:
156                 sink.sectionTitle2_();
157                 break;
158             case 3:
159                 sink.sectionTitle3_();
160                 break;
161             case 4:
162                 sink.sectionTitle4_();
163                 break;
164             case 5:
165                 sink.sectionTitle5_();
166                 break;
167 
168             default:
169                 // TODO: warning - just don't start a section
170                 break;
171         }
172 
173         sink.anchor( HtmlTools.encodeId( name ) );
174         sink.anchor_();
175     }
176 
177     /**
178      * Convenience method to wrap section ending in the current sink.
179      *
180      * @see Sink#section1_()
181      * @see Sink#section2_()
182      * @see Sink#section3_()
183      * @see Sink#section4_()
184      * @see Sink#section5_()
185      * @IllegalStateException if too many closing sections.
186      */
187     protected void endSection()
188     {
189         switch ( section )
190         {
191             case 1:
192                 sink.section1_();
193                 break;
194             case 2:
195                 sink.section2_();
196                 break;
197             case 3:
198                 sink.section3_();
199                 break;
200             case 4:
201                 sink.section4_();
202                 break;
203             case 5:
204                 sink.section5_();
205                 break;
206 
207             default:
208                 // TODO: warning - just don't start a section
209                 break;
210         }
211 
212         section = section - 1;
213 
214         if ( section < 0 )
215         {
216             throw new IllegalStateException( "Too many closing sections" );
217         }
218     }
219 
220     // ----------------------------------------------------------------------
221     // Table handler
222     // ----------------------------------------------------------------------
223 
224     /**
225      * Convenience method to wrap the table start in the current sink.
226      *
227      * @see Sink#table()
228      */
229     protected void startTable()
230     {
231         startTable( new int[] {Sink.JUSTIFY_LEFT}, false );
232     }
233 
234     /**
235      * Convenience method to wrap the table start in the current sink.
236      *
237      * @param justification the justification of table cells.
238      * @param grid whether to draw a grid around cells.
239      *
240      * @see Sink#table()
241      * @see Sink#tableRows(int[],boolean)
242      * @since 2.1
243      */
244     protected void startTable( int[] justification, boolean grid )
245     {
246         sink.table();
247         sink.tableRows( justification, grid );
248     }
249 
250     /**
251      * Convenience method to wrap the table ending in the current sink.
252      *
253      * @see Sink#table_()
254      */
255     protected void endTable()
256     {
257         sink.tableRows_();
258         sink.table_();
259     }
260 
261     /**
262      * Convenience method to wrap the table header cell start in the current sink.
263      *
264      * @param text the text to put in this cell, could be null.
265      * @see #text(String)
266      * @see Sink#tableHeaderCell()
267      * @see Sink#tableHeaderCell_()
268      */
269     protected void tableHeaderCell( String text )
270     {
271         sink.tableHeaderCell();
272 
273         text( text );
274 
275         sink.tableHeaderCell_();
276     }
277 
278     /**
279      * Convenience method to wrap a table cell start in the current sink.
280      * <p>The text could be a link patterned text defined by <code>{text, url}</code></p>
281      *
282      * @param text the text to put in this cell, could be null.
283      * @see #linkPatternedText(String)
284      * @see #tableCell(String)
285      */
286     protected void tableCell( String text )
287     {
288         tableCell( text, false );
289     }
290 
291     /**
292      * Convenience method to wrap a table cell start in the current sink.
293      * <p>The text could be a link patterned text defined by <code>{text, url}</code></p>
294      * <p>If <code>asHtml</code> is true, add the text as Html</p>
295      *
296      * @param text the text to put in this cell, could be null.
297      * @param asHtml <tt>true</tt> to add the text as Html, <tt>false</tt> otherwise.
298      * @see #linkPatternedText(String)
299      * @see Sink#tableCell()
300      * @see Sink#tableCell_()
301      * @see Sink#rawText(String)
302      */
303     protected void tableCell( String text, boolean asHtml )
304     {
305         sink.tableCell();
306 
307         if ( asHtml )
308         {
309             sink.rawText( text );
310         }
311         else
312         {
313             linkPatternedText( text );
314         }
315 
316         sink.tableCell_();
317     }
318 
319     /**
320      * Convenience method to wrap a table row start in the current sink.
321      * <p>The texts in the <code>content</code> could be link patterned texts defined by <code>{text, url}</code></p>
322      *
323      * @param content an array of text to put in the cells in this row, could be null.
324      * @see #tableCell(String)
325      * @see Sink#tableRow()
326      * @see Sink#tableRow_()
327      */
328     protected void tableRow( String[] content )
329     {
330         sink.tableRow();
331 
332         if ( content != null )
333         {
334             for ( int i = 0; i < content.length; i++ )
335             {
336                 tableCell( content[i] );
337             }
338         }
339 
340         sink.tableRow_();
341     }
342 
343     /**
344      * Convenience method to wrap a table header row start in the current sink.
345      * <p>The texts in the <code>content</code> could be link patterned texts defined by <code>{text, url}</code></p>
346      *
347      * @param content an array of text to put in the cells in this row header, could be null.
348      * @see #tableHeaderCell(String)
349      * @see Sink#tableRow()
350      * @see Sink#tableRow_()
351      */
352     protected void tableHeader( String[] content )
353     {
354         sink.tableRow();
355 
356         if ( content != null )
357         {
358             for ( int i = 0; i < content.length; i++ )
359             {
360                 tableHeaderCell( content[i] );
361             }
362         }
363 
364         sink.tableRow_();
365     }
366 
367     /**
368      * Convenience method to wrap a table caption in the current sink.
369      *
370      * @param caption the caption of the table, could be null.
371      * @see #text(String)
372      * @see Sink#tableCaption()
373      * @see Sink#tableCaption_()
374      */
375     protected void tableCaption( String caption )
376     {
377         sink.tableCaption();
378 
379         text( caption );
380 
381         sink.tableCaption_();
382     }
383 
384     // ----------------------------------------------------------------------
385     // Paragraph handler
386     // ----------------------------------------------------------------------
387 
388     /**
389      * Convenience method to wrap a paragraph in the current sink.
390      *
391      * @param paragraph the paragraph to add, could be null.
392      * @see #text(String)
393      * @see Sink#paragraph()
394      * @see Sink#paragraph_()
395      */
396     protected void paragraph( String paragraph )
397     {
398         sink.paragraph();
399 
400         text( paragraph );
401 
402         sink.paragraph_();
403     }
404 
405     /**
406      * Convenience method to wrap a link in the current sink.
407      *
408      * @param href the link to add, cannot be null.
409      * @param name the link name.
410      * @see #text(String)
411      * @see Sink#link(String)
412      * @see Sink#link_()
413      */
414     protected void link( String href, String name )
415     {
416         sink.link( href );
417 
418         text( name );
419 
420         sink.link_();
421     }
422 
423     /**
424      * Convenience method to wrap a text in the current sink.
425      * <p>If text is empty or has a <code>null</code> value, add the <code>"-"</code> charater</p>
426      *
427      * @param text a text, could be null.
428      * @see Sink#text(String)
429      */
430     protected void text( String text )
431     {
432         if ( StringUtils.isEmpty( text ) ) // Take care of spaces
433         {
434             sink.text( "-" );
435         }
436         else
437         {
438             sink.text( text );
439         }
440     }
441 
442     /**
443      * Convenience method to wrap a text as verbatim style in the current sink .
444      *
445      * @param text a text, could be null.
446      * @see #text(String)
447      * @see Sink#verbatim(boolean)
448      * @see Sink#verbatim_()
449      */
450     protected void verbatimText( String text )
451     {
452         sink.verbatim( true );
453 
454         text( text );
455 
456         sink.verbatim_();
457     }
458 
459     /**
460      * Convenience method to wrap a text with a given link href as verbatim style in the current sink.
461      *
462      * @param text a string
463      * @param href an href could be null
464      * @see #link(String, String)
465      * @see #verbatimText(String)
466      * @see Sink#verbatim(boolean)
467      * @see Sink#verbatim_()
468      */
469     protected void verbatimLink( String text, String href )
470     {
471         if ( StringUtils.isEmpty( href ) )
472         {
473             verbatimText( text );
474         }
475         else
476         {
477             sink.verbatim( true );
478 
479             link( href, text );
480 
481             sink.verbatim_();
482         }
483     }
484 
485     /**
486      * Convenience method to add a Javascript code in the current sink.
487      *
488      * @param jsCode a string of Javascript
489      * @see Sink#rawText(String)
490      */
491     protected void javaScript( String jsCode )
492     {
493         sink.rawText( "<script type=\"text/javascript\">\n" + jsCode + "</script>" );
494     }
495 
496     /**
497      * Convenience method to wrap a patterned text in the current link.
498      * <p>The text variable should contained this given pattern <code>{text, url}</code>
499      * to handle the link creation.</p>
500      *
501      * @param text a text with link pattern defined.
502      * @see #text(String)
503      * @see #link(String, String)
504      * @see #applyPattern(String)
505      */
506     public void linkPatternedText( String text )
507     {
508         if ( StringUtils.isEmpty( text ) )
509         {
510             text( text );
511         }
512         else
513         {
514             List<String> segments = applyPattern( text );
515 
516             if ( segments == null )
517             {
518                 text( text );
519             }
520             else
521             {
522                 for ( Iterator<String> it = segments.iterator(); it.hasNext(); )
523                 {
524                     String name = it.next();
525                     String href = it.next();
526 
527                     if ( href == null )
528                     {
529                         text( name );
530                     }
531                     else
532                     {
533                         if ( getValidHref( href ) != null )
534                         {
535                             link( getValidHref( href ), name );
536                         }
537                         else
538                         {
539                             text( href );
540                         }
541                     }
542                 }
543             }
544         }
545     }
546 
547     /**
548      * Create a link pattern text defined by <code>{text, url}</code>.
549      * <p>This created pattern could be used by the method <code>linkPatternedText(String)</code> to
550      * handle a text with link.</p>
551      *
552      * @param text
553      * @param href
554      * @return a link pattern
555      * @see #linkPatternedText(String)
556      */
557     protected static String createLinkPatternedText( String text, String href )
558     {
559         if ( text == null )
560         {
561             return text;
562         }
563 
564         if ( href == null )
565         {
566             return text;
567         }
568 
569         return '{' + text + ", " + href + '}';
570     }
571 
572     /**
573      * Convenience method to display a <code>Properties</code> object as comma separated String.
574      *
575      * @param props the properties to display.
576      * @return the properties object as comma separated String
577      */
578     protected static String propertiesToString( Properties props )
579     {
580         if ( props == null || props.isEmpty() )
581         {
582             return "";
583         }
584 
585         StringBuilder sb = new StringBuilder();
586 
587         for ( Map.Entry<?, ?> entry : props.entrySet() )
588         {
589             if ( sb.length() > 0 )
590             {
591                 sb.append( ", " );
592             }
593 
594             sb.append( entry.getKey() ).append( "=" ).append( entry.getValue() );
595         }
596 
597         return sb.toString();
598     }
599 
600     // ----------------------------------------------------------------------
601     // Private methods
602     // ----------------------------------------------------------------------
603 
604     /**
605      * Return a valid href.
606      * <p>A valid href could start by <code>mailto:</code>.</p>
607      * <p>For a relative path, the href should start by <code>./</code> to be valid.</p>
608      *
609      * @param href an href, could be null.
610      * @return a valid href or <code>null</code> if the href is null or not valid.
611      */
612     private static String getValidHref( String href )
613     {
614         if ( StringUtils.isEmpty( href ) )
615         {
616             return null;
617         }
618 
619         href = href.trim();
620 
621         String[] schemes = {"http", "https"};
622         UrlValidator urlValidator = new UrlValidator( schemes );
623         EmailValidator emailValidator = EmailValidator.getInstance();
624 
625         if ( emailValidator.isValid( href )
626             || ( href.contains( "?" ) && emailValidator.isValid( href.substring( 0, href.indexOf( "?" ) ) ) ) )
627         {
628             return "mailto:" + href;
629         }
630         else if ( href.toLowerCase().startsWith( "mailto:" ) )
631         {
632             return href;
633         }
634         else if ( urlValidator.isValid( href ) )
635         {
636             return href;
637         }
638         else
639         {
640             String hrefTmp;
641             if ( !href.endsWith( "/" ) )
642             {
643                 hrefTmp = href + "/index.html";
644             }
645             else
646             {
647                 hrefTmp = href + "index.html";
648             }
649 
650             if ( urlValidator.isValid( hrefTmp ) )
651             {
652                 return href;
653             }
654 
655             if ( href.startsWith( "./" ) )
656             {
657                 if ( href.length() > 2 )
658                 {
659                     return href.substring( 2, href.length() );
660                 }
661 
662                 return ".";
663             }
664 
665             return null;
666         }
667     }
668 
669     /**
670      * The method parses a text and applies the given pattern <code>{text, url}</code> to create
671      * a list of text/href.
672      *
673      * @param text a text with or without the pattern <code>{text, url}</code>
674      * @return a map of text/href
675      */
676     private static List<String> applyPattern( String text )
677     {
678         if ( StringUtils.isEmpty( text ) )
679         {
680             return null;
681         }
682 
683         // Map defined by key/value name/href
684         // If href == null, it means
685         List<String> segments = new ArrayList<String>();
686 
687         // TODO Special case http://jira.codehaus.org/browse/MEV-40
688         if ( text.indexOf( "${" ) != -1 )
689         {
690             int lastComma = text.lastIndexOf( "," );
691             int lastSemi = text.lastIndexOf( "}" );
692             if ( lastComma != -1 && lastSemi != -1 && lastComma < lastSemi )
693             {
694                 segments.add( text.substring( lastComma + 1, lastSemi ).trim() );
695                 segments.add( null );
696             }
697             else
698             {
699                 segments.add( text );
700                 segments.add( null );
701             }
702 
703             return segments;
704         }
705 
706         boolean inQuote = false;
707         int braceStack = 0;
708         int lastOffset = 0;
709 
710         for ( int i = 0; i < text.length(); i++ )
711         {
712             char ch = text.charAt( i );
713 
714             if ( ch == '\'' && !inQuote && braceStack == 0 )
715             {
716                 // handle: ''
717                 if ( i + 1 < text.length() && text.charAt( i + 1 ) == '\'' )
718                 {
719                     i++;
720                     segments.add( text.substring( lastOffset, i ) );
721                     segments.add( null );
722                     lastOffset = i + 1;
723                 }
724                 else
725                 {
726                     inQuote = true;
727                 }
728             }
729             else
730             {
731                 switch ( ch )
732                 {
733                     case '{':
734                         if ( !inQuote )
735                         {
736                             if ( braceStack == 0 )
737                             {
738                                 if ( i != 0 ) // handle { at first character
739                                 {
740                                     segments.add( text.substring( lastOffset, i ) );
741                                     segments.add( null );
742                                 }
743                                 lastOffset = i + 1;
744                             }
745                             braceStack++;
746                         }
747                         break;
748                     case '}':
749                         if ( !inQuote )
750                         {
751                             braceStack--;
752                             if ( braceStack == 0 )
753                             {
754                                 String subString = text.substring( lastOffset, i );
755                                 lastOffset = i + 1;
756 
757                                 int lastComma = subString.lastIndexOf( "," );
758                                 if ( lastComma != -1 )
759                                 {
760                                     segments.add( subString.substring( 0, lastComma ).trim() );
761                                     segments.add( subString.substring( lastComma + 1 ).trim() );
762                                 }
763                                 else
764                                 {
765                                     segments.add( subString );
766                                     segments.add( null );
767                                 }
768                             }
769                         }
770                         break;
771                     case '\'':
772                         inQuote = false;
773                         break;
774                     default:
775                         break;
776                 }
777             }
778         }
779 
780         if ( !StringUtils.isEmpty( text.substring( lastOffset ) ) )
781         {
782             segments.add( text.substring( lastOffset ) );
783             segments.add( null );
784         }
785 
786         if ( braceStack != 0 )
787         {
788             throw new IllegalArgumentException( "Unmatched braces in the pattern." );
789         }
790 
791         if ( inQuote )
792         {
793             //throw new IllegalArgumentException( "Unmatched quote in the pattern." );
794             //TODO: warning...
795         }
796 
797         return Collections.unmodifiableList( segments );
798     }
799 
800     // ----------------------------------------------------------------------
801     // Abstract methods
802     // ----------------------------------------------------------------------
803 
804     /** {@inheritDoc} */
805     public abstract String getTitle();
806 
807     /**
808      * Renderer the body content of the report.
809      */
810     protected abstract void renderBody();
811 }