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 }