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