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