Skip to content

Commit 681a530

Browse files
committed
Clarify type name handling from different sources
1 parent 1429fee commit 681a530

23 files changed

+410
-134
lines changed
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
package com.oracle.svm.configure;
26+
27+
import jdk.vm.ci.meta.JavaKind;
28+
29+
/*
30+
* There isn't a single standard way of referring to classes by name in the Java ecosystem.
31+
* In the context of Native Image reflection, there are three main ways of referring to a
32+
* class:
33+
*
34+
* * The "type name": this is the result of calling {@code getTypeName()} on a {@code Class}
35+
* object. This is a human-readable name and is the preferred way of specifying classes in
36+
* JSON metadata files.
37+
* * The "reflection name": this is used for calls to {@link Class#forName(String)} and others
38+
* using the same syntax. It is the binary name of the class except for array classes, where
39+
* it is formed using the internal name of the class.
40+
* * The "JNI name": this is used for calls to {code FindClass} through JNI. This name is similar
41+
* to the reflection name but uses '/' instead of '.' as package separator.
42+
*
43+
* This class provides utility methods to be able to switch between those names and avoid
44+
* confusion about which format a given string is encoded as.
45+
*
46+
* Here is a breakdown of the various names of different types of classes:
47+
* | Type | Type name | Reflection name | JNI name |
48+
* | --------------- | ------------------- | -------------------- | -------------------- |
49+
* | Regular class | package.ClassName | package.ClassName | package/ClassName |
50+
* | Primitive type | type | - | - |
51+
* | Array type | package.ClassName[] | [Lpackage.ClassName; | [Lpackage/ClassName; |
52+
* | Primitive array | type[] | [T | [T |
53+
* | Inner class | package.Outer$Inner | package.Outer$Inner | package/Outer$Inner |
54+
* | Anonymous class | package.ClassName$1 | package.ClassName$1 | package/ClassName$1 |
55+
*/
56+
public class ClassNameSupport {
57+
public static String reflectionNameToTypeName(String reflectionName) {
58+
if (!isValidReflectionName(reflectionName)) {
59+
return reflectionName;
60+
}
61+
return reflectionNameToTypeNameUnchecked(reflectionName);
62+
}
63+
64+
public static String jniNameToTypeName(String jniName) {
65+
if (!isValidJNIName(jniName)) {
66+
return jniName;
67+
}
68+
return reflectionNameToTypeNameUnchecked(jniNameToReflectionNameUnchecked(jniName));
69+
}
70+
71+
private static String reflectionNameToTypeNameUnchecked(String reflectionName) {
72+
int arrayDimension = wrappingArrayDimension(reflectionName);
73+
if (arrayDimension > 0) {
74+
return arrayElementTypeToTypeName(reflectionName, arrayDimension) + "[]".repeat(arrayDimension);
75+
}
76+
return reflectionName;
77+
}
78+
79+
public static String typeNameToReflectionName(String typeName) {
80+
if (!isValidTypeName(typeName)) {
81+
return typeName;
82+
}
83+
return typeNameToReflectionNameUnchecked(typeName);
84+
}
85+
86+
public static String typeNameToJNIName(String typeName) {
87+
if (!isValidTypeName(typeName)) {
88+
return typeName;
89+
}
90+
return reflectionNameToJNINameUnchecked(typeNameToReflectionNameUnchecked(typeName));
91+
}
92+
93+
private static String typeNameToReflectionNameUnchecked(String typeName) {
94+
int arrayDimension = trailingArrayDimension(typeName);
95+
if (arrayDimension > 0) {
96+
return "[".repeat(arrayDimension) + typeNameToArrayElementType(typeName.substring(0, typeName.length() - arrayDimension * 2));
97+
}
98+
return typeName;
99+
}
100+
101+
public static String jniNameToReflectionName(String jniName) {
102+
if (!isValidJNIName(jniName)) {
103+
return jniName;
104+
}
105+
return jniNameToReflectionNameUnchecked(jniName);
106+
}
107+
108+
private static String jniNameToReflectionNameUnchecked(String jniName) {
109+
return jniName.replace('/', '.');
110+
}
111+
112+
public static String reflectionNameToJNIName(String reflectionName) {
113+
if (!isValidReflectionName(reflectionName)) {
114+
return reflectionName;
115+
}
116+
return reflectionNameToJNINameUnchecked(reflectionName);
117+
}
118+
119+
private static String reflectionNameToJNINameUnchecked(String reflectionName) {
120+
return reflectionName.replace('.', '/');
121+
}
122+
123+
private static String arrayElementTypeToTypeName(String arrayElementType, int startIndex) {
124+
char typeChar = arrayElementType.charAt(startIndex);
125+
return switch (typeChar) {
126+
case 'L' -> arrayElementType.substring(startIndex + 1, arrayElementType.length() - 1);
127+
case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z' -> JavaKind.fromPrimitiveOrVoidTypeChar(typeChar).getJavaName();
128+
default -> null;
129+
};
130+
}
131+
132+
private static String typeNameToArrayElementType(String typeName) {
133+
Class<?> primitiveType = forPrimitiveName(typeName);
134+
if (primitiveType != null) {
135+
return String.valueOf(JavaKind.fromJavaClass(primitiveType).getTypeChar());
136+
}
137+
return "L" + typeName + ";";
138+
}
139+
140+
public static boolean isValidTypeName(String name) {
141+
return isValidFullyQualifiedClassName(name, 0, name.length() - trailingArrayDimension(name) * 2, '.');
142+
}
143+
144+
public static boolean isValidReflectionName(String name) {
145+
return isValidWrappingArraySyntaxName(name, '.');
146+
}
147+
148+
public static boolean isValidJNIName(String name) {
149+
return isValidWrappingArraySyntaxName(name, '/');
150+
}
151+
152+
private static boolean isValidWrappingArraySyntaxName(String name, char packageSeparator) {
153+
int arrayDimension = wrappingArrayDimension(name);
154+
if (arrayDimension > 0) {
155+
return isValidWrappingArrayElementType(name, arrayDimension, packageSeparator);
156+
}
157+
return isValidFullyQualifiedClassName(name, 0, name.length(), packageSeparator);
158+
}
159+
160+
private static boolean isValidWrappingArrayElementType(String name, int startIndex, char packageSeparator) {
161+
if (startIndex == name.length()) {
162+
return false;
163+
}
164+
return switch (name.charAt(startIndex)) {
165+
case 'L' ->
166+
name.charAt(name.length() - 1) == ';' && isValidFullyQualifiedClassName(name, startIndex + 1, name.length() - 1, packageSeparator);
167+
case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z' -> startIndex == name.length() - 1;
168+
default -> false;
169+
};
170+
}
171+
172+
private static boolean isValidFullyQualifiedClassName(String name, int startIndex, int endIndex, char packageSeparator) {
173+
int lastPackageSeparatorIndex = -1;
174+
for (int i = startIndex; i < endIndex; ++i) {
175+
char current = name.charAt(i);
176+
if (current == packageSeparator) {
177+
if (lastPackageSeparatorIndex == i - 1) {
178+
return false;
179+
}
180+
lastPackageSeparatorIndex = i;
181+
} else if (!Character.isJavaIdentifierPart(current)) {
182+
return false;
183+
}
184+
}
185+
return true;
186+
}
187+
188+
private static int wrappingArrayDimension(String name) {
189+
int arrayDimension = 0;
190+
while (arrayDimension < name.length() && name.charAt(arrayDimension) == '[') {
191+
arrayDimension++;
192+
}
193+
return arrayDimension;
194+
}
195+
196+
private static int trailingArrayDimension(String name) {
197+
int arrayDimension = 0;
198+
while (endsWithTrailingArraySyntax(name, name.length() - arrayDimension * 2)) {
199+
arrayDimension++;
200+
}
201+
return arrayDimension;
202+
}
203+
204+
private static boolean endsWithTrailingArraySyntax(String string, int endIndex) {
205+
return string.charAt(endIndex - 2) == '[' && string.charAt(endIndex - 1) == ']';
206+
}
207+
208+
// Copied from java.lang.Class from JDK 22
209+
public static Class<?> forPrimitiveName(String primitiveName) {
210+
return switch (primitiveName) {
211+
// Integral types
212+
case "int" -> int.class;
213+
case "long" -> long.class;
214+
case "short" -> short.class;
215+
case "char" -> char.class;
216+
case "byte" -> byte.class;
217+
218+
// Floating-point types
219+
case "float" -> float.class;
220+
case "double" -> double.class;
221+
222+
// Other types
223+
case "boolean" -> boolean.class;
224+
case "void" -> void.class;
225+
226+
default -> null;
227+
};
228+
}
229+
}

substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/ConditionalConfigurationParser.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ protected UnresolvedConfigurationCondition parseCondition(EconomicMap<String, Ob
6060
Object object = conditionObject.get(TYPE_REACHED_KEY);
6161
var condition = parseTypeContents(object);
6262
if (condition.isPresent()) {
63-
String className = ((NamedConfigurationTypeDescriptor) condition.get()).name();
64-
return UnresolvedConfigurationCondition.create(className);
63+
NamedConfigurationTypeDescriptor namedDescriptor = checkConditionType(condition.get());
64+
return UnresolvedConfigurationCondition.create(namedDescriptor);
6565
}
6666
} else if (conditionObject.containsKey(TYPE_REACHABLE_KEY)) {
6767
if (runtimeCondition && !checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED)) {
@@ -70,12 +70,19 @@ protected UnresolvedConfigurationCondition parseCondition(EconomicMap<String, Ob
7070
Object object = conditionObject.get(TYPE_REACHABLE_KEY);
7171
var condition = parseTypeContents(object);
7272
if (condition.isPresent()) {
73-
String className = ((NamedConfigurationTypeDescriptor) condition.get()).name();
74-
return UnresolvedConfigurationCondition.create(className, checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED));
73+
NamedConfigurationTypeDescriptor namedDescriptor = checkConditionType(condition.get());
74+
return UnresolvedConfigurationCondition.create(namedDescriptor, checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED));
7575
}
7676
}
7777
}
7878
return UnresolvedConfigurationCondition.alwaysTrue();
7979
}
8080

