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