001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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.util.Collection;
025import java.util.List;
026import java.util.Locale;
027import java.util.StringTokenizer;
028
029import org.apache.commons.beanutils.BeanUtilsBean;
030import org.apache.commons.beanutils.ConversionException;
031import org.apache.commons.beanutils.ConvertUtilsBean;
032import org.apache.commons.beanutils.Converter;
033import org.apache.commons.beanutils.PropertyUtils;
034import org.apache.commons.beanutils.PropertyUtilsBean;
035import org.apache.commons.beanutils.converters.ArrayConverter;
036import org.apache.commons.beanutils.converters.BooleanConverter;
037import org.apache.commons.beanutils.converters.ByteConverter;
038import org.apache.commons.beanutils.converters.CharacterConverter;
039import org.apache.commons.beanutils.converters.DoubleConverter;
040import org.apache.commons.beanutils.converters.FloatConverter;
041import org.apache.commons.beanutils.converters.IntegerConverter;
042import org.apache.commons.beanutils.converters.LongConverter;
043import org.apache.commons.beanutils.converters.ShortConverter;
044
045import com.google.common.collect.Lists;
046
047/**
048 * A Java Bean that implements the component lifecycle interfaces by
049 * calling the bean's setters for all configuration attributes.
050 * @author lkuehne
051 */
052public class AutomaticBean
053    implements Configurable, Contextualizable {
054    /** The configuration of this bean. */
055    private Configuration configuration;
056
057    /**
058     * Creates a BeanUtilsBean that is configured to use
059     * type converters that throw a ConversionException
060     * instead of using the default value when something
061     * goes wrong.
062     *
063     * @return a configured BeanUtilsBean
064     */
065    private static BeanUtilsBean createBeanUtilsBean() {
066        final ConvertUtilsBean cub = new ConvertUtilsBean();
067
068        cub.register(new BooleanConverter(), Boolean.TYPE);
069        cub.register(new BooleanConverter(), Boolean.class);
070        cub.register(new ArrayConverter(
071            boolean[].class, new BooleanConverter()), boolean[].class);
072        cub.register(new ByteConverter(), Byte.TYPE);
073        cub.register(new ByteConverter(), Byte.class);
074        cub.register(new ArrayConverter(byte[].class, new ByteConverter()),
075            byte[].class);
076        cub.register(new CharacterConverter(), Character.TYPE);
077        cub.register(new CharacterConverter(), Character.class);
078        cub.register(new ArrayConverter(char[].class, new CharacterConverter()),
079            char[].class);
080        cub.register(new DoubleConverter(), Double.TYPE);
081        cub.register(new DoubleConverter(), Double.class);
082        cub.register(new ArrayConverter(double[].class, new DoubleConverter()),
083            double[].class);
084        cub.register(new FloatConverter(), Float.TYPE);
085        cub.register(new FloatConverter(), Float.class);
086        cub.register(new ArrayConverter(float[].class, new FloatConverter()),
087            float[].class);
088        cub.register(new IntegerConverter(), Integer.TYPE);
089        cub.register(new IntegerConverter(), Integer.class);
090        cub.register(new ArrayConverter(int[].class, new IntegerConverter()),
091            int[].class);
092        cub.register(new LongConverter(), Long.TYPE);
093        cub.register(new LongConverter(), Long.class);
094        cub.register(new ArrayConverter(long[].class, new LongConverter()),
095            long[].class);
096        cub.register(new ShortConverter(), Short.TYPE);
097        cub.register(new ShortConverter(), Short.class);
098        cub.register(new ArrayConverter(short[].class, new ShortConverter()),
099            short[].class);
100        cub.register(new RelaxedStringArrayConverter(), String[].class);
101
102        // BigDecimal, BigInteger, Class, Date, String, Time, TimeStamp
103        // do not use defaults in the default configuration of ConvertUtilsBean
104
105        return new BeanUtilsBean(cub, new PropertyUtilsBean());
106    }
107
108    /**
109     * Implements the Configurable interface using bean introspection.
110     *
111     * <p>Subclasses are allowed to add behaviour. After the bean
112     * based setup has completed first the method
113     * {@link #finishLocalSetup finishLocalSetup}
114     * is called to allow completion of the bean's local setup,
115     * after that the method {@link #setupChild setupChild}
116     * is called for each {@link Configuration#getChildren child Configuration}
117     * of {@code configuration}.
118     *
119     * @see Configurable
120     */
121    @Override
122    public final void configure(Configuration config)
123        throws CheckstyleException {
124        configuration = config;
125
126        final String[] attributes = config.getAttributeNames();
127
128        for (final String key : attributes) {
129            final String value = config.getAttribute(key);
130
131            tryCopyProperty(config.getName(), key, value, true);
132        }
133
134        finishLocalSetup();
135
136        final Configuration[] childConfigs = config.getChildren();
137        for (final Configuration childConfig : childConfigs) {
138            setupChild(childConfig);
139        }
140    }
141
142    /**
143     * Recheck property and try to copy it.
144     * @param moduleName name of the module/class
145     * @param key key of value
146     * @param value value
147     * @param recheck whether to check for property existence before copy
148     * @throws CheckstyleException then property defined incorrectly
149     */
150    private void tryCopyProperty(String moduleName, String key, Object value, boolean recheck)
151            throws CheckstyleException {
152
153        final BeanUtilsBean beanUtils = createBeanUtilsBean();
154
155        try {
156            if (recheck) {
157                // BeanUtilsBean.copyProperties silently ignores missing setters
158                // for key, so we have to go through great lengths here to
159                // figure out if the bean property really exists.
160                final PropertyDescriptor descriptor =
161                        PropertyUtils.getPropertyDescriptor(this, key);
162                if (descriptor == null) {
163                    final String message = String.format(Locale.ROOT, "Property '%s' in module %s "
164                            + "does not exist, please check the documentation", key, moduleName);
165                    throw new CheckstyleException(message);
166                }
167            }
168            // finally we can set the bean property
169            beanUtils.copyProperty(this, key, value);
170        }
171        catch (final InvocationTargetException | IllegalAccessException
172                | NoSuchMethodException e) {
173            // There is no way to catch IllegalAccessException | NoSuchMethodException
174            // as we do PropertyUtils.getPropertyDescriptor before beanUtils.copyProperty
175            // so we have to join these exceptions with InvocationTargetException
176            // to satisfy UTs coverage
177            final String message = String.format(Locale.ROOT,
178                    "Cannot set property '%s' to '%s' in module %s", key, value, moduleName);
179            throw new CheckstyleException(message, e);
180        }
181        catch (final IllegalArgumentException | ConversionException e) {
182            final String message = String.format(Locale.ROOT, "illegal value '%s' for property "
183                    + "'%s' of module %s", value, key, moduleName);
184            throw new CheckstyleException(message, e);
185        }
186    }
187
188    /**
189     * Implements the Contextualizable interface using bean introspection.
190     * @see Contextualizable
191     */
192    @Override
193    public final void contextualize(Context context)
194        throws CheckstyleException {
195
196        final Collection<String> attributes = context.getAttributeNames();
197
198        for (final String key : attributes) {
199            final Object value = context.get(key);
200
201            tryCopyProperty(getClass().getName(), key, value, false);
202        }
203    }
204
205    /**
206     * Returns the configuration that was used to configure this component.
207     * @return the configuration that was used to configure this component.
208     */
209    protected final Configuration getConfiguration() {
210        return configuration;
211    }
212
213    /**
214     * Provides a hook to finish the part of this component's setup that
215     * was not handled by the bean introspection.
216     * <p>
217     * The default implementation does nothing.
218     * </p>
219     * @throws CheckstyleException if there is a configuration error.
220     */
221    protected void finishLocalSetup() throws CheckstyleException {
222        // No code by default, should be overridden only by demand at subclasses
223    }
224
225    /**
226     * Called by configure() for every child of this component's Configuration.
227     * <p>
228     * The default implementation does nothing.
229     * </p>
230     * @param childConf a child of this component's Configuration
231     * @throws CheckstyleException if there is a configuration error.
232     * @see Configuration#getChildren
233     */
234    protected void setupChild(Configuration childConf)
235        throws CheckstyleException {
236        // No code by default, should be overridden only by demand at subclasses
237    }
238
239    /**
240     * A converter that does not care whether the array elements contain String
241     * characters like '*' or '_'. The normal ArrayConverter class has problems
242     * with this characters.
243     */
244    private static class RelaxedStringArrayConverter implements Converter {
245        @SuppressWarnings({"unchecked", "rawtypes"})
246        @Override
247        public Object convert(Class type, Object value) {
248            // Convert to a String and trim it for the tokenizer.
249            final StringTokenizer tokenizer = new StringTokenizer(
250                value.toString().trim(), ",");
251            final List<String> result = Lists.newArrayList();
252
253            while (tokenizer.hasMoreTokens()) {
254                final String token = tokenizer.nextToken();
255                result.add(token.trim());
256            }
257
258            return result.toArray(new String[result.size()]);
259        }
260    }
261}