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