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