81+
private NamedConfigurationTypeDescriptor checkConditionType(ConfigurationTypeDescriptor type) {
82+
if (!(type instanceof NamedConfigurationTypeDescriptor)) {
83+
failOnSchemaError("condition should be a fully qualified class name.");
84+
}
85+
return (NamedConfigurationTypeDescriptor) type;
86+
}
87+
8188
}

substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/ConfigurationParser.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ protected record TypeDescriptorWithOrigin(ConfigurationTypeDescriptor typeDescri
243243
protected static Optional<TypeDescriptorWithOrigin> parseName(EconomicMap<String, Object> data, boolean treatAllNameEntriesAsType) {
244244
Object name = data.get(NAME_KEY);
245245
if (name != null) {
246-
NamedConfigurationTypeDescriptor typeDescriptor = new NamedConfigurationTypeDescriptor(asString(name));
246+
NamedConfigurationTypeDescriptor typeDescriptor = NamedConfigurationTypeDescriptor.fromJSONName(asString(name));
247247
return Optional.of(new TypeDescriptorWithOrigin(typeDescriptor, treatAllNameEntriesAsType));
248248
} else {
249249
throw failOnSchemaError("must have type or name specified for an element");
@@ -252,7 +252,7 @@ protected static Optional<TypeDescriptorWithOrigin> parseName(EconomicMap<String
252252

253253
protected static Optional<ConfigurationTypeDescriptor> parseTypeContents(Object typeObject) {
254254
if (typeObject instanceof String stringValue) {
255-
return Optional.of(new NamedConfigurationTypeDescriptor(stringValue));
255+
return Optional.of(NamedConfigurationTypeDescriptor.fromJSONName(stringValue));
256256
} else {
257257
EconomicMap<String, Object> type = asMap(typeObject, "type descriptor should be a string or object");
258258
if (type.containsKey(PROXY_KEY)) {
@@ -271,6 +271,6 @@ protected static Optional<ConfigurationTypeDescriptor> parseTypeContents(Object
271271
private static ProxyConfigurationTypeDescriptor getProxyDescriptor(Object proxyObject) {
272272
List<Object> proxyInterfaces = asList(proxyObject, "proxy interface content should be an interface list");
273273
List<String> proxyInterfaceNames = proxyInterfaces.stream().map(obj -> asString(obj, "proxy")).toList();
274-
return new ProxyConfigurationTypeDescriptor(proxyInterfaceNames);
274+
return ProxyConfigurationTypeDescriptor.fromInterfaceTypeNames(proxyInterfaceNames);
275275
}
276276
}

substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/ConfigurationTypeDescriptor.java

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,7 @@
2626

2727
import java.util.Collection;
2828

29-
import org.graalvm.nativeimage.ImageInfo;
30-
31-
import com.oracle.svm.util.LogUtils;
32-
3329
import jdk.graal.compiler.util.json.JsonPrintable;
34-
import jdk.vm.ci.meta.MetaUtil;
3530

3631
/**
3732
* Provides a representation of a Java type based on String type names. This is used to parse types
@@ -43,18 +38,6 @@
4338
* </ul>
4439
*/
4540
public interface ConfigurationTypeDescriptor extends Comparable<ConfigurationTypeDescriptor>, JsonPrintable {
46-
static String canonicalizeTypeName(String typeName) {
47-
if (typeName == null) {
48-
return null;
49-
}
50-
String name = typeName;
51-
if (name.indexOf('[') != -1) {
52-
/* accept "int[][]", "java.lang.String[]" */
53-
name = MetaUtil.internalNameToJava(MetaUtil.toInternalName(name), true, true);
54-
}
55-
return name;
56-
}
57-
5841
enum Kind {
5942
NAMED,
6043
PROXY
@@ -71,11 +54,4 @@ enum Kind {
7154
* type. This is used to filter configurations based on a String-based class filter.
7255
*/
7356
Collection<String> getAllQualifiedJavaNames();
74-
75-
static String checkQualifiedJavaName(String javaName) {
76-
if (ImageInfo.inImageBuildtimeCode() && !(javaName.indexOf('/') == -1 || javaName.indexOf('/') > javaName.lastIndexOf('.'))) {
77-
LogUtils.warning("Type descriptor requires qualified Java name, not internal representation: %s", javaName);
78-
}
79-
return canonicalizeTypeName(javaName);
80-
}
8157
}

substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/LegacySerializationConfigurationParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ protected void parseSerializationDescriptorObject(EconomicMap<String, Object> da
9595
Arrays.asList(CUSTOM_TARGET_CONSTRUCTOR_CLASS_KEY, CONDITIONAL_KEY));
9696
}
9797

98-
NamedConfigurationTypeDescriptor targetSerializationClass = new NamedConfigurationTypeDescriptor(asString(data.get(NAME_KEY)));
98+
NamedConfigurationTypeDescriptor targetSerializationClass = NamedConfigurationTypeDescriptor.fromJSONName(asString(data.get(NAME_KEY)));
9999
UnresolvedConfigurationCondition unresolvedCondition = parseCondition(data, false);
100100
var condition = conditionResolver.resolveCondition(unresolvedCondition);
101101
if (!condition.isPresent()) {

substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/NamedConfigurationTypeDescriptor.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,30 @@
2727
import java.io.IOException;
2828
import java.util.Collection;
2929
import java.util.Collections;
30+
import java.util.Objects;
3031

3132
import jdk.graal.compiler.util.json.JsonWriter;
3233

3334
public record NamedConfigurationTypeDescriptor(String name) implements ConfigurationTypeDescriptor {
3435

35-
public NamedConfigurationTypeDescriptor(String name) {
36-
this.name = ConfigurationTypeDescriptor.checkQualifiedJavaName(name);
36+
public static NamedConfigurationTypeDescriptor fromJSONName(String jsonName) {
37+
if (!ClassNameSupport.isValidTypeName(jsonName) && ClassNameSupport.isValidReflectionName(jsonName)) {
38+
return fromReflectionName(jsonName);
39+
}
40+
return fromTypeName(jsonName);
41+
}
42+
43+
public static NamedConfigurationTypeDescriptor fromTypeName(String typeName) {
44+
Objects.requireNonNull(typeName);
45+
return new NamedConfigurationTypeDescriptor(typeName);
46+
}
47+
48+
public static NamedConfigurationTypeDescriptor fromReflectionName(String reflectionName) {
49+
return fromTypeName(ClassNameSupport.reflectionNameToTypeName(reflectionName));
50+
}
51+
52+
public static NamedConfigurationTypeDescriptor fromJNIName(String jniName) {
53+
return fromTypeName(ClassNameSupport.jniNameToTypeName(jniName));
3754
}
3855

3956
@Override
@@ -60,6 +77,22 @@ public int compareTo(ConfigurationTypeDescriptor other) {
6077
}
6178
}
6279

80+
@Override
81+
public boolean equals(Object object) {
82+
if (this == object) {
83+
return true;
84+
}
85+
if (!(object instanceof NamedConfigurationTypeDescriptor that)) {
86+
return false;
87+
}
88+
return Objects.equals(name, that.name);
89+
}
90+
91+
@Override
92+
public int hashCode() {
93+
return Objects.hashCode(name);
94+
}
95+
6396
@Override
6497
public void printJson(JsonWriter writer) throws IOException {
6598
writer.quote(name);

0 commit comments

Comments
 (0)