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