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