001/* 002 * SonarQube, open source software quality management tool. 003 * Copyright (C) 2008-2013 SonarSource 004 * mailto:contact AT sonarsource DOT com 005 * 006 * SonarQube is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * SonarQube is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.api.server.rule; 021 022import com.google.common.collect.*; 023import org.apache.commons.io.IOUtils; 024import org.apache.commons.lang.StringUtils; 025import org.slf4j.LoggerFactory; 026import org.sonar.api.ServerExtension; 027import org.sonar.api.rule.RuleStatus; 028import org.sonar.api.rule.Severity; 029 030import javax.annotation.CheckForNull; 031import javax.annotation.Nullable; 032import javax.annotation.concurrent.Immutable; 033import java.io.IOException; 034import java.io.InputStream; 035import java.net.URL; 036import java.util.List; 037import java.util.Map; 038import java.util.Set; 039 040/** 041 * Defines the coding rules. For example the Java Findbugs plugin provides an implementation of 042 * this extension point in order to define the rules that it supports. 043 * <p/> 044 * This interface replaces the deprecated class org.sonar.api.rules.RuleRepository. 045 * 046 * @since 4.2 047 */ 048public interface RuleDefinitions extends ServerExtension { 049 050 /** 051 * Instantiated by core but not by plugins 052 */ 053 class Context { 054 private final Map<String, Repository> repositoriesByKey = Maps.newHashMap(); 055 private final ListMultimap<String, ExtendedRepository> extendedRepositoriesByKey = ArrayListMultimap.create(); 056 057 058 public NewRepository newRepository(String key, String language) { 059 return new NewRepositoryImpl(this, key, language, false); 060 } 061 062 public NewExtendedRepository extendRepository(String key, String language) { 063 return new NewRepositoryImpl(this, key, language, true); 064 } 065 066 @CheckForNull 067 public Repository repository(String key) { 068 return repositoriesByKey.get(key); 069 } 070 071 public List<Repository> repositories() { 072 return ImmutableList.copyOf(repositoriesByKey.values()); 073 } 074 075 public List<ExtendedRepository> extendedRepositories(String repositoryKey) { 076 return ImmutableList.copyOf(extendedRepositoriesByKey.get(repositoryKey)); 077 } 078 079 public List<ExtendedRepository> extendedRepositories() { 080 return ImmutableList.copyOf(extendedRepositoriesByKey.values()); 081 } 082 083 private void registerRepository(NewRepositoryImpl newRepository) { 084 if (repositoriesByKey.containsKey(newRepository.key)) { 085 throw new IllegalStateException(String.format("The rule repository '%s' is defined several times", newRepository.key)); 086 } 087 repositoriesByKey.put(newRepository.key, new RepositoryImpl(newRepository)); 088 } 089 090 private void registerExtendedRepository(NewRepositoryImpl newRepository) { 091 extendedRepositoriesByKey.put(newRepository.key, new RepositoryImpl(newRepository)); 092 } 093 } 094 095 interface NewExtendedRepository { 096 NewRule newRule(String ruleKey); 097 098 /** 099 * Reads definition of rule from the annotations provided by the library sonar-check-api. 100 */ 101 NewRule loadAnnotatedClass(Class clazz); 102 103 /** 104 * Reads definitions of rules from the annotations provided by the library sonar-check-api. 105 */ 106 NewExtendedRepository loadAnnotatedClasses(Class... classes); 107 108 /** 109 * Reads definitions of rules from a XML file. Format is : 110 * <pre> 111 * <rules> 112 * <rule> 113 * <!-- required fields --> 114 * <key>the-rule-key</key> 115 * <name>The purpose of the rule</name> 116 * <description> 117 * <![CDATA[The description]]> 118 * </description> 119 * 120 * <!-- optional fields --> 121 * <internalKey>Checker/TreeWalker/LocalVariableName</internalKey> 122 * <severity>BLOCKER</severity> 123 * <cardinality>MULTIPLE</cardinality> 124 * <status>BETA</status> 125 * <param> 126 * <key>the-param-key</key> 127 * <tag>style</tag> 128 * <tag>security</tag> 129 * <description> 130 * <![CDATA[ 131 * the param-description 132 * ]]> 133 * </description> 134 * <defaultValue>42</defaultValue> 135 * </param> 136 * <param> 137 * <key>another-param</key> 138 * </param> 139 * 140 * <!-- deprecated fields --> 141 * <configKey>Checker/TreeWalker/LocalVariableName</configKey> 142 * <priority>BLOCKER</priority> 143 * </rule> 144 * </rules> 145 * 146 * </pre> 147 */ 148 NewExtendedRepository loadXml(InputStream xmlInput, String encoding); 149 150 void done(); 151 } 152 153 interface NewRepository extends NewExtendedRepository { 154 NewRepository setName(String s); 155 156 @CheckForNull 157 NewRule rule(String ruleKey); 158 } 159 160 class NewRepositoryImpl implements NewRepository { 161 private final Context context; 162 private final boolean extended; 163 private final String key; 164 private String language; 165 private String name; 166 private final Map<String, NewRule> newRules = Maps.newHashMap(); 167 168 private NewRepositoryImpl(Context context, String key, String language, boolean extended) { 169 this.extended = extended; 170 this.context = context; 171 this.key = this.name = key; 172 this.language = language; 173 } 174 175 @Override 176 public NewRepositoryImpl setName(@Nullable String s) { 177 if (StringUtils.isNotEmpty(s)) { 178 this.name = s; 179 } 180 return this; 181 } 182 183 @Override 184 public NewRule newRule(String ruleKey) { 185 if (newRules.containsKey(ruleKey)) { 186 // Should fail in a perfect world, but at the time being the Findbugs plugin 187 // defines several times the rule EC_INCOMPATIBLE_ARRAY_COMPARE 188 // See http://jira.codehaus.org/browse/SONARJAVA-428 189 LoggerFactory.getLogger(getClass()).warn(String.format("The rule '%s' of repository '%s' is declared several times", ruleKey, key)); 190 } 191 NewRule newRule = new NewRule(key, ruleKey); 192 newRules.put(ruleKey, newRule); 193 return newRule; 194 } 195 196 @CheckForNull 197 @Override 198 public NewRule rule(String ruleKey) { 199 return newRules.get(ruleKey); 200 } 201 202 @Override 203 public NewRepositoryImpl loadAnnotatedClasses(Class... classes) { 204 new RuleDefinitionsFromAnnotations().loadRules(this, classes); 205 return this; 206 } 207 208 @Override 209 public RuleDefinitions.NewRule loadAnnotatedClass(Class clazz) { 210 return new RuleDefinitionsFromAnnotations().loadRule(this, clazz); 211 } 212 213 @Override 214 public NewRepositoryImpl loadXml(InputStream xmlInput, String encoding) { 215 new RuleDefinitionsFromXml().loadRules(this, xmlInput, encoding); 216 return this; 217 } 218 219 @Override 220 public void done() { 221 // note that some validations can be done here, for example for 222 // verifying that at least one rule is declared 223 224 if (extended) { 225 context.registerExtendedRepository(this); 226 } else { 227 context.registerRepository(this); 228 } 229 } 230 } 231 232 interface ExtendedRepository { 233 String key(); 234 235 String language(); 236 237 @CheckForNull 238 Rule rule(String ruleKey); 239 240 List<Rule> rules(); 241 } 242 243 interface Repository extends ExtendedRepository { 244 String name(); 245 } 246 247 @Immutable 248 class RepositoryImpl implements Repository { 249 private final String key, language, name; 250 private final Map<String, Rule> rulesByKey; 251 252 private RepositoryImpl(NewRepositoryImpl newRepository) { 253 this.key = newRepository.key; 254 this.language = newRepository.language; 255 this.name = newRepository.name; 256 ImmutableMap.Builder<String, Rule> ruleBuilder = ImmutableMap.builder(); 257 for (NewRule newRule : newRepository.newRules.values()) { 258 newRule.validate(); 259 ruleBuilder.put(newRule.key, new Rule(this, newRule)); 260 } 261 this.rulesByKey = ruleBuilder.build(); 262 } 263 264 @Override 265 public String key() { 266 return key; 267 } 268 269 @Override 270 public String language() { 271 return language; 272 } 273 274 @Override 275 public String name() { 276 return name; 277 } 278 279 @Override 280 @CheckForNull 281 public Rule rule(String ruleKey) { 282 return rulesByKey.get(ruleKey); 283 } 284 285 @Override 286 public List<Rule> rules() { 287 return ImmutableList.copyOf(rulesByKey.values()); 288 } 289 290 @Override 291 public boolean equals(Object o) { 292 if (this == o) { 293 return true; 294 } 295 if (o == null || getClass() != o.getClass()) { 296 return false; 297 } 298 RepositoryImpl that = (RepositoryImpl) o; 299 return key.equals(that.key); 300 } 301 302 @Override 303 public int hashCode() { 304 return key.hashCode(); 305 } 306 } 307 308 class NewRule { 309 private final String repoKey, key; 310 private String name, htmlDescription, internalKey, severity = Severity.MAJOR; 311 private boolean template; 312 private RuleStatus status = RuleStatus.defaultStatus(); 313 private final Set<String> tags = Sets.newTreeSet(); 314 private final Map<String, NewParam> paramsByKey = Maps.newHashMap(); 315 316 private NewRule(String repoKey, String key) { 317 this.repoKey = repoKey; 318 this.key = key; 319 } 320 321 public String key() { 322 return this.key; 323 } 324 325 public NewRule setName(@Nullable String s) { 326 this.name = StringUtils.trim(s); 327 return this; 328 } 329 330 public NewRule setTemplate(boolean template) { 331 this.template = template; 332 return this; 333 } 334 335 public NewRule setSeverity(String s) { 336 if (!Severity.ALL.contains(s)) { 337 throw new IllegalArgumentException(String.format("Severity of rule %s is not correct: %s", this, s)); 338 } 339 this.severity = s; 340 return this; 341 } 342 343 public NewRule setHtmlDescription(String s) { 344 this.htmlDescription = StringUtils.trim(s); 345 return this; 346 } 347 348 /** 349 * Load description from a file available in classpath. Example : <code>setHtmlDescription(getClass().getResource("/myrepo/Rule1234.html")</code> 350 */ 351 public NewRule setHtmlDescription(@Nullable URL classpathUrl) { 352 if (classpathUrl != null) { 353 try { 354 setHtmlDescription(IOUtils.toString(classpathUrl)); 355 } catch (IOException e) { 356 throw new IllegalStateException("Fail to read: " + classpathUrl, e); 357 } 358 } else { 359 this.htmlDescription = null; 360 } 361 return this; 362 } 363 364 public NewRule setStatus(RuleStatus status) { 365 if (status.equals(RuleStatus.REMOVED)) { 366 throw new IllegalArgumentException(String.format("Status 'REMOVED' is not accepted on rule '%s'", this)); 367 } 368 this.status = status; 369 return this; 370 } 371 372 public NewParam newParam(String paramKey) { 373 if (paramsByKey.containsKey(paramKey)) { 374 throw new IllegalArgumentException(String.format("The parameter '%s' is declared several times on the rule %s", paramKey, this)); 375 } 376 NewParam param = new NewParam(paramKey); 377 paramsByKey.put(paramKey, param); 378 return param; 379 } 380 381 @CheckForNull 382 public NewParam param(String paramKey) { 383 return paramsByKey.get(paramKey); 384 } 385 386 /** 387 * @see RuleTagFormat 388 */ 389 public NewRule addTags(String... list) { 390 for (String tag : list) { 391 RuleTagFormat.validate(tag); 392 tags.add(tag); 393 } 394 return this; 395 } 396 397 /** 398 * @see RuleTagFormat 399 */ 400 public NewRule setTags(String... list) { 401 tags.clear(); 402 addTags(list); 403 return this; 404 } 405 406 /** 407 * Optional key that can be used by the rule engine. Not displayed 408 * in webapp. For example the Java Checkstyle plugin feeds this field 409 * with the internal path ("Checker/TreeWalker/AnnotationUseStyle"). 410 */ 411 public NewRule setInternalKey(@Nullable String s) { 412 this.internalKey = s; 413 return this; 414 } 415 416 private void validate() { 417 if (StringUtils.isBlank(name)) { 418 throw new IllegalStateException(String.format("Name of rule %s is empty", this)); 419 } 420 if (StringUtils.isBlank(htmlDescription)) { 421 throw new IllegalStateException(String.format("HTML description of rule %s is empty", this)); 422 } 423 } 424 425 @Override 426 public String toString() { 427 return String.format("[repository=%s, key=%s]", repoKey, key); 428 } 429 } 430 431 @Immutable 432 class Rule { 433 private final Repository repository; 434 private final String repoKey, key, name, htmlDescription, internalKey, severity; 435 private final boolean template; 436 private final Set<String> tags; 437 private final Map<String, Param> params; 438 private final RuleStatus status; 439 440 private Rule(Repository repository, NewRule newRule) { 441 this.repository = repository; 442 this.repoKey = newRule.repoKey; 443 this.key = newRule.key; 444 this.name = newRule.name; 445 this.htmlDescription = newRule.htmlDescription; 446 this.internalKey = newRule.internalKey; 447 this.severity = newRule.severity; 448 this.template = newRule.template; 449 this.status = newRule.status; 450 this.tags = ImmutableSortedSet.copyOf(newRule.tags); 451 ImmutableMap.Builder<String, Param> paramsBuilder = ImmutableMap.builder(); 452 for (NewParam newParam : newRule.paramsByKey.values()) { 453 paramsBuilder.put(newParam.key, new Param(newParam)); 454 } 455 this.params = paramsBuilder.build(); 456 } 457 458 public Repository repository() { 459 return repository; 460 } 461 462 public String key() { 463 return key; 464 } 465 466 public String name() { 467 return name; 468 } 469 470 public String severity() { 471 return severity; 472 } 473 474 @CheckForNull 475 public String htmlDescription() { 476 return htmlDescription; 477 } 478 479 public boolean template() { 480 return template; 481 } 482 483 public RuleStatus status() { 484 return status; 485 } 486 487 @CheckForNull 488 public Param param(String key) { 489 return params.get(key); 490 } 491 492 public List<Param> params() { 493 return ImmutableList.copyOf(params.values()); 494 } 495 496 public Set<String> tags() { 497 return tags; 498 } 499 500 /** 501 * @see RuleDefinitions.NewRule#setInternalKey(String) 502 */ 503 @CheckForNull 504 public String internalKey() { 505 return internalKey; 506 } 507 508 @Override 509 public boolean equals(Object o) { 510 if (this == o) { 511 return true; 512 } 513 if (o == null || getClass() != o.getClass()) { 514 return false; 515 } 516 Rule other = (Rule) o; 517 return key.equals(other.key) && repoKey.equals(other.repoKey); 518 } 519 520 @Override 521 public int hashCode() { 522 int result = repoKey.hashCode(); 523 result = 31 * result + key.hashCode(); 524 return result; 525 } 526 527 @Override 528 public String toString() { 529 return String.format("[repository=%s, key=%s]", repoKey, key); 530 } 531 } 532 533 class NewParam { 534 private final String key; 535 private String name, description, defaultValue; 536 private RuleParamType type = RuleParamType.STRING; 537 538 private NewParam(String key) { 539 this.key = this.name = key; 540 } 541 542 public NewParam setName(@Nullable String s) { 543 // name must never be null. 544 this.name = StringUtils.defaultIfBlank(s, key); 545 return this; 546 } 547 548 public NewParam setType(RuleParamType t) { 549 this.type = t; 550 return this; 551 } 552 553 /** 554 * Plain-text description. Can be null. 555 */ 556 public NewParam setDescription(@Nullable String s) { 557 this.description = StringUtils.defaultIfBlank(s, null); 558 return this; 559 } 560 561 public NewParam setDefaultValue(@Nullable String s) { 562 this.defaultValue = s; 563 return this; 564 } 565 } 566 567 @Immutable 568 class Param { 569 private final String key, name, description, defaultValue; 570 private final RuleParamType type; 571 572 private Param(NewParam newParam) { 573 this.key = newParam.key; 574 this.name = newParam.name; 575 this.description = newParam.description; 576 this.defaultValue = newParam.defaultValue; 577 this.type = newParam.type; 578 } 579 580 public String key() { 581 return key; 582 } 583 584 public String name() { 585 return name; 586 } 587 588 @Nullable 589 public String description() { 590 return description; 591 } 592 593 @Nullable 594 public String defaultValue() { 595 return defaultValue; 596 } 597 598 public RuleParamType type() { 599 return type; 600 } 601 602 @Override 603 public boolean equals(Object o) { 604 if (this == o) { 605 return true; 606 } 607 if (o == null || getClass() != o.getClass()) { 608 return false; 609 } 610 Param that = (Param) o; 611 return key.equals(that.key); 612 } 613 614 @Override 615 public int hashCode() { 616 return key.hashCode(); 617 } 618 } 619 620 /** 621 * This method is executed when server is started. 622 */ 623 void define(Context context); 624 625}