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 }