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