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}