View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.api;
21  
22  import java.beans.PropertyDescriptor;
23  import java.lang.reflect.InvocationTargetException;
24  import java.net.URI;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.StringTokenizer;
30  import java.util.regex.Pattern;
31  
32  import org.apache.commons.beanutils.BeanUtilsBean;
33  import org.apache.commons.beanutils.ConversionException;
34  import org.apache.commons.beanutils.ConvertUtilsBean;
35  import org.apache.commons.beanutils.Converter;
36  import org.apache.commons.beanutils.PropertyUtils;
37  import org.apache.commons.beanutils.PropertyUtilsBean;
38  import org.apache.commons.beanutils.converters.ArrayConverter;
39  import org.apache.commons.beanutils.converters.BooleanConverter;
40  import org.apache.commons.beanutils.converters.ByteConverter;
41  import org.apache.commons.beanutils.converters.CharacterConverter;
42  import org.apache.commons.beanutils.converters.DoubleConverter;
43  import org.apache.commons.beanutils.converters.FloatConverter;
44  import org.apache.commons.beanutils.converters.IntegerConverter;
45  import org.apache.commons.beanutils.converters.LongConverter;
46  import org.apache.commons.beanutils.converters.ShortConverter;
47  
48  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifier;
49  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
50  
51  /**
52   * A Java Bean that implements the component lifecycle interfaces by
53   * calling the bean's setters for all configuration attributes.
54   * @author lkuehne
55   */
56  // -@cs[AbstractClassName] We can not brake compatibility with previous versions.
57  public abstract class AutomaticBean
58      implements Configurable, Contextualizable {
59  
60      /**
61       * Enum to specify behaviour regarding ignored modules.
62       */
63      public enum OutputStreamOptions {
64          /**
65           * Close stream in the end.
66           */
67          CLOSE,
68  
69          /**
70           * Do nothing in the end.
71           */
72          NONE
73      }
74  
75      /** Comma separator for StringTokenizer. */
76      private static final String COMMA_SEPARATOR = ",";
77  
78      /** The configuration of this bean. */
79      private Configuration configuration;
80  
81      /**
82       * Provides a hook to finish the part of this component's setup that
83       * was not handled by the bean introspection.
84       * <p>
85       * The default implementation does nothing.
86       * </p>
87       * @throws CheckstyleException if there is a configuration error.
88       */
89      protected abstract void finishLocalSetup() throws CheckstyleException;
90  
91      /**
92       * Creates a BeanUtilsBean that is configured to use
93       * type converters that throw a ConversionException
94       * instead of using the default value when something
95       * goes wrong.
96       *
97       * @return a configured BeanUtilsBean
98       */
99      private static BeanUtilsBean createBeanUtilsBean() {
100         final ConvertUtilsBean cub = new ConvertUtilsBean();
101 
102         registerIntegralTypes(cub);
103         registerCustomTypes(cub);
104 
105         return new BeanUtilsBean(cub, new PropertyUtilsBean());
106     }
107 
108     /**
109      * Register basic types of JDK like boolean, int, and String to use with BeanUtils. All these
110      * types are found in the {@code java.lang} package.
111      * @param cub
112      *            Instance of {@link ConvertUtilsBean} to register types with.
113      */
114     private static void registerIntegralTypes(ConvertUtilsBean cub) {
115         cub.register(new BooleanConverter(), Boolean.TYPE);
116         cub.register(new BooleanConverter(), Boolean.class);
117         cub.register(new ArrayConverter(
118             boolean[].class, new BooleanConverter()), boolean[].class);
119         cub.register(new ByteConverter(), Byte.TYPE);
120         cub.register(new ByteConverter(), Byte.class);
121         cub.register(new ArrayConverter(byte[].class, new ByteConverter()),
122             byte[].class);
123         cub.register(new CharacterConverter(), Character.TYPE);
124         cub.register(new CharacterConverter(), Character.class);
125         cub.register(new ArrayConverter(char[].class, new CharacterConverter()),
126             char[].class);
127         cub.register(new DoubleConverter(), Double.TYPE);
128         cub.register(new DoubleConverter(), Double.class);
129         cub.register(new ArrayConverter(double[].class, new DoubleConverter()),
130             double[].class);
131         cub.register(new FloatConverter(), Float.TYPE);
132         cub.register(new FloatConverter(), Float.class);
133         cub.register(new ArrayConverter(float[].class, new FloatConverter()),
134             float[].class);
135         cub.register(new IntegerConverter(), Integer.TYPE);
136         cub.register(new IntegerConverter(), Integer.class);
137         cub.register(new ArrayConverter(int[].class, new IntegerConverter()),
138             int[].class);
139         cub.register(new LongConverter(), Long.TYPE);
140         cub.register(new LongConverter(), Long.class);
141         cub.register(new ArrayConverter(long[].class, new LongConverter()),
142             long[].class);
143         cub.register(new ShortConverter(), Short.TYPE);
144         cub.register(new ShortConverter(), Short.class);
145         cub.register(new ArrayConverter(short[].class, new ShortConverter()),
146             short[].class);
147         cub.register(new RelaxedStringArrayConverter(), String[].class);
148 
149         // BigDecimal, BigInteger, Class, Date, String, Time, TimeStamp
150         // do not use defaults in the default configuration of ConvertUtilsBean
151     }
152 
153     /**
154      * Register custom types of JDK like URI and Checkstyle specific classes to use with BeanUtils.
155      * None of these types should be found in the {@code java.lang} package.
156      * @param cub
157      *            Instance of {@link ConvertUtilsBean} to register types with.
158      */
159     private static void registerCustomTypes(ConvertUtilsBean cub) {
160         cub.register(new PatternConverter(), Pattern.class);
161         cub.register(new SeverityLevelConverter(), SeverityLevel.class);
162         cub.register(new ScopeConverter(), Scope.class);
163         cub.register(new UriConverter(), URI.class);
164         cub.register(new RelaxedAccessModifierArrayConverter(), AccessModifier[].class);
165     }
166 
167     /**
168      * Implements the Configurable interface using bean introspection.
169      *
170      * <p>Subclasses are allowed to add behaviour. After the bean
171      * based setup has completed first the method
172      * {@link #finishLocalSetup finishLocalSetup}
173      * is called to allow completion of the bean's local setup,
174      * after that the method {@link #setupChild setupChild}
175      * is called for each {@link Configuration#getChildren child Configuration}
176      * of {@code configuration}.
177      *
178      * @see Configurable
179      */
180     @Override
181     public final void configure(Configuration config)
182             throws CheckstyleException {
183         configuration = config;
184 
185         final String[] attributes = config.getAttributeNames();
186 
187         for (final String key : attributes) {
188             final String value = config.getAttribute(key);
189 
190             tryCopyProperty(config.getName(), key, value, true);
191         }
192 
193         finishLocalSetup();
194 
195         final Configuration[] childConfigs = config.getChildren();
196         for (final Configuration childConfig : childConfigs) {
197             setupChild(childConfig);
198         }
199     }
200 
201     /**
202      * Recheck property and try to copy it.
203      * @param moduleName name of the module/class
204      * @param key key of value
205      * @param value value
206      * @param recheck whether to check for property existence before copy
207      * @throws CheckstyleException then property defined incorrectly
208      */
209     private void tryCopyProperty(String moduleName, String key, Object value, boolean recheck)
210             throws CheckstyleException {
211 
212         final BeanUtilsBean beanUtils = createBeanUtilsBean();
213 
214         try {
215             if (recheck) {
216                 // BeanUtilsBean.copyProperties silently ignores missing setters
217                 // for key, so we have to go through great lengths here to
218                 // figure out if the bean property really exists.
219                 final PropertyDescriptor descriptor =
220                         PropertyUtils.getPropertyDescriptor(this, key);
221                 if (descriptor == null) {
222                     final String message = String.format(Locale.ROOT, "Property '%s' in module %s "
223                             + "does not exist, please check the documentation", key, moduleName);
224                     throw new CheckstyleException(message);
225                 }
226             }
227             // finally we can set the bean property
228             beanUtils.copyProperty(this, key, value);
229         }
230         catch (final InvocationTargetException | IllegalAccessException
231                 | NoSuchMethodException ex) {
232             // There is no way to catch IllegalAccessException | NoSuchMethodException
233             // as we do PropertyUtils.getPropertyDescriptor before beanUtils.copyProperty
234             // so we have to join these exceptions with InvocationTargetException
235             // to satisfy UTs coverage
236             final String message = String.format(Locale.ROOT,
237                     "Cannot set property '%s' to '%s' in module %s", key, value, moduleName);
238             throw new CheckstyleException(message, ex);
239         }
240         catch (final IllegalArgumentException | ConversionException ex) {
241             final String message = String.format(Locale.ROOT, "illegal value '%s' for property "
242                     + "'%s' of module %s", value, key, moduleName);
243             throw new CheckstyleException(message, ex);
244         }
245     }
246 
247     /**
248      * Implements the Contextualizable interface using bean introspection.
249      * @see Contextualizable
250      */
251     @Override
252     public final void contextualize(Context context)
253             throws CheckstyleException {
254 
255         final Collection<String> attributes = context.getAttributeNames();
256 
257         for (final String key : attributes) {
258             final Object value = context.get(key);
259 
260             tryCopyProperty(getClass().getName(), key, value, false);
261         }
262     }
263 
264     /**
265      * Returns the configuration that was used to configure this component.
266      * @return the configuration that was used to configure this component.
267      */
268     protected final Configuration getConfiguration() {
269         return configuration;
270     }
271 
272     /**
273      * Called by configure() for every child of this component's Configuration.
274      * <p>
275      * The default implementation throws {@link CheckstyleException} if
276      * {@code childConf} is {@code null} because it doesn't support children. It
277      * must be overridden to validate and support children that are wanted.
278      * </p>
279      *
280      * @param childConf a child of this component's Configuration
281      * @throws CheckstyleException if there is a configuration error.
282      * @see Configuration#getChildren
283      */
284     protected void setupChild(Configuration childConf)
285             throws CheckstyleException {
286         if (childConf != null) {
287             throw new CheckstyleException(childConf.getName() + " is not allowed as a child in "
288                     + configuration.getName() + ". Please review 'Parent Module' section "
289                     + "for this Check in web documentation if Check is standard.");
290         }
291     }
292 
293     /** A converter that converts strings to patterns. */
294     private static class PatternConverter implements Converter {
295         @SuppressWarnings({"unchecked", "rawtypes"})
296         @Override
297         public Object convert(Class type, Object value) {
298             return CommonUtils.createPattern(value.toString());
299         }
300     }
301 
302     /** A converter that converts strings to severity level. */
303     private static class SeverityLevelConverter implements Converter {
304         @SuppressWarnings({"unchecked", "rawtypes"})
305         @Override
306         public Object convert(Class type, Object value) {
307             return SeverityLevel.getInstance(value.toString());
308         }
309     }
310 
311     /** A converter that converts strings to scope. */
312     private static class ScopeConverter implements Converter {
313         @SuppressWarnings({"unchecked", "rawtypes"})
314         @Override
315         public Object convert(Class type, Object value) {
316             return Scope.getInstance(value.toString());
317         }
318     }
319 
320     /** A converter that converts strings to uri. */
321     private static class UriConverter implements Converter {
322         @SuppressWarnings({"unchecked", "rawtypes"})
323         @Override
324         public Object convert(Class type, Object value) {
325             final String url = value.toString();
326             URI result = null;
327 
328             if (!CommonUtils.isBlank(url)) {
329                 try {
330                     result = CommonUtils.getUriByFilename(url);
331                 }
332                 catch (CheckstyleException ex) {
333                     throw new IllegalArgumentException(ex);
334                 }
335             }
336 
337             return result;
338         }
339     }
340 
341     /**
342      * A converter that does not care whether the array elements contain String
343      * characters like '*' or '_'. The normal ArrayConverter class has problems
344      * with this characters.
345      */
346     private static class RelaxedStringArrayConverter implements Converter {
347         @SuppressWarnings({"unchecked", "rawtypes"})
348         @Override
349         public Object convert(Class type, Object value) {
350             // Convert to a String and trim it for the tokenizer.
351             final StringTokenizer tokenizer = new StringTokenizer(
352                 value.toString().trim(), COMMA_SEPARATOR);
353             final List<String> result = new ArrayList<>();
354 
355             while (tokenizer.hasMoreTokens()) {
356                 final String token = tokenizer.nextToken();
357                 result.add(token.trim());
358             }
359 
360             return result.toArray(new String[result.size()]);
361         }
362     }
363 
364     /**
365      * A converter that converts strings to {@link AccessModifier}.
366      * This implementation does not care whether the array elements contain characters like '_'.
367      * The normal {@link ArrayConverter} class has problems with this character.
368      */
369     private static class RelaxedAccessModifierArrayConverter implements Converter {
370 
371         @SuppressWarnings({"unchecked", "rawtypes"})
372         @Override
373         public Object convert(Class type, Object value) {
374             // Converts to a String and trims it for the tokenizer.
375             final StringTokenizer tokenizer = new StringTokenizer(
376                 value.toString().trim(), COMMA_SEPARATOR);
377             final List<AccessModifier> result = new ArrayList<>();
378 
379             while (tokenizer.hasMoreTokens()) {
380                 final String token = tokenizer.nextToken();
381                 result.add(AccessModifier.getInstance(token.trim()));
382             }
383 
384             return result.toArray(new AccessModifier[result.size()]);
385         }
386     }
387 }