001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.api;
020
021import com.google.common.collect.Lists;
022import java.beans.PropertyDescriptor;
023import java.lang.reflect.InvocationTargetException;
024import java.util.Collection;
025import java.util.List;
026import java.util.StringTokenizer;
027import org.apache.commons.beanutils.BeanUtilsBean;
028import org.apache.commons.beanutils.ConversionException;
029import org.apache.commons.beanutils.ConvertUtilsBean;
030import org.apache.commons.beanutils.Converter;
031import org.apache.commons.beanutils.PropertyUtils;
032import org.apache.commons.beanutils.PropertyUtilsBean;
033import org.apache.commons.beanutils.converters.ArrayConverter;
034import org.apache.commons.beanutils.converters.BooleanConverter;
035import org.apache.commons.beanutils.converters.ByteConverter;
036import org.apache.commons.beanutils.converters.CharacterConverter;
037import org.apache.commons.beanutils.converters.DoubleConverter;
038import org.apache.commons.beanutils.converters.FloatConverter;
039import org.apache.commons.beanutils.converters.IntegerConverter;
040import org.apache.commons.beanutils.converters.LongConverter;
041import org.apache.commons.beanutils.converters.ShortConverter;
042
043/**
044 * A Java Bean that implements the component lifecycle interfaces by
045 * calling the bean's setters for all configuration attributes.
046 * @author lkuehne
047 */
048public class AutomaticBean
049    implements Configurable, Contextualizable
050{
051    /** the configuration of this bean */
052    private Configuration mConfiguration;
053
054
055    /**
056     * Creates a BeanUtilsBean that is configured to use
057     * type converters that throw a ConversionException
058     * instead of using the default value when something
059     * goes wrong.
060     *
061     * @return a configured BeanUtilsBean
062     */
063    private static BeanUtilsBean createBeanUtilsBean()
064    {
065        final ConvertUtilsBean cub = new ConvertUtilsBean();
066        // TODO: is there a smarter way to tell beanutils not to use defaults?
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     * 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>aConfiguration</code>.
117     *
118     * @param aConfiguration {@inheritDoc}
119     * @throws CheckstyleException {@inheritDoc}
120     * @see Configurable
121     */
122    public final void configure(Configuration aConfiguration)
123        throws CheckstyleException
124    {
125        mConfiguration = aConfiguration;
126
127        final BeanUtilsBean beanUtils = createBeanUtilsBean();
128
129        // TODO: debug log messages
130        final String[] attributes = aConfiguration.getAttributeNames();
131
132        for (final String key : attributes) {
133            final String value = aConfiguration.getAttribute(key);
134
135            try {
136                // BeanUtilsBean.copyProperties silently ignores missing setters
137                // for key, so we have to go through great lengths here to
138                // figure out if the bean property really exists.
139                final PropertyDescriptor pd =
140                    PropertyUtils.getPropertyDescriptor(this, key);
141                if ((pd == null) || (pd.getWriteMethod() == null)) {
142                    throw new CheckstyleException(
143                        "Property '" + key + "' in module "
144                        + aConfiguration.getName()
145                        + " does not exist, please check the documentation");
146                }
147
148                // finally we can set the bean property
149                beanUtils.copyProperty(this, key, value);
150            }
151            catch (final InvocationTargetException e) {
152                throw new CheckstyleException(
153                    "Cannot set property '" + key + "' in module "
154                    + aConfiguration.getName() + " to '" + value
155                    + "': " + e.getTargetException().getMessage(), e);
156            }
157            catch (final IllegalAccessException e) {
158                throw new CheckstyleException(
159                    "cannot access " + key + " in "
160                    + this.getClass().getName(), e);
161            }
162            catch (final NoSuchMethodException e) {
163                throw new CheckstyleException(
164                    "cannot access " + key + " in "
165                    + this.getClass().getName(), e);
166            }
167            catch (final IllegalArgumentException e) {
168                throw new CheckstyleException(
169                    "illegal value '" + value + "' for property '" + key
170                    + "' of module " + aConfiguration.getName(), e);
171            }
172            catch (final ConversionException e) {
173                throw new CheckstyleException(
174                    "illegal value '" + value + "' for property '" + key
175                    + "' of module " + aConfiguration.getName(), e);
176            }
177
178        }
179
180        finishLocalSetup();
181
182        final Configuration[] childConfigs = aConfiguration.getChildren();
183        for (final Configuration childConfig : childConfigs) {
184            setupChild(childConfig);
185        }
186    }
187
188    /**
189     * Implements the Contextualizable interface using bean introspection.
190     * @param aContext {@inheritDoc}
191     * @throws CheckstyleException {@inheritDoc}
192     * @see Contextualizable
193     */
194    public final void contextualize(Context aContext)
195        throws CheckstyleException
196    {
197        final BeanUtilsBean beanUtils = createBeanUtilsBean();
198
199        // TODO: debug log messages
200        final Collection<String> attributes = aContext.getAttributeNames();
201
202        for (final String key : attributes) {
203            final Object value = aContext.get(key);
204
205            try {
206                beanUtils.copyProperty(this, key, value);
207            }
208            catch (final InvocationTargetException e) {
209                // TODO: log.debug("The bean " + this.getClass()
210                // + " is not interested in " + value)
211                throw new CheckstyleException("cannot set property "
212                    + key + " to value " + value + " in bean "
213                    + this.getClass().getName(), e);
214            }
215            catch (final IllegalAccessException e) {
216                throw new CheckstyleException(
217                    "cannot access " + key + " in "
218                    + this.getClass().getName(), e);
219            }
220            catch (final IllegalArgumentException e) {
221                throw new CheckstyleException(
222                    "illegal value '" + value + "' for property '" + key
223                    + "' of bean " + this.getClass().getName(), e);
224            }
225            catch (final ConversionException e) {
226                throw new CheckstyleException(
227                    "illegal value '" + value + "' for property '" + key
228                    + "' of bean " + this.getClass().getName(), e);
229            }
230        }
231    }
232
233    /**
234     * Returns the configuration that was used to configure this component.
235     * @return the configuration that was used to configure this component.
236     */
237    protected final Configuration getConfiguration()
238    {
239        return mConfiguration;
240    }
241
242    /**
243     * Provides a hook to finish the part of this component's setup that
244     * was not handled by the bean introspection.
245     * <p>
246     * The default implementation does nothing.
247     * </p>
248     * @throws CheckstyleException if there is a configuration error.
249     */
250    protected void finishLocalSetup() throws CheckstyleException
251    {
252    }
253
254    /**
255     * Called by configure() for every child of this component's Configuration.
256     * <p>
257     * The default implementation does nothing.
258     * </p>
259     * @param aChildConf a child of this component's Configuration
260     * @throws CheckstyleException if there is a configuration error.
261     * @see Configuration#getChildren
262     */
263    protected void setupChild(Configuration aChildConf)
264        throws CheckstyleException
265    {
266    }
267
268    /**
269     * A converter that does not care whether the array elements contain String
270     * characters like '*' or '_'. The normal ArrayConverter class has problems
271     * with this characters.
272     */
273    private static class RelaxedStringArrayConverter implements Converter
274    {
275        /** {@inheritDoc} */
276        public Object convert(@SuppressWarnings("rawtypes") Class aType,
277            Object aValue)
278        {
279            if (null == aType) {
280                throw new ConversionException("Cannot convert from null.");
281            }
282
283            // Convert to a String and trim it for the tokenizer.
284            final StringTokenizer st = new StringTokenizer(
285                aValue.toString().trim(), ",");
286            final List<String> result = Lists.newArrayList();
287
288            while (st.hasMoreTokens()) {
289                final String token = st.nextToken();
290                result.add(token.trim());
291            }
292
293            return result.toArray(new String[result.size()]);
294        }
295    }
296}