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