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