View Javadoc
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 }