001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.StringTokenizer;
029import java.util.regex.Pattern;
030
031import javax.annotation.Nullable;
032
033import org.apache.commons.beanutils.BeanUtilsBean;
034import org.apache.commons.beanutils.ConversionException;
035import org.apache.commons.beanutils.ConvertUtilsBean;
036import org.apache.commons.beanutils.Converter;
037import org.apache.commons.beanutils.PropertyUtils;
038import org.apache.commons.beanutils.PropertyUtilsBean;
039import org.apache.commons.beanutils.converters.ArrayConverter;
040import org.apache.commons.beanutils.converters.BooleanConverter;
041import org.apache.commons.beanutils.converters.ByteConverter;
042import org.apache.commons.beanutils.converters.CharacterConverter;
043import org.apache.commons.beanutils.converters.DoubleConverter;
044import org.apache.commons.beanutils.converters.FloatConverter;
045import org.apache.commons.beanutils.converters.IntegerConverter;
046import org.apache.commons.beanutils.converters.LongConverter;
047import org.apache.commons.beanutils.converters.ShortConverter;
048
049import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
050import com.puppycrawl.tools.checkstyle.api.Configurable;
051import com.puppycrawl.tools.checkstyle.api.Configuration;
052import com.puppycrawl.tools.checkstyle.api.Context;
053import com.puppycrawl.tools.checkstyle.api.Contextualizable;
054import com.puppycrawl.tools.checkstyle.api.Scope;
055import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
056import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
057import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
058
059/**
060 * A Java Bean that implements the component lifecycle interfaces by
061 * calling the bean's setters for all configuration attributes.
062 */
063public abstract class AbstractAutomaticBean
064    implements Configurable, Contextualizable {
065
066    /**
067     * Enum to specify behaviour regarding ignored modules.
068     */
069    public enum OutputStreamOptions {
070
071        /**
072         * Close stream in the end.
073         */
074        CLOSE,
075
076        /**
077         * Do nothing in the end.
078         */
079        NONE,
080
081    }
082
083    /** Comma separator for StringTokenizer. */
084    private static final String COMMA_SEPARATOR = ",";
085
086    /** The configuration of this bean. */
087    private Configuration configuration;
088
089    /**
090     * Creates a new {@code AbstractAutomaticBean} instance.
091     */
092    protected AbstractAutomaticBean() {
093        // no code by default
094    }
095
096    /**
097     * Provides a hook to finish the part of this component's setup that
098     * was not handled by the bean introspection.
099     *
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}