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.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     * Provides a hook to finish the part of this component's setup that
091     * was not handled by the bean introspection.
092     *
093     * <p>
094     * The default implementation does nothing.
095     * </p>
096     *
097     * @throws CheckstyleException if there is a configuration error.
098     */
099    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}