001/*
002 * SonarQube
003 * Copyright (C) 2009-2016 SonarSource SA
004 * mailto:contact AT sonarsource DOT com
005 *
006 * This program 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 * This program 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.batch.rule;
021
022import com.google.common.collect.Maps;
023import java.lang.reflect.Field;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.IdentityHashMap;
028import java.util.List;
029import java.util.Map;
030import javax.annotation.CheckForNull;
031import org.apache.commons.lang.StringUtils;
032import org.sonar.api.rule.RuleKey;
033import org.sonar.api.utils.AnnotationUtils;
034import org.sonar.api.utils.FieldUtils2;
035import org.sonar.api.utils.SonarException;
036import org.sonar.check.RuleProperty;
037
038/**
039 * Instantiates checks (objects that provide implementation of coding
040 * rules) that use sonar-check-api annotations. Checks are selected and configured
041 * from the Quality profiles enabled on the current module.
042 * <br>
043 * Example of check class:
044 * <pre>
045 *   {@literal @}org.sonar.check.Rule(key = "S001")
046 *   public class CheckS001 {
047 *     {@literal @}org.sonar.check.RuleProperty
048 *     private String pattern;
049 *
050 *     public String getPattern() {
051 *       return pattern;
052 *     }
053 * }
054 * </pre>
055 * How to use:
056 * <pre>
057 *   public class MyRuleEngine extends BatchExtension {
058 *     private final CheckFactory checkFactory;
059 *
060 *     public MyRuleEngine(CheckFactory checkFactory) {
061 *       this.checkFactory = checkFactory;
062 *     }
063 *
064 *     public void execute() {
065 *       Checks checks = checkFactory.create("my-rule-repository");
066 *       checks.addAnnotatedChecks(CheckS001.class);
067 *       // checks.all() contains an instance of CheckS001
068 *       // with field "pattern" set to the value specified in
069 *       // the Quality profile
070 *
071 *       // Checks are used to detect issues on source code
072 *
073 *       // checks.ruleKey(obj) can be used to create the related issues
074 *     }
075 *   }
076 * </pre>
077 * <br>
078 * It replaces org.sonar.api.checks.AnnotationCheckFactory
079 *
080 * @since 4.2
081 */
082public class Checks<C> {
083  private final ActiveRules activeRules;
084  private final String repository;
085  private final Map<RuleKey, C> checkByRule = new HashMap<>();
086  private final Map<C, RuleKey> ruleByCheck = new IdentityHashMap<>();
087
088  Checks(ActiveRules activeRules, String repository) {
089    this.activeRules = activeRules;
090    this.repository = repository;
091  }
092
093  @CheckForNull
094  public C of(RuleKey ruleKey) {
095    return checkByRule.get(ruleKey);
096  }
097
098  public Collection<C> all() {
099    return checkByRule.values();
100  }
101
102  @CheckForNull
103  public RuleKey ruleKey(C check) {
104    return ruleByCheck.get(check);
105  }
106
107  private void add(RuleKey ruleKey, C obj) {
108    checkByRule.put(ruleKey, obj);
109    ruleByCheck.put(obj, ruleKey);
110  }
111
112  public Checks<C> addAnnotatedChecks(Object... checkClassesOrObjects) {
113    return addAnnotatedChecks((Iterable) Arrays.asList(checkClassesOrObjects));
114  }
115
116  /**
117   * @deprecated since 5.2 use {@link #addAnnotatedChecks(Iterable)}
118   */
119  @Deprecated
120  public Checks<C> addAnnotatedChecks(Collection checkClassesOrObjects) {
121    return addAnnotatedChecks((Iterable) checkClassesOrObjects);
122  }
123
124  public Checks<C> addAnnotatedChecks(Iterable checkClassesOrObjects) {
125    Map<String, Object> checksByEngineKey = Maps.newHashMap();
126    for (Object checkClassesOrObject : checkClassesOrObjects) {
127      String engineKey = annotatedEngineKey(checkClassesOrObject);
128      if (engineKey != null) {
129        checksByEngineKey.put(engineKey, checkClassesOrObject);
130      }
131    }
132
133    for (ActiveRule activeRule : activeRules.findByRepository(repository)) {
134      String engineKey = StringUtils.defaultIfBlank(activeRule.templateRuleKey(), activeRule.ruleKey().rule());
135      Object checkClassesOrObject = checksByEngineKey.get(engineKey);
136      if (checkClassesOrObject != null) {
137        Object obj = instantiate(activeRule, checkClassesOrObject);
138        add(activeRule.ruleKey(), (C) obj);
139      }
140    }
141    return this;
142  }
143
144  private static String annotatedEngineKey(Object annotatedClassOrObject) {
145    String key = null;
146    org.sonar.check.Rule ruleAnnotation = AnnotationUtils.getAnnotation(annotatedClassOrObject, org.sonar.check.Rule.class);
147    if (ruleAnnotation != null) {
148      key = ruleAnnotation.key();
149    }
150    Class clazz = annotatedClassOrObject.getClass();
151    if (annotatedClassOrObject instanceof Class) {
152      clazz = (Class) annotatedClassOrObject;
153    }
154    return StringUtils.defaultIfEmpty(key, clazz.getCanonicalName());
155  }
156
157  private static Object instantiate(ActiveRule activeRule, Object checkClassOrInstance) {
158    try {
159      Object check = checkClassOrInstance;
160      if (check instanceof Class) {
161        check = ((Class) checkClassOrInstance).newInstance();
162      }
163      configureFields(activeRule, check);
164      return check;
165    } catch (InstantiationException | IllegalAccessException e) {
166      throw failToInstantiateCheck(activeRule, checkClassOrInstance, e);
167    }
168  }
169
170  private static RuntimeException failToInstantiateCheck(ActiveRule activeRule, Object checkClassOrInstance, Exception e) {
171    throw new IllegalStateException(String.format("Fail to instantiate class %s for rule %s", checkClassOrInstance, activeRule.ruleKey()), e);
172  }
173
174  private static void configureFields(ActiveRule activeRule, Object check) {
175    for (Map.Entry<String, String> param : activeRule.params().entrySet()) {
176      Field field = getField(check, param.getKey());
177      if (field == null) {
178        throw new IllegalStateException(
179          String.format("The field '%s' does not exist or is not annotated with @RuleProperty in the class %s", param.getKey(), check.getClass().getName()));
180      }
181      if (StringUtils.isNotBlank(param.getValue())) {
182        configureField(check, field, param.getValue());
183      }
184    }
185  }
186
187  @CheckForNull
188  private static Field getField(Object check, String key) {
189    List<Field> fields = FieldUtils2.getFields(check.getClass(), true);
190    for (Field field : fields) {
191      RuleProperty propertyAnnotation = field.getAnnotation(RuleProperty.class);
192      if (propertyAnnotation != null && (StringUtils.equals(key, field.getName()) || StringUtils.equals(key, propertyAnnotation.key()))) {
193        return field;
194      }
195    }
196    return null;
197  }
198
199  private static void configureField(Object check, Field field, String value) {
200    try {
201      field.setAccessible(true);
202
203      if (field.getType().equals(String.class)) {
204        field.set(check, value);
205
206      } else if (int.class == field.getType()) {
207        field.setInt(check, Integer.parseInt(value));
208
209      } else if (short.class == field.getType()) {
210        field.setShort(check, Short.parseShort(value));
211
212      } else if (long.class == field.getType()) {
213        field.setLong(check, Long.parseLong(value));
214
215      } else if (double.class == field.getType()) {
216        field.setDouble(check, Double.parseDouble(value));
217
218      } else if (boolean.class == field.getType()) {
219        field.setBoolean(check, Boolean.parseBoolean(value));
220
221      } else if (byte.class == field.getType()) {
222        field.setByte(check, Byte.parseByte(value));
223
224      } else if (Integer.class == field.getType()) {
225        field.set(check, Integer.parseInt(value));
226
227      } else if (Long.class == field.getType()) {
228        field.set(check, Long.parseLong(value));
229
230      } else if (Double.class == field.getType()) {
231        field.set(check, Double.parseDouble(value));
232
233      } else if (Boolean.class == field.getType()) {
234        field.set(check, Boolean.parseBoolean(value));
235
236      } else {
237        throw new SonarException("The type of the field " + field + " is not supported: " + field.getType());
238      }
239    } catch (IllegalAccessException e) {
240      throw new SonarException("Can not set the value of the field " + field + " in the class: " + check.getClass().getName(), e);
241    }
242  }
243}