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     */
020    package org.sonar.api.server.rule;
021    
022    import com.google.common.collect.*;
023    import org.apache.commons.io.IOUtils;
024    import org.apache.commons.lang.StringUtils;
025    import org.slf4j.LoggerFactory;
026    import org.sonar.api.ServerExtension;
027    import org.sonar.api.rule.RuleStatus;
028    import org.sonar.api.rule.Severity;
029    
030    import javax.annotation.CheckForNull;
031    import javax.annotation.Nullable;
032    import javax.annotation.concurrent.Immutable;
033    import java.io.IOException;
034    import java.io.InputStream;
035    import java.net.URL;
036    import java.util.List;
037    import java.util.Map;
038    import 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     */
048    public 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    }