This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit 0ec78e8e46bd268f21d76ca1232fcd3d1651e54c Author: Robert Lazarski <[email protected]> AuthorDate: Mon Oct 12 08:39:17 2026 -1000 Use a generic library domain for the jpa-schema test mappings The HBM fixtures, the custom write-exclude annotation in the tests and the examples in the javadoc and docs now use Library, Branch and Book entities and an @AuditField marker. Coverage is unchanged: same mapping features, same 40 tests. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../axis2/jpa/schema/AnnotationIntrospector.java | 2 +- .../apache/axis2/jpa/schema/EntitySchemaModel.java | 2 +- .../axis2/jpa/schema/HbmBatchSchemaGenerator.java | 10 +- .../axis2/jpa/schema/HbmXmlIntrospector.java | 16 +- .../jpa/schema/HbmBatchSchemaGeneratorTest.java | 166 ++++++++-------- .../axis2/jpa/schema/JpaSchemaGeneratorTest.java | 188 +++++++++--------- modules/jpa-schema/src/test/resources/Book.hbm.xml | 59 ++++++ .../jpa-schema/src/test/resources/Branch.hbm.xml | 175 +++++++++++++++++ .../src/test/resources/DepartmentBO.hbm.xml | 213 --------------------- .../{CompanyBO.hbm.xml => Library.hbm.xml} | 40 ++-- .../src/test/resources/StockExchangeBO.hbm.xml | 60 ------ src/site/xdoc/docs/openapi-jpa-schema.xml | 40 ++-- 12 files changed, 466 insertions(+), 505 deletions(-) diff --git a/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/AnnotationIntrospector.java b/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/AnnotationIntrospector.java index 807662c0c8..579a9df8cd 100644 --- a/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/AnnotationIntrospector.java +++ b/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/AnnotationIntrospector.java @@ -65,7 +65,7 @@ import java.util.Set; * <p>Usage: * <pre>{@code * AnnotationIntrospector introspector = new AnnotationIntrospector(); - * introspector.addWriteExcludeAnnotation("com.example.IgnoreChanges"); + * introspector.addWriteExcludeAnnotation("com.example.audit.AuditField"); * EntitySchemaModel model = introspector.introspect(Product.class); * }</pre> */ diff --git a/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/EntitySchemaModel.java b/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/EntitySchemaModel.java index ce3d80aaa1..aadbff9956 100644 --- a/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/EntitySchemaModel.java +++ b/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/EntitySchemaModel.java @@ -57,7 +57,7 @@ public class EntitySchemaModel { private boolean nullable = true; private boolean generatedValue; // @GeneratedValue or <generator> private boolean versionField; // @Version or <version> - private boolean ignoredForWrite; // @IgnoreChanges, @Transient, or custom + private boolean ignoredForWrite; // @Transient or a registered write-exclude annotation private boolean transientField; // @Transient — excluded from ALL schemas private Integer maxLength; // @Column(length=) or hbm length attribute private String refEntityName; // for relationships: target entity name diff --git a/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/HbmBatchSchemaGenerator.java b/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/HbmBatchSchemaGenerator.java index b6206e9f97..b548e2ddca 100644 --- a/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/HbmBatchSchemaGenerator.java +++ b/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/HbmBatchSchemaGenerator.java @@ -70,15 +70,15 @@ import java.util.Map; * { * "openapi": "3.0.1", * "info": { - * "title": "Generated from 146 HBM XML mappings", + * "title": "Generated from 3 HBM XML mappings", * "version": "1.0.0" * }, * "components": { * "schemas": { - * "CompanyBO": { "type": "object", "properties": { ... } }, - * "CompanyBOWrite": { "type": "object", "properties": { ... } }, - * "DepartmentBO": { ... }, - * "DepartmentBOWrite": { ... }, + * "Library": { "type": "object", "properties": { ... } }, + * "LibraryWrite": { "type": "object", "properties": { ... } }, + * "Branch": { ... }, + * "BranchWrite": { ... }, * ... * } * } diff --git a/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/HbmXmlIntrospector.java b/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/HbmXmlIntrospector.java index 9eb4a41fe4..9a1ecf75bb 100644 --- a/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/HbmXmlIntrospector.java +++ b/modules/jpa-schema/src/main/java/org/apache/axis2/jpa/schema/HbmXmlIntrospector.java @@ -44,17 +44,17 @@ import java.util.Map; * * <p>Parses the Hibernate 3.0 DTD mapping format: * <pre>{@code - * <hibernate-mapping package="com.example.bo"> - * <class name="CompanyBO" table="`COMPANY`"> - * <id name="id" type="java.lang.Long" column="`companyID`"> + * <hibernate-mapping package="org.example.library"> + * <class name="Library" table="`LIBRARY`"> + * <id name="id" type="java.lang.Long" column="`libraryID`"> * <generator class="native"/> * </id> * <version name="version" type="java.lang.Long" column="OBJ_VERSION"/> * <property name="name" type="java.lang.String" not-null="true"/> - * <many-to-one name="createdUser" class="...UserBO" column="`createdUserID`"/> - * <set name="departments" cascade="all-delete-orphan"> - * <key column="`companyID`"/> - * <one-to-many class="...DepartmentBO"/> + * <many-to-one name="createdBy" class="...Member" column="`createdByID`"/> + * <set name="branches" cascade="all-delete-orphan"> + * <key column="`libraryID`"/> + * <one-to-many class="...Branch"/> * </set> * </class> * </hibernate-mapping> @@ -108,7 +108,7 @@ public class HbmXmlIntrospector { * Parse a .hbm.xml file and produce an {@link EntitySchemaModel}. * * @param inputStream the .hbm.xml file content - * @param resourceName name for logging (e.g. "CompanyBO.hbm.xml") + * @param resourceName name for logging (e.g. "Library.hbm.xml") * @return the schema model, or null on parse failure */ public EntitySchemaModel introspect(InputStream inputStream, String resourceName) { diff --git a/modules/jpa-schema/src/test/java/org/apache/axis2/jpa/schema/HbmBatchSchemaGeneratorTest.java b/modules/jpa-schema/src/test/java/org/apache/axis2/jpa/schema/HbmBatchSchemaGeneratorTest.java index a999f76e75..c654081e38 100644 --- a/modules/jpa-schema/src/test/java/org/apache/axis2/jpa/schema/HbmBatchSchemaGeneratorTest.java +++ b/modules/jpa-schema/src/test/java/org/apache/axis2/jpa/schema/HbmBatchSchemaGeneratorTest.java @@ -36,38 +36,38 @@ import static org.junit.Assert.*; * Tests for {@link HbmBatchSchemaGenerator} — the CLI batch tool that * converts a directory of HBM XML files into an OpenAPI 3.0 schemas JSON. * - * <p>Uses test HBM files on the classpath: CompanyBO.hbm.xml, - * DepartmentBO.hbm.xml, StockExchangeBO.hbm.xml. + * <p>Uses test HBM files on the classpath: Library.hbm.xml, + * Branch.hbm.xml, Book.hbm.xml. */ public class HbmBatchSchemaGeneratorTest { private static final ObjectMapper mapper = new ObjectMapper(); // ═══════════════════════════════════════════════════════════════════════ - // StockExchangeBO introspection — validates a production-style HBM + // Book introspection — validates an HBM // mapping with id+generator, version, not-null, many-to-one, set, // nested column element // ═══════════════════════════════════════════════════════════════════════ @Test - public void testStockExchange_parsesEntity() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { - assertNotNull("StockExchangeBO.hbm.xml must be on classpath", is); + public void testBook_parsesEntity() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { + assertNotNull("Book.hbm.xml must be on classpath", is); HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); assertNotNull(model); - assertEquals("StockExchangeBO", model.getEntityName()); - assertEquals("STOCK_EXCHANGE", model.getTableName()); + assertEquals("Book", model.getEntityName()); + assertEquals("BOOK", model.getTableName()); } } @Test - public void testStockExchange_idAndVersion() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + public void testBook_idAndVersion() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); assertEquals(1, model.getIdFieldNames().size()); assertEquals("id", model.getIdFieldNames().get(0)); @@ -83,25 +83,25 @@ public class HbmBatchSchemaGeneratorTest { } @Test - public void testStockExchange_requiredProperty() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + public void testBook_requiredProperty() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); - EntitySchemaModel.FieldModel name = model.getFields().get("name"); + EntitySchemaModel.FieldModel name = model.getFields().get("title"); assertNotNull(name); assertEquals("string", name.getJsonSchemaType()); - assertFalse("name is not-null=true", name.isNullable()); + assertFalse("title is not-null=true", name.isNullable()); } } @Test - public void testStockExchange_nullableProperties() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + public void testBook_nullableProperties() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); - for (String field : new String[]{"micCode", "country", "city", "website", "timeZoneName"}) { + for (String field : new String[]{"isbn", "language", "publisher", "edition", "summary"}) { EntitySchemaModel.FieldModel f = model.getFields().get(field); assertNotNull(field + " should exist", f); assertTrue(field + " should be nullable", f.isNullable()); @@ -110,38 +110,38 @@ public class HbmBatchSchemaGeneratorTest { } @Test - public void testStockExchange_manyToOneRelationship() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + public void testBook_manyToOneRelationship() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); - EntitySchemaModel.FieldModel region = model.getFields().get("region"); - assertNotNull("region relationship should exist", region); - assertEquals("object", region.getJsonSchemaType()); - assertEquals("RegionBO", region.getRefEntityName()); - assertFalse(region.isCollection()); + EntitySchemaModel.FieldModel author = model.getFields().get("author"); + assertNotNull("author relationship should exist", author); + assertEquals("object", author.getJsonSchemaType()); + assertEquals("Author", author.getRefEntityName()); + assertFalse(author.isCollection()); } } @Test - public void testStockExchange_setCollection() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + public void testBook_setCollection() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); - EntitySchemaModel.FieldModel closedDates = model.getFields().get("closedDates"); - assertNotNull("closedDates set should exist", closedDates); - assertEquals("array", closedDates.getJsonSchemaType()); - assertTrue(closedDates.isCollection()); - assertEquals("ExchangeClosedDateBO", closedDates.getRefEntityName()); + EntitySchemaModel.FieldModel loans = model.getFields().get("loans"); + assertNotNull("loans set should exist", loans); + assertEquals("array", loans.getJsonSchemaType()); + assertTrue(loans.isCollection()); + assertEquals("Loan", loans.getRefEntityName()); } } @Test - public void testStockExchange_nestedColumnNotNull() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + public void testBook_nestedColumnNotNull() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); EntitySchemaModel.FieldModel deleted = model.getFields().get("deleted"); assertNotNull(deleted); @@ -152,53 +152,53 @@ public class HbmBatchSchemaGeneratorTest { } @Test - public void testStockExchange_readSchemaIncludesAll() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + public void testBook_readSchemaIncludesAll() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); JsonNode readSchema = JpaSchemaGenerator.generateReadSchema(model); JsonNode props = readSchema.get("properties"); assertTrue("read has id", props.has("id")); assertTrue("read has version", props.has("version")); - assertTrue("read has name", props.has("name")); - assertTrue("read has region", props.has("region")); - assertTrue("read has closedDates", props.has("closedDates")); + assertTrue("read has title", props.has("title")); + assertTrue("read has author", props.has("author")); + assertTrue("read has loans", props.has("loans")); assertTrue("read has deleted", props.has("deleted")); // ID is readOnly in read schema assertTrue(props.get("id").has("readOnly")); - // region is $ref - assertEquals("#/components/schemas/RegionBO", - props.get("region").get("$ref").asText()); + // author is $ref + assertEquals("#/components/schemas/Author", + props.get("author").get("$ref").asText()); - // closedDates is array of $ref - assertEquals("array", props.get("closedDates").get("type").asText()); - assertEquals("#/components/schemas/ExchangeClosedDateBO", - props.get("closedDates").get("items").get("$ref").asText()); + // loans is array of $ref + assertEquals("array", props.get("loans").get("type").asText()); + assertEquals("#/components/schemas/Loan", + props.get("loans").get("items").get("$ref").asText()); } } @Test - public void testStockExchange_writeSchemaExcludesServerManaged() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + public void testBook_writeSchemaExcludesServerManaged() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); JsonNode writeSchema = JpaSchemaGenerator.generateWriteSchema(model); JsonNode props = writeSchema.get("properties"); assertFalse("write excludes generated ID", props.has("id")); assertFalse("write excludes version", props.has("version")); - assertTrue("write includes name", props.has("name")); - assertTrue("write includes region", props.has("region")); - assertTrue("write includes closedDates", props.has("closedDates")); + assertTrue("write includes title", props.has("title")); + assertTrue("write includes author", props.has("author")); + assertTrue("write includes loans", props.has("loans")); // Write schema $refs use "Write" suffix - assertEquals("#/components/schemas/RegionBOWrite", - props.get("region").get("$ref").asText()); + assertEquals("#/components/schemas/AuthorWrite", + props.get("author").get("$ref").asText()); } } @@ -208,8 +208,8 @@ public class HbmBatchSchemaGeneratorTest { @Test public void testBatchGenerator_producesValidOpenApiJson() throws IOException { - // Find the test resources directory (contains CompanyBO, DepartmentBO, StockExchangeBO) - URL resourceUrl = getClass().getResource("/StockExchangeBO.hbm.xml"); + // Find the test resources directory (contains Library, Branch, Book) + URL resourceUrl = getClass().getResource("/Book.hbm.xml"); assertNotNull("test resource must be on classpath", resourceUrl); File resourceDir = new File(resourceUrl.getFile()).getParentFile(); @@ -237,36 +237,36 @@ public class HbmBatchSchemaGeneratorTest { JsonNode schemas = root.get("components").get("schemas"); // Should have read + write schemas for each entity - // At minimum: CompanyBO, DepartmentBO, StockExchangeBO (3 entities × 2 = 6 schemas) + // At minimum: Library, Branch, Book (3 entities × 2 = 6 schemas) assertTrue("should have at least 6 schemas, got " + schemas.size(), schemas.size() >= 6); // Verify specific entities exist - assertTrue("CompanyBO read schema", schemas.has("CompanyBO")); - assertTrue("CompanyBO write schema", schemas.has("CompanyBOWrite")); - assertTrue("StockExchangeBO read schema", schemas.has("StockExchangeBO")); - assertTrue("StockExchangeBO write schema", schemas.has("StockExchangeBOWrite")); - assertTrue("DepartmentBO read schema", schemas.has("DepartmentBO")); - assertTrue("DepartmentBO write schema", schemas.has("DepartmentBOWrite")); - - // Spot-check StockExchangeBO schema content - JsonNode exchangeRead = schemas.get("StockExchangeBO"); - assertEquals("object", exchangeRead.get("type").asText()); - assertTrue(exchangeRead.get("properties").has("name")); - assertTrue(exchangeRead.get("properties").has("region")); - assertTrue(exchangeRead.get("properties").has("closedDates")); + assertTrue("Library read schema", schemas.has("Library")); + assertTrue("Library write schema", schemas.has("LibraryWrite")); + assertTrue("Book read schema", schemas.has("Book")); + assertTrue("Book write schema", schemas.has("BookWrite")); + assertTrue("Branch read schema", schemas.has("Branch")); + assertTrue("Branch write schema", schemas.has("BranchWrite")); + + // Spot-check Book schema content + JsonNode bookRead = schemas.get("Book"); + assertEquals("object", bookRead.get("type").asText()); + assertTrue(bookRead.get("properties").has("title")); + assertTrue(bookRead.get("properties").has("author")); + assertTrue(bookRead.get("properties").has("loans")); // Write schema should not have id/version - JsonNode exchangeWrite = schemas.get("StockExchangeBOWrite"); - assertFalse(exchangeWrite.get("properties").has("id")); - assertFalse(exchangeWrite.get("properties").has("version")); + JsonNode bookWrite = schemas.get("BookWrite"); + assertFalse(bookWrite.get("properties").has("id")); + assertFalse(bookWrite.get("properties").has("version")); } @Test public void testBatchGenerator_fieldCount() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) { + try (InputStream is = getClass().getResourceAsStream("/Book.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Book.hbm.xml"); // 11 properties + 1 id + 1 version + 1 many-to-one + 1 set = 15 fields assertEquals("exact field count", 15, model.getFields().size()); @@ -274,7 +274,7 @@ public class HbmBatchSchemaGeneratorTest { // Count relationships long relCount = model.getFields().values().stream() .filter(f -> f.getRefEntityName() != null).count(); - assertEquals("2 relationships (region + closedDates)", 2, relCount); + assertEquals("2 relationships (author + loans)", 2, relCount); } } } diff --git a/modules/jpa-schema/src/test/java/org/apache/axis2/jpa/schema/JpaSchemaGeneratorTest.java b/modules/jpa-schema/src/test/java/org/apache/axis2/jpa/schema/JpaSchemaGeneratorTest.java index aa8176fe6a..a042251f0f 100644 --- a/modules/jpa-schema/src/test/java/org/apache/axis2/jpa/schema/JpaSchemaGeneratorTest.java +++ b/modules/jpa-schema/src/test/java/org/apache/axis2/jpa/schema/JpaSchemaGeneratorTest.java @@ -59,10 +59,10 @@ public class JpaSchemaGeneratorTest { // Test entities (annotation-based) // ═══════════════════════════════════════════════════════════════════════ - /** Custom "ignore for write" annotation — mimics project-specific audit markers. */ + /** Custom "exclude from write" annotation — stands in for a project-specific audit marker. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) - public @interface IgnoreChanges {} + public @interface AuditField {} public enum ProductStatus { ACTIVE, INACTIVE, SUSPENDED } @@ -77,7 +77,7 @@ public class JpaSchemaGeneratorTest { @Column(nullable = false, length = 200) private String name; - private Double maxExposure; + private Double unitPrice; @Enumerated(EnumType.STRING) private ProductStatus status; @@ -85,10 +85,10 @@ public class JpaSchemaGeneratorTest { @Version private Long objVersion; - @IgnoreChanges + @AuditField private Timestamp created; - @IgnoreChanges + @AuditField private BigDecimal createdUserID; @Transient @@ -196,11 +196,11 @@ public class JpaSchemaGeneratorTest { @Test public void testAnnotationIntrospector_customWriteExcludeAnnotation() { AnnotationIntrospector introspector = new AnnotationIntrospector(); - introspector.addWriteExcludeAnnotation(IgnoreChanges.class.getName()); + introspector.addWriteExcludeAnnotation(AuditField.class.getName()); EntitySchemaModel model = introspector.introspect(TestProduct.class); EntitySchemaModel.FieldModel createdField = model.getFields().get("created"); - assertTrue("@IgnoreChanges should exclude from write", createdField.isIgnoredForWrite()); + assertTrue("@AuditField should exclude from write", createdField.isIgnoredForWrite()); assertTrue(createdField.isExcludedFromWrite()); EntitySchemaModel.FieldModel createdUserId = model.getFields().get("createdUserID"); @@ -251,7 +251,7 @@ public class JpaSchemaGeneratorTest { @Test public void testReadSchema_includesAllFields() { AnnotationIntrospector introspector = new AnnotationIntrospector(); - introspector.addWriteExcludeAnnotation(IgnoreChanges.class.getName()); + introspector.addWriteExcludeAnnotation(AuditField.class.getName()); EntitySchemaModel model = introspector.introspect(TestProduct.class); ObjectNode readSchema = JpaSchemaGenerator.generateReadSchema(model); @@ -272,7 +272,7 @@ public class JpaSchemaGeneratorTest { @Test public void testWriteSchema_excludesServerManagedFields() { AnnotationIntrospector introspector = new AnnotationIntrospector(); - introspector.addWriteExcludeAnnotation(IgnoreChanges.class.getName()); + introspector.addWriteExcludeAnnotation(AuditField.class.getName()); EntitySchemaModel model = introspector.introspect(TestProduct.class); ObjectNode writeSchema = JpaSchemaGenerator.generateWriteSchema(model); @@ -281,13 +281,13 @@ public class JpaSchemaGeneratorTest { // Write schema should exclude server-managed fields assertFalse("@GeneratedValue ID excluded", props.has("productID")); assertFalse("@Version excluded", props.has("objVersion")); - assertFalse("@IgnoreChanges excluded", props.has("created")); - assertFalse("@IgnoreChanges excluded", props.has("createdUserID")); + assertFalse("@AuditField excluded", props.has("created")); + assertFalse("@AuditField excluded", props.has("createdUserID")); assertFalse("@Transient excluded", props.has("computedLabel")); // Regular fields should be present assertTrue(props.has("name")); - assertTrue(props.has("maxExposure")); + assertTrue(props.has("unitPrice")); assertTrue(props.has("status")); assertTrue(props.has("department")); assertTrue(props.has("lineItems")); @@ -357,15 +357,15 @@ public class JpaSchemaGeneratorTest { // ═══════════════════════════════════════════════════════════════════════ @Test - public void testHbmXml_parsesCompanyEntity() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/CompanyBO.hbm.xml")) { + public void testHbmXml_parsesLibraryEntity() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Library.hbm.xml")) { assertNotNull("Test HBM XML file must be on classpath", is); HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "CompanyBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Library.hbm.xml"); assertNotNull(model); - assertEquals("CompanyBO", model.getEntityName()); + assertEquals("Library", model.getEntityName()); assertFalse("Should have at least one ID field", model.getIdFieldNames().isEmpty()); assertNotNull("Should have a version field", model.getVersionFieldName()); assertTrue("Should have multiple fields", model.getFields().size() > 3); @@ -374,9 +374,9 @@ public class JpaSchemaGeneratorTest { @Test public void testHbmXml_idFieldIsGeneratedAndRequired() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/CompanyBO.hbm.xml")) { + try (InputStream is = getClass().getResourceAsStream("/Library.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "CompanyBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Library.hbm.xml"); String idName = model.getIdFieldNames().get(0); EntitySchemaModel.FieldModel idField = model.getFields().get(idName); @@ -387,9 +387,9 @@ public class JpaSchemaGeneratorTest { @Test public void testHbmXml_versionFieldExcludedFromWrite() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/CompanyBO.hbm.xml")) { + try (InputStream is = getClass().getResourceAsStream("/Library.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "CompanyBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Library.hbm.xml"); EntitySchemaModel.FieldModel vf = model.getFields().get(model.getVersionFieldName()); assertNotNull(vf); @@ -400,9 +400,9 @@ public class JpaSchemaGeneratorTest { @Test public void testHbmXml_generatesBothSchemas() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/CompanyBO.hbm.xml")) { + try (InputStream is = getClass().getResourceAsStream("/Library.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "CompanyBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Library.hbm.xml"); ObjectNode readSchema = JpaSchemaGenerator.generateReadSchema(model); ObjectNode writeSchema = JpaSchemaGenerator.generateWriteSchema(model); @@ -424,9 +424,9 @@ public class JpaSchemaGeneratorTest { @Test public void testHbmXml_propertyTypesMapCorrectly() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/CompanyBO.hbm.xml")) { + try (InputStream is = getClass().getResourceAsStream("/Library.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "CompanyBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Library.hbm.xml"); EntitySchemaModel.FieldModel nameField = model.getFields().get("name"); if (nameField != null) { @@ -436,32 +436,32 @@ public class JpaSchemaGeneratorTest { } // ═══════════════════════════════════════════════════════════════════════ - // HBM XML — production-grade mapping (DepartmentBO: 70+ fields) + // HBM XML — large mapping (Branch: 50+ fields) // ═══════════════════════════════════════════════════════════════════════ @Test - public void testHbmXml_department_parsesLargeEntity() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { - assertNotNull("DepartmentBO.hbm.xml must be on classpath", is); + public void testHbmXml_branch_parsesLargeEntity() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Branch.hbm.xml")) { + assertNotNull("Branch.hbm.xml must be on classpath", is); HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "DepartmentBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Branch.hbm.xml"); assertNotNull(model); - assertEquals("DepartmentBO", model.getEntityName()); - assertEquals("DEPARTMENT", model.getTableName()); + assertEquals("Branch", model.getEntityName()); + assertEquals("BRANCH", model.getTableName()); - // 70+ fields: properties + relationships + component fields + // 50+ fields: properties + relationships + component fields assertTrue("Should have 40+ fields, got " + model.getFields().size(), model.getFields().size() >= 40); } } @Test - public void testHbmXml_department_allTypesMapped() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { + public void testHbmXml_branch_allTypesMapped() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Branch.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "DepartmentBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Branch.hbm.xml"); // String property assertEquals("string", model.getFields().get("name").getJsonSchemaType()); @@ -475,86 +475,86 @@ public class JpaSchemaGeneratorTest { assertEquals("boolean", model.getFields().get("active").getJsonSchemaType()); // Double property - assertEquals("number", model.getFields().get("usageScore").getJsonSchemaType()); + assertEquals("number", model.getFields().get("occupancyRate").getJsonSchemaType()); // Long property - assertEquals("integer", model.getFields().get("loginCount1Week").getJsonSchemaType()); - assertEquals("int64", model.getFields().get("loginCount1Week").getJsonSchemaFormat()); + assertEquals("integer", model.getFields().get("loansLast7Days").getJsonSchemaType()); + assertEquals("int64", model.getFields().get("loansLast7Days").getJsonSchemaFormat()); // text type (maps to string) - assertEquals("string", model.getFields().get("historicalDataOptions").getJsonSchemaType()); + assertEquals("string", model.getFields().get("notes").getJsonSchemaType()); } } @Test - public void testHbmXml_department_manyToOneRelationships() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { + public void testHbmXml_branch_manyToOneRelationships() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Branch.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "DepartmentBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Branch.hbm.xml"); // 4 many-to-one relationships - EntitySchemaModel.FieldModel company = model.getFields().get("company"); - assertNotNull("company relationship must be parsed", company); - assertEquals("object", company.getJsonSchemaType()); - assertEquals("CompanyBO", company.getRefEntityName()); - assertFalse("company is not-null", company.isNullable()); + EntitySchemaModel.FieldModel library = model.getFields().get("library"); + assertNotNull("library relationship must be parsed", library); + assertEquals("object", library.getJsonSchemaType()); + assertEquals("Library", library.getRefEntityName()); + assertFalse("library is not-null", library.isNullable()); - EntitySchemaModel.FieldModel policy = model.getFields().get("passwordPolicy"); + EntitySchemaModel.FieldModel policy = model.getFields().get("accessPolicy"); assertNotNull(policy); - assertEquals("PasswordPolicyBO", policy.getRefEntityName()); - assertTrue("passwordPolicy is nullable", policy.isNullable()); + assertEquals("AccessPolicy", policy.getRefEntityName()); + assertTrue("accessPolicy is nullable", policy.isNullable()); } } @Test - public void testHbmXml_department_collectionSets() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { + public void testHbmXml_branch_collectionSets() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Branch.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "DepartmentBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Branch.hbm.xml"); // 4 <set> collections - EntitySchemaModel.FieldModel users = model.getFields().get("users"); - assertNotNull("users collection must be parsed", users); - assertEquals("array", users.getJsonSchemaType()); - assertTrue(users.isCollection()); - assertEquals("UserBO", users.getRefEntityName()); - - EntitySchemaModel.FieldModel portfolios = model.getFields().get("portfolios"); - assertNotNull("portfolios collection must be parsed", portfolios); - assertEquals("PortfolioBO", portfolios.getRefEntityName()); - - EntitySchemaModel.FieldModel customFields = model.getFields().get("customFields"); - assertNotNull(customFields); - assertEquals("CustomFieldBO", customFields.getRefEntityName()); - - EntitySchemaModel.FieldModel ipRanges = model.getFields().get("ipAddressRanges"); - assertNotNull(ipRanges); - assertEquals("IPAddressRangeBO", ipRanges.getRefEntityName()); + EntitySchemaModel.FieldModel members = model.getFields().get("members"); + assertNotNull("members collection must be parsed", members); + assertEquals("array", members.getJsonSchemaType()); + assertTrue(members.isCollection()); + assertEquals("Member", members.getRefEntityName()); + + EntitySchemaModel.FieldModel catalogItems = model.getFields().get("catalogItems"); + assertNotNull("catalogItems collection must be parsed", catalogItems); + assertEquals("CatalogItem", catalogItems.getRefEntityName()); + + EntitySchemaModel.FieldModel events = model.getFields().get("events"); + assertNotNull(events); + assertEquals("BranchEvent", events.getRefEntityName()); + + EntitySchemaModel.FieldModel reservations = model.getFields().get("reservations"); + assertNotNull(reservations); + assertEquals("RoomReservation", reservations.getRefEntityName()); } } @Test - public void testHbmXml_department_componentFlattened() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { + public void testHbmXml_branch_componentFlattened() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Branch.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "DepartmentBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Branch.hbm.xml"); - // Component fields are flattened with prefix: departmentAddress.city - EntitySchemaModel.FieldModel city = model.getFields().get("departmentAddress.city"); - assertNotNull("component field departmentAddress.city must be present", city); + // Component fields are flattened with prefix: branchAddress.city + EntitySchemaModel.FieldModel city = model.getFields().get("branchAddress.city"); + assertNotNull("component field branchAddress.city must be present", city); assertEquals("string", city.getJsonSchemaType()); - EntitySchemaModel.FieldModel zip = model.getFields().get("departmentAddress.zip"); + EntitySchemaModel.FieldModel zip = model.getFields().get("branchAddress.zip"); assertNotNull(zip); assertEquals("integer", zip.getJsonSchemaType()); } } @Test - public void testHbmXml_department_nestedColumnNotNull() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { + public void testHbmXml_branch_nestedColumnNotNull() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Branch.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "DepartmentBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Branch.hbm.xml"); // "deleted" uses nested <column not-null="true"> instead of attribute EntitySchemaModel.FieldModel deleted = model.getFields().get("deleted"); @@ -566,10 +566,10 @@ public class JpaSchemaGeneratorTest { } @Test - public void testHbmXml_department_schemaGeneration() throws IOException { - try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { + public void testHbmXml_branch_schemaGeneration() throws IOException { + try (InputStream is = getClass().getResourceAsStream("/Branch.hbm.xml")) { HbmXmlIntrospector introspector = new HbmXmlIntrospector(); - EntitySchemaModel model = introspector.introspect(is, "DepartmentBO.hbm.xml"); + EntitySchemaModel model = introspector.introspect(is, "Branch.hbm.xml"); ObjectNode readSchema = JpaSchemaGenerator.generateReadSchema(model); ObjectNode writeSchema = JpaSchemaGenerator.generateWriteSchema(model); @@ -581,9 +581,9 @@ public class JpaSchemaGeneratorTest { assertTrue(readProps.has("id")); assertTrue(readProps.has("version")); assertTrue(readProps.has("name")); - assertTrue(readProps.has("users")); - assertTrue(readProps.has("company")); - assertTrue(readProps.has("departmentAddress.city")); + assertTrue(readProps.has("members")); + assertTrue(readProps.has("library")); + assertTrue(readProps.has("branchAddress.city")); // Write schema excludes generated ID and version assertFalse(writeProps.has("id")); @@ -591,16 +591,16 @@ public class JpaSchemaGeneratorTest { // Write schema still has regular fields, relationships, collections assertTrue(writeProps.has("name")); - assertTrue(writeProps.has("users")); - assertTrue(writeProps.has("company")); - assertTrue(writeProps.has("departmentAddress.city")); + assertTrue(writeProps.has("members")); + assertTrue(writeProps.has("library")); + assertTrue(writeProps.has("branchAddress.city")); // Relationship refs in read schema - assertEquals("#/components/schemas/CompanyBO", - readProps.get("company").get("$ref").asText()); - assertEquals("array", readProps.get("users").get("type").asText()); - assertEquals("#/components/schemas/UserBO", - readProps.get("users").get("items").get("$ref").asText()); + assertEquals("#/components/schemas/Library", + readProps.get("library").get("$ref").asText()); + assertEquals("array", readProps.get("members").get("type").asText()); + assertEquals("#/components/schemas/Member", + readProps.get("members").get("items").get("$ref").asText()); } } } diff --git a/modules/jpa-schema/src/test/resources/Book.hbm.xml b/modules/jpa-schema/src/test/resources/Book.hbm.xml new file mode 100644 index 0000000000..8a9c4c9b52 --- /dev/null +++ b/modules/jpa-schema/src/test/resources/Book.hbm.xml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<!DOCTYPE hibernate-mapping PUBLIC + "-//Hibernate/Hibernate Mapping DTD 3.0//EN" + "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> + +<!-- + Mapping for batch generator testing: a book with its author and loans. + Exercises: id+generator, version, not-null properties, nullable properties, + many-to-one, set collection, nested column element with default. +--> +<hibernate-mapping package="org.example.library"> + <class name="Book" table="BOOK" dynamic-update="false" + dynamic-insert="true" select-before-update="false" optimistic-lock="version" + lazy="true" batch-size="300"> + + <id name="id" type="java.lang.Long" column="bookID"> + <generator class="native"/> + </id> + <version name="version" type="java.lang.Long" access="field" + column="OBJ_VERSION" /> + + <property name="title" column="title" type="java.lang.String" + unique="true" not-null="true" /> + <property name="isbn" column="isbn" type="java.lang.String" + not-null="false" /> + <property name="subtitle" column="subtitle" type="java.lang.String" + not-null="false" /> + <property name="language" column="language" type="java.lang.String" + not-null="false" /> + <property name="publisher" column="publisher" type="java.lang.String" + not-null="false" /> + <property name="edition" column="edition" type="java.lang.String" + not-null="false" /> + <property name="genre" column="genre" type="java.lang.String" + not-null="false" /> + <property name="summary" column="summary" type="java.lang.String" + not-null="false" /> + <property name="coverUrl" column="coverUrl" type="java.lang.String" + not-null="false" /> + <property name="seriesName" column="seriesName" type="java.lang.String" + not-null="false" /> + + <many-to-one name="author" + class="org.example.library.Author" + column="authorID" cascade="save-update" /> + + <set name="loans" table="BOOK_LOAN" inverse="true" lazy="true"> + <key> + <column name="bookID" not-null="true" /> + </key> + <one-to-many class="org.example.library.Loan" /> + </set> + + <property name="deleted" type="java.lang.Boolean"> + <column name="deleted" not-null="true" default="0" /> + </property> + + </class> +</hibernate-mapping> diff --git a/modules/jpa-schema/src/test/resources/Branch.hbm.xml b/modules/jpa-schema/src/test/resources/Branch.hbm.xml new file mode 100644 index 0000000000..bb61389017 --- /dev/null +++ b/modules/jpa-schema/src/test/resources/Branch.hbm.xml @@ -0,0 +1,175 @@ +<?xml version="1.0"?> +<!DOCTYPE hibernate-mapping PUBLIC + "-//Hibernate/Hibernate Mapping DTD 3.0//EN" + "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> + +<!-- Large mapping (50+ fields): many property types, text and sql-type columns, + several many-to-one relationships, a component, set collections with filters, + nested column defaults and a placeholder in the generator. --> + +<hibernate-mapping package="org.example.library"> + <class name="Branch" + table="`BRANCH`" + dynamic-update="false" + dynamic-insert="true" + select-before-update="false" + optimistic-lock="version" + lazy="true" + batch-size="300" + > + + <id name="id" type="java.lang.Long" column="`branchID`"> + <generator class="native"> + ${branch_branchid_seq} + </generator> + </id> + <version name="version" type="java.lang.Long" access="field" column="OBJ_VERSION"/> + + <property name="name" column="`name`" + type="java.lang.String" not-null="true" /> + <property name="code" column="`code`" + type="java.lang.String" not-null="false" /> + <property name="created" column="`created`" + type="timestamp" not-null="true" length="6" /> + <property name="modified" column="`modified`" + type="timestamp" not-null="true" length="6" /> + <property name="openedOn" column="`openedOn`" + type="timestamp" not-null="false" length="6" /> + <property name="closedOn" column="`closedOn`" + type="timestamp" not-null="false" length="6" /> + <property name="active" column="`active`" + type="java.lang.Boolean" not-null="false" /> + <property name="wheelchairAccessible" column="`wheelchairAccessible`" + type="java.lang.Boolean" not-null="false" /> + <property name="hasCafe" column="`hasCafe`" + type="java.lang.Boolean" not-null="false" /> + <property name="hasParking" column="`hasParking`" + type="java.lang.Boolean" not-null="false" /> + <property name="hasMakerSpace" column="`hasMakerSpace`" + type="java.lang.Boolean" not-null="false" /> + <property name="eveningHours" column="`eveningHours`" + type="java.lang.Boolean" not-null="false" /> + <property name="weekendHours" column="`weekendHours`" + type="java.lang.Boolean" not-null="false" /> + <property name="notes" column="`notes`" + type="text" not-null="false" /> + + <!-- sql-type override: varchar(max) - no length constraint --> + <property name="description" type="java.lang.String"> + <column name="`description`" sql-type="varchar(max)" not-null="false" /> + </property> + + <property name="websiteUrl" column="`websiteUrl`" + type="java.lang.String" not-null="false" length="8000" /> + <property name="catalogUrl" column="`catalogUrl`" + type="java.lang.String" not-null="false" length="8000" /> + <property name="occupancyRate" column="`occupancyRate`" + type="java.lang.Double" not-null="false" /> + <property name="loansLast7Days" column="`loansLast7Days`" + type="java.lang.Long" not-null="false" /> + <property name="loansLast30Days" column="`loansLast30Days`" + type="java.lang.Long" not-null="false" /> + <property name="loansLast90Days" column="`loansLast90Days`" + type="java.lang.Long" not-null="false" /> + <property name="loansLast365Days" column="`loansLast365Days`" + type="java.lang.Long" not-null="false" /> + <property name="visitAvg7Days" column="`visitAvg7Days`" + type="java.lang.Double" not-null="false" /> + <property name="visitAvg30Days" column="`visitAvg30Days`" + type="java.lang.Double" not-null="false" /> + <property name="visitAvg90Days" column="`visitAvg90Days`" + type="java.lang.Double" not-null="false" /> + <property name="visitAvg365Days" column="`visitAvg365Days`" + type="java.lang.Double" not-null="false" /> + <property name="memberCount7Days" column="`memberCount7Days`" + type="java.lang.Long" not-null="false" /> + <property name="memberCount30Days" column="`memberCount30Days`" + type="java.lang.Long" not-null="false" /> + <property name="staffed" column="`staffed`" + type="java.lang.Boolean" not-null="true" /> + <property name="lastInventory" column="`lastInventory`" + type="timestamp" not-null="false" length="6" /> + <property name="createdByName" column="`createdByName`" + type="java.lang.String" not-null="false" /> + <property name="modifiedByName" column="`modifiedByName`" + type="java.lang.String" not-null="false" /> + <property name="requiresLibraryCard" column="`requiresLibraryCard`" + type="java.lang.Boolean" not-null="false" /> + <property name="deleted" type="java.lang.Boolean"> + <column name="deleted" not-null="true" default="0"/> + </property> + + <property name="openingNotes" column="`openingNotes`" + type="text" not-null="false" /> + <property name="accessibilityNotes" column="`accessibilityNotes`" + type="text" not-null="false" /> + <property name="announcement" column="`announcement`" + type="text" not-null="false" /> + <property name="announcementExpires" column="`announcementExpires`" + type="timestamp" not-null="false" length="6" /> + + <!-- Multiple many-to-one relationships --> + <many-to-one name="createdBy" column="`createdByID`" + class="org.example.library.Member" not-null="true"/> + + <many-to-one name="modifiedBy" column="`modifiedByID`" + class="org.example.library.Member" not-null="true"/> + + <many-to-one name="library" column="`libraryID`" + class="org.example.library.Library" + cascade="save-update" not-null="true"/> + + <many-to-one name="accessPolicy" column="`accessPolicyID`" + class="org.example.library.AccessPolicy" + cascade="save-update" not-null="false"/> + + <!-- Embedded component --> + <component name="branchAddress" class="org.example.library.Address"> + <property name="address1" type="java.lang.String" + column="`branch_address1`" not-null="false"/> + <property name="address2" type="java.lang.String" + column="`branch_address2`" not-null="false"/> + <property name="city" type="java.lang.String" + column="`branch_city`" not-null="false"/> + <property name="state" type="java.lang.String" + column="`branch_state`" not-null="false"/> + <property name="zip" type="java.lang.Integer" + column="`branch_zip`" not-null="false"/> + <property name="country" type="java.lang.String" + column="`branch_country`" not-null="false"/> + <property name="phone" type="java.lang.String" + column="`branch_phone`" not-null="false"/> + </component> + + <!-- Collection mappings (one-to-many via <set>) --> + <set name="reservations" fetch="subselect" table="`ROOM_RESERVATION`" + cascade="all-delete-orphan" inverse="true" lazy="true"> + <key column="`branchID`"/> + <one-to-many class="org.example.library.RoomReservation"/> + <filter name="deletedFilter"/> + </set> + + <set name="members" fetch="subselect" table="`MEMBER`" + cascade="all-delete-orphan" inverse="true" lazy="true"> + <key column="`branchID`"/> + <one-to-many class="org.example.library.Member"/> + <filter name="deletedFilter"/> + </set> + + <set name="events" fetch="subselect" table="`BRANCH_EVENT`" + cascade="all-delete-orphan" inverse="true" lazy="true"> + <key column="`branchID`"/> + <one-to-many class="org.example.library.BranchEvent"/> + <filter name="deletedFilter"/> + </set> + + <set name="catalogItems" fetch="subselect" table="`CATALOG_ITEM`" + cascade="all-delete-orphan" inverse="true" lazy="true"> + <key column="`branchID`"/> + <one-to-many class="org.example.library.CatalogItem"/> + <filter name="deletedFilter"/> + </set> + + <filter name="deletedFilter"/> + </class> +</hibernate-mapping> diff --git a/modules/jpa-schema/src/test/resources/DepartmentBO.hbm.xml b/modules/jpa-schema/src/test/resources/DepartmentBO.hbm.xml deleted file mode 100644 index ae1f61ce0c..0000000000 --- a/modules/jpa-schema/src/test/resources/DepartmentBO.hbm.xml +++ /dev/null @@ -1,213 +0,0 @@ -<?xml version="1.0"?> -<!DOCTYPE hibernate-mapping PUBLIC - "-//Hibernate/Hibernate Mapping DTD//EN" - "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd" > - -<!-- Sanitized production-grade HBM mapping: 70+ fields, multiple types, - collections, components, sql-type overrides, nested column defaults. - Tests that the parser handles real-world complexity. --> - -<hibernate-mapping package="com.example.bo"> - <class name="DepartmentBO" - table="`DEPARTMENT`" - dynamic-update="false" - dynamic-insert="true" - select-before-update="false" - optimistic-lock="version" - lazy="true" - batch-size="300" - > - - <id name="id" type="java.lang.Long" column="`departmentID`"> - <generator class="native"> - ${department_departmentid_seq} - </generator> - </id> - <version name="version" type="java.lang.Long" access="field" column="OBJ_VERSION"/> - - <property name="name" column="`name`" - type="java.lang.String" - not-null="true" /> - - <property name="codeName" column="`codeName`" - type="java.lang.String" - not-null="false" /> - - <property name="created" column="`created`" - type="timestamp" not-null="true" length="6" /> - - <property name="modified" column="`modified`" - type="timestamp" not-null="true" length="6" /> - - <property name="trialBegin" column="`trialBegin`" - type="timestamp" not-null="false" length="6" /> - - <property name="trialEnd" column="`trialEnd`" - type="timestamp" not-null="false" length="6" /> - - <property name="active" column="`active`" - type="java.lang.Boolean" not-null="false" /> - - <property name="restrictDetails" column="`restrictDetails`" - type="java.lang.Boolean" not-null="false" /> - - <property name="dataDeletionRequested" column="`dataDeletionRequested`" - type="java.lang.Boolean" not-null="false" /> - - <property name="historicalDataApproved" column="`historicalDataApproved`" - type="java.lang.Boolean" not-null="false" /> - - <property name="historicalDataOptions" column="`historicalDataOptions`" - type="text" not-null="false" /> - - <property name="aggregateJobRunning" column="`aggregateJobRunning`" - type="java.lang.Boolean" not-null="false" /> - - <property name="aggregateJobQueued" column="`aggregateJobQueued`" - type="java.lang.Boolean" not-null="false" /> - - <property name="useBeta" column="`useBeta`" - type="java.lang.Boolean" not-null="false" /> - - <property name="isReportable" column="`isReportable`" - type="java.lang.Boolean" not-null="false" /> - - <property name="ssoEnabled" column="`ssoEnabled`" - type="java.lang.Boolean" not-null="false" /> - - <!-- sql-type override: nvarchar(max) — no length constraint --> - <property name="ssoMetadataXML" type="java.lang.String"> - <column name="`ssoMetadataXML`" sql-type="nvarchar(max)" not-null="false" /> - </property> - - <property name="ssoLoginURL" column="`ssoLoginURL`" - type="java.lang.String" not-null="false" length="8000" /> - - <property name="ssoLogoutURL" column="`ssoLogoutURL`" - type="java.lang.String" not-null="false" length="8000" /> - - <property name="usageScore" column="`usageScore`" - type="java.lang.Double" not-null="false" /> - - <property name="loginCount1Week" column="`loginCount1Week`" - type="java.lang.Long" not-null="false" /> - <property name="loginCount4Week" column="`loginCount4Week`" - type="java.lang.Long" not-null="false" /> - <property name="loginCount12Week" column="`loginCount12Week`" - type="java.lang.Long" not-null="false" /> - <property name="loginCount52Week" column="`loginCount52Week`" - type="java.lang.Long" not-null="false" /> - - <property name="loginAvg1Week" column="`loginAvg1Week`" - type="java.lang.Double" not-null="false" /> - <property name="loginAvg4Week" column="`loginAvg4Week`" - type="java.lang.Double" not-null="false" /> - <property name="loginAvg12Week" column="`loginAvg12Week`" - type="java.lang.Double" not-null="false" /> - <property name="loginAvg52Week" column="`loginAvg52Week`" - type="java.lang.Double" not-null="false" /> - - <property name="userCount1Week" column="`userCount1Week`" - type="java.lang.Long" not-null="false" /> - <property name="userCount4Week" column="`userCount4Week`" - type="java.lang.Long" not-null="false" /> - - <property name="nonLocked" column="`nonLocked`" - type="java.lang.Boolean" not-null="true" /> - - <property name="dataModified" column="`dataModified`" - type="timestamp" not-null="false" length="6" /> - - <property name="modifiedAppInstance" column="`modifiedAppInstance`" - type="java.lang.String" not-null="false" /> - - <property name="createdDBUserName" column="`createdDBUserName`" - type="java.lang.String" not-null="false" /> - - <property name="modifiedDBUserName" column="`modifiedDBUserName`" - type="java.lang.String" not-null="false" /> - - <property name="require2FactorAuth" column="`require2FactorAuth`" - type="java.lang.Boolean" not-null="false" /> - - <property name="deleted" type="java.lang.Boolean"> - <column name="deleted" not-null="true" default="0"/> - </property> - - <property name="configurationOverrides" column="`configurationOverrides`" - type="text" not-null="false" /> - - <property name="layoutOverrides" column="`layoutOverrides`" - type="text" not-null="false" /> - - <property name="customMessage" column="`customMessage`" - type="text" not-null="false" /> - - <property name="customMessageExpiration" column="`customMessageExpiration`" - type="timestamp" not-null="false" length="6" /> - - <!-- Multiple many-to-one relationships --> - <many-to-one name="createdUser" column="`createdUserID`" - class="com.example.bo.UserBO" not-null="true"/> - - <many-to-one name="modifiedUser" column="`modifiedUserID`" - class="com.example.bo.UserBO" not-null="true"/> - - <many-to-one name="company" column="`companyID`" - class="com.example.bo.CompanyBO" - cascade="save-update" not-null="true"/> - - <many-to-one name="passwordPolicy" column="`passwordPolicyID`" - class="com.example.bo.PasswordPolicyBO" - cascade="save-update" not-null="false"/> - - <!-- Embedded component --> - <component name="departmentAddress" class="com.example.bo.component.Address"> - <property name="address1" type="java.lang.String" - column="`department_address1`" not-null="false"/> - <property name="address2" type="java.lang.String" - column="`department_address2`" not-null="false"/> - <property name="city" type="java.lang.String" - column="`department_city`" not-null="false"/> - <property name="state" type="java.lang.String" - column="`department_state`" not-null="false"/> - <property name="zip" type="java.lang.Integer" - column="`department_zip`" not-null="false"/> - <property name="country" type="java.lang.String" - column="`department_country`" not-null="false"/> - <property name="phone" type="java.lang.String" - column="`department_phone`" not-null="false"/> - </component> - - <!-- Collection mappings (one-to-many via <set>) --> - <set name="ipAddressRanges" fetch="subselect" table="`IPADDRESSRANGE`" - cascade="all-delete-orphan" inverse="true" lazy="true"> - <key column="`departmentID`"/> - <one-to-many class="com.example.bo.IPAddressRangeBO"/> - <filter name="deletedFilter"/> - </set> - - <set name="users" fetch="subselect" table="`APPUSER`" - cascade="all-delete-orphan" inverse="true" lazy="true"> - <key column="`departmentID`"/> - <one-to-many class="com.example.bo.UserBO"/> - <filter name="deletedFilter"/> - </set> - - <set name="customFields" fetch="subselect" table="`CUSTOMFIELD`" - cascade="all-delete-orphan" inverse="true" lazy="true"> - <key column="`departmentID`"/> - <one-to-many class="com.example.bo.CustomFieldBO"/> - <filter name="deletedFilter"/> - </set> - - <set name="portfolios" fetch="subselect" table="`PORTFOLIO`" - cascade="all-delete-orphan" inverse="true" lazy="true"> - <key column="`departmentID`"/> - <one-to-many class="com.example.bo.PortfolioBO"/> - <filter name="deletedFilter"/> - </set> - - <filter name="deletedFilter"/> - </class> -</hibernate-mapping> diff --git a/modules/jpa-schema/src/test/resources/CompanyBO.hbm.xml b/modules/jpa-schema/src/test/resources/Library.hbm.xml similarity index 57% rename from modules/jpa-schema/src/test/resources/CompanyBO.hbm.xml rename to modules/jpa-schema/src/test/resources/Library.hbm.xml index e001f7578c..1f7d8ca681 100644 --- a/modules/jpa-schema/src/test/resources/CompanyBO.hbm.xml +++ b/modules/jpa-schema/src/test/resources/Library.hbm.xml @@ -3,15 +3,17 @@ "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> -<hibernate-mapping package="com.example.bo"> +<!-- Small mapping in quoted-identifier style: backtick-quoted table and columns, + id+generator, version, typed properties, many-to-one, set and component. --> +<hibernate-mapping package="org.example.library"> - <class name="CompanyBO" table="`COMPANY`" + <class name="Library" table="`LIBRARY`" dynamic-insert="true" optimistic-lock="version" lazy="true" batch-size="300"> - <id name="id" type="java.lang.Long" column="`companyID`"> + <id name="id" type="java.lang.Long" column="`libraryID`"> <generator class="native"/> </id> @@ -28,13 +30,13 @@ <property name="created" column="`created`" type="timestamp" not-null="true" length="6"/> - <property name="require2FactorAuth" column="`require2FactorAuth`" + <property name="requiresMembership" column="`requiresMembership`" type="java.lang.Boolean" not-null="false"/> - <property name="maxUsers" column="`maxUsers`" + <property name="maxMembers" column="`maxMembers`" type="java.lang.Integer" not-null="false"/> - <property name="annualFee" column="`annualFee`" + <property name="annualBudget" column="`annualBudget`" type="java.lang.Double" not-null="false"/> <property name="deleted" type="java.lang.Boolean"> @@ -42,27 +44,27 @@ </property> <many-to-one - name="createdUser" - column="`createdUserID`" - class="com.example.bo.UserBO" + name="createdBy" + column="`createdByID`" + class="org.example.library.Member" not-null="true"/> <many-to-one - name="passwordPolicy" - class="com.example.bo.PasswordPolicyBO" - column="`passwordPolicyID`" + name="accessPolicy" + class="org.example.library.AccessPolicy" + column="`accessPolicyID`" not-null="false"/> - <set name="departments" table="`Department`" + <set name="branches" table="`BRANCH`" inverse="true" cascade="all-delete-orphan" lazy="true"> - <key column="`companyID`"/> - <one-to-many class="com.example.bo.DepartmentBO"/> + <key column="`libraryID`"/> + <one-to-many class="org.example.library.Branch"/> </set> - <component name="companyAddress" class="com.example.bo.component.Address"> - <property name="address1" type="java.lang.String" column="`company_address1`" not-null="false"/> - <property name="city" type="java.lang.String" column="`company_city`" not-null="false"/> - <property name="state" type="java.lang.String" column="`company_state`" not-null="false"/> + <component name="libraryAddress" class="org.example.library.Address"> + <property name="address1" type="java.lang.String" column="`library_address1`" not-null="false"/> + <property name="city" type="java.lang.String" column="`library_city`" not-null="false"/> + <property name="state" type="java.lang.String" column="`library_state`" not-null="false"/> </component> </class> diff --git a/modules/jpa-schema/src/test/resources/StockExchangeBO.hbm.xml b/modules/jpa-schema/src/test/resources/StockExchangeBO.hbm.xml deleted file mode 100644 index 2ae97d4434..0000000000 --- a/modules/jpa-schema/src/test/resources/StockExchangeBO.hbm.xml +++ /dev/null @@ -1,60 +0,0 @@ -<?xml version="1.0"?> -<!DOCTYPE hibernate-mapping PUBLIC - "-//Hibernate/Hibernate Mapping DTD//EN" - "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd" > - -<!-- - Sanitized HBM mapping for batch generator testing. - Models a stock exchange with trading hours, location, and relationships. - Exercises: id+generator, version, not-null properties, nullable properties, - many-to-one, set collection, nested column element with default. ---> -<hibernate-mapping package="org.example.bo"> - <class name="StockExchangeBO" table="STOCK_EXCHANGE" dynamic-update="false" - dynamic-insert="true" select-before-update="false" optimistic-lock="version" - lazy="true" batch-size="300"> - - <id name="id" type="java.lang.Long" column="exchangeID"> - <generator class="native"/> - </id> - <version name="version" type="java.lang.Long" access="field" - column="OBJ_VERSION" /> - - <property name="name" column="name" type="java.lang.String" - unique="true" not-null="true" /> - <property name="micCode" column="micCode" - type="java.lang.String" not-null="false" /> - <property name="operatingMicCode" column="operatingMicCode" - type="java.lang.String" not-null="false" /> - <property name="country" column="country" - type="java.lang.String" not-null="false" /> - <property name="isoCountryCode" column="isoCountryCode" - type="java.lang.String" not-null="false" /> - <property name="city" column="city" - type="java.lang.String" not-null="false" /> - <property name="website" column="website" - type="java.lang.String" not-null="false" /> - <property name="openTimeLocal" column="openTimeLocal" - type="java.lang.String" not-null="false" /> - <property name="closeTimeLocal" column="closeTimeLocal" - type="java.lang.String" not-null="false" /> - <property name="timeZoneName" column="timeZoneName" - type="java.lang.String" not-null="false" /> - - <many-to-one name="region" - class="org.example.bo.RegionBO" - column="regionID" cascade="save-update" /> - - <set name="closedDates" table="ExchangeClosed" inverse="true" lazy="true"> - <key> - <column name="exchangeID" not-null="true" /> - </key> - <one-to-many class="org.example.bo.ExchangeClosedDateBO" /> - </set> - - <property name="deleted" type="java.lang.Boolean"> - <column name="deleted" not-null="true" default="0" /> - </property> - - </class> -</hibernate-mapping> diff --git a/src/site/xdoc/docs/openapi-jpa-schema.xml b/src/site/xdoc/docs/openapi-jpa-schema.xml index bc317d232b..9e2dc17047 100644 --- a/src/site/xdoc/docs/openapi-jpa-schema.xml +++ b/src/site/xdoc/docs/openapi-jpa-schema.xml @@ -135,17 +135,17 @@ Map<String, ObjectNode> schemas = JpaSchemaGenerator.generateBothSchemas(m <h2 id="custom_annotations">Custom Audit Annotation Support</h2> <p>Many enterprise codebases have project-specific annotations that mark -fields as server-managed (e.g., <code>@IgnoreChanges</code> for audit +fields as server-managed (e.g., an <code>@AuditField</code> marker for audit timestamps, <code>@CreatedBy</code>, <code>@LastModifiedDate</code>). Register these with the introspector to exclude them from write schemas:</p> <pre> AnnotationIntrospector introspector = new AnnotationIntrospector(); -introspector.addWriteExcludeAnnotation("com.example.IgnoreChanges"); +introspector.addWriteExcludeAnnotation("com.example.audit.AuditField"); introspector.addWriteExcludeAnnotation("com.example.audit.CreatedBy"); EntitySchemaModel model = introspector.introspect(Product.class); -// Fields annotated with @IgnoreChanges or @CreatedBy are now +// Fields annotated with @AuditField or @CreatedBy are now // excluded from the write schema but present in the read schema. </pre> @@ -157,8 +157,8 @@ files and produces the same <code>EntitySchemaModel</code>:</p> <pre> HbmXmlIntrospector introspector = new HbmXmlIntrospector(); -try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { - EntitySchemaModel model = introspector.introspect(is, "DepartmentBO.hbm.xml"); +try (InputStream is = getClass().getResourceAsStream("/Branch.hbm.xml")) { + EntitySchemaModel model = introspector.introspect(is, "Branch.hbm.xml"); ObjectNode readSchema = JpaSchemaGenerator.generateReadSchema(model); } </pre> @@ -198,8 +198,8 @@ try (InputStream is = getClass().getResourceAsStream("/DepartmentBO.hbm.xml")) { <li><strong>Schema generation</strong> (6 tests): read includes all fields, write excludes server-managed fields, required array, relationship $refs, enum values, generateBothSchemas</li> - <li><strong>HBM XML introspection</strong> (10 tests): parses CompanyBO and DepartmentBO - (70+ field production-grade entity), verifies type mapping, relationships, + <li><strong>HBM XML introspection</strong> (10 tests): parses Library and Branch + (a 50+ field entity), verifies type mapping, relationships, collections, component flattening, nested column not-null</li> </ul> @@ -270,17 +270,17 @@ directory, different output artifact:</p> { "openapi": "3.0.1", "info": { - "title": "Generated from 146 HBM XML mappings", + "title": "Generated from 3 HBM XML mappings", "version": "1.0.0" }, "components": { "schemas": { - "CompanyBO": { "type": "object", "properties": { ... } }, - "CompanyBOWrite": { "type": "object", "properties": { ... } }, - "DepartmentBO": { ... }, - "DepartmentBOWrite": { ... }, - "ProductBO": { ... }, - "ProductBOWrite": { ... } + "Book": { "type": "object", "properties": { ... } }, + "BookWrite": { "type": "object", "properties": { ... } }, + "Branch": { ... }, + "BranchWrite": { ... }, + "Library": { ... }, + "LibraryWrite": { ... } } } } @@ -288,19 +288,17 @@ directory, different output artifact:</p> <p>Each entity produces two schemas:</p> <ul> - <li><strong>EntityBO</strong> — read schema (GET responses): all fields, + <li><strong>Entity</strong> — read schema (GET responses): all fields, generated IDs marked <code>readOnly</code></li> - <li><strong>EntityBOWrite</strong> — write schema (POST/PUT requests): + <li><strong>EntityWrite</strong> — write schema (POST/PUT requests): excludes generated IDs, version fields, and custom audit annotations</li> </ul> <p>Console output reports each entity processed:</p> <pre> - OK CompanyBO.hbm.xml → CompanyBO (12 fields, 2 relationships) - OK DepartmentBO.hbm.xml → DepartmentBO (58 fields, 8 relationships) - OK ProductBO.hbm.xml → ProductBO (8 fields, 1 relationships) - OK OrderBO.hbm.xml → OrderBO (15 fields, 3 relationships) - SKIP HistoryData.hbm.xml (no entity found) + OK Book.hbm.xml → Book (15 fields, 2 relationships) + OK Branch.hbm.xml → Branch (55 fields, 8 relationships) + OK Library.hbm.xml → Library (15 fields, 3 relationships) Generated 8 schemas (4 entities × 2 [read + write]) from 5 HBM files Output: /path/to/resources-axis2/openapi-schemas.json
