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