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