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