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