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