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