1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.reporting;
20
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Properties;
27
28 import org.apache.maven.doxia.markup.Markup;
29 import org.apache.maven.doxia.sink.Sink;
30 import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
31 import org.apache.maven.shared.utils.StringUtils;
32
33 /**
34 * <p>An abstract class to manage report generation, with many helper methods to ease the job: you just need to
35 * implement getTitle() and renderBody().</p>
36 *
37 * <p><strong>TODO</strong> Later it may be appropriate to create something like a VelocityMavenReportRenderer
38 * that could take a velocity template and pipe that through Doxia rather than coding them
39 * up like this.</p>
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 * @since 2.0
45 * @see #getTitle()
46 * @see #renderBody()
47 */
48 public abstract class AbstractMavenReportRenderer implements MavenReportRenderer {
49 /** The current sink to use */
50 protected Sink sink;
51
52 /** The current section number */
53 private int section;
54
55 /**
56 * Default constructor.
57 *
58 * @param sink the sink to use.
59 */
60 public AbstractMavenReportRenderer(Sink sink) {
61 this.sink = sink;
62 }
63
64 /** {@inheritDoc} */
65 @Override
66 public void render() {
67 sink.head();
68
69 sink.title();
70 text(getTitle());
71 sink.title_();
72
73 sink.head_();
74
75 sink.body();
76 renderBody();
77 sink.body_();
78
79 sink.flush();
80
81 sink.close();
82 }
83
84 // ----------------------------------------------------------------------
85 // Section handler
86 // ----------------------------------------------------------------------
87
88 /**
89 * Convenience method to wrap section creation in the current sink.
90 * An anchor will be derived from the name.
91 *
92 * @param name the name of this section, could be null.
93 * @see #text(String)
94 * @see Sink#section(int, org.apache.maven.doxia.sink.SinkEventAttributes)
95 * @see Sink#sectionTitle(int, org.apache.maven.doxia.sink.SinkEventAttributes)
96 * @see Sink#sectionTitle_(int)
97 */
98 protected void startSection(String name) {
99 startSection(name, name);
100 }
101
102 /**
103 * Convenience method to wrap section creation in the current sink.
104 *
105 * @param name the name of this section, could be null.
106 * @param anchor the anchor of this section, could be null.
107 * @see #text(String)
108 * @see Sink#section(int, org.apache.maven.doxia.sink.SinkEventAttributes)
109 * @see Sink#sectionTitle(int, org.apache.maven.doxia.sink.SinkEventAttributes)
110 * @see Sink#sectionTitle_(int)
111 */
112 protected void startSection(String name, String anchor) {
113 section++;
114 sink.section(section, null);
115 sink.anchor(anchor);
116 sink.anchor_();
117 sink.sectionTitle(section, null);
118 text(name);
119 sink.sectionTitle_(section);
120 }
121
122 /**
123 * Convenience method to wrap section ending in the current sink.
124 *
125 * @see Sink#section_(int)
126 * @throws IllegalStateException if too many closing sections.
127 */
128 protected void endSection() {
129 sink.section_(section);
130 section--;
131
132 if (section < 0) {
133 throw new IllegalStateException("Too many closing sections");
134 }
135 }
136
137 // ----------------------------------------------------------------------
138 // Table handler
139 // ----------------------------------------------------------------------
140
141 /**
142 * Convenience method to wrap the table start in the current sink.
143 *
144 * @see Sink#table()
145 */
146 protected void startTable() {
147 startTable(null, false);
148 }
149
150 /**
151 * Convenience method to wrap the table start in the current sink.
152 *
153 * @param justification the justification of table cells.
154 * @param grid whether to draw a grid around cells.
155 *
156 * @see Sink#table()
157 * @see Sink#tableRows(int[],boolean)
158 * @since 2.1
159 */
160 protected void startTable(int[] justification, boolean grid) {
161 sink.table();
162 sink.tableRows(justification, grid);
163 }
164
165 /**
166 * Convenience method to wrap the table ending in the current sink.
167 *
168 * @see Sink#table_()
169 */
170 protected void endTable() {
171 sink.tableRows_();
172 sink.table_();
173 }
174
175 /**
176 * Convenience method to wrap the table header cell start in the current sink.
177 *
178 * @param text the text to put in this cell, could be null.
179 * @see #text(String)
180 * @see Sink#tableHeaderCell()
181 * @see Sink#tableHeaderCell_()
182 */
183 protected void tableHeaderCell(String text) {
184 sink.tableHeaderCell();
185
186 text(text);
187
188 sink.tableHeaderCell_();
189 }
190
191 /**
192 * Convenience method to wrap a table cell start in the current sink.
193 * <p>The text could be a link patterned text defined by <code>{text, url}</code></p>
194 *
195 * @param text the text to put in this cell, could be null.
196 * @see #linkPatternedText(String)
197 * @see #tableCell(String)
198 */
199 protected void tableCell(String text) {
200 tableCell(text, false);
201 }
202
203 /**
204 * Convenience method to wrap a table cell start in the current sink.
205 * <p>The text could be a link patterned text defined by <code>{text, url}</code></p>
206 * <p>If <code>asHtml</code> is true, add the text as Html</p>
207 *
208 * @param text the text to put in this cell, could be null.
209 * @param asHtml {@code true} to add the text as Html, {@code false} otherwise.
210 * @see #linkPatternedText(String)
211 * @see Sink#tableCell()
212 * @see Sink#tableCell_()
213 * @see Sink#rawText(String)
214 */
215 protected void tableCell(String text, boolean asHtml) {
216 sink.tableCell();
217
218 if (asHtml) {
219 sink.rawText(text);
220 } else {
221 linkPatternedText(text);
222 }
223
224 sink.tableCell_();
225 }
226
227 /**
228 * Convenience method to wrap a table row start in the current sink.
229 * <p>The texts in the <code>content</code> could be link patterned texts defined by <code>{text, url}</code></p>
230 *
231 * @param content an array of text to put in the cells in this row, could be null.
232 * @see #tableCell(String)
233 * @see Sink#tableRow()
234 * @see Sink#tableRow_()
235 */
236 protected void tableRow(String[] content) {
237 sink.tableRow();
238
239 if (content != null) {
240 for (int i = 0; i < content.length; i++) {
241 tableCell(content[i]);
242 }
243 }
244
245 sink.tableRow_();
246 }
247
248 /**
249 * Convenience method to wrap a table header row start in the current sink.
250 *
251 * @param content an array of text to put in the cells in this row header, could be null.
252 * @see #tableHeaderCell(String)
253 * @see Sink#tableRow()
254 * @see Sink#tableRow_()
255 */
256 protected void tableHeader(String[] content) {
257 sink.tableRow();
258
259 if (content != null) {
260 for (int i = 0; i < content.length; i++) {
261 tableHeaderCell(content[i]);
262 }
263 }
264
265 sink.tableRow_();
266 }
267
268 /**
269 * Convenience method to wrap a table caption in the current sink.
270 *
271 * @param caption the caption of the table, could be null.
272 * @see #text(String)
273 * @see Sink#tableCaption()
274 * @see Sink#tableCaption_()
275 */
276 protected void tableCaption(String caption) {
277 sink.tableCaption();
278
279 text(caption);
280
281 sink.tableCaption_();
282 }
283
284 // ----------------------------------------------------------------------
285 // Paragraph handler
286 // ----------------------------------------------------------------------
287
288 /**
289 * Convenience method to wrap a paragraph in the current sink.
290 *
291 * @param paragraph the paragraph to add, could be null.
292 * @see #text(String)
293 * @see Sink#paragraph()
294 * @see Sink#paragraph_()
295 */
296 protected void paragraph(String paragraph) {
297 sink.paragraph();
298
299 text(paragraph);
300
301 sink.paragraph_();
302 }
303
304 /**
305 * Convenience method to wrap a link in the current sink.
306 *
307 * @param href the link to add, cannot be null.
308 * @param name the link name.
309 * @see #text(String)
310 * @see Sink#link(String)
311 * @see Sink#link_()
312 */
313 protected void link(String href, String name) {
314 sink.link(href);
315
316 text(name);
317
318 sink.link_();
319 }
320
321 /**
322 * Convenience method to wrap a text in the current sink.
323 * <p>If text is empty or has a <code>null</code> value, add the <code>"-"</code> character</p>
324 *
325 * @param text a text, could be null.
326 * @see Sink#text(String)
327 */
328 protected void text(String text) {
329 if (text == null || text.isEmpty()) // Take care of spaces
330 {
331 sink.text("-");
332 } else {
333 sink.text(text);
334 }
335 }
336
337 /**
338 * Convenience method to wrap a text as verbatim style in the current sink .
339 *
340 * @param text a text, could be null.
341 * @see #text(String)
342 * @see Sink#verbatim(org.apache.maven.doxia.sink.SinkEventAttributes)
343 * @see Sink#verbatim_()
344 */
345 protected void verbatimText(String text) {
346 sink.verbatim();
347
348 text(text);
349
350 sink.verbatim_();
351 }
352
353 /**
354 * Convenience method to wrap a text with a given link href as verbatim style in the current sink.
355 *
356 * @param text a string
357 * @param href an href could be null
358 * @see #link(String, String)
359 * @see #verbatimText(String)
360 * @see Sink#verbatim(org.apache.maven.doxia.sink.SinkEventAttributes)
361 * @see Sink#verbatim_()
362 */
363 protected void verbatimLink(String text, String href) {
364 if (href == null || href.isEmpty()) {
365 verbatimText(text);
366 } else {
367 sink.verbatim();
368
369 link(href, text);
370
371 sink.verbatim_();
372 }
373 }
374
375 /**
376 * Convenience method to wrap source code as verbatim style in the current sink .
377 *
378 * @param source a source code, could be null.
379 * @see #text(String)
380 * @see Sink#verbatim(org.apache.maven.doxia.sink.SinkEventAttributes)
381 * @see Sink#verbatim_()
382 */
383 protected void verbatimSource(String source) {
384 sink.verbatim(SinkEventAttributeSet.SOURCE);
385
386 text(source);
387
388 sink.verbatim_();
389 }
390
391 /**
392 * Convenience method to add a Javascript code in the current sink.
393 *
394 * @param jsCode a string of Javascript
395 * @see Sink#rawText(String)
396 */
397 protected void javaScript(String jsCode) {
398 sink.rawText(Markup.EOL + "<script>" + Markup.EOL + jsCode + Markup.EOL + "</script>" + Markup.EOL);
399 }
400
401 /**
402 * Convenience method to wrap a patterned text in the current link.
403 * <p>The text variable should contained this given pattern <code>{text, url}</code>
404 * to handle the link creation.</p>
405 *
406 * @param text a text with link pattern defined.
407 * @see #text(String)
408 * @see #link(String, String)
409 * @see #applyPattern(String)
410 */
411 public void linkPatternedText(String text) {
412 if (text == null || text.isEmpty()) {
413 text(text);
414 } else {
415 List<String> segments = applyPattern(text);
416
417 if (segments == null) {
418 text(text);
419 } else {
420 for (Iterator<String> it = segments.iterator(); it.hasNext(); ) {
421 String name = it.next();
422 String href = it.next();
423
424 if (href == null) {
425 text(name);
426 } else {
427 link(href, name);
428 }
429 }
430 }
431 }
432 }
433
434 /**
435 * Create a link pattern text defined by <code>{text, url}</code>.
436 * <p>This created pattern could be used by the method <code>linkPatternedText(String)</code> to
437 * handle a text with link.</p>
438 *
439 * @param text
440 * @param href
441 * @return a link pattern
442 * @see #linkPatternedText(String)
443 */
444 protected static String createLinkPatternedText(String text, String href) {
445 if (text == null) {
446 return text;
447 }
448
449 if (href == null) {
450 return text;
451 }
452
453 return '{' + text + ", " + href + '}';
454 }
455
456 /**
457 * Convenience method to display a <code>Properties</code> object as comma separated String.
458 *
459 * @param props the properties to display.
460 * @return the properties object as comma separated String
461 */
462 protected static String propertiesToString(Properties props) {
463 if (props == null || props.isEmpty()) {
464 return "";
465 }
466
467 StringBuilder sb = new StringBuilder();
468
469 for (Map.Entry<?, ?> entry : props.entrySet()) {
470 if (sb.length() > 0) {
471 sb.append(", ");
472 }
473
474 sb.append(entry.getKey()).append("=").append(entry.getValue());
475 }
476
477 return sb.toString();
478 }
479
480 // ----------------------------------------------------------------------
481 // Private methods
482 // ----------------------------------------------------------------------
483
484 /**
485 * The method parses a text and applies the given pattern <code>{text, url}</code> to create
486 * a list of text/href.
487 *
488 * @param text a text with or without the pattern <code>{text, url}</code>
489 * @return a map of text/href
490 */
491 private static List<String> applyPattern(String text) {
492 if (text == null || text.isEmpty()) {
493 return null;
494 }
495
496 // Map defined by key/value name/href
497 // If href == null, it means
498 List<String> segments = new ArrayList<>();
499
500 // TODO Special case http://jira.codehaus.org/browse/MEV-40
501 if (text.indexOf("${") != -1) {
502 int lastComma = text.lastIndexOf(",");
503 int lastSemi = text.lastIndexOf("}");
504 if (lastComma != -1 && lastSemi != -1 && lastComma < lastSemi) {
505 segments.add(text.substring(lastComma + 1, lastSemi).trim());
506 segments.add(null);
507 } else {
508 segments.add(text);
509 segments.add(null);
510 }
511
512 return segments;
513 }
514
515 boolean inQuote = false;
516 int braceStack = 0;
517 int lastOffset = 0;
518
519 for (int i = 0; i < text.length(); i++) {
520 char ch = text.charAt(i);
521
522 if (ch == '\'' && !inQuote && braceStack == 0) {
523 // handle: ''
524 if (i + 1 < text.length() && text.charAt(i + 1) == '\'') {
525 i++;
526 segments.add(text.substring(lastOffset, i));
527 segments.add(null);
528 lastOffset = i + 1;
529 } else {
530 inQuote = true;
531 }
532 } else {
533 switch (ch) {
534 case '{':
535 if (!inQuote) {
536 if (braceStack == 0) {
537 if (i != lastOffset) // handle { at first character
538 {
539 segments.add(text.substring(lastOffset, i));
540 segments.add(null);
541 }
542 lastOffset = i + 1;
543 }
544 braceStack++;
545 }
546 break;
547 case '}':
548 if (!inQuote) {
549 braceStack--;
550 if (braceStack == 0) {
551 String subString = text.substring(lastOffset, i);
552 lastOffset = i + 1;
553
554 int lastComma = subString.lastIndexOf(",");
555 if (lastComma != -1) {
556 segments.add(
557 subString.substring(0, lastComma).trim());
558 segments.add(
559 subString.substring(lastComma + 1).trim());
560 } else {
561 segments.add(subString);
562 segments.add(null);
563 }
564 }
565 }
566 break;
567 case '\'':
568 inQuote = false;
569 break;
570 default:
571 break;
572 }
573 }
574 }
575
576 if (!StringUtils.isEmpty(text.substring(lastOffset))) {
577 segments.add(text.substring(lastOffset));
578 segments.add(null);
579 }
580
581 if (braceStack != 0) {
582 throw new IllegalArgumentException("Unmatched braces in the pattern.");
583 }
584
585 if (inQuote) {
586 // throw new IllegalArgumentException( "Unmatched quote in the pattern." );
587 // TODO: warning...
588 }
589
590 return Collections.unmodifiableList(segments);
591 }
592
593 // ----------------------------------------------------------------------
594 // Abstract methods
595 // ----------------------------------------------------------------------
596
597 @Override
598 public abstract String getTitle();
599
600 /**
601 * Renderer the body content of the report.
602 */
603 protected abstract void renderBody();
604 }