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 }