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