From 59580b33c5067ce7f8128281078c370d5ec67453 Mon Sep 17 00:00:00 2001 From: Richard Zowalla Date: Wed, 2 Sep 2026 19:49:41 +0200 Subject: [PATCH] [OPENJPA-2993] Recognise is() boolean getters again PR #144 required the character after the get/is/set prefix to be upper case, which made the JavaBeans style getaWord()/setaWord()/isaBoolean() accessors stop being accessors: the property vanished with no error and no warning. 68671e667 took that requirement off isNormalGetter and isSetter again, under the title "Support lowercase property names after get/set prefix", but missed the identical one in isBooleanGetter. This removes it there too, so all three agree. No property or column name changes: OpenJPA lower-cases the first character after the prefix unconditionally, so getaWord() and getAWord() have always resolved to the same property. What changes is only that an entity whose boolean property is spelled isaXxx() is recognised again, as it was in 4.1.x. PropertyAccessCapitalization goes back to the JavaBeans spelling it was written for, so it and PropertyAccessCapitalizationOldBehavior once more cover the two spellings rather than the same one twice. TestAccessorNameDerivation pins the derivation rule itself, including its deliberate divergence from Introspector.decapitalize, without needing a database. --- .../meta/AbstractMetaDataDefaults.java | 20 ++- .../meta/TestAccessorNameDerivation.java | 147 ++++++++++++++++++ .../access/PropertyAccessCapitalization.java | 32 ++-- .../TestPropertyAccessCapitalization.java | 20 ++- 4 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 openjpa-kernel/src/test/java/org/apache/openjpa/meta/TestAccessorNameDerivation.java diff --git a/openjpa-kernel/src/main/java/org/apache/openjpa/meta/AbstractMetaDataDefaults.java b/openjpa-kernel/src/main/java/org/apache/openjpa/meta/AbstractMetaDataDefaults.java index d5bec65cc9..0a975d2591 100644 --- a/openjpa-kernel/src/main/java/org/apache/openjpa/meta/AbstractMetaDataDefaults.java +++ b/openjpa-kernel/src/main/java/org/apache/openjpa/meta/AbstractMetaDataDefaults.java @@ -284,6 +284,14 @@ protected List getPropertyAccessNames(ClassMetaData meta) { * member cannot be managed. Default behavior: For fields, returns the * field name. For getter methods, returns the minus "get" or "is" with * the next letter lower-cased. For other methods, returns null. + *

