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