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.CommonUtil; 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 CommonUtil.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 (!CommonUtil.isBlank(url)) { 335 try { 336 result = CommonUtil.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(CommonUtil.EMPTY_STRING_ARRAY); 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 /** Constant for optimization. */ 381 private static final AccessModifier[] EMPTY_MODIFIER_ARRAY = new AccessModifier[0]; 382 383 @SuppressWarnings({"unchecked", "rawtypes"}) 384 @Override 385 public Object convert(Class type, Object value) { 386 // Converts to a String and trims it for the tokenizer. 387 final StringTokenizer tokenizer = new StringTokenizer( 388 value.toString().trim(), COMMA_SEPARATOR); 389 final List<AccessModifier> result = new ArrayList<>(); 390 391 while (tokenizer.hasMoreTokens()) { 392 final String token = tokenizer.nextToken(); 393 result.add(AccessModifier.getInstance(token.trim())); 394 } 395 396 return result.toArray(EMPTY_MODIFIER_ARRAY); 397 } 398 399 } 400 401}