001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.beans.PropertyDescriptor;
023import java.lang.reflect.InvocationTargetException;
024import java.net.URI;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.List;
028import java.util.Locale;
029import java.util.StringTokenizer;
030import java.util.regex.Pattern;
031
032import javax.annotation.Nullable;
033
034import org.apache.commons.beanutils.BeanUtilsBean;
035import org.apache.commons.beanutils.ConversionException;
036import org.apache.commons.beanutils.ConvertUtilsBean;
037import org.apache.commons.beanutils.Converter;
038import org.apache.commons.beanutils.PropertyUtils;
039import org.apache.commons.beanutils.PropertyUtilsBean;
040import org.apache.commons.beanutils.converters.ArrayConverter;
041import org.apache.commons.beanutils.converters.BooleanConverter;
042import org.apache.commons.beanutils.converters.ByteConverter;
043import org.apache.commons.beanutils.converters.CharacterConverter;
044import org.apache.commons.beanutils.converters.DoubleConverter;
045import org.apache.commons.beanutils.converters.FloatConverter;
046import org.apache.commons.beanutils.converters.IntegerConverter;
047import org.apache.commons.beanutils.converters.LongConverter;
048import org.apache.commons.beanutils.converters.ShortConverter;
049
050import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
051import com.puppycrawl.tools.checkstyle.api.Configurable;
052import com.puppycrawl.tools.checkstyle.api.Configuration;
053import com.puppycrawl.tools.checkstyle.api.Context;
054import com.puppycrawl.tools.checkstyle.api.Contextualizable;
055import com.puppycrawl.tools.checkstyle.api.Scope;
056import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
057import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
058import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
059
060/**
061 * A Java Bean that implements the component lifecycle interfaces by
062 * calling the bean's setters for all configuration attributes.
063 */
064public abstract class AbstractAutomaticBean
065 implements Configurable, Contextualizable {
066
067 /**
068 * Enum to specify behaviour regarding ignored modules.
069 */
070 public enum OutputStreamOptions {
071
072 /**
073 * Close stream in the end.
074 */
075 CLOSE,
076
077 /**
078 * Do nothing in the end.
079 */
080 NONE,
081
082 }
083
084 /** Comma separator for StringTokenizer. */
085 private static final String COMMA_SEPARATOR = ",";
086
087 /** The configuration of this bean. */
088 private Configuration configuration;
089
090 /**
091 * Provides a hook to finish the part of this component's setup that
092 * was not handled by the bean introspection.
093 *
094 * <p>
095 * The default implementation does nothing.
096 * </p>
097 *
098 * @throws CheckstyleException if there is a configuration error.
099 */
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 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 = String.format(Locale.ROOT,
250 "Cannot set property '%s' to '%s'", key, value);
251 throw new CheckstyleException(message, exc);
252 }
253 catch (final IllegalArgumentException | ConversionException exc) {
254 final String message = String.format(Locale.ROOT, "illegal value '%s' for property "
255 + "'%s'", value, key);
256 throw new CheckstyleException(message, exc);
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 exc) {
375 throw new IllegalArgumentException(exc);
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}