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