Skip to content

Commit d9baa0c

Browse files
Add HbmBatchSchemaGenerator tests with StockExchangeBO fixture
11 new tests covering the batch generator and a production-style HBM XML mapping (StockExchangeBO) that exercises: - id with native generator - version field (excluded from write schema) - not-null required property (name) - nullable properties (micCode, country, city, etc.) - many-to-one relationship (region → RegionBO) - set collection (closedDates → ExchangeClosedDateBO) - nested <column not-null="true"> element (deleted) - read schema: all fields, ID readOnly, $ref relationships - write schema: excludes id/version, $ref uses Write suffix - end-to-end batch: scans test resources dir, produces valid OpenAPI 3.0 JSON with read+write schemas for all entities StockExchangeBO.hbm.xml is a sanitized, OSS-clean test fixture derived from a real-world exchange entity mapping. 40 tests total (29 existing + 11 new), all pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2940ab7 commit d9baa0c

2 files changed

Lines changed: 340 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.axis2.jpa.schema;
21+
22+
import com.fasterxml.jackson.databind.JsonNode;
23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
25+
import org.junit.Test;
26+
27+
import java.io.File;
28+
import java.io.IOException;
29+
import java.io.InputStream;
30+
import java.net.URL;
31+
import java.nio.file.Files;
32+
33+
import static org.junit.Assert.*;
34+
35+
/**
36+
* Tests for {@link HbmBatchSchemaGenerator} — the CLI batch tool that
37+
* converts a directory of HBM XML files into an OpenAPI 3.0 schemas JSON.
38+
*
39+
* <p>Uses test HBM files on the classpath: CompanyBO.hbm.xml,
40+
* DepartmentBO.hbm.xml, StockExchangeBO.hbm.xml.
41+
*/
42+
public class HbmBatchSchemaGeneratorTest {
43+
44+
private static final ObjectMapper mapper = new ObjectMapper();
45+
46+
// ═══════════════════════════════════════════════════════════════════════
47+
// StockExchangeBO introspection — validates a production-style HBM
48+
// mapping with id+generator, version, not-null, many-to-one, set,
49+
// nested column element
50+
// ═══════════════════════════════════════════════════════════════════════
51+
52+
@Test
53+
public void testStockExchange_parsesEntity() throws IOException {
54+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
55+
assertNotNull("StockExchangeBO.hbm.xml must be on classpath", is);
56+
57+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
58+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
59+
60+
assertNotNull(model);
61+
assertEquals("StockExchangeBO", model.getEntityName());
62+
assertEquals("STOCK_EXCHANGE", model.getTableName());
63+
}
64+
}
65+
66+
@Test
67+
public void testStockExchange_idAndVersion() throws IOException {
68+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
69+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
70+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
71+
72+
assertEquals(1, model.getIdFieldNames().size());
73+
assertEquals("id", model.getIdFieldNames().get(0));
74+
75+
EntitySchemaModel.FieldModel idField = model.getFields().get("id");
76+
assertTrue("ID should be generated", idField.isGeneratedValue());
77+
assertFalse("ID should not be nullable", idField.isNullable());
78+
79+
assertEquals("version", model.getVersionFieldName());
80+
assertTrue(model.getFields().get("version").isVersionField());
81+
assertTrue(model.getFields().get("version").isExcludedFromWrite());
82+
}
83+
}
84+
85+
@Test
86+
public void testStockExchange_requiredProperty() throws IOException {
87+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
88+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
89+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
90+
91+
EntitySchemaModel.FieldModel name = model.getFields().get("name");
92+
assertNotNull(name);
93+
assertEquals("string", name.getJsonSchemaType());
94+
assertFalse("name is not-null=true", name.isNullable());
95+
}
96+
}
97+
98+
@Test
99+
public void testStockExchange_nullableProperties() throws IOException {
100+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
101+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
102+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
103+
104+
for (String field : new String[]{"micCode", "country", "city", "website", "timeZoneName"}) {
105+
EntitySchemaModel.FieldModel f = model.getFields().get(field);
106+
assertNotNull(field + " should exist", f);
107+
assertTrue(field + " should be nullable", f.isNullable());
108+
}
109+
}
110+
}
111+
112+
@Test
113+
public void testStockExchange_manyToOneRelationship() throws IOException {
114+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
115+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
116+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
117+
118+
EntitySchemaModel.FieldModel region = model.getFields().get("region");
119+
assertNotNull("region relationship should exist", region);
120+
assertEquals("object", region.getJsonSchemaType());
121+
assertEquals("RegionBO", region.getRefEntityName());
122+
assertFalse(region.isCollection());
123+
}
124+
}
125+
126+
@Test
127+
public void testStockExchange_setCollection() throws IOException {
128+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
129+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
130+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
131+
132+
EntitySchemaModel.FieldModel closedDates = model.getFields().get("closedDates");
133+
assertNotNull("closedDates set should exist", closedDates);
134+
assertEquals("array", closedDates.getJsonSchemaType());
135+
assertTrue(closedDates.isCollection());
136+
assertEquals("ExchangeClosedDateBO", closedDates.getRefEntityName());
137+
}
138+
}
139+
140+
@Test
141+
public void testStockExchange_nestedColumnNotNull() throws IOException {
142+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
143+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
144+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
145+
146+
EntitySchemaModel.FieldModel deleted = model.getFields().get("deleted");
147+
assertNotNull(deleted);
148+
assertEquals("boolean", deleted.getJsonSchemaType());
149+
assertFalse("deleted with nested column not-null=true should be required",
150+
deleted.isNullable());
151+
}
152+
}
153+
154+
@Test
155+
public void testStockExchange_readSchemaIncludesAll() throws IOException {
156+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
157+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
158+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
159+
160+
JsonNode readSchema = JpaSchemaGenerator.generateReadSchema(model);
161+
JsonNode props = readSchema.get("properties");
162+
163+
assertTrue("read has id", props.has("id"));
164+
assertTrue("read has version", props.has("version"));
165+
assertTrue("read has name", props.has("name"));
166+
assertTrue("read has region", props.has("region"));
167+
assertTrue("read has closedDates", props.has("closedDates"));
168+
assertTrue("read has deleted", props.has("deleted"));
169+
170+
// ID is readOnly in read schema
171+
assertTrue(props.get("id").has("readOnly"));
172+
173+
// region is $ref
174+
assertEquals("#/components/schemas/RegionBO",
175+
props.get("region").get("$ref").asText());
176+
177+
// closedDates is array of $ref
178+
assertEquals("array", props.get("closedDates").get("type").asText());
179+
assertEquals("#/components/schemas/ExchangeClosedDateBO",
180+
props.get("closedDates").get("items").get("$ref").asText());
181+
}
182+
}
183+
184+
@Test
185+
public void testStockExchange_writeSchemaExcludesServerManaged() throws IOException {
186+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
187+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
188+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
189+
190+
JsonNode writeSchema = JpaSchemaGenerator.generateWriteSchema(model);
191+
JsonNode props = writeSchema.get("properties");
192+
193+
assertFalse("write excludes generated ID", props.has("id"));
194+
assertFalse("write excludes version", props.has("version"));
195+
assertTrue("write includes name", props.has("name"));
196+
assertTrue("write includes region", props.has("region"));
197+
assertTrue("write includes closedDates", props.has("closedDates"));
198+
199+
// Write schema $refs use "Write" suffix
200+
assertEquals("#/components/schemas/RegionBOWrite",
201+
props.get("region").get("$ref").asText());
202+
}
203+
}
204+
205+
// ═══════════════════════════════════════════════════════════════════════
206+
// Batch generator — end-to-end test using test resources directory
207+
// ═══════════════════════════════════════════════════════════════════════
208+
209+
@Test
210+
public void testBatchGenerator_producesValidOpenApiJson() throws IOException {
211+
// Find the test resources directory (contains CompanyBO, DepartmentBO, StockExchangeBO)
212+
URL resourceUrl = getClass().getResource("/StockExchangeBO.hbm.xml");
213+
assertNotNull("test resource must be on classpath", resourceUrl);
214+
File resourceDir = new File(resourceUrl.getFile()).getParentFile();
215+
216+
File outputFile = File.createTempFile("openapi-schemas-", ".json");
217+
outputFile.deleteOnExit();
218+
219+
// Run the batch generator
220+
HbmBatchSchemaGenerator.main(new String[]{
221+
resourceDir.getAbsolutePath(),
222+
outputFile.getAbsolutePath()
223+
});
224+
225+
// Verify the output is valid JSON
226+
assertTrue("output file should exist", outputFile.exists());
227+
assertTrue("output file should not be empty", outputFile.length() > 0);
228+
229+
JsonNode root = mapper.readTree(outputFile);
230+
231+
// OpenAPI structure
232+
assertEquals("3.0.1", root.get("openapi").asText());
233+
assertNotNull(root.get("info"));
234+
assertNotNull(root.get("components"));
235+
assertNotNull(root.get("components").get("schemas"));
236+
237+
JsonNode schemas = root.get("components").get("schemas");
238+
239+
// Should have read + write schemas for each entity
240+
// At minimum: CompanyBO, DepartmentBO, StockExchangeBO (3 entities × 2 = 6 schemas)
241+
assertTrue("should have at least 6 schemas, got " + schemas.size(),
242+
schemas.size() >= 6);
243+
244+
// Verify specific entities exist
245+
assertTrue("CompanyBO read schema", schemas.has("CompanyBO"));
246+
assertTrue("CompanyBO write schema", schemas.has("CompanyBOWrite"));
247+
assertTrue("StockExchangeBO read schema", schemas.has("StockExchangeBO"));
248+
assertTrue("StockExchangeBO write schema", schemas.has("StockExchangeBOWrite"));
249+
assertTrue("DepartmentBO read schema", schemas.has("DepartmentBO"));
250+
assertTrue("DepartmentBO write schema", schemas.has("DepartmentBOWrite"));
251+
252+
// Spot-check StockExchangeBO schema content
253+
JsonNode exchangeRead = schemas.get("StockExchangeBO");
254+
assertEquals("object", exchangeRead.get("type").asText());
255+
assertTrue(exchangeRead.get("properties").has("name"));
256+
assertTrue(exchangeRead.get("properties").has("region"));
257+
assertTrue(exchangeRead.get("properties").has("closedDates"));
258+
259+
// Write schema should not have id/version
260+
JsonNode exchangeWrite = schemas.get("StockExchangeBOWrite");
261+
assertFalse(exchangeWrite.get("properties").has("id"));
262+
assertFalse(exchangeWrite.get("properties").has("version"));
263+
}
264+
265+
@Test
266+
public void testBatchGenerator_fieldCount() throws IOException {
267+
try (InputStream is = getClass().getResourceAsStream("/StockExchangeBO.hbm.xml")) {
268+
HbmXmlIntrospector introspector = new HbmXmlIntrospector();
269+
EntitySchemaModel model = introspector.introspect(is, "StockExchangeBO.hbm.xml");
270+
271+
// 11 properties + 1 id + 1 version + 1 many-to-one + 1 set = 15 fields
272+
assertEquals("exact field count", 15, model.getFields().size());
273+
274+
// Count relationships
275+
long relCount = model.getFields().values().stream()
276+
.filter(f -> f.getRefEntityName() != null).count();
277+
assertEquals("2 relationships (region + closedDates)", 2, relCount);
278+
}
279+
}
280+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE hibernate-mapping PUBLIC
3+
"-//Hibernate/Hibernate Mapping DTD//EN"
4+
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd" >
5+
6+
<!--
7+
Sanitized HBM mapping for batch generator testing.
8+
Models a stock exchange with trading hours, location, and relationships.
9+
Exercises: id+generator, version, not-null properties, nullable properties,
10+
many-to-one, set collection, nested column element with default.
11+
-->
12+
<hibernate-mapping package="org.example.bo">
13+
<class name="StockExchangeBO" table="STOCK_EXCHANGE" dynamic-update="false"
14+
dynamic-insert="true" select-before-update="false" optimistic-lock="version"
15+
lazy="true" batch-size="300">
16+
17+
<id name="id" type="java.lang.Long" column="exchangeID">
18+
<generator class="native"/>
19+
</id>
20+
<version name="version" type="java.lang.Long" access="field"
21+
column="OBJ_VERSION" />
22+
23+
<property name="name" column="name" type="java.lang.String"
24+
unique="true" not-null="true" />
25+
<property name="micCode" column="micCode"
26+
type="java.lang.String" not-null="false" />
27+
<property name="operatingMicCode" column="operatingMicCode"
28+
type="java.lang.String" not-null="false" />
29+
<property name="country" column="country"
30+
type="java.lang.String" not-null="false" />
31+
<property name="isoCountryCode" column="isoCountryCode"
32+
type="java.lang.String" not-null="false" />
33+
<property name="city" column="city"
34+
type="java.lang.String" not-null="false" />
35+
<property name="website" column="website"
36+
type="java.lang.String" not-null="false" />
37+
<property name="openTimeLocal" column="openTimeLocal"
38+
type="java.lang.String" not-null="false" />
39+
<property name="closeTimeLocal" column="closeTimeLocal"
40+
type="java.lang.String" not-null="false" />
41+
<property name="timeZoneName" column="timeZoneName"
42+
type="java.lang.String" not-null="false" />
43+
44+
<many-to-one name="region"
45+
class="org.example.bo.RegionBO"
46+
column="regionID" cascade="save-update" />
47+
48+
<set name="closedDates" table="ExchangeClosed" inverse="true" lazy="true">
49+
<key>
50+
<column name="exchangeID" not-null="true" />
51+
</key>
52+
<one-to-many class="org.example.bo.ExchangeClosedDateBO" />
53+
</set>
54+
55+
<property name="deleted" type="java.lang.Boolean">
56+
<column name="deleted" not-null="true" default="0" />
57+
</property>
58+
59+
</class>
60+
</hibernate-mapping>

0 commit comments

Comments
 (0)