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