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    *      http://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.HashMap;
20  import java.util.LinkedHashSet;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.concurrent.locks.ReadWriteLock;
24  import java.util.concurrent.locks.ReentrantReadWriteLock;
25  
26  import org.apache.commons.jexl3.JexlContext;
27  import org.apache.commons.jexl3.introspection.JexlUberspect;
28  
29  /**
30   * Helper resolving a simple class name into a fully-qualified class name (hence FqcnResolver) using
31   * package names as roots of import.
32   * <p>This only keeps names of classes to avoid any class loading/reloading/permissions issue.</p>
33   */
34   final class FqcnResolver implements JexlContext.ClassNameResolver {
35      /**
36       * The class loader.
37       */
38      private final JexlUberspect uberspect;
39      /**
40       * A lock for RW concurrent ops.
41       */
42      private final ReadWriteLock lock = new ReentrantReadWriteLock();
43      /**
44       * The set of packages to be used as import roots.
45       */
46      private final Set<String> imports = new LinkedHashSet<>();
47      /**
48       * The map of solved fqcns based on imports keyed on (simple) name,
49       * valued as fully-qualified class name.
50       */
51      private final Map<String, String> fqcns = new HashMap<>();
52      /**
53       * Optional parent solver.
54       */
55      private final FqcnResolver parent;
56  
57      /**
58       * Creates a class name solver.
59       *
60       * @param solver the parent solver
61       * @throws NullPointerException if parent solver is null
62       */
63      FqcnResolver(final FqcnResolver solver) {
64          if (solver == null) {
65              throw new NullPointerException("parent solver can not be null");
66          }
67          this.parent = solver;
68          this.uberspect = solver.uberspect;
69      }
70  
71      /**
72       * Creates a class name solver.
73       *
74       * @param uber   the optional class loader
75       * @param packages the optional package names
76       */
77      FqcnResolver(final JexlUberspect uber, final Iterable<String> packages) {
78          this.uberspect = uber;
79          this.parent = null;
80          importCheck(packages);
81      }
82  
83      /**
84       * Gets a fully qualified class name from a simple class name and imports.
85       *
86       * @param name the simple name
87       * @return the fqcn
88       */
89      String getQualifiedName(final String name) {
90          String fqcn;
91          if (parent != null && (fqcn = parent.getQualifiedName(name)) != null) {
92              return  fqcn;
93          }
94          lock.readLock().lock();
95          try {
96              fqcn = fqcns.get(name);
97          } finally {
98              lock.readLock().unlock();
99          }
100         if (fqcn == null) {
101             final ClassLoader loader = uberspect.getClassLoader();
102             for (final String pkg : imports) {
103                 Class<?> clazz;
104                 try {
105                     clazz = loader.loadClass(pkg + "." + name);
106                 } catch (final ClassNotFoundException e) {
107                     // not in this package
108                     continue;
109                 }
110                 // solved it, insert in map and return
111                 if (clazz != null) {
112                     fqcn = clazz.getName();
113                     lock.writeLock().lock();
114                     try {
115                         fqcns.put(name, fqcn);
116                     } finally {
117                         lock.writeLock().unlock();
118                     }
119                     break;
120                 }
121             }
122         }
123         return fqcn;
124     }
125 
126     /**
127      * Adds a collection of packages as import root, checks the names are one of a package.
128      * @param names the package names
129      */
130     private void importCheck(final Iterable<String> names) {
131         if (names != null) {
132             names.forEach(this::importCheck);
133         }
134     }
135 
136     /**
137      * Adds a package as import root, checks the name if one of a package.
138      * @param name the package name
139      */
140     private void importCheck(final String name) {
141         // check the package name actually points to a package to avoid clutter
142         if (name != null && Package.getPackage(name) != null) {
143             imports.add(name);
144         }
145     }
146 
147     /**
148      * Imports a list of packages as solving roots.
149      *
150      * @param packages the packages
151      * @return this solver
152      */
153     FqcnResolver importPackages(final Iterable<String> packages) {
154         if (packages != null) {
155             lock.writeLock().lock();
156             try {
157                 if (parent == null) {
158                     importCheck(packages);
159                 } else {
160                     packages.forEach(pkg ->{ if (!parent.isImporting(pkg)) { importCheck(pkg); }});
161                 }
162             } finally {
163                 lock.writeLock().unlock();
164             }
165         }
166         return this;
167     }
168 
169     /**
170      * Checks is a package is imported by this solver of one of its ascendants.
171      *
172      * @param pkg the package name
173      * @return true if an import exists for this package, false otherwise
174      */
175     boolean isImporting(final String pkg) {
176         if (parent != null && parent.isImporting(pkg)) {
177             return true;
178         }
179         lock.readLock().lock();
180         try {
181             return imports.contains(pkg);
182         } finally {
183             lock.readLock().unlock();
184         }
185     }
186 
187     @Override
188     public String resolveClassName(final String name) {
189         return getQualifiedName(name);
190     }
191 }