+ * Note that this deliberately diverges from + * java.beans.Introspector.decapitalize(), which leaves a name + * whose first two characters are both upper case untouched. Here the first + * character is lower-cased unconditionally, so getAWord() and + * getaWord() both yield aWord, so that both + * accessor spellings map to a single property. Changing it would rename + * existing properties and their default column names. See OPENJPA-2993. */ public static String getFieldName(Member member) { if (member instanceof Field) { @@ -379,11 +387,21 @@ public static boolean isNormalGetter(Method method) { * Affirms if the given method matches the following signature * public boolean isXXX() * public Boolean isXXX() + *

+ * The case of the character following the is prefix is + * deliberately not constrained. OpenJPA derives the property name by + * lower-casing the first character after the prefix (see + * {@link #getFieldName}), so isaBoolean() and + * isABoolean() both resolve to the property + * aBoolean. This mirrors {@link #isNormalGetter} and + * PersistenceMetaDataDefaults.isSetter, neither of which + * carries such a requirement; requiring an upper-case character here + * silently dropped JavaBeans-style is<lowercase>() + * properties. See OPENJPA-2993. */ public static boolean isBooleanGetter(Method method) { String methodName = method.getName(); return startsWith(methodName, "is") - && Character.isUpperCase(methodName.charAt(2)) && method.getParameterTypes().length == 0 && isBoolean(method.getReturnType()); } diff --git a/openjpa-kernel/src/test/java/org/apache/openjpa/meta/TestAccessorNameDerivation.java b/openjpa-kernel/src/test/java/org/apache/openjpa/meta/TestAccessorNameDerivation.java new file mode 100644 index 0000000000..c3e3b9838b --- /dev/null +++ b/openjpa-kernel/src/test/java/org/apache/openjpa/meta/TestAccessorNameDerivation.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openjpa.meta; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Pins the accessor recognition and property-name derivation rules of + * {@link AbstractMetaDataDefaults}. + *

+ * OpenJPA lower-cases the first character after the get / + * is prefix unconditionally, so the JavaBeans-Introspector + * spelling (getaWord()) and the capitalized spelling + * (getAWord()) resolve to the same property. Neither getter form + * may require a particular case for that character. See OPENJPA-2467 and + * OPENJPA-2993. + */ +public class TestAccessorNameDerivation { + + private static class Fixture { + public int getaWord() { + return 0; + } + + public int getAWord() { + return 0; + } + + public int getaCAPITAL() { + return 0; + } + + public int getACAPITAL() { + return 0; + } + + public boolean isaBoolean() { + return false; + } + + public boolean isABoolean() { + return false; + } + + public int getA() { + return 0; + } + + public int getA1() { + return 0; + } + + public String getURL() { + return null; + } + + public String isValue() { + return null; + } + + public void getaway() { + } + } + + private static Method method(String name) { + try { + return Fixture.class.getDeclaredMethod(name); + } + catch (NoSuchMethodException nsme) { + throw new IllegalArgumentException(name, nsme); + } + } + + private static String fieldName(String methodName) { + return AbstractMetaDataDefaults.getFieldName(method(methodName)); + } + + @Test + public void testBothGetterSpellingsYieldTheSameName() { + assertTrue(AbstractMetaDataDefaults.isNormalGetter(method("getaWord"))); + assertTrue(AbstractMetaDataDefaults.isNormalGetter(method("getAWord"))); + assertEquals("aWord", fieldName("getaWord")); + assertEquals("aWord", fieldName("getAWord")); + assertEquals("aCAPITAL", fieldName("getaCAPITAL")); + assertEquals("aCAPITAL", fieldName("getACAPITAL")); + } + + @Test + public void testBothBooleanGetterSpellingsYieldTheSameName() { + // OPENJPA-2993: is() must stay a recognized boolean getter, + // otherwise the property is silently dropped from the metadata. + assertTrue(AbstractMetaDataDefaults.isBooleanGetter(method("isaBoolean"))); + assertTrue(AbstractMetaDataDefaults.isBooleanGetter(method("isABoolean"))); + assertTrue(AbstractMetaDataDefaults.isGetter(method("isaBoolean"), false)); + assertEquals("aBoolean", fieldName("isaBoolean")); + assertEquals("aBoolean", fieldName("isABoolean")); + } + + @Test + public void testNonGetters() { + // non-boolean return type + assertFalse(AbstractMetaDataDefaults.isBooleanGetter(method("isValue"))); + assertNull(fieldName("isValue")); + // void return type + assertFalse(AbstractMetaDataDefaults.isNormalGetter(method("getaway"))); + assertNull(fieldName("getaway")); + } + + @Test + public void testShortNames() { + assertEquals("a", fieldName("getA")); + assertEquals("a1", fieldName("getA1")); + } + + @Test + public void testDivergenceFromIntrospectorDecapitalize() { + // OPENJPA-2993: OpenJPA deliberately does not implement the JavaBeans + // rule that leaves a name starting with two upper-case characters + // alone. Aligning with it would rename existing properties and their + // default column names, so the divergence is documented, not fixed. + assertEquals("uRL", fieldName("getURL")); + assertEquals("URL", java.beans.Introspector.decapitalize("URL")); + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/access/PropertyAccessCapitalization.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/access/PropertyAccessCapitalization.java index 7fc53c840f..ce5f191822 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/access/PropertyAccessCapitalization.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/access/PropertyAccessCapitalization.java @@ -22,6 +22,20 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +/** + * Uses the JavaBeans-Introspector / IDE-generated accessor spelling + * (getaWord(), getaCAPITAL(), + * isaBoolean()) for fields whose first character is lower case and + * whose second character is upper case. + * {@link PropertyAccessCapitalizationOldBehavior} is the + * getAWord() / getACAPITAL() / + * isABoolean() twin. Both spellings must resolve to the same + * properties aWord, aCAPITAL and + * aBoolean, because + * AbstractMetaDataDefaults.getFieldName() lower-cases the first + * character after the prefix unconditionally. See OPENJPA-2467 and + * OPENJPA-2993; do not rename these accessors to make a test pass. + */ @Entity @Table(name = "CAPITALIZATION_TABLE") public class PropertyAccessCapitalization { @@ -42,11 +56,11 @@ public class PropertyAccessCapitalization { private boolean BOOLEAN; private boolean Bool; - public int getACAPITAL() { + public int getaCAPITAL() { return aCAPITAL; } - public void setACAPITAL(int aCAPITAL) { + public void setaCAPITAL(int aCAPITAL) { this.aCAPITAL = aCAPITAL; } @@ -83,11 +97,11 @@ public void setWord(int word) { this.word = word; } - public int getAWord() { + public int getaWord() { return aWord; } - public void setAWord(int aWord) { + public void setaWord(int aWord) { this.aWord = aWord; } @@ -139,11 +153,11 @@ public void setAnother(int another) { Another = another; } - public boolean isABoolean() { + public boolean isaBoolean() { return aBoolean; } - public void setABoolean(boolean aBoolean) { + public void setaBoolean(boolean aBoolean) { this.aBoolean = aBoolean; } @@ -198,11 +212,11 @@ public boolean equals(Object obj) { return false; if (getA1() != other.getA1()) return false; - if (isABoolean() != other.isABoolean()) + if (isaBoolean() != other.isaBoolean()) return false; - if (getACAPITAL() != other.getACAPITAL()) + if (getaCAPITAL() != other.getaCAPITAL()) return false; - if (getAWord() != other.getAWord()) + if (getaWord() != other.getaWord()) return false; if (getAaWord() != other.getAaWord()) return false; diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/access/TestPropertyAccessCapitalization.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/access/TestPropertyAccessCapitalization.java index 27b0fdfffd..b945eb2600 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/access/TestPropertyAccessCapitalization.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/access/TestPropertyAccessCapitalization.java @@ -22,6 +22,9 @@ import jakarta.persistence.EntityManager; +import org.apache.openjpa.meta.AccessCode; +import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.persistence.JPAFacadeHelper; import org.apache.openjpa.persistence.test.SingleEMFTestCase; public class TestPropertyAccessCapitalization extends SingleEMFTestCase { @@ -37,17 +40,17 @@ public void testCorrectCapitalization() { Random r = new Random(); entity.setId(r.nextInt()); entity.setWord(r.nextInt()); - entity.setAWord(r.nextInt()); + entity.setaWord(r.nextInt()); entity.setAaWord(r.nextInt()); entity.setAaaWord(r.nextInt()); entity.setCAPITAL(r.nextInt()); - entity.setACAPITAL(r.nextInt()); + entity.setaCAPITAL(r.nextInt()); entity.setAnother(r.nextInt()); entity.setA1(r.nextInt()); entity.setB1(r.nextInt()); entity.setA(r.nextInt()); entity.setB(r.nextInt()); - entity.setABoolean(true); + entity.setaBoolean(true); entity.setBBoolean(true); entity.setBOOLEAN(true); entity.setBool(true); @@ -92,4 +95,15 @@ public void testOldCapitalization() { em.find(PropertyAccessCapitalizationOldBehavior.class, entity.getId()); assertEquals(entity, persistentEntity); } + + public void testBothAccessorSpellingsYieldTheSamePropertyNames() { + ClassMetaData beanStyle = JPAFacadeHelper.getMetaData(emf, PropertyAccessCapitalization.class); + ClassMetaData oldStyle = JPAFacadeHelper.getMetaData(emf, PropertyAccessCapitalizationOldBehavior.class); + for (ClassMetaData meta : new ClassMetaData[] { beanStyle, oldStyle }) { + assertTrue("Access type should be PROPERTY", AccessCode.isProperty(meta.getAccessType())); + assertNotNull(meta + ": 'aWord' should be persistent", meta.getField("aWord")); + assertNotNull(meta + ": 'aCAPITAL' should be persistent", meta.getField("aCAPITAL")); + assertNotNull(meta + ": 'aBoolean' should be persistent", meta.getField("aBoolean")); + } + } }