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.introspection;
18  
19  import java.lang.invoke.MethodHandle;
20  import java.lang.invoke.MethodHandles;
21  import java.lang.invoke.MethodType;
22  
23  /**
24   * Utility for Java9+ backport in Java8 of class and module related methods, and Java16+ backport of
25   * record introspection ({@code Class#isRecord()}, {@code Class#getRecordComponents()}).
26   */
27  final class ClassTool {
28  
29      /** {@code Class#isRecord()}; null on a pre Java-16 runtime. */
30      private static final MethodHandle IS_RECORD;
31  
32      /** {@code Class#getRecordComponents()}; null on a pre Java-16 runtime. */
33      private static final MethodHandle GET_RECORD_COMPONENTS;
34  
35      /** {@code java.lang.reflect.RecordComponent#getName()}; null on a pre Java-16 runtime. */
36      private static final MethodHandle RECORD_COMPONENT_GET_NAME;
37  
38      /** {@code Class#getModule()}; null on a pre Java-9 runtime. */
39      private static final MethodHandle GET_MODULE;
40  
41      /** {@code Class#getPackageName()}; null on a pre Java-9 runtime. */
42      private static final MethodHandle GET_PKGNAME;
43  
44      /** {@code Module#isExported(String, Module)}; null on a pre Java-9 runtime. */
45      private static final MethodHandle IS_EXPORTED;
46  
47      /** The {@code java.lang.Module} that declares this class; null on a pre Java-9 runtime. */
48      private static final Object JEXL_MODULE;
49  
50      static {
51          final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
52          final ClassLoader loader = ClassTool.class.getClassLoader();
53  
54          // Java 16+ record introspection backport
55          MethodHandle isRecord = null;
56          MethodHandle getRecordComponents = null;
57          MethodHandle recordComponentGetName = null;
58          try {
59              final Class<?> componentc = loader.loadClass("java.lang.reflect.RecordComponent");
60              final Class<?> componentArrayc = java.lang.reflect.Array.newInstance(componentc, 0).getClass();
61              isRecord = LOOKUP.findVirtual(Class.class, "isRecord", MethodType.methodType(boolean.class));
62              getRecordComponents = LOOKUP.findVirtual(Class.class, "getRecordComponents", MethodType.methodType(componentArrayc));
63              recordComponentGetName = LOOKUP.findVirtual(componentc, "getName", MethodType.methodType(String.class));
64          } catch (final Throwable xnotfound) {
65              // ignore all; records unsupported on this runtime
66          }
67          IS_RECORD = isRecord;
68          GET_RECORD_COMPONENTS = getRecordComponents;
69          RECORD_COMPONENT_GET_NAME = recordComponentGetName;
70  
71          // Java 9+ module reflection backport
72          MethodHandle getModule = null;
73          MethodHandle getPackageName = null;
74          MethodHandle isExported = null;
75          Object myModule = null;
76          try {
77              final Class<?> modulec = loader.loadClass("java.lang.Module");
78              if (modulec != null) {
79                  getModule = LOOKUP.findVirtual(Class.class, "getModule", MethodType.methodType(modulec));
80                  if (getModule != null) {
81                      getPackageName = LOOKUP.findVirtual(Class.class, "getPackageName", MethodType.methodType(String.class));
82                      if (getPackageName != null) {
83                          myModule = getModule.invoke(ClassTool.class);
84                          isExported = LOOKUP.findVirtual(modulec, "isExported", MethodType.methodType(boolean.class, String.class, modulec));
85                      }
86                  }
87              }
88          } catch (final Throwable e) {
89              // ignore all
90          }
91          JEXL_MODULE = myModule;
92          GET_MODULE = getModule;
93          GET_PKGNAME = getPackageName;
94          IS_EXPORTED = isExported;
95      }
96  
97      /**
98       * Whether the given class is a record on this runtime.
99       *
100      * @param clazz the class to check
101      * @return true if clazz is a record, false if it is not or if records are unsupported here
102      */
103     static boolean isRecord(final Class<?> clazz) {
104         try {
105             return IS_RECORD != null && (boolean) IS_RECORD.invoke(clazz);
106         } catch (final Throwable xfail) {
107             return false;
108         }
109     }
110 
111     /**
112      * Whether the given class declares a record component named {@code property}.
113      * <p>Used only to confirm {@code property} is a genuine record component before the accessor is
114      * resolved through the (permission-checked) {@link Introspector}; the accessor method instances
115      * gathered here are discarded.</p>
116      *
117      * @param clazz the record class
118      * @param property the property name to match against the record's components
119      * @return true if clazz declares a record component named property
120      */
121     static boolean hasRecordComponent(final Class<?> clazz, final String property) {
122         if (GET_RECORD_COMPONENTS != null && RECORD_COMPONENT_GET_NAME != null) {
123             try {
124                 final Object[] components = (Object[]) GET_RECORD_COMPONENTS.invoke(clazz);
125                 for (final Object component : components) {
126                     if (property.equals(RECORD_COMPONENT_GET_NAME.invoke(component))) {
127                         return true;
128                     }
129                 }
130             } catch (final Throwable xfail) {
131                 // ignore and fall through to return false
132             }
133         }
134         return false;
135     }
136 
137     /**
138      * Gets the package name of a class (class.getPackage() may return null).
139      *
140      * @param clz The class
141      * @return The class package name
142      */
143     static String getPackageName(final Class<?> clz) {
144         String pkgName = "";
145         if (clz != null) {
146             // use native if we can
147             if (GET_PKGNAME != null) {
148                 try {
149                     return (String) GET_PKGNAME.invoke(clz);
150                 } catch (final Throwable xany) {
151                     return "";
152                 }
153             }
154             // remove array
155             Class<?> clazz = clz;
156             while (clazz.isArray()) {
157                 clazz = clazz.getComponentType();
158             }
159             // mimic getPackageName()
160             if (clazz.isPrimitive()) {
161                 return "java.lang";
162             }
163             // remove enclosing
164             Class<?> walk = clazz.getEnclosingClass();
165             while (walk != null) {
166                 clazz = walk;
167                 walk = walk.getEnclosingClass();
168             }
169             final Package pkg = clazz.getPackage();
170             // pkg may be null for unobvious reasons
171             if (pkg == null) {
172                 final String name = clazz.getName();
173                 final int dot = name.lastIndexOf('.');
174                 if (dot > 0) {
175                     pkgName = name.substring(0, dot);
176                 }
177             } else {
178                 pkgName = pkg.getName();
179             }
180         }
181         return pkgName;
182     }
183 
184     /**
185      * Checks whether a class is exported by its module (Java 9+) to JEXL.
186      * The code performs the following sequence through reflection (since the same jar can run
187      * on a Java8 or Java9+ runtime and the module features does not exist on 8).
188      * {@code
189      * Module jexlModule = ClassTool.class.getModule();
190      * Module module = declarator.getModule();
191      * return module.isExported(declarator.getPackageName(), jexlModule);
192      * }
193      * This is required since some classes and methods may not be exported thus not callable through
194      * reflection.  A package can be non-exported, <em>unconditionally</em> exported (to all reading
195      * modules), or use <em>qualified</em> exports to only export the package to specifically named
196      * modules.  This method is only concerned with whether JEXL may reflectively access the
197      * package, so a qualified export naming the JEXL module is the least-privilege access required.
198      * The declarator's module may also use: unqualified exports, qualified {@code opens}, or
199      * unqualified {@code opens}, in increasing order of privilege; the last two allow reflective
200      * access to non-public members and are not recommended.
201      *
202      * @param declarator The class
203      * @return true if class is exported (to JEXL) or no module support exists
204      */
205     static boolean isExported(final Class<?> declarator) {
206         if (IS_EXPORTED != null) {
207             try {
208                 final Object module = GET_MODULE.invoke(declarator);
209                 if (module != null) {
210                     final String pkgName = (String) GET_PKGNAME.invoke(declarator);
211                     return (Boolean) IS_EXPORTED.invoke(module, pkgName, JEXL_MODULE);
212                 }
213             } catch (final Throwable e) {
214                 // ignore
215             }
216         }
217         return true;
218     }
219 
220 }