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