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 configuration;
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>configuration</code>.
117     *
118     * @param configuration {@inheritDoc}
119     * @throws CheckstyleException {@inheritDoc}
120     * @see Configurable
121     */
122    @Override
123    public final void configure(Configuration configuration)
124        throws CheckstyleException
125    {
126        this.configuration = configuration;
127
128        final BeanUtilsBean beanUtils = createBeanUtilsBean();
129
130        // TODO: debug log messages
131        final String[] attributes = configuration.getAttributeNames();
132
133        for (final String key : attributes) {
134            final String value = configuration.getAttribute(key);
135
136            try {
137                // BeanUtilsBean.copyProperties silently ignores missing setters
138                // for key, so we have to go through great lengths here to
139                // figure out if the bean property really exists.
140                final PropertyDescriptor pd =
141                    PropertyUtils.getPropertyDescriptor(this, key);
142                if ((pd == null) || (pd.getWriteMethod() == null)) {
143                    throw new CheckstyleException(
144                        "Property '" + key + "' in module "
145                        + configuration.getName()
146                        + " does not exist, please check the documentation");
147                }
148
149                // finally we can set the bean property
150                beanUtils.copyProperty(this, key, value);
151            }
152            catch (final InvocationTargetException e) {
153                throw new CheckstyleException(
154                    "Cannot set property '" + key + "' in module "
155                    + configuration.getName() + " to '" + value
156                    + "': " + e.getTargetException().getMessage(), e);
157            }
158            catch (final IllegalAccessException e) {
159                throw new CheckstyleException(
160                    "cannot access " + key + " in "
161                    + this.getClass().getName(), e);
162            }
163            catch (final NoSuchMethodException e) {
164                throw new CheckstyleException(
165                    "cannot access " + key + " in "
166                    + this.getClass().getName(), e);
167            }
168            catch (final IllegalArgumentException e) {
169                throw new CheckstyleException(
170                    "illegal value '" + value + "' for property '" + key
171                    + "' of module " + configuration.getName(), e);
172            }
173            catch (final ConversionException e) {
174                throw new CheckstyleException(
175                    "illegal value '" + value + "' for property '" + key
176                    + "' of module " + configuration.getName(), e);
177            }
178
179        }
180
181        finishLocalSetup();
182
183        final Configuration[] childConfigs = configuration.getChildren();
184        for (final Configuration childConfig : childConfigs) {
185            setupChild(childConfig);
186        }
187    }
188
189    /**
190     * Implements the Contextualizable interface using bean introspection.
191     * @param context {@inheritDoc}
192     * @throws CheckstyleException {@inheritDoc}
193     * @see Contextualizable
194     */
195    @Override
196    public final void contextualize(Context context)
197        throws CheckstyleException
198    {
199        final BeanUtilsBean beanUtils = createBeanUtilsBean();
200
201        // TODO: debug log messages
202        final Collection<String> attributes = context.getAttributeNames();
203
204        for (final String key : attributes) {
205            final Object value = context.get(key);
206
207            try {
208                beanUtils.copyProperty(this, key, value);
209            }
210            catch (final InvocationTargetException e) {
211                // TODO: log.debug("The bean " + this.getClass()
212                // + " is not interested in " + value)
213                throw new CheckstyleException("cannot set property "
214                    + key + " to value " + value + " in bean "
215                    + this.getClass().getName(), e);
216            }
217            catch (final IllegalAccessException e) {
218                throw new CheckstyleException(
219                    "cannot access " + key + " in "
220                    + this.getClass().getName(), e);
221            }
222            catch (final IllegalArgumentException e) {
223                throw new CheckstyleException(
224                    "illegal value '" + value + "' for property '" + key
225                    + "' of bean " + this.getClass().getName(), e);
226            }
227            catch (final ConversionException e) {
228                throw new CheckstyleException(
229                    "illegal value '" + value + "' for property '" + key
230                    + "' of bean " + this.getClass().getName(), e);
231            }
232        }
233    }
234
235    /**
236     * Returns the configuration that was used to configure this component.
237     * @return the configuration that was used to configure this component.
238     */
239    protected final Configuration getConfiguration()
240    {
241        return configuration;
242    }
243
244    /**
245     * Provides a hook to finish the part of this component's setup that
246     * was not handled by the bean introspection.
247     * <p>
248     * The default implementation does nothing.
249     * </p>
250     * @throws CheckstyleException if there is a configuration error.
251     */
252    protected void finishLocalSetup() throws CheckstyleException
253    {
254    }
255
256    /**
257     * Called by configure() for every child of this component's Configuration.
258     * <p>
259     * The default implementation does nothing.
260     * </p>
261     * @param childConf a child of this component's Configuration
262     * @throws CheckstyleException if there is a configuration error.
263     * @see Configuration#getChildren
264     */
265    protected void setupChild(Configuration childConf)
266        throws CheckstyleException
267    {
268    }
269
270    /**
271     * A converter that does not care whether the array elements contain String
272     * characters like '*' or '_'. The normal ArrayConverter class has problems
273     * with this characters.
274     */
275    private static class RelaxedStringArrayConverter implements Converter
276    {
277        /** {@inheritDoc} */
278        @Override
279        public Object convert(@SuppressWarnings("rawtypes") Class type,
280            Object value)
281        {
282            if (null == type) {
283                throw new ConversionException("Cannot convert from null.");
284            }
285
286            // Convert to a String and trim it for the tokenizer.
287            final StringTokenizer st = new StringTokenizer(
288                value.toString().trim(), ",");
289            final List<String> result = Lists.newArrayList();
290
291            while (st.hasMoreTokens()) {
292                final String token = st.nextToken();
293                result.add(token.trim());
294            }
295
296            return result.toArray(new String[result.size()]);
297        }
298    }
299}