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