Skip to content

Commit 6cbfd20

Browse files
authored
HHH-20148 Avoid depending on module java.desktop (hibernate#11768)
* HHH-20148 Avoid depending on module java.desktop + Reconfigure the compiler to disallow the java.desktop module --------- Co-authored-by: Sanne Grinovero <sanne@commonhaus.dev>
1 parent d99d507 commit 6cbfd20

File tree

17 files changed

+680
-43
lines changed

17 files changed

+680
-43
lines changed

hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlProcessingHelper.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
*/
55
package org.hibernate.boot.models.xml.internal;
66

7-
import java.beans.Introspector;
8-
97
import org.hibernate.boot.jaxb.mapping.spi.JaxbEntityMappingsImpl;
108
import org.hibernate.boot.jaxb.mapping.spi.JaxbManagedType;
119
import org.hibernate.boot.models.MemberResolutionException;
1210
import org.hibernate.internal.util.StringHelper;
11+
12+
import static org.hibernate.internal.util.StringHelper.decapitalize;
1313
import org.hibernate.models.spi.FieldDetails;
1414
import org.hibernate.models.spi.MethodDetails;
1515
import org.hibernate.models.spi.MutableClassDetails;
@@ -74,14 +74,14 @@ public static MutableMemberDetails findAttributeMember(
7474
if ( methodDetails.getMethodKind() == MethodDetails.MethodKind.GETTER ) {
7575
if ( methodDetails.getName().startsWith( "get" ) ) {
7676
final String stemName = methodDetails.getName().substring( 3 );
77-
final String decapitalizedStemName = Introspector.decapitalize( stemName );
77+
final String decapitalizedStemName = decapitalize( stemName );
7878
if ( stemName.equals( attributeName ) || decapitalizedStemName.equals( attributeName ) ) {
7979
return (MutableMemberDetails) methodDetails;
8080
}
8181
}
8282
else if ( methodDetails.getName().startsWith( "is" ) ) {
8383
final String stemName = methodDetails.getName().substring( 2 );
84-
final String decapitalizedStemName = Introspector.decapitalize( stemName );
84+
final String decapitalizedStemName = decapitalize( stemName );
8585
if ( stemName.equals( attributeName ) || decapitalizedStemName.equals( attributeName ) ) {
8686
return (MutableMemberDetails) methodDetails;
8787
}

hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
import jakarta.persistence.Transient;
3232

33-
import static java.beans.Introspector.decapitalize;
33+
import static org.hibernate.internal.util.StringHelper.decapitalize;
3434
import static java.lang.Character.isLowerCase;
3535
import static java.lang.Thread.currentThread;
3636

hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,4 +922,37 @@ public static String safeInterning(final String string) {
922922
return string == null ? null : string.intern();
923923
}
924924

925+
/**
926+
* Converts a string to normal Java variable name capitalization following
927+
* the JavaBeans Introspector rules. This normally means converting the first
928+
* character from upper case to lower case, but in the (unusual) special case
929+
* when there is more than one character and both the first and second characters
930+
* are upper case, we leave it alone.
931+
* <p>
932+
* Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays as "URL".
933+
* <p>
934+
* This is a reimplementation of {@code java.beans.Introspector.decapitalize()}
935+
* to avoid pulling in the java.desktop module dependency.
936+
*
937+
* @param name The string to be decapitalized.
938+
* @return The decapitalized version of the string.
939+
*/
940+
public static String decapitalize(final String name) {
941+
if ( name == null || name.isEmpty() ) {
942+
return name;
943+
}
944+
final char firstChar = name.charAt( 0 );
945+
// Already lowercase - return as-is to avoid allocation
946+
if ( Character.isLowerCase( firstChar ) ) {
947+
return name;
948+
}
949+
// Both first and second chars uppercase - return unchanged per JavaBeans spec
950+
if ( name.length() > 1 && Character.isUpperCase( name.charAt( 1 ) ) ) {
951+
return name;
952+
}
953+
final char[] chars = name.toCharArray();
954+
chars[0] = Character.toLowerCase( firstChar );
955+
return new String( chars );
956+
}
957+
925958
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.internal.util.beans;
6+
7+
/**
8+
* Describes the properties of a JavaBean.
9+
* This is a reimplementation to avoid dependency on the {@code java.desktop} module.
10+
*/
11+
public interface BeanInfo {
12+
/**
13+
* Returns an array of {@link PropertyDescriptor}s describing the
14+
* editable properties of the bean.
15+
*
16+
* @return An array of PropertyDescriptor objects, or null if the
17+
* information should be obtained by automatic analysis.
18+
*/
19+
PropertyDescriptor[] getPropertyDescriptors();
20+
}

hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanInfoHelper.java

Lines changed: 158 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,49 @@
33
* Copyright Red Hat Inc. and Hibernate Authors
44
*/
55
package org.hibernate.internal.util.beans;
6-
import java.beans.BeanInfo;
7-
import java.beans.IntrospectionException;
8-
import java.beans.Introspector;
6+
97
import java.lang.reflect.InvocationTargetException;
8+
import java.lang.reflect.Method;
9+
import java.lang.reflect.Modifier;
10+
import java.util.LinkedHashMap;
11+
import java.util.Map;
12+
13+
import static org.hibernate.internal.util.StringHelper.decapitalize;
1014

1115
/**
12-
* Utility for helping deal with {@link BeanInfo}
16+
* Utility for helping deal with {@link BeanInfo}.
17+
* <p>
18+
* This is a simplified reimplementation to avoid dependency on the {@code java.desktop} module.
19+
* Unlike the JDK's {@code java.beans.Introspector}, this implementation:
20+
* <ul>
21+
* <li>Does not validate type compatibility between getters and setters</li>
22+
* <li>Does not introspect methods from implemented interfaces (only class hierarchy)</li>
23+
* <li>Takes the first matching setter when overloads exist</li>
24+
* </ul>
25+
* These simplifications are acceptable for Hibernate's use case of property-name-based injection.
26+
* <p>
27+
* Caching is implemented using {@link ClassValue}, which is the JVM's built-in mechanism for
28+
* associating computed values with classes. Unlike the JDK's {@code Introspector} cache, this
29+
* approach is inherently GC-friendly: when a class is unloaded, the associated {@link BeanInfo}
30+
* is automatically released, preventing classloader leaks without requiring explicit cache flushing.
1331
*
1432
* @author Steve Ebersole
1533
*/
1634
public class BeanInfoHelper {
35+
36+
/**
37+
* Cache of BeanInfo instances, keyed by class.
38+
* Uses ClassValue which automatically handles class unloading - when a class is GC'd,
39+
* its associated BeanInfo is also released, preventing classloader leaks.
40+
* This cache is for the common case where stopClass is null (introspect up to Object).
41+
*/
42+
private static final ClassValue<BeanInfo> BEAN_INFO_CACHE = new ClassValue<>() {
43+
@Override
44+
protected BeanInfo computeValue(final Class<?> type) {
45+
return computeBeanInfo( type, null );
46+
}
47+
};
48+
1749
public interface BeanInfoDelegate {
1850
void processBeanInfo(BeanInfo beanInfo) throws Exception;
1951
}
@@ -48,7 +80,7 @@ public static void visitBeanInfo(Class<?> beanClass, BeanInfoDelegate delegate)
4880

4981
public static void visitBeanInfo(Class<?> beanClass, Class<?> stopClass, BeanInfoDelegate delegate) {
5082
try {
51-
BeanInfo info = Introspector.getBeanInfo( beanClass, stopClass );
83+
final BeanInfo info = getBeanInfo( beanClass, stopClass );
5284
try {
5385
delegate.processBeanInfo( info );
5486
}
@@ -61,11 +93,11 @@ public static void visitBeanInfo(Class<?> beanClass, Class<?> stopClass, BeanInf
6193
catch ( Exception e ) {
6294
throw new BeanIntrospectionException( "Error delegating bean info use", e );
6395
}
64-
finally {
65-
Introspector.flushFromCaches( beanClass );
66-
}
6796
}
68-
catch ( IntrospectionException e ) {
97+
catch ( BeanIntrospectionException e ) {
98+
throw e;
99+
}
100+
catch ( Exception e ) {
69101
throw new BeanIntrospectionException( "Unable to determine bean info from class [" + beanClass.getName() + "]", e );
70102
}
71103
}
@@ -76,7 +108,7 @@ public static <T> T visitBeanInfo(Class<?> beanClass, ReturningBeanInfoDelegate<
76108

77109
public static <T> T visitBeanInfo(Class<?> beanClass, Class<?> stopClass, ReturningBeanInfoDelegate<T> delegate) {
78110
try {
79-
BeanInfo info = Introspector.getBeanInfo( beanClass, stopClass );
111+
final BeanInfo info = getBeanInfo( beanClass, stopClass );
80112
try {
81113
return delegate.processBeanInfo( info );
82114
}
@@ -89,14 +121,126 @@ public static <T> T visitBeanInfo(Class<?> beanClass, Class<?> stopClass, Return
89121
catch ( Exception e ) {
90122
throw new BeanIntrospectionException( "Error delegating bean info use", e );
91123
}
92-
finally {
93-
Introspector.flushFromCaches( beanClass );
94-
}
95124
}
96-
catch ( IntrospectionException e ) {
125+
catch ( BeanIntrospectionException e ) {
126+
throw e;
127+
}
128+
catch ( Exception e ) {
97129
throw new BeanIntrospectionException( "Unable to determine bean info from class [" + beanClass.getName() + "]", e );
98130
}
99131
}
100132

133+
/**
134+
* Introspect a JavaBean and return a BeanInfo object describing its properties.
135+
* This method walks up the class hierarchy to collect all properties.
136+
* <p>
137+
* Results are cached using {@link ClassValue} for the common case where stopClass is null.
138+
* The cache is GC-friendly and does not cause classloader leaks.
139+
*
140+
* @param beanClass The class to introspect
141+
* @param stopClass The base class at which to stop the analysis. Any methods
142+
* declared in the stopClass or its superclasses will be ignored.
143+
* May be null.
144+
* @return A BeanInfo object describing the target bean
145+
*/
146+
public static BeanInfo getBeanInfo(final Class<?> beanClass, final Class<?> stopClass) {
147+
// Use cache for the common case (stopClass == null means introspect up to Object)
148+
if ( stopClass == null ) {
149+
return BEAN_INFO_CACHE.get( beanClass );
150+
}
151+
// Non-null stopClass is rare; compute without caching to keep cache simple
152+
return computeBeanInfo( beanClass, stopClass );
153+
}
154+
155+
private static BeanInfo computeBeanInfo(final Class<?> beanClass, final Class<?> stopClass) {
156+
// LinkedHashMap for reproducible ordering (important for build-time code)
157+
final Map<String, PropertyDescriptor> properties = new LinkedHashMap<>();
158+
159+
// Walk from subclass to superclass; subclass properties take precedence
160+
Class<?> currentClass = beanClass;
161+
while ( currentClass != null && currentClass != stopClass && currentClass != Object.class ) {
162+
introspectClass( currentClass, properties );
163+
currentClass = currentClass.getSuperclass();
164+
}
165+
166+
final PropertyDescriptor[] descriptors = properties.values().toArray( new PropertyDescriptor[0] );
167+
return new SimpleBeanInfo( descriptors );
168+
}
101169

170+
/**
171+
* Simple implementation of {@link BeanInfo} that holds a fixed array of property descriptors.
172+
*/
173+
private static class SimpleBeanInfo implements BeanInfo {
174+
private final PropertyDescriptor[] propertyDescriptors;
175+
176+
SimpleBeanInfo(PropertyDescriptor[] propertyDescriptors) {
177+
this.propertyDescriptors = propertyDescriptors;
178+
}
179+
180+
@Override
181+
public PropertyDescriptor[] getPropertyDescriptors() {
182+
return propertyDescriptors;
183+
}
184+
}
185+
186+
private static void introspectClass(final Class<?> clazz, final Map<String, PropertyDescriptor> properties) {
187+
for ( final Method method : clazz.getDeclaredMethods() ) {
188+
final int modifiers = method.getModifiers();
189+
// Skip static, non-public, bridge, and synthetic methods
190+
if ( Modifier.isStatic( modifiers )
191+
|| !Modifier.isPublic( modifiers )
192+
|| method.isBridge()
193+
|| method.isSynthetic() ) {
194+
continue;
195+
}
196+
197+
final String methodName = method.getName();
198+
final int paramCount = method.getParameterCount();
199+
final Class<?> returnType = method.getReturnType();
200+
201+
// Check for getter: getXxx() or isXxx()
202+
if ( paramCount == 0 && returnType != void.class ) {
203+
String propertyName = null;
204+
if ( methodName.startsWith( "get" ) && methodName.length() > 3 ) {
205+
propertyName = decapitalize( methodName.substring( 3 ) );
206+
}
207+
else if ( methodName.startsWith( "is" ) && methodName.length() > 2
208+
&& ( returnType == boolean.class || returnType == Boolean.class ) ) {
209+
propertyName = decapitalize( methodName.substring( 2 ) );
210+
}
211+
212+
if ( propertyName != null && !propertyName.isEmpty() ) {
213+
final PropertyDescriptor existing = properties.get( propertyName );
214+
if ( existing == null ) {
215+
properties.put( propertyName, new PropertyDescriptor( propertyName, method, null ) );
216+
}
217+
else if ( existing.getReadMethod() == null ) {
218+
// Merge: we had a setter, now add the getter
219+
properties.put( propertyName,
220+
new PropertyDescriptor( propertyName, method, existing.getWriteMethod() ) );
221+
}
222+
// else: subclass already defined a getter, keep it (subclass precedence)
223+
}
224+
}
225+
226+
// Check for setter: setXxx(value)
227+
// Note: if overloaded setters exist, we take the first one found.
228+
// This is acceptable for name-based property matching.
229+
if ( paramCount == 1 && methodName.startsWith( "set" ) && methodName.length() > 3 ) {
230+
final String propertyName = decapitalize( methodName.substring( 3 ) );
231+
if ( !propertyName.isEmpty() ) {
232+
final PropertyDescriptor existing = properties.get( propertyName );
233+
if ( existing == null ) {
234+
properties.put( propertyName, new PropertyDescriptor( propertyName, null, method ) );
235+
}
236+
else if ( existing.getWriteMethod() == null ) {
237+
// Merge: we had a getter, now add the setter
238+
properties.put( propertyName,
239+
new PropertyDescriptor( propertyName, existing.getReadMethod(), method ) );
240+
}
241+
// else: subclass already defined a setter, keep it (subclass precedence)
242+
}
243+
}
244+
}
245+
}
102246
}

hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanIntrospectionException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import org.hibernate.HibernateException;
88

99
/**
10-
* Indicates a problem dealing with {@link java.beans.BeanInfo} via the {@link BeanInfoHelper} delegate.
10+
* Indicates a problem dealing with {@link BeanInfo} via the {@link BeanInfoHelper} delegate.
1111
*
1212
* @author Steve Ebersole
1313
*/
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.internal.util.beans;
6+
7+
import java.lang.reflect.Method;
8+
9+
import org.checkerframework.checker.nullness.qual.Nullable;
10+
11+
/**
12+
* Describes a single property of a JavaBean.
13+
* This is a reimplementation to avoid dependency on the {@code java.desktop} module.
14+
*/
15+
public final class PropertyDescriptor {
16+
private final String name;
17+
private final @Nullable Method readMethod;
18+
private final @Nullable Method writeMethod;
19+
20+
public PropertyDescriptor(final String name, final @Nullable Method readMethod, final @Nullable Method writeMethod) {
21+
this.name = name;
22+
this.readMethod = readMethod;
23+
this.writeMethod = writeMethod;
24+
}
25+
26+
/**
27+
* Gets the programmatic name of this property.
28+
*/
29+
public String getName() {
30+
return name;
31+
}
32+
33+
/**
34+
* Gets the method used to read the property value.
35+
*
36+
* @return The getter method, or null if the property is write-only.
37+
*/
38+
public @Nullable Method getReadMethod() {
39+
return readMethod;
40+
}
41+
42+
/**
43+
* Gets the method used to write the property value.
44+
*
45+
* @return The setter method, or null if the property is read-only.
46+
*/
47+
public @Nullable Method getWriteMethod() {
48+
return writeMethod;
49+
}
50+
}

hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
import java.util.function.Consumer;
5858
import java.util.function.Function;
5959

60-
import static java.beans.Introspector.decapitalize;
60+
import static org.hibernate.internal.util.StringHelper.decapitalize;
6161
import static java.lang.reflect.Modifier.isStatic;
6262
import static java.util.Collections.emptyMap;
6363

0 commit comments

Comments
 (0)