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    *      https://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. <strong>static final</strong>) 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   * <p>
62   * Public access is through {@link StringLookupFactory}.
63   * </p>
64   *
65   * @see StringLookupFactory
66   * @since 1.5
67   */
68  class ConstantStringLookup extends AbstractStringLookup {
69  
70      /** An internally used cache for already retrieved values. */
71      private static final ConcurrentHashMap<String, String> CONSTANT_CACHE = new ConcurrentHashMap<>();
72  
73      /** Constant for the field separator. */
74      private static final char FIELD_SEPARATOR = '.';
75  
76      /**
77       * Defines the singleton for this class.
78       */
79      static final ConstantStringLookup INSTANCE = new ConstantStringLookup();
80  
81      /**
82       * Clears the shared cache with the so far resolved constants.
83       */
84      static void clear() {
85          CONSTANT_CACHE.clear();
86      }
87  
88      /**
89       * Loads the class with the specified name. If an application has special needs regarding the class loaders to be
90       * used, it can hook in here. This implementation delegates to the {@code getClass()} method of Commons Lang's
91       * <code><a href="https://commons.apache.org/lang/api-release/org/apache/commons/lang/ClassUtils.html">
92       * ClassUtils</a></code>.
93       *
94       * @param className the name of the class to be loaded.
95       * @return The corresponding class object.
96       * @throws ClassNotFoundException if the class cannot be loaded.
97       */
98      protected Class<?> fetchClass(final String className) throws ClassNotFoundException {
99          return ClassUtils.getClass(className);
100     }
101 
102     /**
103      * Tries to resolve the specified variable. The passed in variable name is interpreted as the name of a <b>static
104      * final</b> member field of a class. If the value has already been obtained, it can be retrieved from an internal
105      * cache. Otherwise this method will invoke the {@code resolveField()} method and pass in the name of the class and
106      * the field.
107      *
108      * @param key the name of the variable to be resolved.
109      * @return The value of this variable or <strong>null</strong> if it cannot be resolved.
110      */
111     @Override
112     public synchronized String lookup(final String key) {
113         if (key == null) {
114             return null;
115         }
116         String result;
117         result = CONSTANT_CACHE.get(key);
118         if (result != null) {
119             return result;
120         }
121         final int fieldPos = key.lastIndexOf(FIELD_SEPARATOR);
122         if (fieldPos < 0) {
123             return null;
124         }
125         try {
126             final Object value = resolveField(key.substring(0, fieldPos), key.substring(fieldPos + 1));
127             if (value != null) {
128                 final String string = Objects.toString(value, null);
129                 CONSTANT_CACHE.put(key, string);
130                 result = string;
131             }
132         } catch (final Exception ex) {
133             // TODO it would be nice to log
134             return null;
135         }
136         return result;
137     }
138 
139     /**
140      * Determines the value of the specified constant member field of a class. This implementation will call
141      * {@code fetchClass()} to obtain the {@link Class} object for the target class. Then it will use
142      * reflection to obtain the field's value. For this to work the field must be accessible.
143      *
144      * @param className the name of the class.
145      * @param fieldName the name of the member field of that class to read.
146      * @return The field's value.
147      * @throws ReflectiveOperationException if an error occurs.
148      */
149     protected Object resolveField(final String className, final String fieldName) throws ReflectiveOperationException {
150         final Class<?> clazz = fetchClass(className);
151         if (clazz == null) {
152             return null;
153         }
154         return clazz.getField(fieldName).get(null);
155     }
156 }