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. 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 * @throws 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 {@code true} to add the text as Html, {@code false} 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 *
345 * @param content an array of text to put in the cells in this row header, could be null.
346 * @see #tableHeaderCell(String)
347 * @see Sink#tableRow()
348 * @see Sink#tableRow_()
349 */
350 protected void tableHeader( String[] content )
351 {
352 sink.tableRow();
353
354 if ( content != null )
355 {
356 for ( int i = 0; i < content.length; i++ )
357 {
358 tableHeaderCell( content[i] );
359 }
360 }
361
362 sink.tableRow_();
363 }
364
365 /**
366 * Convenience method to wrap a table caption in the current sink.
367 *
368 * @param caption the caption of the table, could be null.
369 * @see #text(String)
370 * @see Sink#tableCaption()
371 * @see Sink#tableCaption_()
372 */
373 protected void tableCaption( String caption )
374 {
375 sink.tableCaption();
376
377 text( caption );
378
379 sink.tableCaption_();
380 }
381
382 // ----------------------------------------------------------------------
383 // Paragraph handler
384 // ----------------------------------------------------------------------
385
386 /**
387 * Convenience method to wrap a paragraph in the current sink.
388 *
389 * @param paragraph the paragraph to add, could be null.
390 * @see #text(String)
391 * @see Sink#paragraph()
392 * @see Sink#paragraph_()
393 */
394 protected void paragraph( String paragraph )
395 {
396 sink.paragraph();
397
398 text( paragraph );
399
400 sink.paragraph_();
401 }
402
403 /**
404 * Convenience method to wrap a link in the current sink.
405 *
406 * @param href the link to add, cannot be null.
407 * @param name the link name.
408 * @see #text(String)
409 * @see Sink#link(String)
410 * @see Sink#link_()
411 */
412 protected void link( String href, String name )
413 {
414 sink.link( href );
415
416 text( name );
417
418 sink.link_();
419 }
420
421 /**
422 * Convenience method to wrap a text in the current sink.
423 * <p>If text is empty or has a <code>null</code> value, add the <code>"-"</code> charater</p>
424 *
425 * @param text a text, could be null.
426 * @see Sink#text(String)
427 */
428 protected void text( String text )
429 {
430 if ( StringUtils.isEmpty( text ) ) // Take care of spaces
431 {
432 sink.text( "-" );
433 }
434 else
435 {
436 sink.text( text );
437 }
438 }
439
440 /**
441 * Convenience method to wrap a text as verbatim style in the current sink .
442 *
443 * @param text a text, could be null.
444 * @see #text(String)
445 * @see Sink#verbatim(boolean)
446 * @see Sink#verbatim_()
447 */
448 protected void verbatimText( String text )
449 {
450 sink.verbatim( SinkEventAttributeSet.BOXED );
451
452 text( text );
453
454 sink.verbatim_();
455 }
456
457 /**
458 * Convenience method to wrap a text with a given link href as verbatim style in the current sink.
459 *
460 * @param text a string
461 * @param href an href could be null
462 * @see #link(String, String)
463 * @see #verbatimText(String)
464 * @see Sink#verbatim(boolean)
465 * @see Sink#verbatim_()
466 */
467 protected void verbatimLink( String text, String href )
468 {
469 if ( StringUtils.isEmpty( href ) )
470 {
471 verbatimText( text );
472 }
473 else
474 {
475 sink.verbatim( SinkEventAttributeSet.BOXED );
476
477 link( href, text );
478
479 sink.verbatim_();
480 }
481 }
482
483 /**
484 * Convenience method to add a Javascript code in the current sink.
485 *
486 * @param jsCode a string of Javascript
487 * @see Sink#rawText(String)
488 */
489 protected void javaScript( String jsCode )
490 {
491 sink.rawText( "<script type=\"text/javascript\">\n" + jsCode + "</script>" );
492 }
493
494 /**
495 * Convenience method to wrap a patterned text in the current link.
496 * <p>The text variable should contained this given pattern <code>{text, url}</code>
497 * to handle the link creation.</p>
498 *
499 * @param text a text with link pattern defined.
500 * @see #text(String)
501 * @see #link(String, String)
502 * @see #applyPattern(String)
503 */
504 public void linkPatternedText( String text )
505 {
506 if ( StringUtils.isEmpty( text ) )
507 {
508 text( text );
509 }
510 else
511 {
512 List<String> segments = applyPattern( text );
513
514 if ( segments == null )
515 {
516 text( text );
517 }
518 else
519 {
520 for ( Iterator<String> it = segments.iterator(); it.hasNext(); )
521 {
522 String name = it.next();
523 String href = it.next();
524
525 if ( href == null )
526 {
527 text( name );
528 }
529 else
530 {
531 link( href, name );
532 }
533 }
534 }
535 }
536 }
537
538 /**
539 * Create a link pattern text defined by <code>{text, url}</code>.
540 * <p>This created pattern could be used by the method <code>linkPatternedText(String)</code> to
541 * handle a text with link.</p>
542 *
543 * @param text
544 * @param href
545 * @return a link pattern
546 * @see #linkPatternedText(String)
547 */
548 protected static String createLinkPatternedText( String text, String href )
549 {
550 if ( text == null )
551 {
552 return text;
553 }
554
555 if ( href == null )
556 {
557 return text;
558 }
559
560 return '{' + text + ", " + href + '}';
561 }
562
563 /**
564 * Convenience method to display a <code>Properties</code> object as comma separated String.
565 *
566 * @param props the properties to display.
567 * @return the properties object as comma separated String
568 */
569 protected static String propertiesToString( Properties props )
570 {
571 if ( props == null || props.isEmpty() )
572 {
573 return "";
574 }
575
576 StringBuilder sb = new StringBuilder();
577
578 for ( Map.Entry<?, ?> entry : props.entrySet() )
579 {
580 if ( sb.length() > 0 )
581 {
582 sb.append( ", " );
583 }
584
585 sb.append( entry.getKey() ).append( "=" ).append( entry.getValue() );
586 }
587
588 return sb.toString();
589 }
590
591 // ----------------------------------------------------------------------
592 // Private methods
593 // ----------------------------------------------------------------------
594
595 /**
596 * The method parses a text and applies the given pattern <code>{text, url}</code> to create
597 * a list of text/href.
598 *
599 * @param text a text with or without the pattern <code>{text, url}</code>
600 * @return a map of text/href
601 */
602 private static List<String> applyPattern( String text )
603 {
604 if ( StringUtils.isEmpty( text ) )
605 {
606 return null;
607 }
608
609 // Map defined by key/value name/href
610 // If href == null, it means
611 List<String> segments = new ArrayList<>();
612
613 // TODO Special case http://jira.codehaus.org/browse/MEV-40
614 if ( text.indexOf( "${" ) != -1 )
615 {
616 int lastComma = text.lastIndexOf( "," );
617 int lastSemi = text.lastIndexOf( "}" );
618 if ( lastComma != -1 && lastSemi != -1 && lastComma < lastSemi )
619 {
620 segments.add( text.substring( lastComma + 1, lastSemi ).trim() );
621 segments.add( null );
622 }
623 else
624 {
625 segments.add( text );
626 segments.add( null );
627 }
628
629 return segments;
630 }
631
632 boolean inQuote = false;
633 int braceStack = 0;
634 int lastOffset = 0;
635
636 for ( int i = 0; i < text.length(); i++ )
637 {
638 char ch = text.charAt( i );
639
640 if ( ch == '\'' && !inQuote && braceStack == 0 )
641 {
642 // handle: ''
643 if ( i + 1 < text.length() && text.charAt( i + 1 ) == '\'' )
644 {
645 i++;
646 segments.add( text.substring( lastOffset, i ) );
647 segments.add( null );
648 lastOffset = i + 1;
649 }
650 else
651 {
652 inQuote = true;
653 }
654 }
655 else
656 {
657 switch ( ch )
658 {
659 case '{':
660 if ( !inQuote )
661 {
662 if ( braceStack == 0 )
663 {
664 if ( i != lastOffset ) // handle { at first character
665 {
666 segments.add( text.substring( lastOffset, i ) );
667 segments.add( null );
668 }
669 lastOffset = i + 1;
670 }
671 braceStack++;
672 }
673 break;
674 case '}':
675 if ( !inQuote )
676 {
677 braceStack--;
678 if ( braceStack == 0 )
679 {
680 String subString = text.substring( lastOffset, i );
681 lastOffset = i + 1;
682
683 int lastComma = subString.lastIndexOf( "," );
684 if ( lastComma != -1 )
685 {
686 segments.add( subString.substring( 0, lastComma ).trim() );
687 segments.add( subString.substring( lastComma + 1 ).trim() );
688 }
689 else
690 {
691 segments.add( subString );
692 segments.add( null );
693 }
694 }
695 }
696 break;
697 case '\'':
698 inQuote = false;
699 break;
700 default:
701 break;
702 }
703 }
704 }
705
706 if ( !StringUtils.isEmpty( text.substring( lastOffset ) ) )
707 {
708 segments.add( text.substring( lastOffset ) );
709 segments.add( null );
710 }
711
712 if ( braceStack != 0 )
713 {
714 throw new IllegalArgumentException( "Unmatched braces in the pattern." );
715 }
716
717 if ( inQuote )
718 {
719 //throw new IllegalArgumentException( "Unmatched quote in the pattern." );
720 //TODO: warning...
721 }
722
723 return Collections.unmodifiableList( segments );
724 }
725
726 // ----------------------------------------------------------------------
727 // Abstract methods
728 // ----------------------------------------------------------------------
729
730 @Override
731 public abstract String getTitle();
732
733 /**
734 * Renderer the body content of the report.
735 */
736 protected abstract void renderBody();
737 }