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