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