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.maven.doxia.sink.Sink;
23  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
24  import org.apache.maven.doxia.util.HtmlTools;
25  
26  import org.apache.maven.shared.utils.StringUtils;
27  
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Properties;
34  
35  /**
36   * <p>An abstract class to manage report generation, with many helper methods to ease the job: you just need to
37   * implement getTitle() and renderBody().</p>
38   *
39   * <p><strong>TODO</strong> Later it may be appropriate to create something like a VelocityMavenReportRenderer
40   * that could take a velocity template and pipe that through Doxia rather than coding them
41   * up like this.</p>
42   *
43   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
44   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
45   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
46   * @since 2.0
47   * @see #getTitle()
48   * @see #renderBody()
49   */
50  public abstract class AbstractMavenReportRenderer
51      implements MavenReportRenderer
52  {
53      /** The current sink to use */
54      protected Sink sink;
55  
56      /** The current section number */
57      private int section;
58  
59      /**
60       * Default constructor.
61       *
62       * @param sink the sink to use.
63       */
64      public AbstractMavenReportRenderer( Sink sink )
65      {
66          this.sink = sink;
67      }
68  
69      /** {@inheritDoc} */
70      @Override
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      * @throws 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 {@code true} to add the text as Html, {@code false} 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      *
345      * @param content an array of text to put in the cells in this row header, could be null.
346      * @see #tableHeaderCell(String)
347      * @see Sink#tableRow()
348      * @see Sink#tableRow_()
349      */
350     protected void tableHeader( String[] content )
351     {
352         sink.tableRow();
353 
354         if ( content != null )
355         {
356             for ( int i = 0; i < content.length; i++ )
357             {
358                 tableHeaderCell( content[i] );
359             }
360         }
361 
362         sink.tableRow_();
363     }
364 
365     /**
366      * Convenience method to wrap a table caption in the current sink.
367      *
368      * @param caption the caption of the table, could be null.
369      * @see #text(String)
370      * @see Sink#tableCaption()
371      * @see Sink#tableCaption_()
372      */
373     protected void tableCaption( String caption )
374     {
375         sink.tableCaption();
376 
377         text( caption );
378 
379         sink.tableCaption_();
380     }
381 
382     // ----------------------------------------------------------------------
383     // Paragraph handler
384     // ----------------------------------------------------------------------
385 
386     /**
387      * Convenience method to wrap a paragraph in the current sink.
388      *
389      * @param paragraph the paragraph to add, could be null.
390      * @see #text(String)
391      * @see Sink#paragraph()
392      * @see Sink#paragraph_()
393      */
394     protected void paragraph( String paragraph )
395     {
396         sink.paragraph();
397 
398         text( paragraph );
399 
400         sink.paragraph_();
401     }
402 
403     /**
404      * Convenience method to wrap a link in the current sink.
405      *
406      * @param href the link to add, cannot be null.
407      * @param name the link name.
408      * @see #text(String)
409      * @see Sink#link(String)
410      * @see Sink#link_()
411      */
412     protected void link( String href, String name )
413     {
414         sink.link( href );
415 
416         text( name );
417 
418         sink.link_();
419     }
420 
421     /**
422      * Convenience method to wrap a text in the current sink.
423      * <p>If text is empty or has a <code>null</code> value, add the <code>"-"</code> charater</p>
424      *
425      * @param text a text, could be null.
426      * @see Sink#text(String)
427      */
428     protected void text( String text )
429     {
430         if ( StringUtils.isEmpty( text ) ) // Take care of spaces
431         {
432             sink.text( "-" );
433         }
434         else
435         {
436             sink.text( text );
437         }
438     }
439 
440     /**
441      * Convenience method to wrap a text as verbatim style in the current sink .
442      *
443      * @param text a text, could be null.
444      * @see #text(String)
445      * @see Sink#verbatim(boolean)
446      * @see Sink#verbatim_()
447      */
448     protected void verbatimText( String text )
449     {
450         sink.verbatim( SinkEventAttributeSet.BOXED );
451 
452         text( text );
453 
454         sink.verbatim_();
455     }
456 
457     /**
458      * Convenience method to wrap a text with a given link href as verbatim style in the current sink.
459      *
460      * @param text a string
461      * @param href an href could be null
462      * @see #link(String, String)
463      * @see #verbatimText(String)
464      * @see Sink#verbatim(boolean)
465      * @see Sink#verbatim_()
466      */
467     protected void verbatimLink( String text, String href )
468     {
469         if ( StringUtils.isEmpty( href ) )
470         {
471             verbatimText( text );
472         }
473         else
474         {
475             sink.verbatim( SinkEventAttributeSet.BOXED );
476 
477             link( href, text );
478 
479             sink.verbatim_();
480         }
481     }
482 
483     /**
484      * Convenience method to add a Javascript code in the current sink.
485      *
486      * @param jsCode a string of Javascript
487      * @see Sink#rawText(String)
488      */
489     protected void javaScript( String jsCode )
490     {
491         sink.rawText( "<script type=\"text/javascript\">\n" + jsCode + "</script>" );
492     }
493 
494     /**
495      * Convenience method to wrap a patterned text in the current link.
496      * <p>The text variable should contained this given pattern <code>{text, url}</code>
497      * to handle the link creation.</p>
498      *
499      * @param text a text with link pattern defined.
500      * @see #text(String)
501      * @see #link(String, String)
502      * @see #applyPattern(String)
503      */
504     public void linkPatternedText( String text )
505     {
506         if ( StringUtils.isEmpty( text ) )
507         {
508             text( text );
509         }
510         else
511         {
512             List<String> segments = applyPattern( text );
513 
514             if ( segments == null )
515             {
516                 text( text );
517             }
518             else
519             {
520                 for ( Iterator<String> it = segments.iterator(); it.hasNext(); )
521                 {
522                     String name = it.next();
523                     String href = it.next();
524 
525                     if ( href == null )
526                     {
527                         text( name );
528                     }
529                     else
530                     {
531                        link( href, name );
532                     }
533                 }
534             }
535         }
536     }
537 
538     /**
539      * Create a link pattern text defined by <code>{text, url}</code>.
540      * <p>This created pattern could be used by the method <code>linkPatternedText(String)</code> to
541      * handle a text with link.</p>
542      *
543      * @param text
544      * @param href
545      * @return a link pattern
546      * @see #linkPatternedText(String)
547      */
548     protected static String createLinkPatternedText( String text, String href )
549     {
550         if ( text == null )
551         {
552             return text;
553         }
554 
555         if ( href == null )
556         {
557             return text;
558         }
559 
560         return '{' + text + ", " + href + '}';
561     }
562 
563     /**
564      * Convenience method to display a <code>Properties</code> object as comma separated String.
565      *
566      * @param props the properties to display.
567      * @return the properties object as comma separated String
568      */
569     protected static String propertiesToString( Properties props )
570     {
571         if ( props == null || props.isEmpty() )
572         {
573             return "";
574         }
575 
576         StringBuilder sb = new StringBuilder();
577 
578         for ( Map.Entry<?, ?> entry : props.entrySet() )
579         {
580             if ( sb.length() > 0 )
581             {
582                 sb.append( ", " );
583             }
584 
585             sb.append( entry.getKey() ).append( "=" ).append( entry.getValue() );
586         }
587 
588         return sb.toString();
589     }
590 
591     // ----------------------------------------------------------------------
592     // Private methods
593     // ----------------------------------------------------------------------
594 
595     /**
596      * The method parses a text and applies the given pattern <code>{text, url}</code> to create
597      * a list of text/href.
598      *
599      * @param text a text with or without the pattern <code>{text, url}</code>
600      * @return a map of text/href
601      */
602     private static List<String> applyPattern( String text )
603     {
604         if ( StringUtils.isEmpty( text ) )
605         {
606             return null;
607         }
608 
609         // Map defined by key/value name/href
610         // If href == null, it means
611         List<String> segments = new ArrayList<>();
612 
613         // TODO Special case http://jira.codehaus.org/browse/MEV-40
614         if ( text.indexOf( "${" ) != -1 )
615         {
616             int lastComma = text.lastIndexOf( "," );
617             int lastSemi = text.lastIndexOf( "}" );
618             if ( lastComma != -1 && lastSemi != -1 && lastComma < lastSemi )
619             {
620                 segments.add( text.substring( lastComma + 1, lastSemi ).trim() );
621                 segments.add( null );
622             }
623             else
624             {
625                 segments.add( text );
626                 segments.add( null );
627             }
628 
629             return segments;
630         }
631 
632         boolean inQuote = false;
633         int braceStack = 0;
634         int lastOffset = 0;
635 
636         for ( int i = 0; i < text.length(); i++ )
637         {
638             char ch = text.charAt( i );
639 
640             if ( ch == '\'' && !inQuote && braceStack == 0 )
641             {
642                 // handle: ''
643                 if ( i + 1 < text.length() && text.charAt( i + 1 ) == '\'' )
644                 {
645                     i++;
646                     segments.add( text.substring( lastOffset, i ) );
647                     segments.add( null );
648                     lastOffset = i + 1;
649                 }
650                 else
651                 {
652                     inQuote = true;
653                 }
654             }
655             else
656             {
657                 switch ( ch )
658                 {
659                     case '{':
660                         if ( !inQuote )
661                         {
662                             if ( braceStack == 0 )
663                             {
664                                 if ( i != lastOffset ) // handle { at first character
665                                 {
666                                     segments.add( text.substring( lastOffset, i ) );
667                                     segments.add( null );
668                                 }
669                                 lastOffset = i + 1;
670                             }
671                             braceStack++;
672                         }
673                         break;
674                     case '}':
675                         if ( !inQuote )
676                         {
677                             braceStack--;
678                             if ( braceStack == 0 )
679                             {
680                                 String subString = text.substring( lastOffset, i );
681                                 lastOffset = i + 1;
682 
683                                 int lastComma = subString.lastIndexOf( "," );
684                                 if ( lastComma != -1 )
685                                 {
686                                     segments.add( subString.substring( 0, lastComma ).trim() );
687                                     segments.add( subString.substring( lastComma + 1 ).trim() );
688                                 }
689                                 else
690                                 {
691                                     segments.add( subString );
692                                     segments.add( null );
693                                 }
694                             }
695                         }
696                         break;
697                     case '\'':
698                         inQuote = false;
699                         break;
700                     default:
701                         break;
702                 }
703             }
704         }
705 
706         if ( !StringUtils.isEmpty( text.substring( lastOffset ) ) )
707         {
708             segments.add( text.substring( lastOffset ) );
709             segments.add( null );
710         }
711 
712         if ( braceStack != 0 )
713         {
714             throw new IllegalArgumentException( "Unmatched braces in the pattern." );
715         }
716 
717         if ( inQuote )
718         {
719             //throw new IllegalArgumentException( "Unmatched quote in the pattern." );
720             //TODO: warning...
721         }
722 
723         return Collections.unmodifiableList( segments );
724     }
725 
726     // ----------------------------------------------------------------------
727     // Abstract methods
728     // ----------------------------------------------------------------------
729 
730     @Override
731     public abstract String getTitle();
732 
733     /**
734      * Renderer the body content of the report.
735      */
736     protected abstract void renderBody();
737 }