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