001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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 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     * Provides a hook to finish the part of this component's setup that
090     * was not handled by the bean introspection.
091     * <p>
092     * The default implementation does nothing.
093     * </p>
094     *
095     * @throws CheckstyleException if there is a configuration error.
096     */
097    protected abstract void finishLocalSetup() throws CheckstyleException;
098
099    /**
100     * Creates a BeanUtilsBean that is configured to use
101     * type converters that throw a ConversionException
102     * instead of using the default value when something
103     * goes wrong.
104     *
105     * @return a configured BeanUtilsBean
106     */
107    private static BeanUtilsBean createBeanUtilsBean() {
108        final ConvertUtilsBean cub = new ConvertUtilsBean();
109
110        registerIntegralTypes(cub);
111        registerCustomTypes(cub);
112
113        return new BeanUtilsBean(cub, new PropertyUtilsBean());
114    }
115
116    /**
117     * Register basic types of JDK like boolean, int, and String to use with BeanUtils. All these
118     * types are found in the {@code java.lang} package.
119     *
120     * @param cub
121     *            Instance of {@link ConvertUtilsBean} to register types with.
122     */
123    private static void registerIntegralTypes(ConvertUtilsBean cub) {
124        cub.register(new BooleanConverter(), Boolean.TYPE);
125        cub.register(new BooleanConverter(), Boolean.class);
126        cub.register(new ArrayConverter(
127            boolean[].class, new BooleanConverter()), boolean[].class);
128        cub.register(new ByteConverter(), Byte.TYPE);
129        cub.register(new ByteConverter(), Byte.class);
130        cub.register(new ArrayConverter(byte[].class, new ByteConverter()),
131            byte[].class);
132        cub.register(new CharacterConverter(), Character.TYPE);
133        cub.register(new CharacterConverter(), Character.class);
134        cub.register(new ArrayConverter(char[].class, new CharacterConverter()),
135            char[].class);
136        cub.register(new DoubleConverter(), Double.TYPE);
137        cub.register(new DoubleConverter(), Double.class);
138        cub.register(new ArrayConverter(double[].class, new DoubleConverter()),
139            double[].class);
140        cub.register(new FloatConverter(), Float.TYPE);
141        cub.register(new FloatConverter(), Float.class);
142        cub.register(new ArrayConverter(float[].class, new FloatConverter()),
143            float[].class);
144        cub.register(new IntegerConverter(), Integer.TYPE);
145        cub.register(new IntegerConverter(), Integer.class);
146        cub.register(new ArrayConverter(int[].class, new IntegerConverter()),
147            int[].class);
148        cub.register(new LongConverter(), Long.TYPE);
149        cub.register(new LongConverter(), Long.class);
150        cub.register(new ArrayConverter(long[].class, new LongConverter()),
151            long[].class);
152        cub.register(new ShortConverter(), Short.TYPE);
153        cub.register(new ShortConverter(), Short.class);
154        cub.register(new ArrayConverter(short[].class, new ShortConverter()),
155            short[].class);
156        cub.register(new RelaxedStringArrayConverter(), String[].class);
157
158        // BigDecimal, BigInteger, Class, Date, String, Time, TimeStamp
159        // do not use defaults in the default configuration of ConvertUtilsBean
160    }
161
162    /**
163     * Register custom types of JDK like URI and Checkstyle specific classes to use with BeanUtils.
164     * None of these types should be found in the {@code java.lang} package.
165     *
166     * @param cub
167     *            Instance of {@link ConvertUtilsBean} to register types with.
168     */
169    private static void registerCustomTypes(ConvertUtilsBean cub) {
170        cub.register(new PatternConverter(), Pattern.class);
171        cub.register(new SeverityLevelConverter(), SeverityLevel.class);
172        cub.register(new ScopeConverter(), Scope.class);
173        cub.register(new UriConverter(), URI.class);
174        cub.register(new RelaxedAccessModifierArrayConverter(), AccessModifierOption[].class);
175    }
176
177    /**
178     * Implements the Configurable interface using bean introspection.
179     *
180     * <p>Subclasses are allowed to add behaviour. After the bean
181     * based setup has completed first the method
182     * {@link #finishLocalSetup finishLocalSetup}
183     * is called to allow completion of the bean's local setup,
184     * after that the method {@link #setupChild setupChild}
185     * is called for each {@link Configuration#getChildren child Configuration}
186     * of {@code configuration}.
187     *
188     * @see Configurable
189     */
190    @Override
191    public final void configure(Configuration config)
192            throws CheckstyleException {
193        configuration = config;
194
195        final String[] attributes = config.getPropertyNames();
196
197        for (final String key : attributes) {
198            final String value = config.getProperty(key);
199
200            tryCopyProperty(key, value, true);
201        }
202
203        finishLocalSetup();
204
205        final Configuration[] childConfigs = config.getChildren();
206        for (final Configuration childConfig : childConfigs) {
207            setupChild(childConfig);
208        }
209    }
210
211    /**
212     * Recheck property and try to copy it.
213     *
214     * @param key key of value
215     * @param value value
216     * @param recheck whether to check for property existence before copy
217     * @throws CheckstyleException when property defined incorrectly
218     */
219    private void tryCopyProperty(String key, Object value, boolean recheck)
220            throws CheckstyleException {
221        final BeanUtilsBean beanUtils = createBeanUtilsBean();
222
223        try {
224            if (recheck) {
225                // BeanUtilsBean.copyProperties silently ignores missing setters
226                // for key, so we have to go through great lengths here to
227                // figure out if the bean property really exists.
228                final PropertyDescriptor descriptor =
229                        PropertyUtils.getPropertyDescriptor(this, key);
230                if (descriptor == null) {
231                    final String message = String.format(Locale.ROOT, "Property '%s' "
232                            + "does not exist, please check the documentation", key);
233                    throw new CheckstyleException(message);
234                }
235            }
236            // finally we can set the bean property
237            beanUtils.copyProperty(this, key, value);
238        }
239        catch (final InvocationTargetException | IllegalAccessException
240                | NoSuchMethodException ex) {
241            // There is no way to catch IllegalAccessException | NoSuchMethodException
242            // as we do PropertyUtils.getPropertyDescriptor before beanUtils.copyProperty,
243            // so we have to join these exceptions with InvocationTargetException
244            // to satisfy UTs coverage
245            final String message = String.format(Locale.ROOT,
246                    "Cannot set property '%s' to '%s'", key, value);
247            throw new CheckstyleException(message, ex);
248        }
249        catch (final IllegalArgumentException | ConversionException ex) {
250            final String message = String.format(Locale.ROOT, "illegal value '%s' for property "
251                    + "'%s'", value, key);
252            throw new CheckstyleException(message, ex);
253        }
254    }
255
256    /**
257     * Implements the Contextualizable interface using bean introspection.
258     *
259     * @see Contextualizable
260     */
261    @Override
262    public final void contextualize(Context context)
263            throws CheckstyleException {
264        final Collection<String> attributes = context.getAttributeNames();
265
266        for (final String key : attributes) {
267            final Object value = context.get(key);
268
269            tryCopyProperty(key, value, false);
270        }
271    }
272
273    /**
274     * Returns the configuration that was used to configure this component.
275     *
276     * @return the configuration that was used to configure this component.
277     */
278    protected final Configuration getConfiguration() {
279        return configuration;
280    }
281
282    /**
283     * Called by configure() for every child of this component's Configuration.
284     * <p>
285     * The default implementation throws {@link CheckstyleException} if
286     * {@code childConf} is {@code null} because it doesn't support children. It
287     * must be overridden to validate and support children that are wanted.
288     * </p>
289     *
290     * @param childConf a child of this component's Configuration
291     * @throws CheckstyleException if there is a configuration error.
292     * @see Configuration#getChildren
293     */
294    protected void setupChild(Configuration childConf)
295            throws CheckstyleException {
296        if (childConf != null) {
297            throw new CheckstyleException(childConf.getName() + " is not allowed as a child in "
298                    + configuration.getName() + ". Please review 'Parent Module' section "
299                    + "for this Check in web documentation if Check is standard.");
300        }
301    }
302
303    /** A converter that converts a string to a pattern. */
304    private static final class PatternConverter implements Converter {
305
306        @SuppressWarnings("unchecked")
307        @Override
308        public Object convert(Class type, Object value) {
309            return CommonUtil.createPattern(value.toString());
310        }
311
312    }
313
314    /** A converter that converts strings to severity level. */
315    private static final class SeverityLevelConverter implements Converter {
316
317        @SuppressWarnings("unchecked")
318        @Override
319        public Object convert(Class type, Object value) {
320            return SeverityLevel.getInstance(value.toString());
321        }
322
323    }
324
325    /** A converter that converts strings to scope. */
326    private static final class ScopeConverter implements Converter {
327
328        @SuppressWarnings("unchecked")
329        @Override
330        public Object convert(Class type, Object value) {
331            return Scope.getInstance(value.toString());
332        }
333
334    }
335
336    /** A converter that converts strings to uri. */
337    private static final class UriConverter implements Converter {
338
339        @SuppressWarnings("unchecked")
340        @Override
341        public Object convert(Class type, Object value) {
342            final String url = value.toString();
343            URI result = null;
344
345            if (!CommonUtil.isBlank(url)) {
346                try {
347                    result = CommonUtil.getUriByFilename(url);
348                }
349                catch (CheckstyleException ex) {
350                    throw new IllegalArgumentException(ex);
351                }
352            }
353
354            return result;
355        }
356
357    }
358
359    /**
360     * A converter that does not care whether the array elements contain String
361     * characters like '*' or '_'. The normal ArrayConverter class has problems
362     * with these characters.
363     */
364    private static final class RelaxedStringArrayConverter implements Converter {
365
366        @SuppressWarnings("unchecked")
367        @Override
368        public Object convert(Class type, Object value) {
369            final StringTokenizer tokenizer = new StringTokenizer(
370                value.toString().trim(), COMMA_SEPARATOR);
371            final List<String> result = new ArrayList<>();
372
373            while (tokenizer.hasMoreTokens()) {
374                final String token = tokenizer.nextToken();
375                result.add(token.trim());
376            }
377
378            return result.toArray(CommonUtil.EMPTY_STRING_ARRAY);
379        }
380
381    }
382
383    /**
384     * A converter that converts strings to {@link AccessModifierOption}.
385     * This implementation does not care whether the array elements contain characters like '_'.
386     * The normal {@link ArrayConverter} class has problems with this character.
387     */
388    private static final class RelaxedAccessModifierArrayConverter implements Converter {
389
390        /** Constant for optimization. */
391        private static final AccessModifierOption[] EMPTY_MODIFIER_ARRAY =
392                new AccessModifierOption[0];
393
394        @SuppressWarnings("unchecked")
395        @Override
396        public Object convert(Class type, Object value) {
397            // Converts to a String and trims it for the tokenizer.
398            final StringTokenizer tokenizer = new StringTokenizer(
399                value.toString().trim(), COMMA_SEPARATOR);
400            final List<AccessModifierOption> result = new ArrayList<>();
401
402            while (tokenizer.hasMoreTokens()) {
403                final String token = tokenizer.nextToken();
404                result.add(AccessModifierOption.getInstance(token));
405            }
406
407            return result.toArray(EMPTY_MODIFIER_ARRAY);
408        }
409
410    }
411
412}