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