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