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