001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 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.base.Objects;
023import com.google.common.base.Strings;
024import com.google.common.collect.ImmutableList;
025import com.google.common.collect.ImmutableMap;
026import com.google.common.collect.ImmutableSortedSet;
027import com.google.common.collect.Maps;
028import com.google.common.collect.Sets;
029import java.io.IOException;
030import java.net.URL;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037import javax.annotation.CheckForNull;
038import javax.annotation.Nullable;
039import javax.annotation.concurrent.Immutable;
040import org.apache.commons.io.IOUtils;
041import org.apache.commons.lang.StringUtils;
042import org.sonar.api.ExtensionPoint;
043import org.sonar.api.rule.RuleStatus;
044import org.sonar.api.rule.Severity;
045import org.sonar.api.server.ServerSide;
046import org.sonar.api.server.debt.DebtRemediationFunction;
047import org.sonar.api.utils.log.Loggers;
048
049import static com.google.common.base.Preconditions.checkArgument;
050import static com.google.common.base.Preconditions.checkState;
051import static java.lang.String.format;
052import static org.apache.commons.lang.StringUtils.trimToNull;
053
054/**
055 * Defines some coding rules of the same repository. For example the Java Findbugs plugin provides an implementation of
056 * this extension point in order to define the rules that it supports.
057 * <p/>
058 * This interface replaces the deprecated class org.sonar.api.rules.RuleRepository.
059 * <p/>
060 * <h3>How to use</h3>
061 * <pre>
062 * public class MyJsRulesDefinition implements RulesDefinition {
063 *
064 *   {@literal @}Override
065 *   public void define(Context context) {
066 *     NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer");
067 *
068 *     // define a rule programmatically. Note that rules
069 *     // could be loaded from files (JSON, XML, ...)
070 *     NewRule x1Rule = repository.createRule("x1")
071 *      .setName("No empty line")
072 *      .setHtmlDescription("Generate an issue on empty lines")
073 *
074 *      // optional tags
075 *      .setTags("style", "stupid")
076 *
077 *     // optional status. Default value is READY.
078 *     .setStatus(RuleStatus.BETA)
079 *
080 *     // default severity when the rule is activated on a Quality profile. Default value is MAJOR.
081 *     .setSeverity(Severity.MINOR);
082 *
083 *     x1Rule
084 *       .setDebtSubCharacteristic("INTEGRATION_TESTABILITY")
085 *       .setDebtRemediationFunction(x1Rule.debtRemediationFunctions().linearWithOffset("1h", "30min"));
086 *
087 *     x1Rule.createParam("acceptWhitespace")
088 *       .setDefaultValue("false")
089 *       .setType(RuleParamType.BOOLEAN)
090 *       .setDescription("Accept whitespaces on the line");
091 *
092 *     // don't forget to call done() to finalize the definition
093 *     repository.done();
094 *   }
095 * }
096 * </pre>
097 * <p/>
098 * If rules are declared in a XML file with the standard SonarQube format (see
099 * {@link org.sonar.api.server.rule.RulesDefinitionXmlLoader}), then it can be loaded by using :
100 * <p/>
101 * <pre>
102 * public class MyJsRulesDefinition implements RulesDefinition {
103 *
104 *   private final RulesDefinitionXmlLoader xmlLoader;
105 *
106 *   public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader) {
107 *     this.xmlLoader = xmlLoader;
108 *   }
109 *
110 *   {@literal @}Override
111 *   public void define(Context context) {
112 *     NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer");
113 *     // see javadoc of RulesDefinitionXmlLoader for the format
114 *     xmlLoader.load(repository, getClass().getResourceAsStream("/path/to/rules.xml"));
115 *     repository.done();
116 *   }
117 * }
118 * </pre>
119 * <p/>
120 * In the above example, XML file must contain name and description of each rule. If it's not the case, then the
121 * (deprecated) English bundles can be used :
122 * <p/>
123 * <pre>
124 * public class MyJsRulesDefinition implements RulesDefinition {
125 *
126 *   private final RulesDefinitionXmlLoader xmlLoader;
127 *   private final RulesDefinitionI18nLoader i18nLoader;
128 *
129 *   public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader, RulesDefinitionI18nLoader i18nLoader) {
130 *     this.xmlLoader = xmlLoader;
131 *     this.i18nLoader = i18nLoader;
132 *   }
133 *
134 *   {@literal @}Override
135 *   public void define(Context context) {
136 *     NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer");
137 *     xmlLoader.load(repository, getClass().getResourceAsStream("/path/to/rules.xml"), "UTF-8");
138 *     i18nLoader.load(repository);
139 *     repository.done();
140 *   }
141 * }
142 * </pre>
143 *
144 * @since 4.3
145 */
146@ServerSide
147@ExtensionPoint
148public interface RulesDefinition {
149
150  /**
151   * Default sub-characteristics of technical debt model. See http://www.sqale.org
152   */
153  final class SubCharacteristics {
154    /**
155     * Related to characteristic REUSABILITY
156     */
157    public static final String MODULARITY = "MODULARITY";
158
159    /**
160     * Related to characteristic REUSABILITY
161     */
162    public static final String TRANSPORTABILITY = "TRANSPORTABILITY";
163
164    /**
165     * Related to characteristic PORTABILITY
166     */
167    public static final String COMPILER_RELATED_PORTABILITY = "COMPILER_RELATED_PORTABILITY";
168
169    /**
170     * Related to characteristic PORTABILITY
171     */
172    public static final String HARDWARE_RELATED_PORTABILITY = "HARDWARE_RELATED_PORTABILITY";
173
174    /**
175     * Related to characteristic PORTABILITY
176     */
177    public static final String LANGUAGE_RELATED_PORTABILITY = "LANGUAGE_RELATED_PORTABILITY";
178
179    /**
180     * Related to characteristic PORTABILITY
181     */
182    public static final String OS_RELATED_PORTABILITY = "OS_RELATED_PORTABILITY";
183
184    /**
185     * Related to characteristic PORTABILITY
186     */
187    public static final String SOFTWARE_RELATED_PORTABILITY = "SOFTWARE_RELATED_PORTABILITY";
188
189    /**
190     * Related to characteristic PORTABILITY
191     */
192    public static final String TIME_ZONE_RELATED_PORTABILITY = "TIME_ZONE_RELATED_PORTABILITY";
193
194    /**
195     * Related to characteristic MAINTAINABILITY
196     */
197    public static final String READABILITY = "READABILITY";
198
199    /**
200     * Related to characteristic MAINTAINABILITY
201     */
202    public static final String UNDERSTANDABILITY = "UNDERSTANDABILITY";
203
204    /**
205     * Related to characteristic SECURITY
206     */
207    public static final String API_ABUSE = "API_ABUSE";
208
209    /**
210     * Related to characteristic SECURITY
211     */
212    public static final String ERRORS = "ERRORS";
213
214    /**
215     * Related to characteristic SECURITY
216     */
217    public static final String INPUT_VALIDATION_AND_REPRESENTATION = "INPUT_VALIDATION_AND_REPRESENTATION";
218
219    /**
220     * Related to characteristic SECURITY
221     */
222    public static final String SECURITY_FEATURES = "SECURITY_FEATURES";
223
224    /**
225     * Related to characteristic EFFICIENCY
226     */
227    public static final String CPU_EFFICIENCY = "CPU_EFFICIENCY";
228
229    /**
230     * Related to characteristic EFFICIENCY
231     */
232    public static final String MEMORY_EFFICIENCY = "MEMORY_EFFICIENCY";
233
234    /**
235     * Related to characteristic EFFICIENCY
236     */
237    public static final String NETWORK_USE = "NETWORK_USE";
238
239    /**
240     * Related to characteristic CHANGEABILITY
241     */
242    public static final String ARCHITECTURE_CHANGEABILITY = "ARCHITECTURE_CHANGEABILITY";
243
244    /**
245     * Related to characteristic CHANGEABILITY
246     */
247    public static final String DATA_CHANGEABILITY = "DATA_CHANGEABILITY";
248
249    /**
250     * Related to characteristic CHANGEABILITY
251     */
252    public static final String LOGIC_CHANGEABILITY = "LOGIC_CHANGEABILITY";
253
254    /**
255     * Related to characteristic RELIABILITY
256     */
257    public static final String ARCHITECTURE_RELIABILITY = "ARCHITECTURE_RELIABILITY";
258
259    /**
260     * Related to characteristic RELIABILITY
261     */
262    public static final String DATA_RELIABILITY = "DATA_RELIABILITY";
263
264    /**
265     * Related to characteristic RELIABILITY
266     */
267    public static final String EXCEPTION_HANDLING = "EXCEPTION_HANDLING";
268
269    /**
270     * Related to characteristic RELIABILITY
271     */
272    public static final String FAULT_TOLERANCE = "FAULT_TOLERANCE";
273
274    /**
275     * Related to characteristic RELIABILITY
276     */
277    public static final String INSTRUCTION_RELIABILITY = "INSTRUCTION_RELIABILITY";
278
279    /**
280     * Related to characteristic RELIABILITY
281     */
282    public static final String LOGIC_RELIABILITY = "LOGIC_RELIABILITY";
283
284    /**
285     * Related to characteristic RELIABILITY
286     */
287    public static final String RESOURCE_RELIABILITY = "RESOURCE_RELIABILITY";
288
289    /**
290     * Related to characteristic RELIABILITY
291     */
292    public static final String SYNCHRONIZATION_RELIABILITY = "SYNCHRONIZATION_RELIABILITY";
293
294    /**
295     * Related to characteristic RELIABILITY
296     */
297    public static final String UNIT_TESTS = "UNIT_TESTS";
298
299    /**
300     * Related to characteristic TESTABILITY
301     */
302    public static final String INTEGRATION_TESTABILITY = "INTEGRATION_TESTABILITY";
303
304    /**
305     * Related to characteristic TESTABILITY
306     */
307    public static final String UNIT_TESTABILITY = "UNIT_TESTABILITY";
308
309    /**
310     * Related to characteristic ACCESSIBILITY
311     */
312    public static final String USABILITY_ACCESSIBILITY = "USABILITY_ACCESSIBILITY";
313
314    /**
315     * Related to characteristic ACCESSIBILITY
316     */
317    public static final String USABILITY_COMPLIANCE = "USABILITY_COMPLIANCE";
318
319    /**
320     * Related to characteristic ACCESSIBILITY
321     */
322    public static final String USABILITY_EASE_OF_USE = "USABILITY_EASE_OF_USE";
323
324    /**
325     * Related to characteristic REUSABILITY
326     */
327    public static final String REUSABILITY_COMPLIANCE = "REUSABILITY_COMPLIANCE";
328
329    /**
330     * Related to characteristic PORTABILITY
331     */
332    public static final String PORTABILITY_COMPLIANCE = "PORTABILITY_COMPLIANCE";
333
334    /**
335     * Related to characteristic MAINTAINABILITY
336     */
337    public static final String MAINTAINABILITY_COMPLIANCE = "MAINTAINABILITY_COMPLIANCE";
338
339    /**
340     * Related to characteristic SECURITY
341     */
342    public static final String SECURITY_COMPLIANCE = "SECURITY_COMPLIANCE";
343
344    /**
345     * Related to characteristic EFFICIENCY
346     */
347    public static final String EFFICIENCY_COMPLIANCE = "EFFICIENCY_COMPLIANCE";
348
349    /**
350     * Related to characteristic CHANGEABILITY
351     */
352    public static final String CHANGEABILITY_COMPLIANCE = "CHANGEABILITY_COMPLIANCE";
353
354    /**
355     * Related to characteristic RELIABILITY
356     */
357    public static final String RELIABILITY_COMPLIANCE = "RELIABILITY_COMPLIANCE";
358
359    /**
360     * Related to characteristic TESTABILITY
361     */
362    public static final String TESTABILITY_COMPLIANCE = "TESTABILITY_COMPLIANCE";
363
364    private SubCharacteristics() {
365      // only constants
366    }
367  }
368
369  /**
370   * Instantiated by core but not by plugins, except for their tests.
371   */
372  class Context {
373    private final Map<String, Repository> repositoriesByKey = Maps.newHashMap();
374
375    /**
376     * New builder for {@link org.sonar.api.server.rule.RulesDefinition.Repository}.
377     * <p/>
378     * A plugin can add rules to a repository that is defined then executed by another plugin. For instance
379     * the FbContrib plugin contributes to the Findbugs plugin rules. In this case no need
380     * to execute {@link org.sonar.api.server.rule.RulesDefinition.NewRepository#setName(String)}
381     */
382    public NewRepository createRepository(String key, String language) {
383      return new NewRepositoryImpl(this, key, language);
384    }
385
386    /**
387     * @deprecated since 5.2. Simply use {@link #createRepository(String, String)}
388     */
389    @Deprecated
390    public NewRepository extendRepository(String key, String language) {
391      return createRepository(key, language);
392    }
393
394    @CheckForNull
395    public Repository repository(String key) {
396      return repositoriesByKey.get(key);
397    }
398
399    public List<Repository> repositories() {
400      return ImmutableList.copyOf(repositoriesByKey.values());
401    }
402
403    /**
404     * @deprecated returns empty list since 5.2. Concept of "extended repository" was misleading and not valuable. Simply declare
405     * repositories and use {@link #repositories()}. See http://jira.sonarsource.com/browse/SONAR-6709
406     */
407    @Deprecated
408    public List<ExtendedRepository> extendedRepositories(String repositoryKey) {
409      return Collections.emptyList();
410    }
411
412    /**
413     * @deprecated returns empty list since 5.2. Concept of "extended repository" was misleading and not valuable. Simply declare
414     * repositories and use {@link #repositories()}. See http://jira.sonarsource.com/browse/SONAR-6709
415     */
416    @Deprecated
417    public List<ExtendedRepository> extendedRepositories() {
418      return Collections.emptyList();
419    }
420
421    private void registerRepository(NewRepositoryImpl newRepository) {
422      Repository existing = repositoriesByKey.get(newRepository.key());
423      if (existing != null) {
424        checkState(existing.language().equals(newRepository.language),
425          "The rule repository '%s' must not be defined for two different languages: %s and %s",
426          newRepository.key, existing.language(), newRepository.language);
427      }
428      repositoriesByKey.put(newRepository.key, new RepositoryImpl(newRepository, existing));
429    }
430  }
431
432  interface NewExtendedRepository {
433    /**
434     * Create a rule with specified key. Max length of key is 200 characters.
435     */
436    NewRule createRule(String ruleKey);
437
438    @CheckForNull
439    NewRule rule(String ruleKey);
440
441    Collection<NewRule> rules();
442
443    String key();
444
445    void done();
446  }
447
448  interface NewRepository extends NewExtendedRepository {
449    NewRepository setName(String s);
450  }
451
452  class NewRepositoryImpl implements NewRepository {
453    private final Context context;
454    private final String key;
455    private String language;
456    private String name;
457    private final Map<String, NewRule> newRules = Maps.newHashMap();
458
459    private NewRepositoryImpl(Context context, String key, String language) {
460      this.context = context;
461      this.key = this.name = key;
462      this.language = language;
463    }
464
465    @Override
466    public String key() {
467      return key;
468    }
469
470    @Override
471    public NewRepositoryImpl setName(@Nullable String s) {
472      if (StringUtils.isNotEmpty(s)) {
473        this.name = s;
474      }
475      return this;
476    }
477
478    @Override
479    public NewRule createRule(String ruleKey) {
480      if (newRules.containsKey(ruleKey)) {
481        // Should fail in a perfect world, but at the time being the Findbugs plugin
482        // defines several times the rule EC_INCOMPATIBLE_ARRAY_COMPARE
483        // See http://jira.sonarsource.com/browse/SONARJAVA-428
484        Loggers.get(getClass()).warn(format("The rule '%s' of repository '%s' is declared several times", ruleKey, key));
485      }
486      NewRule newRule = new NewRule(key, ruleKey);
487      newRules.put(ruleKey, newRule);
488      return newRule;
489    }
490
491    @CheckForNull
492    @Override
493    public NewRule rule(String ruleKey) {
494      return newRules.get(ruleKey);
495    }
496
497    @Override
498    public Collection<NewRule> rules() {
499      return newRules.values();
500    }
501
502    @Override
503    public void done() {
504      // note that some validations can be done here, for example for
505      // verifying that at least one rule is declared
506
507      context.registerRepository(this);
508    }
509
510    @Override
511    public String toString() {
512      return Objects.toStringHelper(this)
513        .add("key", key)
514        .add("language", language)
515        .toString();
516    }
517  }
518
519  interface ExtendedRepository {
520    String key();
521
522    String language();
523
524    @CheckForNull
525    Rule rule(String ruleKey);
526
527    List<Rule> rules();
528  }
529
530  interface Repository extends ExtendedRepository {
531    String name();
532  }
533
534  @Immutable
535  class RepositoryImpl implements Repository {
536    private final String key;
537    private final String language;
538    private final String name;
539    private final Map<String, Rule> rulesByKey;
540
541    private RepositoryImpl(NewRepositoryImpl newRepository, @Nullable Repository mergeInto) {
542      this.key = newRepository.key;
543      this.language = newRepository.language;
544
545      Map<String, Rule> ruleBuilder = new HashMap<>();
546      if (mergeInto != null) {
547        if (!StringUtils.equals(newRepository.language, mergeInto.language()) || !StringUtils.equals(newRepository.key, mergeInto.key())) {
548          throw new IllegalArgumentException(format("Bug - language and key of the repositories to be merged should be the sames: %s and %s", newRepository, mergeInto));
549        }
550        this.name = StringUtils.defaultIfBlank(mergeInto.name(), newRepository.name);
551        for (Rule rule : mergeInto.rules()) {
552          if (!newRepository.key().startsWith("common-") && ruleBuilder.containsKey(rule.key())) {
553            Loggers.get(getClass()).warn("The rule '{}' of repository '{}' is declared several times", rule.key(), mergeInto.key());
554          }
555          ruleBuilder.put(rule.key(), rule);
556        }
557      } else {
558        this.name = newRepository.name;
559      }
560      for (NewRule newRule : newRepository.newRules.values()) {
561        newRule.validate();
562        ruleBuilder.put(newRule.key, new Rule(this, newRule));
563      }
564      this.rulesByKey = ImmutableMap.copyOf(ruleBuilder);
565    }
566
567    @Override
568    public String key() {
569      return key;
570    }
571
572    @Override
573    public String language() {
574      return language;
575    }
576
577    @Override
578    public String name() {
579      return name;
580    }
581
582    @Override
583    @CheckForNull
584    public Rule rule(String ruleKey) {
585      return rulesByKey.get(ruleKey);
586    }
587
588    @Override
589    public List<Rule> rules() {
590      return ImmutableList.copyOf(rulesByKey.values());
591    }
592
593    @Override
594    public boolean equals(Object o) {
595      if (this == o) {
596        return true;
597      }
598      if (o == null || getClass() != o.getClass()) {
599        return false;
600      }
601      RepositoryImpl that = (RepositoryImpl) o;
602      return key.equals(that.key);
603    }
604
605    @Override
606    public int hashCode() {
607      return key.hashCode();
608    }
609
610    @Override
611    public String toString() {
612      return Objects.toStringHelper(this)
613        .add("language", language)
614        .add("key", key)
615        .toString();
616    }
617  }
618
619  /**
620   * Factory of {@link org.sonar.api.server.debt.DebtRemediationFunction}.
621   */
622  interface DebtRemediationFunctions {
623
624    /**
625     * Shortcut for {@code create(Type.LINEAR, coefficient, null)}.
626     * @param coefficient the duration to fix one issue. See {@link DebtRemediationFunction} for details about format.
627     * @see org.sonar.api.server.debt.DebtRemediationFunction.Type#LINEAR
628     */
629    DebtRemediationFunction linear(String coefficient);
630
631    /**
632     * Shortcut for {@code create(Type.LINEAR_OFFSET, coefficient, offset)}.
633     * @param coefficient duration to fix one point of complexity. See {@link DebtRemediationFunction} for details and format.
634     * @param offset duration to make basic analysis. See {@link DebtRemediationFunction} for details and format.
635     * @see org.sonar.api.server.debt.DebtRemediationFunction.Type#LINEAR_OFFSET
636     */
637    DebtRemediationFunction linearWithOffset(String coefficient, String offset);
638
639    /**
640     * Shortcut for {@code create(Type.CONSTANT_ISSUE, null, constant)}.
641     * @param constant cost per issue. See {@link DebtRemediationFunction} for details and format.
642     * @see org.sonar.api.server.debt.DebtRemediationFunction.Type#CONSTANT_ISSUE
643     */
644    DebtRemediationFunction constantPerIssue(String constant);
645
646    /**
647     * Flexible way to create a {@link DebtRemediationFunction}. An unchecked exception is thrown if
648     * coefficient and/or offset are not valid according to the given @{code type}.
649     * @since 5.3
650     */
651    DebtRemediationFunction create(DebtRemediationFunction.Type type, @Nullable String coefficient, @Nullable String offset);
652  }
653
654  class NewRule {
655    private final String repoKey;
656    private final String key;
657    private String name;
658    private String htmlDescription;
659    private String markdownDescription;
660    private String internalKey;
661    private String severity = Severity.MAJOR;
662    private boolean template;
663    private RuleStatus status = RuleStatus.defaultStatus();
664    private String debtSubCharacteristic;
665    private DebtRemediationFunction debtRemediationFunction;
666    private String effortToFixDescription;
667    private final Set<String> tags = Sets.newTreeSet();
668    private final Map<String, NewParam> paramsByKey = Maps.newHashMap();
669    private final DebtRemediationFunctions functions;
670
671    private NewRule(String repoKey, String key) {
672      this.repoKey = repoKey;
673      this.key = key;
674      this.functions = new DefaultDebtRemediationFunctions(repoKey, key);
675    }
676
677    public String key() {
678      return this.key;
679    }
680
681    /**
682     * Required rule name
683     */
684    public NewRule setName(String s) {
685      this.name = trimToNull(s);
686      return this;
687    }
688
689    public NewRule setTemplate(boolean template) {
690      this.template = template;
691      return this;
692    }
693
694    public NewRule setSeverity(String s) {
695      checkArgument(Severity.ALL.contains(s), "Severity of rule %s is not correct: %s", this, s);
696      this.severity = s;
697      return this;
698    }
699
700    /**
701     * The optional description, in HTML format, has no max length. It's exclusive with markdown description
702     * (see {@link #setMarkdownDescription(String)})
703     */
704    public NewRule setHtmlDescription(@Nullable String s) {
705      checkState(markdownDescription == null, "Rule '%s' already has a Markdown description", this);
706      this.htmlDescription = trimToNull(s);
707      return this;
708    }
709
710    /**
711     * Load description from a file available in classpath. Example : <code>setHtmlDescription(getClass().getResource("/myrepo/Rule1234.html")</code>
712     */
713    public NewRule setHtmlDescription(@Nullable URL classpathUrl) {
714      if (classpathUrl != null) {
715        try {
716          setHtmlDescription(IOUtils.toString(classpathUrl));
717        } catch (IOException e) {
718          throw new IllegalStateException("Fail to read: " + classpathUrl, e);
719        }
720      } else {
721        this.htmlDescription = null;
722      }
723      return this;
724    }
725
726    /**
727     * The optional description, in a restricted Markdown format, has no max length. It's exclusive with HTML description
728     * (see {@link #setHtmlDescription(String)})
729     */
730    public NewRule setMarkdownDescription(@Nullable String s) {
731      checkState(htmlDescription == null, "Rule '%s' already has an HTML description", this);
732      this.markdownDescription = trimToNull(s);
733      return this;
734    }
735
736    /**
737     * Load description from a file available in classpath. Example : <code>setMarkdownDescription(getClass().getResource("/myrepo/Rule1234.md")</code>
738     */
739    public NewRule setMarkdownDescription(@Nullable URL classpathUrl) {
740      if (classpathUrl != null) {
741        try {
742          setMarkdownDescription(IOUtils.toString(classpathUrl));
743        } catch (IOException e) {
744          throw new IllegalStateException("Fail to read: " + classpathUrl, e);
745        }
746      } else {
747        this.markdownDescription = null;
748      }
749      return this;
750    }
751
752    /**
753     * Default value is {@link org.sonar.api.rule.RuleStatus#READY}. The value
754     * {@link org.sonar.api.rule.RuleStatus#REMOVED} is not accepted and raises an
755     * {@link java.lang.IllegalArgumentException}.
756     */
757    public NewRule setStatus(RuleStatus status) {
758      checkArgument(RuleStatus.REMOVED != status, "Status 'REMOVED' is not accepted on rule '%s'", this);
759      this.status = status;
760      return this;
761    }
762
763    /**
764     * SQALE sub-characteristic. See http://www.sqale.org
765     *
766     * @see org.sonar.api.server.rule.RulesDefinition.SubCharacteristics for constant values
767     */
768    public NewRule setDebtSubCharacteristic(@Nullable String s) {
769      this.debtSubCharacteristic = s;
770      return this;
771    }
772
773    /**
774     * Factory of {@link org.sonar.api.server.debt.DebtRemediationFunction}
775     */
776    public DebtRemediationFunctions debtRemediationFunctions() {
777      return functions;
778    }
779
780    /**
781     * @see #debtRemediationFunctions()
782     */
783    public NewRule setDebtRemediationFunction(@Nullable DebtRemediationFunction fn) {
784      this.debtRemediationFunction = fn;
785      return this;
786    }
787
788    /**
789     * For rules that use LINEAR or LINEAR_OFFSET remediation functions, the meaning
790     * of the function parameter (= "effort to fix") must be set. This description
791     * explains what 1 point of "effort to fix" represents for the rule.
792     * <p/>
793     * Example: for the "Insufficient condition coverage", this description for the
794     * remediation function coefficient/offset would be something like
795     * "Effort to test one uncovered condition".
796     */
797    public NewRule setEffortToFixDescription(@Nullable String s) {
798      this.effortToFixDescription = s;
799      return this;
800    }
801
802    /**
803     * Create a parameter with given unique key. Max length of key is 128 characters.
804     */
805    public NewParam createParam(String paramKey) {
806      checkArgument(!paramsByKey.containsKey(paramKey), "The parameter '%s' is declared several times on the rule %s", paramKey, this);
807      NewParam param = new NewParam(paramKey);
808      paramsByKey.put(paramKey, param);
809      return param;
810    }
811
812    @CheckForNull
813    public NewParam param(String paramKey) {
814      return paramsByKey.get(paramKey);
815    }
816
817    public Collection<NewParam> params() {
818      return paramsByKey.values();
819    }
820
821    /**
822     * @see RuleTagFormat
823     */
824    public NewRule addTags(String... list) {
825      for (String tag : list) {
826        RuleTagFormat.validate(tag);
827        tags.add(tag);
828      }
829      return this;
830    }
831
832    /**
833     * @see RuleTagFormat
834     */
835    public NewRule setTags(String... list) {
836      tags.clear();
837      addTags(list);
838      return this;
839    }
840
841    /**
842     * Optional key that can be used by the rule engine. Not displayed
843     * in webapp. For example the Java Checkstyle plugin feeds this field
844     * with the internal path ("Checker/TreeWalker/AnnotationUseStyle").
845     */
846    public NewRule setInternalKey(@Nullable String s) {
847      this.internalKey = s;
848      return this;
849    }
850
851    private void validate() {
852      if (Strings.isNullOrEmpty(name)) {
853        throw new IllegalStateException(format("Name of rule %s is empty", this));
854      }
855      if (Strings.isNullOrEmpty(htmlDescription) && Strings.isNullOrEmpty(markdownDescription)) {
856        throw new IllegalStateException(format("One of HTML description or Markdown description must be defined for rule %s", this));
857      }
858      if ((Strings.isNullOrEmpty(debtSubCharacteristic) && debtRemediationFunction != null) || (!Strings.isNullOrEmpty(debtSubCharacteristic) && debtRemediationFunction == null)) {
859        throw new IllegalStateException(format("Both debt sub-characteristic and debt remediation function should be defined on rule '%s'", this));
860      }
861    }
862
863    @Override
864    public String toString() {
865      return format("[repository=%s, key=%s]", repoKey, key);
866    }
867  }
868
869  @Immutable
870  class Rule {
871    private final Repository repository;
872    private final String repoKey;
873    private final String key;
874    private final String name;
875    private final String htmlDescription;
876    private final String markdownDescription;
877    private final String internalKey;
878    private final String severity;
879    private final boolean template;
880    private final String debtSubCharacteristic;
881    private final DebtRemediationFunction debtRemediationFunction;
882    private final String effortToFixDescription;
883    private final Set<String> tags;
884    private final Map<String, Param> params;
885    private final RuleStatus status;
886
887    private Rule(Repository repository, NewRule newRule) {
888      this.repository = repository;
889      this.repoKey = newRule.repoKey;
890      this.key = newRule.key;
891      this.name = newRule.name;
892      this.htmlDescription = newRule.htmlDescription;
893      this.markdownDescription = newRule.markdownDescription;
894      this.internalKey = newRule.internalKey;
895      this.severity = newRule.severity;
896      this.template = newRule.template;
897      this.status = newRule.status;
898      this.debtSubCharacteristic = newRule.debtSubCharacteristic;
899      this.debtRemediationFunction = newRule.debtRemediationFunction;
900      this.effortToFixDescription = newRule.effortToFixDescription;
901      this.tags = ImmutableSortedSet.copyOf(newRule.tags);
902      ImmutableMap.Builder<String, Param> paramsBuilder = ImmutableMap.builder();
903      for (NewParam newParam : newRule.paramsByKey.values()) {
904        paramsBuilder.put(newParam.key, new Param(newParam));
905      }
906      this.params = paramsBuilder.build();
907    }
908
909    public Repository repository() {
910      return repository;
911    }
912
913    public String key() {
914      return key;
915    }
916
917    public String name() {
918      return name;
919    }
920
921    public String severity() {
922      return severity;
923    }
924
925    @CheckForNull
926    public String htmlDescription() {
927      return htmlDescription;
928    }
929
930    @CheckForNull
931    public String markdownDescription() {
932      return markdownDescription;
933    }
934
935    public boolean template() {
936      return template;
937    }
938
939    public RuleStatus status() {
940      return status;
941    }
942
943    @CheckForNull
944    public String debtSubCharacteristic() {
945      return debtSubCharacteristic;
946    }
947
948    @CheckForNull
949    public DebtRemediationFunction debtRemediationFunction() {
950      return debtRemediationFunction;
951    }
952
953    @CheckForNull
954    public String effortToFixDescription() {
955      return effortToFixDescription;
956    }
957
958    @CheckForNull
959    public Param param(String key) {
960      return params.get(key);
961    }
962
963    public List<Param> params() {
964      return ImmutableList.copyOf(params.values());
965    }
966
967    public Set<String> tags() {
968      return tags;
969    }
970
971    /**
972     * @see RulesDefinition.NewRule#setInternalKey(String)
973     */
974    @CheckForNull
975    public String internalKey() {
976      return internalKey;
977    }
978
979    @Override
980    public boolean equals(Object o) {
981      if (this == o) {
982        return true;
983      }
984      if (o == null || getClass() != o.getClass()) {
985        return false;
986      }
987      Rule other = (Rule) o;
988      return key.equals(other.key) && repoKey.equals(other.repoKey);
989    }
990
991    @Override
992    public int hashCode() {
993      int result = repoKey.hashCode();
994      result = 31 * result + key.hashCode();
995      return result;
996    }
997
998    @Override
999    public String toString() {
1000      return format("[repository=%s, key=%s]", repoKey, key);
1001    }
1002  }
1003
1004  class NewParam {
1005    private final String key;
1006    private String name;
1007    private String description;
1008    private String defaultValue;
1009    private RuleParamType type = RuleParamType.STRING;
1010
1011    private NewParam(String key) {
1012      this.key = this.name = key;
1013    }
1014
1015    public String key() {
1016      return key;
1017    }
1018
1019    public NewParam setName(@Nullable String s) {
1020      // name must never be null.
1021      this.name = StringUtils.defaultIfBlank(s, key);
1022      return this;
1023    }
1024
1025    public NewParam setType(RuleParamType t) {
1026      this.type = t;
1027      return this;
1028    }
1029
1030    /**
1031     * Plain-text description. Can be null. Max length is 4000 characters.
1032     */
1033    public NewParam setDescription(@Nullable String s) {
1034      this.description = StringUtils.defaultIfBlank(s, null);
1035      return this;
1036    }
1037
1038    /**
1039     * Empty default value will be converted to null. Max length is 4000 characters.
1040     */
1041    public NewParam setDefaultValue(@Nullable String s) {
1042      this.defaultValue = Strings.emptyToNull(s);
1043      return this;
1044    }
1045  }
1046
1047  @Immutable
1048  class Param {
1049    private final String key;
1050    private final String name;
1051    private final String description;
1052    private final String defaultValue;
1053    private final RuleParamType type;
1054
1055    private Param(NewParam newParam) {
1056      this.key = newParam.key;
1057      this.name = newParam.name;
1058      this.description = newParam.description;
1059      this.defaultValue = newParam.defaultValue;
1060      this.type = newParam.type;
1061    }
1062
1063    public String key() {
1064      return key;
1065    }
1066
1067    public String name() {
1068      return name;
1069    }
1070
1071    @Nullable
1072    public String description() {
1073      return description;
1074    }
1075
1076    @Nullable
1077    public String defaultValue() {
1078      return defaultValue;
1079    }
1080
1081    public RuleParamType type() {
1082      return type;
1083    }
1084
1085    @Override
1086    public boolean equals(Object o) {
1087      if (this == o) {
1088        return true;
1089      }
1090      if (o == null || getClass() != o.getClass()) {
1091        return false;
1092      }
1093      Param that = (Param) o;
1094      return key.equals(that.key);
1095    }
1096
1097    @Override
1098    public int hashCode() {
1099      return key.hashCode();
1100    }
1101  }
1102
1103  /**
1104   * This method is executed when server is started.
1105   */
1106  void define(Context context);
1107
1108}