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