1 /* 2 * Copyright 2008-2013 smartics, Kronseder & Reiner GmbH 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package de.smartics.maven.issues; 17 18 import java.io.File; 19 import java.util.List; 20 import java.util.Locale; 21 22 import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; 23 import org.apache.maven.plugin.logging.Log; 24 import org.apache.maven.reporting.MavenReportException; 25 import org.apache.maven.reporting.MavenReportRenderer; 26 import org.codehaus.plexus.util.StringUtils; 27 import org.eclipse.mylyn.tasks.core.data.TaskData; 28 29 import de.smartics.maven.issues.cache.TaskDataCache; 30 31 /** 32 * The issues Mojo reports on issues. 33 * 34 * @author <a href="mailto:robert.reiner@smartics.de">Robert Reiner</a> 35 * @version $Revision:591 $ 36 */ 37 public abstract class AbstractIssuesConnectionMojo extends AbstractReportMojo 38 { 39 // ********************************* Fields ********************************* 40 41 // --- constants ------------------------------------------------------------ 42 43 // --- members -------------------------------------------------------------- 44 45 /** 46 * In offline mode the plugin will not generate a report. 47 * 48 * @parameter default-value="${settings.offline}" 49 * @required 50 * @readonly 51 * @since 1.0 52 */ 53 protected boolean offline; 54 55 /** 56 * A simple flag to skip the generation of the reports. If set on the command 57 * line use <code>-Dissues.skip</code>. 58 * 59 * @parameter expression="${issues.skip}" default-value="false" 60 * @since 1.0 61 */ 62 protected boolean skip; 63 64 /** 65 * List of report output names to be skipped. This is useful to remove the 66 * generation of a particular report without removing the description. This 67 * configuration can also be used to remove configured reports in a parent's 68 * POM. 69 * 70 * @parameter 71 * @since 1.0 72 */ 73 private List<String> skipReports; 74 75 // ... version .............................................................. 76 77 /** 78 * The name of the class that creates comparable version instances. The class 79 * specified here has to implement 80 * {@link de.smartics.maven.issues.version.VersionFactory}. 81 * 82 * @required 83 * @parameter 84 * default-value="de.smartics.maven.issues.bugzilla.BugzillaVersionFactory" 85 * @since 1.0 86 */ 87 private String versionFactory; 88 89 /** 90 * The range of version to be included in the search. Use <code>[]</code> to 91 * specify inclusion and <code>()</code> for exclusion of the bounds. 92 * <p> 93 * Examples: 94 * </p> 95 * <ul> 96 * <li><code>1.0</code> Version 1.0</li> 97 * <li><code>[1.0,2.0)</code> Versions 1.0 (included) to 2.0 (not included)</li> 98 * <li><code>[1.0,2.0]</code> Versions 1.0 to 2.0 (both included)</li> 99 * <li><code>[1.5,)</code> Versions 1.5 and higher</li> 100 * <li><code>(,1.0],[1.2,)</code> Versions up to 1.0 (included) and 1.2 or 101 * higher</li> 102 * </ul> 103 * <p> 104 * If this value is specified the property 105 * {@link #isCurrentReleaseVersionOnly()} is overridden. 106 * </p> 107 * <p> 108 * It is required that the first specified restriction has at least one 109 * inclusive bound or is starting from version 0.0.0. 110 * </p> 111 * <ul> 112 * <li> 113 * <li><code>(,1.0)</code> is correct. 114 * <li><code>[0.5.0,1.0)</code> is correct. 115 * <li><code>(0.5.0,1.0]</code> is correct. 116 * <li><code>(0.5.0,1.0)</code> is <em>not</em> correct. 117 * </ul> 118 * 119 * @parameter 120 * @since 1.0 121 */ 122 private String versionRange; 123 124 // ... issue management infrastructure ...................................... 125 126 /** 127 * The directory to store task data information for the task cache. This is an 128 * optimization to not fetch tasks already fetched. 129 * 130 * @parameter expression="${project.build.directory}/task-data" 131 * @required 132 * @readonly 133 * @since 1.0 134 */ 135 protected String buildDirectory; 136 137 /** 138 * The directory to store task data information for the task cache. This is an 139 * optimization to not fetch tasks already fetched. 140 * 141 * @parameter expression="${issues.repositoryFacadeFactoryClass}" 142 * default-value= 143 * "de.smartics.maven.issues.factory.DefaultRepositoryFacadeFactory" 144 * @required 145 * @since 1.0 146 */ 147 protected String repositoryFacadeFactoryClass; 148 149 /** 150 * The factory creates repository facades to access the issues stored in the 151 * issue management system. 152 */ 153 protected RepositoryFacadeFactory repositoryFacadeFactory; 154 155 // ... issue management information ......................................... 156 157 /** 158 * The name of the issue management system to connect to. 159 * 160 * @parameter default-value="${project.issueManagement.system}" 161 * @since 1.0 162 */ 163 protected String issueManagementSystem; 164 165 /** 166 * The URL to the issue management system to connect to. 167 * 168 * @parameter default-value="${project.issueManagement.url}" 169 * @since 1.0 170 */ 171 protected String issueManagementUrl; 172 173 /** 174 * The name of the user for authentication to access a private installation of 175 * a issue management system. 176 * 177 * @parameter default-value="" 178 * @since 1.0 179 */ 180 protected String issueManagementUser; 181 182 /** 183 * The password for authentication to access a private installation of a issue 184 * management system. 185 * 186 * @parameter default-value="" 187 * @since 1.0 188 */ 189 protected String issueManagementPassword; 190 191 /** 192 * The name of the user for HTTP basic authentication to the issue management 193 * webserver. 194 * 195 * @parameter default-value="" 196 * @since 1.0 197 */ 198 protected String webUser; 199 200 /** 201 * The password for HTTP basic authentication to the issue management 202 * webserver. 203 * 204 * @parameter default-value="" 205 * @since 1.0 206 */ 207 protected String webPassword; 208 209 /** 210 * Sets the version of the task repository. 211 * 212 * @parameter 213 * @since 1.0 214 */ 215 protected String repositoryVersion; 216 217 /** 218 * Maximum number of entries to be displayed by the issue report. Use 219 * <code>-1</code> for unlimited entries. 220 * 221 * @parameter default-value=-1 222 * @since 1.0 223 */ 224 private int maxEntries; 225 226 /** 227 * Maximum number retries to connect to the issue management system. 228 * 229 * @parameter default-value=3 230 * @since 1.0 231 */ 232 protected int maxRetries; 233 234 /** 235 * The timeout in milliseconds between retries of connection attempts. 236 * 237 * @parameter default-value=7000 238 * @since 1.0 239 */ 240 protected long timeout; 241 242 /** 243 * The flag indicates whether (<code>true</code>) or not (<code>false</code>) 244 * logout problems from the task repository should be ignored. 245 * <p> 246 * This is a kind of hack that grabs for the string 'Logout' in the exception 247 * message reported from the task repository. It may be useful if the task 248 * repository's response is quite slow. 249 * 250 * @parameter default-value="false" 251 * @since 1.0 252 */ 253 protected boolean ignoreLogoutProblem; 254 255 /** 256 * Informs that the column names retrieved by the query should be logged at 257 * the end of the query. This allows to grab the keywords returned by the 258 * issue management system. 259 * 260 * @parameter default-value="false" 261 * @since 1.0 262 */ 263 protected boolean logColumns; 264 265 /** 266 * The encoding to read and write tasks. 267 * 268 * @parameter default-value="UTF-8" 269 * @since 1.0 270 */ 271 private String taskEncoding; 272 273 // ****************************** Initializer ******************************* 274 275 // ****************************** Constructors ****************************** 276 277 // ****************************** Inner Classes ***************************** 278 279 // ********************************* Methods ******************************** 280 281 // --- init ----------------------------------------------------------------- 282 283 // --- get&set -------------------------------------------------------------- 284 285 // --- business ------------------------------------------------------------- 286 287 /** 288 * {@inheritDoc} 289 * 290 * @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale) 291 */ 292 @Override 293 protected void executeReport(final Locale locale) throws MavenReportException 294 { 295 super.executeReport(locale); 296 297 try 298 { 299 if (repositoryFacadeFactory == null) 300 { 301 repositoryFacadeFactory = create(repositoryFacadeFactoryClass); 302 } 303 304 final IssueManagementConfig config = createIssueManagementConfig(); 305 prepareResources(); 306 307 final VersionFactory versionFactoryInstance = 308 createInstance(versionFactory); 309 final RepositoryFacade facade = repositoryFacadeFactory.create(config); 310 final ArtifactVersionRange versionRange = 311 createVersionRange(versionFactoryInstance); 312 final QueryData queryData = createQueryData(versionRange); 313 final List<TaskData> issues = facade.queryTasks(queryData); 314 final MavenReportRenderer renderer = 315 createRenderer(locale, versionFactoryInstance, versionRange, issues); 316 renderer.render(); 317 } 318 catch (final Exception e) 319 { 320 final String message = "Cannot generate " + getName(locale) + "."; 321 if (getLog().isWarnEnabled()) 322 { 323 getLog().warn(message, e); 324 } 325 throw new MavenReportException(message, e); 326 } 327 } 328 329 protected ArtifactVersionRange createVersionRange( 330 final VersionFactory versionFactoryInstance) 331 { 332 if (versionRange != null) 333 { 334 try 335 { 336 return versionFactoryInstance.createRange(versionRange); 337 } 338 catch (final InvalidVersionSpecificationException e) 339 { 340 throw new IllegalArgumentException("Cannot parse version range '" 341 + versionRange + "'.", e); 342 } 343 } 344 else 345 { 346 return null; 347 } 348 } 349 350 /** 351 * Creates the query data that enables to create a query URL to be sent to the 352 * issue management system. 353 * 354 * @param versionRange the version range used to create the query data. 355 * @return an instance of query data suitable to run in a specific issue 356 * management system. 357 */ 358 protected abstract QueryData createQueryData(ArtifactVersionRange versionRange); 359 360 /** 361 * Creates a report neutral configuration that provides information to access 362 * an issue management system. 363 * 364 * @return a valid configuration instance. 365 * @throws IllegalArgumentException if the configuration cannot be build due 366 * to missing or invalid information. 367 */ 368 private IssueManagementConfig createIssueManagementConfig() 369 throws IllegalArgumentException 370 { 371 normalizeConfigurationParameters(); 372 373 final IssueManagementConfig config = 374 new IssueManagementConfig(issueManagementSystem, issueManagementUrl); 375 config.setIgnoreLogoutProblem(ignoreLogoutProblem); 376 config.setIssueManagementPassword(issueManagementPassword); 377 config.setIssueManagementUser(issueManagementUser); 378 config.setMaxEntries(maxEntries); 379 config.setMaxRetries(maxRetries); 380 config.setTimeout(timeout); 381 config.setRepositoryVersion(repositoryVersion); 382 config.setWebPassword(webPassword); 383 config.setWebUser(webUser); 384 config.setBuildDirectory(buildDirectory); 385 config.setLogColumns(logColumns); 386 return config; 387 } 388 389 /** 390 * Prepares the static resources if not already prepared. 391 * 392 * @throws MavenReportException if the resource cannot be prepared. 393 */ 394 private void prepareResources() throws MavenReportException 395 { 396 final File taskDataDir = new File(buildDirectory); 397 if (!taskDataDir.exists()) 398 { 399 final boolean created = taskDataDir.mkdirs(); 400 if (!created) 401 { 402 throw new MavenReportException( 403 "Cannot create task data directory to store seralized task data."); 404 } 405 } 406 407 TaskDataCache cache = ResourceLocator.getInstance().getTaskDataCache(); 408 if (cache == null) 409 { 410 cache = new TaskDataCache(taskEncoding, taskDataDir); 411 ResourceLocator.getInstance().setTaskDataCacheIfUnset(cache); 412 } 413 } 414 415 /** 416 * Creates a renderer. 417 * 418 * @param locale the locale to use for the report. 419 * @param issues the issues to render. 420 * @return the renderer ready to run. 421 * @throws MavenReportException if the renderer cannot be created or 422 * configured. 423 */ 424 protected abstract MavenReportRenderer createRenderer(Locale locale, 425 VersionFactory versionFactoryInstance, ArtifactVersionRange versionRange, 426 List<TaskData> issues) throws MavenReportException; 427 428 /** 429 * Normalizes configuration parameters to ensure a successful run of the 430 * report. 431 */ 432 private void normalizeConfigurationParameters() 433 { 434 if (issueManagementUrl.endsWith("/")) 435 { 436 issueManagementUrl = StringUtils.chop(issueManagementUrl); 437 } 438 } 439 440 /** 441 * Checks if all required information is provided to run the report 442 * successfully. If the plugin is running in offline mode, this method returns 443 * <code>false</code>. 444 * 445 * @return <code>true</code> if a report can be rendered, <code>false</code> 446 * otherwise. 447 * @see org.apache.maven.reporting.AbstractMavenReport#canGenerateReport() 448 */ 449 @Override 450 public boolean canGenerateReport() 451 { 452 if (offline || skip || skipReport()) 453 { 454 return false; 455 } 456 457 repositoryFacadeFactory = create(repositoryFacadeFactoryClass); 458 return validateIssueManagementConfiguration(); 459 } 460 461 private boolean skipReport() 462 { 463 final String reportId = getOutputName(); 464 return this.skipReports != null && skipReports.contains(reportId); 465 } 466 467 /** 468 * Creates the factory with the given class name. 469 * 470 * @param repositoryFacadeFactoryClassName the name of the factory to create. 471 * @return the factory of the given name. 472 * @throws IllegalArgumentException if the class cannot be instantiated. 473 */ 474 @SuppressWarnings("unchecked") 475 private RepositoryFacadeFactory create( 476 final String repositoryFacadeFactoryClassName) 477 throws IllegalArgumentException 478 { 479 try 480 { 481 final Class<RepositoryFacadeFactory> clazz = 482 (Class<RepositoryFacadeFactory>) Class 483 .forName(repositoryFacadeFactoryClassName); 484 final RepositoryFacadeFactory factory = clazz.newInstance(); 485 return factory; 486 } 487 catch (final Exception e) 488 { 489 throw new IllegalArgumentException("Cannot create factory class '" 490 + repositoryFacadeFactoryClassName 491 + "'.", e); 492 } 493 } 494 495 /** 496 * Checks whether all required issue management configuration information is 497 * set. 498 * <p> 499 * The checking includes 500 * </p> 501 * <ul> 502 * <li>Is the URL to the issue management server provided?</li> 503 * <li>Is the name of the issue management system provided?</li> 504 * <li>Is the issue management server supported.</li> 505 * </ul> 506 * 507 * @return <code>true</code> if all required issue management information is 508 * provided and valid, <code>false</code> otherwise. 509 */ 510 private boolean validateIssueManagementConfiguration() 511 { 512 if (StringUtils.isBlank(issueManagementUrl)) 513 { 514 getLog().error("URL to issue management system missing."); 515 return false; 516 } 517 else if (StringUtils.isBlank(issueManagementSystem)) 518 { 519 getLog().error("Name of issue management system missing."); 520 return false; 521 } 522 else if (!repositoryFacadeFactory.supports(issueManagementSystem)) 523 { 524 getLog().error( 525 "The issue report does not support '" + issueManagementSystem + "'."); 526 return false; 527 } 528 529 return true; 530 } 531 532 /** 533 * Creates the instance of a class expecting a no-args constructor. 534 * 535 * @param <T> the type of the instance to be created. 536 * @param className the name of the class to create an instance of. 537 * @return the created instance. 538 * @throws IllegalArgumentException if a problem prevents the creation of the 539 * instance. 540 */ 541 @SuppressWarnings("unchecked") 542 protected <T> T createInstance(final String className) 543 throws IllegalArgumentException 544 { 545 try 546 { 547 final Class<T> clazz = (Class<T>) Class.forName(className); 548 final T instance = clazz.newInstance(); 549 return instance; 550 } 551 catch (final Exception e) 552 { 553 final String message = 554 "Cannot create instance for class '" + className + "'."; 555 final Log log = getLog(); 556 if (log.isWarnEnabled()) 557 { 558 log.warn(message, e); 559 } 560 throw new IllegalArgumentException(message, e); 561 } 562 } 563 564 // --- object basics -------------------------------------------------------- 565 566 }