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