001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.core; 018 019import java.lang.reflect.Field; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.Locale; 023import java.util.Map; 024import java.util.Set; 025 026import org.springframework.lang.Nullable; 027import org.springframework.util.Assert; 028import org.springframework.util.ReflectionUtils; 029 030/** 031 * This class can be used to parse other classes containing constant definitions 032 * in public static final members. The {@code asXXXX} methods of this class 033 * allow these constant values to be accessed via their string names. 034 * 035 * <p>Consider class Foo containing {@code public final static int CONSTANT1 = 66;} 036 * An instance of this class wrapping {@code Foo.class} will return the constant value 037 * of 66 from its {@code asNumber} method given the argument {@code "CONSTANT1"}. 038 * 039 * <p>This class is ideal for use in PropertyEditors, enabling them to 040 * recognize the same names as the constants themselves, and freeing them 041 * from maintaining their own mapping. 042 * 043 * @author Rod Johnson 044 * @author Juergen Hoeller 045 * @since 16.03.2003 046 */ 047public class Constants { 048 049 /** The name of the introspected class. */ 050 private final String className; 051 052 /** Map from String field name to object value. */ 053 private final Map<String, Object> fieldCache = new HashMap<>(); 054 055 056 /** 057 * Create a new Constants converter class wrapping the given class. 058 * <p>All <b>public</b> static final variables will be exposed, whatever their type. 059 * @param clazz the class to analyze 060 * @throws IllegalArgumentException if the supplied {@code clazz} is {@code null} 061 */ 062 public Constants(Class<?> clazz) { 063 Assert.notNull(clazz, "Class must not be null"); 064 this.className = clazz.getName(); 065 Field[] fields = clazz.getFields(); 066 for (Field field : fields) { 067 if (ReflectionUtils.isPublicStaticFinal(field)) { 068 String name = field.getName(); 069 try { 070 Object value = field.get(null); 071 this.fieldCache.put(name, value); 072 } 073 catch (IllegalAccessException ex) { 074 // just leave this field and continue 075 } 076 } 077 } 078 } 079 080 081 /** 082 * Return the name of the analyzed class. 083 */ 084 public final String getClassName() { 085 return this.className; 086 } 087 088 /** 089 * Return the number of constants exposed. 090 */ 091 public final int getSize() { 092 return this.fieldCache.size(); 093 } 094 095 /** 096 * Exposes the field cache to subclasses: 097 * a Map from String field name to object value. 098 */ 099 protected final Map<String, Object> getFieldCache() { 100 return this.fieldCache; 101 } 102 103 104 /** 105 * Return a constant value cast to a Number. 106 * @param code the name of the field (never {@code null}) 107 * @return the Number value 108 * @throws ConstantException if the field name wasn't found 109 * or if the type wasn't compatible with Number 110 * @see #asObject 111 */ 112 public Number asNumber(String code) throws ConstantException { 113 Object obj = asObject(code); 114 if (!(obj instanceof Number)) { 115 throw new ConstantException(this.className, code, "not a Number"); 116 } 117 return (Number) obj; 118 } 119 120 /** 121 * Return a constant value as a String. 122 * @param code the name of the field (never {@code null}) 123 * @return the String value 124 * Works even if it's not a string (invokes {@code toString()}). 125 * @throws ConstantException if the field name wasn't found 126 * @see #asObject 127 */ 128 public String asString(String code) throws ConstantException { 129 return asObject(code).toString(); 130 } 131 132 /** 133 * Parse the given String (upper or lower case accepted) and return 134 * the appropriate value if it's the name of a constant field in the 135 * class that we're analysing. 136 * @param code the name of the field (never {@code null}) 137 * @return the Object value 138 * @throws ConstantException if there's no such field 139 */ 140 public Object asObject(String code) throws ConstantException { 141 Assert.notNull(code, "Code must not be null"); 142 String codeToUse = code.toUpperCase(Locale.ENGLISH); 143 Object val = this.fieldCache.get(codeToUse); 144 if (val == null) { 145 throw new ConstantException(this.className, codeToUse, "not found"); 146 } 147 return val; 148 } 149 150 151 /** 152 * Return all names of the given group of constants. 153 * <p>Note that this method assumes that constants are named 154 * in accordance with the standard Java convention for constant 155 * values (i.e. all uppercase). The supplied {@code namePrefix} 156 * will be uppercased (in a locale-insensitive fashion) prior to 157 * the main logic of this method kicking in. 158 * @param namePrefix prefix of the constant names to search (may be {@code null}) 159 * @return the set of constant names 160 */ 161 public Set<String> getNames(@Nullable String namePrefix) { 162 String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); 163 Set<String> names = new HashSet<>(); 164 for (String code : this.fieldCache.keySet()) { 165 if (code.startsWith(prefixToUse)) { 166 names.add(code); 167 } 168 } 169 return names; 170 } 171 172 /** 173 * Return all names of the group of constants for the 174 * given bean property name. 175 * @param propertyName the name of the bean property 176 * @return the set of values 177 * @see #propertyToConstantNamePrefix 178 */ 179 public Set<String> getNamesForProperty(String propertyName) { 180 return getNames(propertyToConstantNamePrefix(propertyName)); 181 } 182 183 /** 184 * Return all names of the given group of constants. 185 * <p>Note that this method assumes that constants are named 186 * in accordance with the standard Java convention for constant 187 * values (i.e. all uppercase). The supplied {@code nameSuffix} 188 * will be uppercased (in a locale-insensitive fashion) prior to 189 * the main logic of this method kicking in. 190 * @param nameSuffix suffix of the constant names to search (may be {@code null}) 191 * @return the set of constant names 192 */ 193 public Set<String> getNamesForSuffix(@Nullable String nameSuffix) { 194 String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); 195 Set<String> names = new HashSet<>(); 196 for (String code : this.fieldCache.keySet()) { 197 if (code.endsWith(suffixToUse)) { 198 names.add(code); 199 } 200 } 201 return names; 202 } 203 204 205 /** 206 * Return all values of the given group of constants. 207 * <p>Note that this method assumes that constants are named 208 * in accordance with the standard Java convention for constant 209 * values (i.e. all uppercase). The supplied {@code namePrefix} 210 * will be uppercased (in a locale-insensitive fashion) prior to 211 * the main logic of this method kicking in. 212 * @param namePrefix prefix of the constant names to search (may be {@code null}) 213 * @return the set of values 214 */ 215 public Set<Object> getValues(@Nullable String namePrefix) { 216 String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); 217 Set<Object> values = new HashSet<>(); 218 this.fieldCache.forEach((code, value) -> { 219 if (code.startsWith(prefixToUse)) { 220 values.add(value); 221 } 222 }); 223 return values; 224 } 225 226 /** 227 * Return all values of the group of constants for the 228 * given bean property name. 229 * @param propertyName the name of the bean property 230 * @return the set of values 231 * @see #propertyToConstantNamePrefix 232 */ 233 public Set<Object> getValuesForProperty(String propertyName) { 234 return getValues(propertyToConstantNamePrefix(propertyName)); 235 } 236 237 /** 238 * Return all values of the given group of constants. 239 * <p>Note that this method assumes that constants are named 240 * in accordance with the standard Java convention for constant 241 * values (i.e. all uppercase). The supplied {@code nameSuffix} 242 * will be uppercased (in a locale-insensitive fashion) prior to 243 * the main logic of this method kicking in. 244 * @param nameSuffix suffix of the constant names to search (may be {@code null}) 245 * @return the set of values 246 */ 247 public Set<Object> getValuesForSuffix(@Nullable String nameSuffix) { 248 String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); 249 Set<Object> values = new HashSet<>(); 250 this.fieldCache.forEach((code, value) -> { 251 if (code.endsWith(suffixToUse)) { 252 values.add(value); 253 } 254 }); 255 return values; 256 } 257 258 259 /** 260 * Look up the given value within the given group of constants. 261 * <p>Will return the first match. 262 * @param value constant value to look up 263 * @param namePrefix prefix of the constant names to search (may be {@code null}) 264 * @return the name of the constant field 265 * @throws ConstantException if the value wasn't found 266 */ 267 public String toCode(Object value, @Nullable String namePrefix) throws ConstantException { 268 String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); 269 for (Map.Entry<String, Object> entry : this.fieldCache.entrySet()) { 270 if (entry.getKey().startsWith(prefixToUse) && entry.getValue().equals(value)) { 271 return entry.getKey(); 272 } 273 } 274 throw new ConstantException(this.className, prefixToUse, value); 275 } 276 277 /** 278 * Look up the given value within the group of constants for 279 * the given bean property name. Will return the first match. 280 * @param value constant value to look up 281 * @param propertyName the name of the bean property 282 * @return the name of the constant field 283 * @throws ConstantException if the value wasn't found 284 * @see #propertyToConstantNamePrefix 285 */ 286 public String toCodeForProperty(Object value, String propertyName) throws ConstantException { 287 return toCode(value, propertyToConstantNamePrefix(propertyName)); 288 } 289 290 /** 291 * Look up the given value within the given group of constants. 292 * <p>Will return the first match. 293 * @param value constant value to look up 294 * @param nameSuffix suffix of the constant names to search (may be {@code null}) 295 * @return the name of the constant field 296 * @throws ConstantException if the value wasn't found 297 */ 298 public String toCodeForSuffix(Object value, @Nullable String nameSuffix) throws ConstantException { 299 String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); 300 for (Map.Entry<String, Object> entry : this.fieldCache.entrySet()) { 301 if (entry.getKey().endsWith(suffixToUse) && entry.getValue().equals(value)) { 302 return entry.getKey(); 303 } 304 } 305 throw new ConstantException(this.className, suffixToUse, value); 306 } 307 308 309 /** 310 * Convert the given bean property name to a constant name prefix. 311 * <p>Uses a common naming idiom: turning all lower case characters to 312 * upper case, and prepending upper case characters with an underscore. 313 * <p>Example: "imageSize" -> "IMAGE_SIZE"<br> 314 * Example: "imagesize" -> "IMAGESIZE".<br> 315 * Example: "ImageSize" -> "_IMAGE_SIZE".<br> 316 * Example: "IMAGESIZE" -> "_I_M_A_G_E_S_I_Z_E" 317 * @param propertyName the name of the bean property 318 * @return the corresponding constant name prefix 319 * @see #getValuesForProperty 320 * @see #toCodeForProperty 321 */ 322 public String propertyToConstantNamePrefix(String propertyName) { 323 StringBuilder parsedPrefix = new StringBuilder(); 324 for (int i = 0; i < propertyName.length(); i++) { 325 char c = propertyName.charAt(i); 326 if (Character.isUpperCase(c)) { 327 parsedPrefix.append("_"); 328 parsedPrefix.append(c); 329 } 330 else { 331 parsedPrefix.append(Character.toUpperCase(c)); 332 } 333 } 334 return parsedPrefix.toString(); 335 } 336 337 338 /** 339 * Exception thrown when the {@link Constants} class is asked for 340 * an invalid constant name. 341 */ 342 @SuppressWarnings("serial") 343 public static class ConstantException extends IllegalArgumentException { 344 345 /** 346 * Thrown when an invalid constant name is requested. 347 * @param className name of the class containing the constant definitions 348 * @param field invalid constant name 349 * @param message description of the problem 350 */ 351 public ConstantException(String className, String field, String message) { 352 super("Field '" + field + "' " + message + " in class [" + className + "]"); 353 } 354 355 /** 356 * Thrown when an invalid constant value is looked up. 357 * @param className name of the class containing the constant definitions 358 * @param namePrefix prefix of the searched constant names 359 * @param value the looked up constant value 360 */ 361 public ConstantException(String className, String namePrefix, Object value) { 362 super("No '" + namePrefix + "' field with value '" + value + "' found in class [" + className + "]"); 363 } 364 } 365 366}