| <!DOCTYPE html> |
| |
| |
| <!-- |
| | Generated by Apache Maven Doxia Site Renderer 2.0.0 from src/site/xdoc/docs/openapi-jpa-schema.xml at 2026-05-18 |
| | Rendered using Apache Maven Fluido Skin 2.0.0-M11 |
| --> |
| <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <meta name="generator" content="Apache Maven Doxia Site Renderer 2.0.0" /> |
| <title>JPA/Hibernate Schema Generation for OpenAPI – Apache Axis2</title> |
| <link rel="stylesheet" href="../css/apache-maven-fluido-2.0.0-M11.min.css" /> |
| <link rel="stylesheet" href="../css/site.css" /> |
| <link rel="stylesheet" href="../css/print.css" media="print" /> |
| <script src="../js/apache-maven-fluido-2.0.0-M11.min.js"></script> |
| </head> |
| <body> |
| <div class="container-fluid container-fluid-top"> |
| <header> |
| <div id="banner"> |
| <div class="pull-left"><div id="bannerLeft"><h1><a href="https://www.apache.org/"><img class="class java.lang.Object" src="https://www.apache.org/images/asf_logo_wide.png" /> Apache Axis2</a></h1></div></div> |
| <div class="pull-right"><div id="bannerRight"><h1><a href="https://axis.apache.org/axis2/java/core/"><img class="class java.lang.Object" src="https://axis.apache.org/axis2/java/core/images/axis.jpg" /></a></h1></div></div> |
| <div class="clear"><hr/></div> |
| </div> |
| |
| <div id="breadcrumbs"> |
| <ul class="breadcrumb"> |
| <li id="publishDate">Last Published: 2026-05-17<span class="divider">|</span> |
| </li> |
| <li id="projectVersion">Version: 2.0.1<span class="divider">|</span></li> |
| <li><a href="https://www.apache.org" class="externalLink">Apache</a><span class="divider">/</span></li> |
| <li><a href="../index.html">Axis2/Java</a><span class="divider">/</span></li> |
| <li class="active">JPA/Hibernate Schema Generation for OpenAPI</li> |
| </ul> |
| </div> |
| </header> |
| <div class="row-fluid"> |
| <header id="leftColumn" class="span2"> |
| <nav class="well sidebar-nav"> |
| <ul class="nav nav-list"> |
| <li class="nav-header">Axis2/Java</li> |
| <li><a href="../index.html">Home</a></li> |
| <li><a href="../download.html">Downloads</a></li> |
| <li><a href="javascript:void(0)"><span class="icon-chevron-down"></span>Release Notes</a> |
| <ul class="nav nav-list"> |
| <li><a href="../release-notes/1.6.1.html">1.6.1</a></li> |
| <li><a href="../release-notes/1.6.2.html">1.6.2</a></li> |
| <li><a href="../release-notes/1.6.3.html">1.6.3</a></li> |
| <li><a href="../release-notes/1.6.4.html">1.6.4</a></li> |
| <li><a href="../release-notes/1.7.0.html">1.7.0</a></li> |
| <li><a href="../release-notes/1.7.1.html">1.7.1</a></li> |
| <li><a href="../release-notes/1.7.2.html">1.7.2</a></li> |
| <li><a href="../release-notes/1.7.3.html">1.7.3</a></li> |
| <li><a href="../release-notes/1.7.4.html">1.7.4</a></li> |
| <li><a href="../release-notes/1.7.5.html">1.7.5</a></li> |
| <li><a href="../release-notes/1.7.6.html">1.7.6</a></li> |
| <li><a href="../release-notes/1.7.7.html">1.7.7</a></li> |
| <li><a href="../release-notes/1.7.8.html">1.7.8</a></li> |
| <li><a href="../release-notes/1.7.9.html">1.7.9</a></li> |
| <li><a href="../release-notes/1.8.0.html">1.8.0</a></li> |
| <li><a href="../release-notes/1.8.1.html">1.8.1</a></li> |
| <li><a href="../release-notes/1.8.2.html">1.8.2</a></li> |
| <li><a href="../release-notes/2.0.0.html">2.0.0</a></li> |
| <li><a href="../release-notes/2.0.1.html">2.0.1</a></li> |
| </ul></li> |
| <li><a href="../modules/index.html">Modules</a></li> |
| <li><a href="../tools/index.html">Tools</a></li> |
| <li class="nav-header">Documentation</li> |
| <li><a href="../docs/toc.html">Table of Contents</a></li> |
| <li><a href="../docs/installationguide.html">Installation Guide</a></li> |
| <li><a href="../docs/quickstartguide.html">QuickStart Guide</a></li> |
| <li><a href="../docs/userguide.html">User Guide</a></li> |
| <li><a href="../docs/jaxws-guide.html">JAXWS Guide</a></li> |
| <li><a href="../docs/pojoguide.html">POJO Guide</a></li> |
| <li><a href="../docs/spring.html">Spring Guide</a></li> |
| <li><a href="../docs/webadminguide.html">Web Administrator's Guide</a></li> |
| <li><a href="../docs/migration.html">Migration Guide (from Axis1)</a></li> |
| <li class="nav-header">Resources</li> |
| <li><a href="../faq.html">FAQ</a></li> |
| <li><a href="https://github.com/apache/axis-axis2-java-core" class="externalLink">Source Code</a></li> |
| <li class="nav-header">Get Involved</li> |
| <li><a href="../overview.html">Overview</a></li> |
| <li><a href="../mail-lists.html">Mailing Lists</a></li> |
| <li><a href="../release-process.html">Release Process</a></li> |
| <li><a href="../guidelines.html">Developer Guidelines</a></li> |
| <li><a href="../siteHowTo.html">Build the Site</a></li> |
| <li class="nav-header">Project Information</li> |
| <li><a href="https://github.com/apache/axis-axis2-java-core/graphs/contributors" class="externalLink">Contributors</a></li> |
| <li><a href="https://issues.apache.org/jira/projects/AXIS2/issues" class="externalLink">Issues</a></li> |
| <li class="nav-header">Apache</li> |
| <li><a href="https://www.apache.org/licenses/LICENSE-2.0.html" class="externalLink">License</a></li> |
| <li><a href="https://www.apache.org/foundation/sponsorship.html" class="externalLink">Sponsorship</a></li> |
| <li><a href="https://www.apache.org/foundation/thanks.html" class="externalLink">Thanks</a></li> |
| <li><a href="https://www.apache.org/security/" class="externalLink">Security</a></li> |
| </ul> |
| </nav> |
| <div class="well sidebar-nav"> |
| <div id="poweredBy"> |
| <div class="clear"></div> |
| <div class="clear"></div> |
| <a href="https://maven.apache.org/" class="builtBy" target="_blank"><img class="builtBy" alt="Built by Maven" src="../images/logos/maven-feather.png" /></a> |
| </div> |
| </div> |
| </header> |
| <main id="bodyColumn" class="span10"> |
| <html xmlns="http://www.w3.org/1999/xhtml"> |
| |
| |
| <section><a id="JPA.2FHibernate_Schema_Generation_for_OpenAPI"></a> |
| <h1>JPA/Hibernate Schema Generation for OpenAPI</h1> |
| |
| |
| <p>The <code>axis2-jpa-schema</code> module generates JSON Schema definitions |
| from JPA annotations or Hibernate XML mappings (<code>.hbm.xml</code>). |
| These schemas can be embedded in OpenAPI 3.0 specifications to document |
| request/response bodies that map directly to database entities.</p> |
| |
| |
| <p><strong>Module:</strong> <code>modules/jpa-schema</code><br /> |
| <strong>Package:</strong> <code>org.apache.axis2.jpa.schema</code></p> |
| |
| <section><a id="JPA_Annotation_Mode"></a> |
| <h2 id="annotation_mode">JPA Annotation Mode</h2> |
| |
| |
| <p>The <code>AnnotationIntrospector</code> uses Java reflection to read |
| Jakarta Persistence annotations from compiled <code>@Entity</code> classes. |
| Unlike the <a href="#hbm_xml_mode">HBM XML mode</a> (which reads raw XML |
| files and has a standalone CLI tool), annotation mode requires the entity |
| classes to be compiled and on the classpath. Typical integration points:</p> |
| |
| |
| <ul> |
| |
| <li><strong>At startup</strong> — a Spring bean or context listener scans |
| entity classes and generates schemas on first request</li> |
| |
| <li><strong>At build time</strong> — a Maven/Gradle task that runs after |
| compilation (e.g., in the <code>process-classes</code> phase)</li> |
| |
| <li><strong>In unit tests</strong> — call |
| <code>introspector.introspect(Product.class)</code> directly</li> |
| </ul> |
| |
| |
| <p>The introspector reads standard Jakarta Persistence annotations |
| (<code>@Entity</code>, <code>@Column</code>, <code>@Id</code>, |
| <code>@ManyToOne</code>, <code>@OneToMany</code>, etc.) and produces an |
| <code>EntitySchemaModel</code> that the <code>JpaSchemaGenerator</code> |
| converts to JSON Schema.</p> |
| |
| <section><a id="Supported_Annotations"></a> |
| <h3>Supported Annotations</h3> |
| |
| |
| <table class="bodyTableBorder"> |
| |
| <tr class="a"> |
| <th>Annotation</th> |
| <th>Schema Effect</th></tr> |
| |
| <tr class="b"> |
| <td><code>@Entity</code></td> |
| <td>Entity is eligible for introspection</td></tr> |
| |
| <tr class="a"> |
| <td><code>@Table(name="...")</code></td> |
| <td>Recorded in schema description</td></tr> |
| |
| <tr class="b"> |
| <td><code>@Id</code></td> |
| <td>Marked as required; <code>readOnly=true</code> in read schema</td></tr> |
| |
| <tr class="a"> |
| <td><code>@GeneratedValue</code></td> |
| <td>Excluded from write schema (server-managed)</td></tr> |
| |
| <tr class="b"> |
| <td><code>@Column(nullable, length)</code></td> |
| <td>Maps to <code>required</code> and <code>maxLength</code></td></tr> |
| |
| <tr class="a"> |
| <td><code>@Version</code></td> |
| <td>Excluded from write schema (server-managed)</td></tr> |
| |
| <tr class="b"> |
| <td><code>@Transient</code></td> |
| <td>Excluded from all schemas</td></tr> |
| |
| <tr class="a"> |
| <td><code>@Enumerated(STRING)</code></td> |
| <td>Emitted as <code>{"type":"string","enum":[...]}</code></td></tr> |
| |
| <tr class="b"> |
| <td><code>@ManyToOne</code></td> |
| <td>Emitted as <code>{"$ref":"#/components/schemas/Entity"}</code></td></tr> |
| |
| <tr class="a"> |
| <td><code>@OneToMany</code></td> |
| <td>Emitted as <code>{"type":"array","items":{"$ref":"..."}}</code></td></tr> |
| </table> |
| |
| </section><section><a id="Example"></a> |
| <h3>Example</h3> |
| |
| |
| <pre> |
| @Entity |
| @Table(name = "PRODUCT") |
| public class Product { |
| |
| @Id |
| @GeneratedValue(strategy = GenerationType.IDENTITY) |
| private BigDecimal productID; |
| |
| @Column(nullable = false, length = 200) |
| private String name; |
| |
| @Enumerated(EnumType.STRING) |
| private ProductStatus status; |
| |
| @Version |
| private Long objVersion; |
| |
| @ManyToOne |
| private Department department; |
| |
| @OneToMany |
| private List<LineItem> lineItems; |
| } |
| </pre> |
| |
| |
| <pre> |
| AnnotationIntrospector introspector = new AnnotationIntrospector(); |
| EntitySchemaModel model = introspector.introspect(Product.class); |
| ObjectNode readSchema = JpaSchemaGenerator.generateReadSchema(model); |
| ObjectNode writeSchema = JpaSchemaGenerator.generateWriteSchema(model); |
| </pre> |
| |
| </section></section><section><a id="Read_vs_Write_Schema_Generation"></a> |
| <h2 id="read_write_schemas">Read vs Write Schema Generation</h2> |
| |
| |
| <p>The generator produces two schema variants per entity:</p> |
| |
| |
| <table class="bodyTableBorder"> |
| |
| <tr class="a"> |
| <th>Variant</th> |
| <th>Includes</th> |
| <th>Use Case</th></tr> |
| |
| <tr class="b"> |
| <td><strong>Read schema</strong></td> |
| <td>All fields (IDs as <code>readOnly</code>)</td> |
| <td>GET response bodies</td></tr> |
| |
| <tr class="a"> |
| <td><strong>Write schema</strong></td> |
| <td>Excludes <code>@GeneratedValue</code> IDs, <code>@Version</code>, and custom audit fields</td> |
| <td>POST/PUT request bodies</td></tr> |
| </table> |
| |
| |
| <p>The <code>generateBothSchemas()</code> method returns both at once:</p> |
| |
| <pre> |
| Map<String, ObjectNode> schemas = JpaSchemaGenerator.generateBothSchemas(model); |
| // "Product" → read schema |
| // "ProductWrite" → write schema |
| </pre> |
| |
| <section><a id="What_Gets_Excluded_from_Write"></a> |
| <h3>What Gets Excluded from Write</h3> |
| |
| <ul> |
| |
| <li><code>@Id @GeneratedValue</code> — server assigns the ID</li> |
| |
| <li><code>@Version</code> — server manages optimistic locking</li> |
| |
| <li><code>@Transient</code> — excluded from both read and write</li> |
| |
| <li>Custom annotations (see below)</li> |
| </ul> |
| |
| </section></section><section><a id="Custom_Audit_Annotation_Support"></a> |
| <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 |
| 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.CreatedBy"); |
| |
| EntitySchemaModel model = introspector.introspect(Product.class); |
| // Fields annotated with @IgnoreChanges or @CreatedBy are now |
| // excluded from the write schema but present in the read schema. |
| </pre> |
| |
| </section><section><a id="Hibernate_XML_Mapping_Mode_.28.hbm.xml.29"></a> |
| <h2 id="hbm_xml_mode">Hibernate XML Mapping Mode (.hbm.xml)</h2> |
| |
| |
| <p>For codebases that use Hibernate XML mappings instead of (or alongside) |
| JPA annotations, the <code>HbmXmlIntrospector</code> parses <code>.hbm.xml</code> |
| 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"); |
| ObjectNode readSchema = JpaSchemaGenerator.generateReadSchema(model); |
| } |
| </pre> |
| |
| <section><a id="Supported_HBM_XML_Elements"></a> |
| <h3>Supported HBM XML Elements</h3> |
| |
| |
| <table class="bodyTableBorder"> |
| |
| <tr class="a"> |
| <th>HBM XML Element</th> |
| <th>Schema Effect</th></tr> |
| |
| <tr class="b"> |
| <td><code><id></code> with <code><generator></code></td> |
| <td>Required, <code>readOnly</code>, excluded from write</td></tr> |
| |
| <tr class="a"> |
| <td><code><version></code></td> |
| <td>Excluded from write schema</td></tr> |
| |
| <tr class="b"> |
| <td><code><property></code></td> |
| <td>Mapped by Hibernate type → JSON Schema type</td></tr> |
| |
| <tr class="a"> |
| <td><code><many-to-one></code></td> |
| <td><code>$ref</code> to referenced entity</td></tr> |
| |
| <tr class="b"> |
| <td><code><set></code> / <code><list></code></td> |
| <td>Array of <code>$ref</code></td></tr> |
| |
| <tr class="a"> |
| <td><code><component></code></td> |
| <td>Flattened with dot-notation prefix (e.g., <code>address.city</code>)</td></tr> |
| |
| <tr class="b"> |
| <td>Nested <code><column not-null="true"></code></td> |
| <td>Maps to required</td></tr> |
| </table> |
| |
| </section><section><a id="Type_Mapping"></a> |
| <h3>Type Mapping</h3> |
| |
| |
| <table class="bodyTableBorder"> |
| |
| <tr class="a"> |
| <th>Hibernate / Java Type</th> |
| <th>JSON Schema</th></tr> |
| |
| <tr class="b"> |
| <td><code>string</code>, <code>text</code></td> |
| <td><code>{"type":"string"}</code></td></tr> |
| |
| <tr class="a"> |
| <td><code>integer</code>, <code>int</code></td> |
| <td><code>{"type":"integer","format":"int32"}</code></td></tr> |
| |
| <tr class="b"> |
| <td><code>long</code>, <code>big_integer</code></td> |
| <td><code>{"type":"integer","format":"int64"}</code></td></tr> |
| |
| <tr class="a"> |
| <td><code>double</code>, <code>float</code>, <code>big_decimal</code></td> |
| <td><code>{"type":"number"}</code></td></tr> |
| |
| <tr class="b"> |
| <td><code>boolean</code>, <code>yes_no</code></td> |
| <td><code>{"type":"boolean"}</code></td></tr> |
| |
| <tr class="a"> |
| <td><code>timestamp</code>, <code>date</code></td> |
| <td><code>{"type":"string","format":"date-time"}</code></td></tr> |
| </table> |
| |
| </section></section><section><a id="Test_Coverage"></a> |
| <h2>Test Coverage</h2> |
| |
| |
| <p>The <code>JpaSchemaGeneratorTest</code> class provides comprehensive tests:</p> |
| |
| <ul> |
| |
| <li><strong>Annotation introspection</strong> (12 tests): entity detection, ID fields, |
| column constraints, version, transient, enums, ManyToOne, OneToMany, |
| custom write-exclude annotations</li> |
| |
| <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, |
| collections, component flattening, nested column not-null</li> |
| </ul> |
| |
| </section><section><a id="Relationship_.24ref_Convention"></a> |
| <h2>Relationship $ref Convention</h2> |
| |
| |
| <p>Relationships are emitted as <code>$ref</code> pointers using the OpenAPI |
| components convention:</p> |
| |
| |
| <pre> |
| // ManyToOne |
| {"$ref": "#/components/schemas/Department"} |
| |
| // OneToMany |
| {"type": "array", "items": {"$ref": "#/components/schemas/LineItem"}} |
| |
| // Write schema uses "Write" suffix |
| {"$ref": "#/components/schemas/DepartmentWrite"} |
| </pre> |
| |
| |
| <p>The caller is responsible for ensuring that referenced entity schemas |
| are also generated and added to the OpenAPI components section. The |
| batch generator (below) handles this automatically by processing all |
| HBM files in a directory at once.</p> |
| |
| </section><section><a id="Batch_Generation_from_HBM_XML_Directory"></a> |
| <h2 id="batch_generation">Batch Generation from HBM XML Directory</h2> |
| |
| |
| <p><code>HbmBatchSchemaGenerator</code> is a standalone command-line tool |
| that scans a directory of <code>*.hbm.xml</code> files and produces a single |
| OpenAPI 3.0 JSON document containing read and write schemas for every |
| entity found. This is the recommended approach for projects with many |
| HBM-mapped entities.</p> |
| |
| <section><a id="Command_Line"></a> |
| <h3>Command Line</h3> |
| |
| <pre> |
| java -cp axis2-jpa-schema.jar:jackson-databind.jar:jackson-core.jar:jackson-annotations.jar:commons-logging.jar \ |
| org.apache.axis2.jpa.schema.HbmBatchSchemaGenerator \ |
| src/main/resources \ |
| resources-axis2/openapi-schemas.json |
| </pre> |
| |
| </section><section><a id="Ant_Integration"></a> |
| <h3>Ant Integration</h3> |
| |
| |
| <p>The batch generator follows the same pattern as Hibernate Tools' |
| <code>hbm2java</code> and <code>hbm2ddl</code> tasks — same HBM input |
| directory, different output artifact:</p> |
| |
| |
| <pre> |
| <target name="openapi-schema" |
| description="Generate OpenAPI schemas from HBM XML mappings"> |
| <java classname="org.apache.axis2.jpa.schema.HbmBatchSchemaGenerator" |
| fork="true" failonerror="true"> |
| <classpath> |
| <fileset dir="lib" includes="axis2-jpa-schema*.jar,jackson-*.jar,commons-logging*.jar"/> |
| </classpath> |
| <!-- Input: directory containing *.hbm.xml --> |
| <arg value="src/main/resources"/> |
| <!-- Output: OpenAPI 3.0 JSON with all schemas --> |
| <arg value="build/openapi-schemas.json"/> |
| </java> |
| </target> |
| </pre> |
| |
| </section><section><a id="Output"></a> |
| <h3>Output</h3> |
| |
| |
| <p>The tool produces a valid OpenAPI 3.0 document:</p> |
| |
| |
| <pre> |
| { |
| "openapi": "3.0.1", |
| "info": { |
| "title": "Generated from 146 HBM XML mappings", |
| "version": "1.0.0" |
| }, |
| "components": { |
| "schemas": { |
| "CompanyBO": { "type": "object", "properties": { ... } }, |
| "CompanyBOWrite": { "type": "object", "properties": { ... } }, |
| "DepartmentBO": { ... }, |
| "DepartmentBOWrite": { ... }, |
| "ProductBO": { ... }, |
| "ProductBOWrite": { ... } |
| } |
| } |
| } |
| </pre> |
| |
| |
| <p>Each entity produces two schemas:</p> |
| |
| <ul> |
| |
| <li><strong>EntityBO</strong> — read schema (GET responses): all fields, |
| generated IDs marked <code>readOnly</code></li> |
| |
| <li><strong>EntityBOWrite</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) |
| |
| Generated 8 schemas (4 entities × 2 [read + write]) from 5 HBM files |
| Output: /path/to/resources-axis2/openapi-schemas.json |
| </pre> |
| |
| </section><section><a id="Build_Pipeline_Position"></a> |
| <h3>Build Pipeline Position</h3> |
| |
| |
| <p>The schema generator reads HBM XML files directly — it does not |
| depend on compiled Java classes, a running database, or a Hibernate |
| <code>SessionFactory</code>. This means it can run:</p> |
| |
| |
| <ul> |
| |
| <li><strong>Before <code>codegen</code></strong> — HBM files are the |
| source of truth; the generator reads them before Java classes are |
| generated</li> |
| |
| <li><strong>In CI</strong> — no database connection required, so it |
| runs in any CI environment</li> |
| |
| <li><strong>On schema change</strong> — regenerate whenever an HBM |
| file changes; diff the output JSON to see exactly which fields or |
| relationships changed</li> |
| </ul> |
| |
| |
| <p>For projects that use Ant with Hibernate Tools, add |
| <code>openapi-schema</code> to your existing build pipeline after |
| code generation and before packaging. The schemas will reflect the |
| same entity definitions that the generated Java code and DDL use.</p> |
| |
| </section></section> |
| </html> </main> |
| </div> |
| </div> |
| <hr/> |
| <footer> |
| <div class="container-fluid"> |
| <div class="row-fluid"> |
| <p>© 2004–2026 |
| <a href="https://www.apache.org/">The Apache Software Foundation</a> |
| </p> |
| </div> |
| </div> |
| </footer> |
| </body> |
| </html> |