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