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