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