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 }