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