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")); + } + } }