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 }