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  package org.apache.commons.jexl3.internal;
18  
19  import java.util.Collections;
20  import java.util.LinkedHashSet;
21  import java.util.Map;
22  import java.util.Objects;
23  import java.util.Set;
24  import java.util.concurrent.ConcurrentHashMap;
25  
26  import org.apache.commons.jexl3.JexlEngine;
27  import org.apache.commons.jexl3.JexlException;
28  import org.apache.commons.jexl3.introspection.JexlPropertyGet;
29  import org.apache.commons.jexl3.introspection.JexlUberspect;
30  
31  /**
32   * Helper resolving a simple class name into a Fully Qualified Class Name (hence FqcnResolver) using
33   * package names and classes as roots of import.
34   * <p>This only keeps the names of the classes to avoid any class loading/reloading/permissions issue.</p>
35   */
36  public class FqcnResolver implements JexlUberspect.ClassConstantResolver {
37  
38      /**
39       * The uberspect.
40       */
41      private final JexlUberspect uberspect;
42  
43      /**
44       * The set of packages to be used as import roots.
45       */
46      private final Set<String> imports = Collections.synchronizedSet(new LinkedHashSet<>());
47  
48      /**
49       * The map of solved fqcns based on imports keyed on (simple) name,
50       * valued as fully qualified class name.
51       */
52      private final Map<String, String> fqcns = new ConcurrentHashMap<>();
53  
54      /**
55       * Optional parent solver.
56       */
57      private final FqcnResolver parent;
58  
59      /**
60       * Creates a class name solver.
61       *
62       * @param solver the parent solver
63       * @throws NullPointerException if parent solver is null
64       */
65      FqcnResolver(final FqcnResolver solver) {
66          this.parent = Objects.requireNonNull(solver, "solver");
67          this.uberspect = solver.uberspect;
68      }
69  
70      /**
71       * Creates a class name solver.
72       *
73       * @param uber     the optional class loader
74       * @param packages the optional package names
75       */
76      FqcnResolver(final JexlUberspect uber, final Iterable<String> packages) {
77          this.uberspect = Objects.requireNonNull(uber, "uberspect");
78          this.parent = null;
79          importCheck(packages);
80      }
81  
82      /**
83       * Gets a fully qualified class name from a simple class name and imports.
84       *
85       * @param name the simple name
86       * @return the fqcn
87       */
88      String getQualifiedName(final String name) {
89          String fqcn;
90          if (parent != null && (fqcn = parent.getQualifiedName(name)) != null) {
91              return fqcn;
92          }
93          return fqcns.computeIfAbsent(name, this::solveClassName);
94      }
95  
96      /**
97       * Attempts to solve a fully qualified class name from a simple class name.
98       * <p>It tries to solve the class name as package.classname or package$classname (inner class).</p>
99       *
100      * @param name the simple class name
101      * @return the fully qualified class name or null if not found
102      */
103     private String solveClassName(final String name) {
104         for (final String pkg : imports) {
105             // try package.classname or fqcn$classname (inner class)
106             for (final char dot : new char[]{'.', '$'}) {
107                 final Class<?> clazz = uberspect.getClassByName(pkg + dot + name);
108                 // solved it
109                 if (clazz != null) {
110                     return clazz.getName();
111                 }
112             }
113         }
114         return null;
115     }
116 
117     /**
118      * Adds a collection of packages/classes as import root, check each name point to one or the other.
119      *
120      * @param names the package names
121      */
122     private void importCheck(final Iterable<String> names) {
123         if (names != null) {
124             names.forEach(this::importCheck);
125         }
126     }
127 
128     /**
129      * Adds a package as import root, checks the name points to a package or a class.
130      *
131      * @param name the package name
132      */
133     private void importCheck(final String name) {
134         if (name == null || name.isEmpty()) {
135             return;
136         }
137         // check the package name actually points to a package to avoid clutter
138         final Package pkg = Package.getPackage(name);
139         if (pkg == null) {
140             // if it is a class, solve it now
141             final Class<?> clazz = uberspect.getClassByName(name);
142             if (clazz == null) {
143                 throw new JexlException(null, "Cannot import '" + name + "' as it is neither a package nor a class");
144             }
145             fqcns.put(name, clazz.getName());
146         }
147         imports.add(name);
148     }
149 
150     /**
151      * Imports a list of packages as solving roots.
152      *
153      * @param packages the packages
154      * @return this solver
155      */
156     FqcnResolver importPackages(final Iterable<String> packages) {
157         if (packages != null) {
158             if (parent == null) {
159                 importCheck(packages);
160             } else {
161                 packages.forEach(pkg -> {
162                     if (!parent.isImporting(pkg)) {
163                         importCheck(pkg);
164                     }
165                 });
166             }
167         }
168         return this;
169     }
170 
171     /**
172      * Checks is a package is imported by this solver of one of its ascendants.
173      *
174      * @param pkg the package name
175      * @return true if an import exists for this package, false otherwise
176      */
177     boolean isImporting(final String pkg) {
178         if (parent != null && parent.isImporting(pkg)) {
179             return true;
180         }
181         return imports.contains(pkg);
182     }
183 
184     @Override
185     public String resolveClassName(final String name) {
186         return getQualifiedName(name);
187     }
188 
189     @Override
190     public Object resolveConstant(final String cname) {
191         return getConstant(cname.split("\\."));
192     }
193 
194     private Object getConstant(final String... ids) {
195         if (ids.length == 1) {
196             final String pname = ids[0];
197             for (final String cname : fqcns.keySet()) {
198                 final Object constant = getConstant(cname, pname);
199                 if (constant != JexlEngine.TRY_FAILED) {
200                     return constant;
201                 }
202             }
203         } else if (ids.length == 2) {
204             final String cname = ids[0];
205             final String id = ids[1];
206             final String fqcn = resolveClassName(cname);
207             if (fqcn != null) {
208                 final Class<?> clazz = uberspect.getClassByName(fqcn);
209                 if (clazz != null) {
210                     final JexlPropertyGet getter = uberspect.getPropertyGet(clazz, id);
211                     if (getter != null && getter.isConstant()) {
212                         try {
213                             return getter.invoke(clazz);
214                         } catch (final Exception xany) {
215                             // ignore
216                         }
217                     }
218                 }
219             }
220         }
221         return JexlEngine.TRY_FAILED;
222     }
223 }