001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 ex) {
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, ex);
252        }
253        catch (final IllegalArgumentException | ConversionException ex) {
254            final String message = String.format(Locale.ROOT, "illegal value '%s' for property "
255                    + "'%s'", value, key);
256            throw new CheckstyleException(message, ex);
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 ex) {
375                    throw new IllegalArgumentException(ex);
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}