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