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