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