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 // ****************************** Initializer ******************************* 266 267 // ****************************** Constructors ****************************** 268 269 // ****************************** Inner Classes ***************************** 270 271 // ********************************* Methods ******************************** 272 273 // --- init ----------------------------------------------------------------- 274 275 // --- get&set -------------------------------------------------------------- 276 277 // --- business ------------------------------------------------------------- 278 279 /** 280 * {@inheritDoc} 281 * 282 * @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale) 283 */ 284 @Override 285 protected void executeReport(final Locale locale) throws MavenReportException 286 { 287 super.executeReport(locale); 288 289 try 290 { 291 if (repositoryFacadeFactory == null) 292 { 293 repositoryFacadeFactory = create(repositoryFacadeFactoryClass); 294 } 295 296 final IssueManagementConfig config = createIssueManagementConfig(); 297 prepareResources(); 298 299 final VersionFactory versionFactoryInstance = 300 createInstance(versionFactory); 301 final RepositoryFacade facade = repositoryFacadeFactory.create(config); 302 final ArtifactVersionRange versionRange = 303 createVersionRange(versionFactoryInstance); 304 final QueryData queryData = createQueryData(versionRange); 305 final List<TaskData> issues = facade.queryTasks(queryData); 306 final MavenReportRenderer renderer = 307 createRenderer(locale, versionFactoryInstance, versionRange, issues); 308 renderer.render(); 309 } 310 catch (final Exception e) 311 { 312 final String message = "Cannot generate " + getName(locale) + "."; 313 if (getLog().isWarnEnabled()) 314 { 315 getLog().warn(message, e); 316 } 317 throw new MavenReportException(message, e); 318 } 319 } 320 321 protected ArtifactVersionRange createVersionRange( 322 final VersionFactory versionFactoryInstance) 323 { 324 if (versionRange != null) 325 { 326 try 327 { 328 return versionFactoryInstance.createRange(versionRange); 329 } 330 catch (final InvalidVersionSpecificationException e) 331 { 332 throw new IllegalArgumentException("Cannot parse version range '" 333 + versionRange + "'.", e); 334 } 335 } 336 else 337 { 338 return null; 339 } 340 } 341 342 /** 343 * Creates the query data that enables to create a query URL to be sent to the 344 * issue management system. 345 * 346 * @param versionRange the version range used to create the query data. 347 * @return an instance of query data suitable to run in a specific issue 348 * management system. 349 */ 350 protected abstract QueryData createQueryData(ArtifactVersionRange versionRange); 351 352 /** 353 * Creates a report neutral configuration that provides information to access 354 * an issue management system. 355 * 356 * @return a valid configuration instance. 357 * @throws IllegalArgumentException if the configuration cannot be build due 358 * to missing or invalid information. 359 */ 360 private IssueManagementConfig createIssueManagementConfig() 361 throws IllegalArgumentException 362 { 363 normalizeConfigurationParameters(); 364 365 final IssueManagementConfig config = 366 new IssueManagementConfig(issueManagementSystem, issueManagementUrl); 367 config.setIgnoreLogoutProblem(ignoreLogoutProblem); 368 config.setIssueManagementPassword(issueManagementPassword); 369 config.setIssueManagementUser(issueManagementUser); 370 config.setMaxEntries(maxEntries); 371 config.setMaxRetries(maxRetries); 372 config.setTimeout(timeout); 373 config.setRepositoryVersion(repositoryVersion); 374 config.setWebPassword(webPassword); 375 config.setWebUser(webUser); 376 config.setBuildDirectory(buildDirectory); 377 config.setLogColumns(logColumns); 378 return config; 379 } 380 381 /** 382 * Prepares the static resources if not already prepared. 383 * 384 * @throws MavenReportException if the resource cannot be prepared. 385 */ 386 private void prepareResources() throws MavenReportException 387 { 388 final File taskDataDir = new File(buildDirectory); 389 if (!taskDataDir.exists()) 390 { 391 final boolean created = taskDataDir.mkdirs(); 392 if (!created) 393 { 394 throw new MavenReportException( 395 "Cannot create task data directory to store seralized task data."); 396 } 397 } 398 399 TaskDataCache cache = ResourceLocator.getInstance().getTaskDataCache(); 400 if (cache == null) 401 { 402 cache = new TaskDataCache(taskDataDir); 403 ResourceLocator.getInstance().setTaskDataCacheIfUnset(cache); 404 } 405 } 406 407 /** 408 * Creates a renderer. 409 * 410 * @param locale the locale to use for the report. 411 * @param issues the issues to render. 412 * @return the renderer ready to run. 413 * @throws MavenReportException if the renderer cannot be created or 414 * configured. 415 */ 416 protected abstract MavenReportRenderer createRenderer(Locale locale, 417 VersionFactory versionFactoryInstance, ArtifactVersionRange versionRange, 418 List<TaskData> issues) throws MavenReportException; 419 420 /** 421 * Normalizes configuration parameters to ensure a successful run of the 422 * report. 423 */ 424 private void normalizeConfigurationParameters() 425 { 426 if (issueManagementUrl.endsWith("/")) 427 { 428 issueManagementUrl = StringUtils.chop(issueManagementUrl); 429 } 430 } 431 432 /** 433 * Checks if all required information is provided to run the report 434 * successfully. If the plugin is running in offline mode, this method returns 435 * <code>false</code>. 436 * 437 * @return <code>true</code> if a report can be rendered, <code>false</code> 438 * otherwise. 439 * @see org.apache.maven.reporting.AbstractMavenReport#canGenerateReport() 440 */ 441 @Override 442 public boolean canGenerateReport() 443 { 444 if (offline || skip || skipReport()) 445 { 446 return false; 447 } 448 449 repositoryFacadeFactory = create(repositoryFacadeFactoryClass); 450 return validateIssueManagementConfiguration(); 451 } 452 453 private boolean skipReport() 454 { 455 final String reportId = getOutputName(); 456 return this.skipReports != null && skipReports.contains(reportId); 457 } 458 459 /** 460 * Creates the factory with the given class name. 461 * 462 * @param repositoryFacadeFactoryClassName the name of the factory to create. 463 * @return the factory of the given name. 464 * @throws IllegalArgumentException if the class cannot be instantiated. 465 */ 466 @SuppressWarnings("unchecked") 467 private RepositoryFacadeFactory create( 468 final String repositoryFacadeFactoryClassName) 469 throws IllegalArgumentException 470 { 471 try 472 { 473 final Class<RepositoryFacadeFactory> clazz = 474 (Class<RepositoryFacadeFactory>) Class 475 .forName(repositoryFacadeFactoryClassName); 476 final RepositoryFacadeFactory factory = clazz.newInstance(); 477 return factory; 478 } 479 catch (final Exception e) 480 { 481 throw new IllegalArgumentException("Cannot create factory class '" 482 + repositoryFacadeFactoryClassName 483 + "'.", e); 484 } 485 } 486 487 /** 488 * Checks whether all required issue management configuration information is 489 * set. 490 * <p> 491 * The checking includes 492 * </p> 493 * <ul> 494 * <li>Is the URL to the issue management server provided?</li> 495 * <li>Is the name of the issue management system provided?</li> 496 * <li>Is the issue management server supported.</li> 497 * </ul> 498 * 499 * @return <code>true</code> if all required issue management information is 500 * provided and valid, <code>false</code> otherwise. 501 */ 502 private boolean validateIssueManagementConfiguration() 503 { 504 if (StringUtils.isBlank(issueManagementUrl)) 505 { 506 getLog().error("URL to issue management system missing."); 507 return false; 508 } 509 else if (StringUtils.isBlank(issueManagementSystem)) 510 { 511 getLog().error("Name of issue management system missing."); 512 return false; 513 } 514 else if (!repositoryFacadeFactory.supports(issueManagementSystem)) 515 { 516 getLog().error( 517 "The issue report does not support '" + issueManagementSystem + "'."); 518 return false; 519 } 520 521 return true; 522 } 523 524 /** 525 * Creates the instance of a class expecting a no-args constructor. 526 * 527 * @param <T> the type of the instance to be created. 528 * @param className the name of the class to create an instance of. 529 * @return the created instance. 530 * @throws IllegalArgumentException if a problem prevents the creation of the 531 * instance. 532 */ 533 @SuppressWarnings("unchecked") 534 protected <T> T createInstance(final String className) 535 throws IllegalArgumentException 536 { 537 try 538 { 539 final Class<T> clazz = (Class<T>) Class.forName(className); 540 final T instance = clazz.newInstance(); 541 return instance; 542 } 543 catch (final Exception e) 544 { 545 final String message = 546 "Cannot create instance for class '" + className + "'."; 547 final Log log = getLog(); 548 if (log.isWarnEnabled()) 549 { 550 log.warn(message, e); 551 } 552 throw new IllegalArgumentException(message, e); 553 } 554 } 555 556 // --- object basics -------------------------------------------------------- 557 558 }