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     * <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 = String.format(Locale.ROOT, "Property '%s' "
235                            + "does not exist, please check the documentation", key);
236                    throw new CheckstyleException(message);
237                }
238            }
239            // finally we can set the bean property
240            beanUtils.copyProperty(this, key, value);
241        }
242        catch (final InvocationTargetException | IllegalAccessException
243                | NoSuchMethodException ex) {
244            // There is no way to catch IllegalAccessException | NoSuchMethodException
245            // as we do PropertyUtils.getPropertyDescriptor before beanUtils.copyProperty,
246            // so we have to join these exceptions with InvocationTargetException
247            // to satisfy UTs coverage
248            final String message = String.format(Locale.ROOT,
249                    "Cannot set property '%s' to '%s'", key, value);
250            throw new CheckstyleException(message, ex);
251        }
252        catch (final IllegalArgumentException | ConversionException ex) {
253            final String message = String.format(Locale.ROOT, "illegal value '%s' for property "
254                    + "'%s'", value, key);
255            throw new CheckstyleException(message, ex);
256        }
257    }
258
259    /**
260     * Implements the Contextualizable interface using bean introspection.
261     *
262     * @see Contextualizable
263     */
264    @Override
265    public final void contextualize(Context context)
266            throws CheckstyleException {
267        final Collection<String> attributes = context.getAttributeNames();
268
269        for (final String key : attributes) {
270            final Object value = context.get(key);
271
272            tryCopyProperty(key, value, false);
273        }
274    }
275
276    /**
277     * Returns the configuration that was used to configure this component.
278     *
279     * @return the configuration that was used to configure this component.
280     */
281    protected final Configuration getConfiguration() {
282        return configuration;
283    }
284
285    /**
286     * Called by configure() for every child of this component's Configuration.
287     * <p>
288     * The default implementation throws {@link CheckstyleException} if
289     * {@code childConf} is {@code null} because it doesn't support children. It
290     * must be overridden to validate and support children that are wanted.
291     * </p>
292     *
293     * @param childConf a child of this component's Configuration
294     * @throws CheckstyleException if there is a configuration error.
295     * @see Configuration#getChildren
296     */
297    protected void setupChild(Configuration childConf)
298            throws CheckstyleException {
299        if (childConf != null) {
300            throw new CheckstyleException(childConf.getName() + " is not allowed as a child in "
301                    + configuration.getName() + ". Please review 'Parent Module' section "
302                    + "for this Check in web documentation if Check is standard.");
303        }
304    }
305
306    /** A converter that converts a string to a pattern. */
307    private static final class PatternConverter implements Converter {
308
309        @SuppressWarnings("unchecked")
310        @Override
311        public Object convert(Class type, Object value) {
312            return CommonUtil.createPattern(value.toString());
313        }
314
315    }
316
317    /** A converter that converts a comma-separated string into an array of patterns. */
318    private static final class PatternArrayConverter implements Converter {
319
320        @SuppressWarnings("unchecked")
321        @Override
322        public Object convert(Class type, Object value) {
323            final StringTokenizer tokenizer = new StringTokenizer(
324                    value.toString(), COMMA_SEPARATOR);
325            final List<Pattern> result = new ArrayList<>();
326
327            while (tokenizer.hasMoreTokens()) {
328                final String token = tokenizer.nextToken();
329                result.add(CommonUtil.createPattern(token.trim()));
330            }
331
332            return result.toArray(new Pattern[0]);
333        }
334    }
335
336    /** A converter that converts strings to severity level. */
337    private static final class SeverityLevelConverter implements Converter {
338
339        @SuppressWarnings("unchecked")
340        @Override
341        public Object convert(Class type, Object value) {
342            return SeverityLevel.getInstance(value.toString());
343        }
344
345    }
346
347    /** A converter that converts strings to scope. */
348    private static final class ScopeConverter implements Converter {
349
350        @SuppressWarnings("unchecked")
351        @Override
352        public Object convert(Class type, Object value) {
353            return Scope.getInstance(value.toString());
354        }
355
356    }
357
358    /** A converter that converts strings to uri. */
359    private static final class UriConverter implements Converter {
360
361        @SuppressWarnings("unchecked")
362        @Override
363        @Nullable
364        public Object convert(Class type, Object value) {
365            final String url = value.toString();
366            URI result = null;
367
368            if (!CommonUtil.isBlank(url)) {
369                try {
370                    result = CommonUtil.getUriByFilename(url);
371                }
372                catch (CheckstyleException ex) {
373                    throw new IllegalArgumentException(ex);
374                }
375            }
376
377            return result;
378        }
379
380    }
381
382    /**
383     * A converter that does not care whether the array elements contain String
384     * characters like '*' or '_'. The normal ArrayConverter class has problems
385     * with these characters.
386     */
387    private static final class RelaxedStringArrayConverter implements Converter {
388
389        @SuppressWarnings("unchecked")
390        @Override
391        public Object convert(Class type, Object value) {
392            final StringTokenizer tokenizer = new StringTokenizer(
393                value.toString().trim(), COMMA_SEPARATOR);
394            final List<String> result = new ArrayList<>();
395
396            while (tokenizer.hasMoreTokens()) {
397                final String token = tokenizer.nextToken();
398                result.add(token.trim());
399            }
400
401            return result.toArray(CommonUtil.EMPTY_STRING_ARRAY);
402        }
403
404    }
405
406    /**
407     * A converter that converts strings to {@link AccessModifierOption}.
408     * This implementation does not care whether the array elements contain characters like '_'.
409     * The normal {@link ArrayConverter} class has problems with this character.
410     */
411    private static final class RelaxedAccessModifierArrayConverter implements Converter {
412
413        /** Constant for optimization. */
414        private static final AccessModifierOption[] EMPTY_MODIFIER_ARRAY =
415                new AccessModifierOption[0];
416
417        @SuppressWarnings("unchecked")
418        @Override
419        public Object convert(Class type, Object value) {
420            // Converts to a String and trims it for the tokenizer.
421            final StringTokenizer tokenizer = new StringTokenizer(
422                value.toString().trim(), COMMA_SEPARATOR);
423            final List<AccessModifierOption> result = new ArrayList<>();
424
425            while (tokenizer.hasMoreTokens()) {
426                final String token = tokenizer.nextToken();
427                result.add(AccessModifierOption.getInstance(token));
428            }
429
430            return result.toArray(EMPTY_MODIFIER_ARRAY);
431        }
432
433    }
434
435}