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