View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  
18  package org.apache.commons.text.lookup;
19  
20  import java.util.Objects;
21  import java.util.concurrent.ConcurrentHashMap;
22  
23  import org.apache.commons.lang3.ClassUtils;
24  import org.apache.commons.text.StringSubstitutor;
25  
26  /**
27   * <p>
28   * Looks up the value of a fully-qualified static final value.
29   * </p>
30   * <p>
31   * Sometimes it is necessary in a configuration file to refer to a constant defined in a class. This can be done with
32   * this lookup implementation. Variable names must be in the format {@code apackage.AClass.AFIELD}. The
33   * {@code lookup(String)} method will split the passed in string at the last dot, separating the fully qualified class
34   * name and the name of the constant (i.e. <b>static final</b>) member field. Then the class is loaded and the field's
35   * value is obtained using reflection.
36   * </p>
37   * <p>
38   * Once retrieved values are cached for fast access. This class is thread-safe. It can be used as a standard (i.e.
39   * global) lookup object and serve multiple clients concurrently.
40   * </p>
41   * <p>
42   * Using a {@link StringLookup} from the {@link StringLookupFactory}:
43   * </p>
44   *
45   * <pre>
46   * StringLookupFactory.INSTANCE.constantStringLookup().lookup("java.awt.event.KeyEvent.VK_ESCAPE");
47   * </pre>
48   * <p>
49   * Using a {@link StringSubstitutor}:
50   * </p>
51   *
52   * <pre>
53   * StringSubstitutor.createInterpolator().replace("... ${const:java.awt.event.KeyEvent.VK_ESCAPE} ..."));
54   * </pre>
55   * <p>
56   * The above examples convert {@code java.awt.event.KeyEvent.VK_ESCAPE} to {@code "27"}.
57   * </p>
58   * <p>
59   * This class was adapted from Apache Commons Configuration.
60   * </p>
61   *
62   * @since 1.5
63   */
64  class ConstantStringLookup extends AbstractStringLookup {
65  
66      /** An internally used cache for already retrieved values. */
67      private static final ConcurrentHashMap<String, String> CONSTANT_CACHE = new ConcurrentHashMap<>();
68  
69      /** Constant for the field separator. */
70      private static final char FIELD_SEPARATOR = '.';
71  
72      /**
73       * Defines the singleton for this class.
74       */
75      static final ConstantStringLookup INSTANCE = new ConstantStringLookup();
76  
77      /**
78       * Clears the shared cache with the so far resolved constants.
79       */
80      static void clear() {
81          CONSTANT_CACHE.clear();
82      }
83  
84      /**
85       * Loads the class with the specified name. If an application has special needs regarding the class loaders to be
86       * used, it can hook in here. This implementation delegates to the {@code getClass()} method of Commons Lang's
87       * <code><a href="https://commons.apache.org/lang/api-release/org/apache/commons/lang/ClassUtils.html">
88       * ClassUtils</a></code>.
89       *
90       * @param className the name of the class to be loaded
91       * @return The corresponding class object
92       * @throws ClassNotFoundException if the class cannot be loaded
93       */
94      protected Class<?> fetchClass(final String className) throws ClassNotFoundException {
95          return ClassUtils.getClass(className);
96      }
97  
98      /**
99       * Tries to resolve the specified variable. The passed in variable name is interpreted as the name of a <b>static
100      * final</b> member field of a class. If the value has already been obtained, it can be retrieved from an internal
101      * cache. Otherwise this method will invoke the {@code resolveField()} method and pass in the name of the class and
102      * the field.
103      *
104      * @param key the name of the variable to be resolved
105      * @return The value of this variable or <b>null</b> if it cannot be resolved
106      */
107     @Override
108     public synchronized String lookup(final String key) {
109         if (key == null) {
110             return null;
111         }
112         String result;
113         result = CONSTANT_CACHE.get(key);
114         if (result != null) {
115             return result;
116         }
117         final int fieldPos = key.lastIndexOf(FIELD_SEPARATOR);
118         if (fieldPos < 0) {
119             return null;
120         }
121         try {
122             final Object value = resolveField(key.substring(0, fieldPos), key.substring(fieldPos + 1));
123             if (value != null) {
124                 final String string = Objects.toString(value, null);
125                 CONSTANT_CACHE.put(key, string);
126                 result = string;
127             }
128         } catch (final Exception ex) {
129             // TODO it would be nice to log
130             return null;
131         }
132         return result;
133     }
134 
135     /**
136      * Determines the value of the specified constant member field of a class. This implementation will call
137      * {@code fetchClass()} to obtain the {@link Class} object for the target class. Then it will use
138      * reflection to obtain the field's value. For this to work the field must be accessible.
139      *
140      * @param className the name of the class
141      * @param fieldName the name of the member field of that class to read
142      * @return The field's value
143      * @throws ReflectiveOperationException if an error occurs
144      */
145     protected Object resolveField(final String className, final String fieldName) throws ReflectiveOperationException {
146         final Class<?> clazz = fetchClass(className);
147         if (clazz == null) {
148             return null;
149         }
150         return clazz.getField(fieldName).get(null);
151     }
152 }