getPassword();
-
/**
* The Hibernate properties file to read from the main resource set.
*
- * Defaults to {@code hibernate.properties}. The task uses the file as defaults for JDBC driver,
+ * Defaults to {@code hibernate.properties}. The task uses the file for JDBC driver,
* URL, user name, password, catalog, and schema. Direct task properties take precedence.
*/
@Input
@@ -247,8 +105,7 @@ public GenerateSchemaAnnotationsTask() {
* The optional Hibernate Tools reverse-engineering XML file to read from the main resource set.
*
* The task supports schema selection, table filters, table exclusions, column exclusions, and
- * user-defined foreign keys from this file. Annotation type names are still derived from physical
- * table and column names.
+ * user-defined foreign keys from this file.
*/
@Input
@Optional
@@ -264,8 +121,7 @@ public GenerateSchemaAnnotationsTask() {
* The optional catalog name passed to JDBC metadata lookup.
*
* If not specified, the task reads {@code hibernate.default_catalog} from the configured
- * Hibernate properties file. If that value is also not specified, the task uses
- * {@link Connection#getCatalog()} when available.
+ * Hibernate properties file.
*/
@Input
@Optional
@@ -275,15 +131,14 @@ public GenerateSchemaAnnotationsTask() {
* The optional schema name passed to JDBC metadata lookup.
*
* If not specified, the task reads {@code hibernate.default_schema} from the configured
- * Hibernate properties file. If that value is also not specified, the task uses
- * {@link Connection#getSchema()} when available.
+ * Hibernate properties file.
*/
@Input
@Optional
abstract public Property getSchemaName();
/**
- * The table-name pattern passed to {@link DatabaseMetaData#getTables(String, String, String, String[])}.
+ * The table-name pattern passed to {@link java.sql.DatabaseMetaData#getTables}.
*
* Defaults to {@code %}.
*/
@@ -301,669 +156,107 @@ public GenerateSchemaAnnotationsTask() {
@TaskAction
public void generateSchemaAnnotations() {
- final var configuration = resolveTaskConfiguration();
- validatePackageName( configuration.packageName );
-
+ String packageName = getPackageName().get();
getLogger().lifecycle( "Starting schema annotation generation" );
- final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
+ ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
URLClassLoader classLoader = null;
- Driver registeredDriver = null;
try {
classLoader = new URLClassLoader( resolveProjectClassPath(), oldLoader );
Thread.currentThread().setContextClassLoader( classLoader );
- registeredDriver = registerDriver( classLoader, configuration.jdbcDriver );
- generateSchemaAnnotations( configuration );
+ Properties properties = resolveHibernateProperties();
+ RevengStrategy strategy = createReverseEngineeringStrategy( packageName );
+ properties.put( MetadataConstants.PREFER_BASIC_COMPOSITE_IDS, true );
+ MetadataDescriptor metadataDescriptor =
+ MetadataDescriptorFactory.createReverseEngineeringDescriptor( strategy, properties );
+
+ Exporter exporter = ExporterFactory.createExporter( ExporterType.SCHEMA );
+ exporter.getProperties().put( ExporterConstants.METADATA_DESCRIPTOR, metadataDescriptor );
+ exporter.getProperties().put(
+ ExporterConstants.DESTINATION_FOLDER,
+ getOutputDirectory().get().getAsFile()
+ );
+ exporter.getProperties().put( "schemaPackage", packageName );
+ exporter.start();
}
catch (Exception e) {
throw new GradleException( "Unable to generate schema annotations", e );
}
finally {
- deregisterDriver( registeredDriver );
- closeClassLoader( classLoader );
Thread.currentThread().setContextClassLoader( oldLoader );
+ closeClassLoader( classLoader );
getLogger().lifecycle( "Finished schema annotation generation" );
}
}
- private TaskConfiguration resolveTaskConfiguration() {
- final var hibernateProperties = loadHibernateProperties();
- return new TaskConfiguration(
- requiredConfiguration(
- getJdbcDriver(),
- hibernateProperties,
- "jdbcDriver",
- JDBC_DRIVER_PROPERTIES
- ),
- requiredConfiguration( getJdbcUrl(), hibernateProperties, "jdbcUrl", JDBC_URL_PROPERTIES ),
- optionalConfiguration( getUsername(), hibernateProperties, JDBC_USERNAME_PROPERTIES ),
- optionalConfiguration( getPassword(), hibernateProperties, JDBC_PASSWORD_PROPERTIES ),
- getPackageName().get(),
- optionalNonBlankConfiguration( getCatalogName(), hibernateProperties, DEFAULT_CATALOG_PROPERTIES ),
- optionalNonBlankConfiguration( getSchemaName(), hibernateProperties, DEFAULT_SCHEMA_PROPERTIES ),
- getTableNamePattern().get(),
- optionalTaskConfiguration( getRevengFile() )
- );
- }
-
- private Properties loadHibernateProperties() {
- final boolean explicitPropertiesFile = getHibernateProperties().isPresent();
- final String filename = hibernatePropertiesFilename();
- if ( filename.isBlank() ) {
- return new Properties();
- }
-
- final var propertiesFile = RevengFileHelper.findResourceFile( getProject(), filename );
+ private Properties resolveHibernateProperties() {
+ String filename = getHibernateProperties().getOrElse( RevengSpec.DEFAULT_HIBERNATE_PROPERTIES );
+ File propertiesFile = RevengFileHelper.findResourceFile( getProject(), filename );
+ Properties properties;
if ( propertiesFile != null ) {
- return loadPropertiesFile( getLogger(), propertiesFile );
- }
- if ( explicitPropertiesFile ) {
- throw new GradleException( "Hibernate properties file `" + filename + "` could not be found" );
- }
- return new Properties();
- }
-
- private String hibernatePropertiesFilename() {
- return getHibernateProperties().getOrElse( RevengSpec.DEFAULT_HIBERNATE_PROPERTIES );
- }
-
- private String requiredConfiguration(
- Property taskProperty,
- Properties hibernateProperties,
- String propertyName,
- String... hibernatePropertyNames) {
- final String result = optionalNonBlankConfiguration( taskProperty, hibernateProperties, hibernatePropertyNames );
- if ( result != null ) {
- return result;
- }
-
- throw new GradleException(
- "Schema annotation generation requires `" + propertyName
- + "` or one of the following properties in `" + hibernatePropertiesFilename()
- + "`: " + String.join( ", ", hibernatePropertyNames )
- );
- }
-
- private String optionalNonBlankConfiguration(
- Property taskProperty,
- Properties hibernateProperties,
- String... hibernatePropertyNames) {
- final String result = optionalConfiguration( taskProperty, hibernateProperties, hibernatePropertyNames );
- return result == null || result.isBlank() ? null : result;
- }
-
- private String optionalConfiguration(
- Property taskProperty,
- Properties hibernateProperties,
- String... hibernatePropertyNames) {
- if ( taskProperty.isPresent() ) {
- return taskProperty.get();
- }
- else {
- for ( String propertyName : hibernatePropertyNames ) {
- if ( hibernateProperties.containsKey( propertyName ) ) {
- return hibernateProperties.getProperty( propertyName );
- }
- }
- return null;
- }
- }
-
- private String optionalTaskConfiguration(Property taskProperty) {
- return !taskProperty.isPresent() || taskProperty.get().isBlank() ? null : taskProperty.get();
- }
-
- private void generateSchemaAnnotations(TaskConfiguration configuration) throws SQLException, IOException {
- getLogger().lifecycle( "Connecting to database: " + configuration.jdbcUrl );
- final var revengStrategy = createReverseEngineeringStrategy( configuration );
- try ( var connection = createConnection( configuration ) ) {
- final var tables = readTables( connection, configuration, revengStrategy );
- writeTables( configuration.packageName, tables );
+ properties = loadPropertiesFile( getLogger(), propertiesFile );
}
- finally {
- if ( revengStrategy != null ) {
- revengStrategy.close();
- }
- }
- }
-
- private RevengStrategy createReverseEngineeringStrategy(TaskConfiguration configuration) {
- if ( configuration.revengFile == null ) {
- return null;
- }
- else {
- final var revengFile = findRequiredResourceFile( getProject(), configuration.revengFile );
- final var strategy = RevengStrategyFactory.createReverseEngineeringStrategy(
- null,
- new File[] {revengFile}
+ else if ( getHibernateProperties().isPresent() ) {
+ throw new GradleException(
+ "Hibernate properties file '" + filename + "' could not be found"
);
- final var settings = new RevengSettings( strategy );
- settings.setDefaultPackageName( configuration.packageName );
- strategy.setSettings( settings );
- return strategy;
- }
- }
-
- private Connection createConnection(TaskConfiguration configuration) throws SQLException {
- if ( configuration.username == null && configuration.password == null ) {
- return DriverManager.getConnection( configuration.jdbcUrl );
- }
- else {
- final var properties = new Properties();
- if ( configuration.username != null ) {
- properties.put( "user", configuration.username );
- }
- if ( configuration.password != null ) {
- properties.put( "password", configuration.password );
- }
- return DriverManager.getConnection( configuration.jdbcUrl, properties );
- }
- }
-
- private List readTables(
- Connection connection,
- TaskConfiguration configuration,
- RevengStrategy revengStrategy) throws SQLException {
- final var metadata = connection.getMetaData();
- final String catalog = configuration.catalogName == null ? determineCatalog( connection ) : configuration.catalogName;
- final String schema = configuration.schemaName == null ? determineSchema( connection ) : configuration.schemaName;
- final String tableNamePattern = configuration.tableNamePattern;
-
- final List tables = new ArrayList<>();
- final var schemaSelections = revengStrategy == null ? null : revengStrategy.getSchemaSelections();
- if ( schemaSelections == null ) {
- readTables( metadata, catalog, schema, tableNamePattern, revengStrategy, tables );
}
else {
- for ( var schemaSelection : schemaSelections ) {
- readTables(
- metadata,
- toJdbcPattern( schemaSelection.getMatchCatalog() ),
- toJdbcPattern( schemaSelection.getMatchSchema() ),
- toJdbcPattern( schemaSelection.getMatchTable() ),
- revengStrategy,
- tables
- );
- }
- }
-
- tables.sort( Comparator.comparing( table -> table.name.toLowerCase( Locale.ROOT ) ) );
- validateNoDuplicateTableNames( tables );
- final var userForeignKeyColumns = readUserForeignKeyColumns( tables, revengStrategy );
- for ( var table : tables ) {
- readColumns( metadata, table, revengStrategy, userForeignKeyColumns.get( table.identifier() ) );
- }
- return tables;
- }
-
- private void readTables(
- DatabaseMetaData metadata,
- String catalog,
- String schema,
- String tableNamePattern,
- RevengStrategy revengStrategy,
- List tables) throws SQLException {
- try ( var resultSet = metadata.getTables( catalog, schema, tableNamePattern, TABLE_TYPES ) ) {
- while ( resultSet.next() ) {
- final String tableName = resultSet.getString( "TABLE_NAME" );
- final var table = new Table(
- resultSet.getString( "TABLE_CAT" ),
- resultSet.getString( "TABLE_SCHEM" ),
- tableName
- );
- if ( !isExcludedTable( revengStrategy, table ) ) {
- validateJavaIdentifier( javaAnnotationName( tableName ), "table" );
- if ( !tables.contains( table ) ) {
- tables.add( table );
- }
+ properties = new Properties();
+ }
+ if ( getCatalogName().isPresent() ) {
+ properties.put( "hibernate.default_catalog", getCatalogName().get() );
+ }
+ if ( getSchemaName().isPresent() ) {
+ properties.put( "hibernate.default_schema", getSchemaName().get() );
+ }
+ return properties;
+ }
+
+ private RevengStrategy createReverseEngineeringStrategy(String packageName) {
+ File[] revengFiles = null;
+ if ( getRevengFile().isPresent() && !getRevengFile().get().isBlank() ) {
+ File revengFile = findRequiredResourceFile( getProject(), getRevengFile().get() );
+ revengFiles = new File[] { revengFile };
+ }
+ RevengStrategy strategy = RevengStrategyFactory
+ .createReverseEngineeringStrategy( null, revengFiles );
+ RevengSettings settings = new RevengSettings( strategy );
+ settings.setDefaultPackageName( packageName );
+ strategy.setSettings( settings );
+
+ String tableNamePattern = getTableNamePattern().getOrElse( "%" );
+ String schemaName = getSchemaName().getOrNull();
+ String catalogName = getCatalogName().getOrNull();
+ if ( schemaName != null || catalogName != null || !"%".equals( tableNamePattern ) ) {
+ TableSelectorStrategy selector = new TableSelectorStrategy( strategy );
+ selector.addSchemaSelection( new RevengStrategy.SchemaSelection() {
+ @Override
+ public String getMatchCatalog() {
+ return catalogName;
}
- }
- }
- }
-
- private String toJdbcPattern(String value) {
- return value == null ? null : value.replace( ".*", "%" );
- }
- private boolean isExcludedTable(RevengStrategy revengStrategy, Table table) {
- if ( revengStrategy == null ) {
- return false;
- }
- else {
- for ( var identifier : table.identifiers() ) {
- if ( revengStrategy.excludeTable( identifier ) ) {
- return true;
+ @Override
+ public String getMatchSchema() {
+ return schemaName;
}
- }
- return false;
- }
- }
- private Map> readUserForeignKeyColumns(
- List tables,
- RevengStrategy revengStrategy) {
- final Map> result = new HashMap<>();
- if ( revengStrategy != null ) {
- for ( var referencedTable : tables ) {
- for ( var identifier : referencedTable.identifiers() ) {
- final var foreignKeys = revengStrategy.getForeignKeys( identifier );
- if ( foreignKeys != null ) {
- for ( var foreignKey : foreignKeys ) {
- addUserForeignKey( tables, result, foreignKey );
- }
- }
+ @Override
+ public String getMatchTable() {
+ return tableNamePattern;
}
- }
- }
- return result;
- }
-
- private void addUserForeignKey(
- List tables,
- Map> userForeignKeyColumns,
- ForeignKey foreignKey) {
- final var dependentTable = findTable( tables, TableIdentifier.create( foreignKey.getTable() ) );
- if ( dependentTable != null ) {
- final String referencedTableName = foreignKey.getReferencedTable().getName();
- final var columns = foreignKey.getColumns();
- final var referencedColumns = foreignKey.getReferencedColumns();
- if ( columns.size() != referencedColumns.size() ) {
- throw new GradleException(
- "Foreign key `" + foreignKey.getName() + "` in reverse-engineering file has "
- + columns.size() + " local column(s) and " + referencedColumns.size()
- + " referenced column(s)"
- );
- }
-
- final var tableForeignKeyColumns =
- userForeignKeyColumns.computeIfAbsent( dependentTable.identifier(), key -> new HashMap<>() );
- for ( int i = 0; i < columns.size(); i++ ) {
- final var column = columns.get( i );
- final var referencedColumn = referencedColumns.get( i );
- putForeignKeyColumn(
- dependentTable.name,
- tableForeignKeyColumns,
- column.getName(),
- new ForeignKeyColumn( referencedTableName, referencedColumn.getName() )
- );
- }
+ } );
+ return selector;
}
+ return strategy;
}
- private Table findTable(List tables, TableIdentifier identifier) {
- for ( var table : tables ) {
- if ( table.matches( identifier ) ) {
- return table;
- }
- }
- return null;
- }
-
- private String determineCatalog(Connection connection) {
- try {
- return connection.getCatalog();
- }
- catch (SQLException | AbstractMethodError e) {
- return null;
- }
- }
-
- private String determineSchema(Connection connection) {
- try {
- return connection.getSchema();
- }
- catch (SQLException | AbstractMethodError e) {
- return null;
- }
- }
-
- private void readColumns(
- DatabaseMetaData metadata,
- Table table,
- RevengStrategy revengStrategy,
- Map userForeignKeyColumns) throws SQLException {
- final Set columnNames = new HashSet<>();
- final Set columnAnnotationNames = new HashSet<>();
- final var foreignKeyColumns = readForeignKeyColumns( metadata, table );
- if ( userForeignKeyColumns != null ) {
- for ( var entry : userForeignKeyColumns.entrySet() ) {
- putForeignKeyColumn( table.name, foreignKeyColumns, entry.getKey(), entry.getValue() );
- }
- }
- final var uniqueColumnNames = readUniqueColumnNames( metadata, table );
- try ( var resultSet = metadata.getColumns( table.catalog, table.schema, table.name, "%" ) ) {
- while ( resultSet.next() ) {
- final String columnName = resultSet.getString( "COLUMN_NAME" );
- if ( !isExcludedColumn( revengStrategy, table, columnName ) ) {
- final String columnAnnotationName = javaAnnotationName( columnName );
- validateJavaIdentifier( columnAnnotationName, "column" );
- if ( !columnNames.add( columnName ) ) {
- throw new GradleException(
- "Table `" + table.name + "` has multiple columns named `" + columnName + "`"
- );
- }
- if ( !columnAnnotationNames.add( columnAnnotationName ) ) {
- throw new GradleException(
- "Table `" + table.name + "` has multiple columns with names that map to generated "
- + "annotation `" + columnAnnotationName + "`"
- );
- }
- final var jdbcType =
- resolveJdbcType( table.name, columnName,
- resultSet.getInt( "DATA_TYPE" ) );
- table.columns.add(
- new Column(
- columnName,
- isNullable( resultSet ),
- uniqueColumnNames.contains( columnName ),
- length( resultSet, jdbcType ),
- precision( resultSet, jdbcType ),
- scale( resultSet, jdbcType ),
- foreignKeyColumns.get( columnName ),
- resultSet.getInt( "ORDINAL_POSITION" )
- )
- );
- }
- }
- }
- table.columns.sort( comparingInt( column -> column.position ) );
- }
-
- private Set readUniqueColumnNames(DatabaseMetaData metadata, Table table) throws SQLException {
- final var primaryKeyColumnNames = readPrimaryKeyColumnNames( metadata, table );
- final Map> uniqueIndexColumns = new HashMap<>();
- try ( var resultSet = metadata.getIndexInfo( table.catalog, table.schema, table.name, true, false ) ) {
- while ( resultSet.next() ) {
- if ( resultSet.getShort( "TYPE" ) == DatabaseMetaData.tableIndexStatistic
- || resultSet.getBoolean( "NON_UNIQUE" ) ) {
- continue;
- }
- final String indexName = resultSet.getString( "INDEX_NAME" );
- final String columnName = resultSet.getString( "COLUMN_NAME" );
- if ( indexName != null && columnName != null ) {
- uniqueIndexColumns.computeIfAbsent( indexName, key -> new HashSet<>() )
- .add( columnName );
- }
- }
- }
-
- final Set result = new HashSet<>();
- for ( var columnNames : uniqueIndexColumns.values() ) {
- if ( columnNames.size() == 1 ) {
- final var columnName = columnNames.iterator().next();
- if ( !primaryKeyColumnNames.contains( columnName ) ) {
- result.add( columnName );
- }
- }
- }
- return result;
- }
-
- private Set readPrimaryKeyColumnNames(DatabaseMetaData metadata, Table table) throws SQLException {
- final Set primaryKeyColumnNames = new HashSet<>();
- try ( var resultSet = metadata.getPrimaryKeys( table.catalog, table.schema, table.name ) ) {
- while ( resultSet.next() ) {
- primaryKeyColumnNames.add( resultSet.getString( "COLUMN_NAME" ) );
- }
- }
- return primaryKeyColumnNames;
- }
-
- private Map readForeignKeyColumns(DatabaseMetaData metadata, Table table)
- throws SQLException {
- final Map foreignKeyColumns = new HashMap<>();
- try ( var resultSet = metadata.getImportedKeys( table.catalog, table.schema, table.name ) ) {
- while ( resultSet.next() ) {
- final String columnName = resultSet.getString( "FKCOLUMN_NAME" );
- final var foreignKeyColumn = new ForeignKeyColumn(
- resultSet.getString( "PKTABLE_NAME" ),
- resultSet.getString( "PKCOLUMN_NAME" )
- );
- putForeignKeyColumn( table.name, foreignKeyColumns, columnName, foreignKeyColumn );
- }
- }
- return foreignKeyColumns;
- }
-
- private void putForeignKeyColumn(
- String tableName,
- Map foreignKeyColumns,
- String columnName,
- ForeignKeyColumn foreignKeyColumn) {
- final var previous = foreignKeyColumns.putIfAbsent( columnName, foreignKeyColumn );
- if ( previous != null && !previous.equals( foreignKeyColumn ) ) {
- throw new GradleException(
- "Column `" + tableName + "." + columnName
- + "` is part of multiple foreign keys with different referenced columns"
- );
- }
- }
-
- private boolean isExcludedColumn(RevengStrategy revengStrategy, Table table, String columnName) {
- if ( revengStrategy == null ) {
- return false;
- }
- else {
- for ( var identifier : table.identifiers() ) {
- if ( revengStrategy.excludeColumn( identifier, columnName ) ) {
- return true;
- }
- }
- return false;
- }
- }
-
- private boolean isNullable(ResultSet resultSet) throws SQLException {
- return resultSet.getInt( "NULLABLE" ) != DatabaseMetaData.columnNoNulls;
- }
-
- private int length(ResultSet resultSet, JDBCType type) throws SQLException {
- return isLengthType( type ) ? getInt( resultSet, "COLUMN_SIZE" ) : 255;
- }
-
- private int precision(ResultSet resultSet, JDBCType type) throws SQLException {
- return isNumericType( type ) ? getInt( resultSet, "COLUMN_SIZE" ) : 0;
- }
-
- private int scale(ResultSet resultSet, JDBCType type) throws SQLException {
- return isNumericType( type ) ? getInt( resultSet, "DECIMAL_DIGITS" ) : 0;
- }
-
- private int getInt(ResultSet resultSet, String columnName) throws SQLException {
- final int result = resultSet.getInt( columnName );
- return resultSet.wasNull() ? 0 : result;
- }
-
- private boolean isLengthType(JDBCType type) {
- return switch ( type ) {
- case CHAR, VARCHAR, LONGVARCHAR, NCHAR, NVARCHAR, LONGNVARCHAR,
- BINARY, VARBINARY, LONGVARBINARY -> true;
- default -> false;
- };
- }
-
- private boolean isNumericType(JDBCType type) {
- return switch ( type ) {
- case BIT, TINYINT, SMALLINT, INTEGER, BIGINT, FLOAT, REAL, DOUBLE, NUMERIC, DECIMAL -> true;
- default -> false;
- };
- }
-
- private JDBCType resolveJdbcType(String tableName, String columnName, int typeCode) {
- try {
- return JDBCType.valueOf( typeCode );
- }
- catch (IllegalArgumentException e) {
- throw new GradleException(
- "Column `" + tableName + "." + columnName + "` uses JDBC type code `"
- + typeCode + "`, which is not defined by java.sql.JDBCType",
- e
- );
- }
- }
-
- private void writeTables(String packageName, List tables) throws IOException {
- final var outputDirectory = getOutputDirectory().get().getAsFile();
- final var packageDirectory = packageDirectory( outputDirectory.toPath(), packageName );
- Files.createDirectories( packageDirectory );
- for ( var table : tables ) {
- final var outputFile = packageDirectory.resolve( javaAnnotationName( table.name ) + ".java" );
- Files.writeString( outputFile, renderTable( packageName, table ), UTF_8 );
- }
- getLogger().lifecycle( "Generated " + tables.size() + " schema annotation type(s) into " + packageDirectory );
- }
-
- private Path packageDirectory(Path outputDirectory, String packageName) {
- Path result = outputDirectory;
- for ( String namePart : packageName.split( "\\." ) ) {
- result = result.resolve( namePart );
- }
- return result;
- }
-
- private String renderTable(String packageName, Table table) {
- final var result = new StringBuilder();
- final String tableAnnotationName = javaAnnotationName( table.name );
- result.append( "package " ).append( packageName ).append( ";" ).append( lineSeparator() )
- .append( lineSeparator() )
- .append( "import java.lang.annotation.Retention;" ).append( lineSeparator() )
- .append( lineSeparator() )
- .append( "import org.hibernate.annotations.schema.ColumnMapping;" ).append( lineSeparator() )
- .append( "import org.hibernate.annotations.schema.JoinColumnMapping;" ).append( lineSeparator() )
- .append( "import org.hibernate.annotations.schema.TableMapping;" ).append( lineSeparator() )
- .append( lineSeparator() )
- .append( "import jakarta.persistence.Column;" ).append( lineSeparator() )
- .append( "import jakarta.persistence.JoinColumn;" ).append( lineSeparator() )
- .append( "import jakarta.persistence.Table;" ).append( lineSeparator() )
- .append( lineSeparator() )
- .append( "import static java.lang.annotation.RetentionPolicy.RUNTIME;" ).append( lineSeparator() )
- .append( lineSeparator() )
- .append( "@Retention(RUNTIME)" ).append( lineSeparator() )
- .append( "@TableMapping(@Table(name = " ).append( javaStringLiteral( table.name ) ).append( "))" )
- .append( lineSeparator() )
- .append( "public @interface " ).append( tableAnnotationName ).append( " {" ).append( lineSeparator() );
-
- for ( var column : table.columns ) {
- result.append( lineSeparator() )
- .append( "\t@Retention(RUNTIME)" ).append( lineSeparator() )
- .append( renderColumnAnnotation( column ) )
- .append( lineSeparator() )
- .append( "\t@interface " ).append( javaAnnotationName( column.name ) ).append( " {" )
- .append( lineSeparator() )
- .append( "\t}" ).append( lineSeparator() );
- }
-
- result.append( "}" ).append( lineSeparator() );
- return result.toString();
- }
-
- private String renderColumnAnnotation(Column column) {
- if ( column.foreignKeyColumn != null ) {
- return new StringBuilder()
- .append( "\t@JoinColumnMapping(@JoinColumn(name = " ).append( javaStringLiteral( column.name ) )
- .append( ", referencedColumnName = " )
- .append( javaStringLiteral( column.foreignKeyColumn.referencedColumnName ) )
- .append( ", nullable = " ).append( column.nullable )
- .append( "))" )
- .toString();
- }
- else {
- return new StringBuilder()
- .append( "\t@ColumnMapping(@Column(name = " ).append( javaStringLiteral( column.name ) )
- .append( ", nullable = " ).append( column.nullable )
- .append( ", unique = " ).append( column.unique )
- .append( ", length = " ).append( column.length )
- .append( ", precision = " ).append( column.precision )
- .append( ", scale = " ).append( column.scale )
- .append( "))" )
- .toString();
- }
- }
-
- private String javaStringLiteral(String value) {
- final var result = new StringBuilder( "\"" );
- for ( int i = 0; i < value.length(); i++ ) {
- final char character = value.charAt( i );
- switch ( character ) {
- case '\b':
- result.append( "\\b" );
- break;
- case '\t':
- result.append( "\\t" );
- break;
- case '\n':
- result.append( "\\n" );
- break;
- case '\f':
- result.append( "\\f" );
- break;
- case '\r':
- result.append( "\\r" );
- break;
- case '"':
- result.append( "\\\"" );
- break;
- case '\\':
- result.append( "\\\\" );
- break;
- default:
- if ( character < ' ' ) {
- appendUnicodeEscape( result, character );
- }
- else {
- result.append( character );
- }
- }
- }
- return result.append( '"' ).toString();
- }
-
- private void appendUnicodeEscape(StringBuilder result, char character) {
- result.append( "\\u" );
- final String hex = Integer.toHexString( character );
- for ( int i = hex.length(); i < 4; i++ ) {
- result.append( '0' );
- }
- result.append( hex );
- }
-
- private void validatePackageName(String packageName) {
- if ( packageName == null || packageName.isBlank() ) {
- throw new GradleException( "A package name must be specified" );
- }
- for ( String namePart : packageName.split( "\\.", -1 ) ) {
- validateJavaIdentifier( namePart, "package name part" );
- }
- }
-
- private void validateNoDuplicateTableNames(List tables) {
- final Set tableAnnotationNames = new HashSet<>();
- for ( var table : tables ) {
- final String tableAnnotationName = javaAnnotationName( table.name );
- if ( !tableAnnotationNames.add( tableAnnotationName ) ) {
- throw new GradleException(
- "Multiple tables match generated annotation `" + tableAnnotationName + "`"
- );
+ private void closeClassLoader(URLClassLoader classLoader) {
+ if ( classLoader != null ) {
+ try {
+ classLoader.close();
}
- }
- }
-
- private String javaAnnotationName(String name) {
- return name.toUpperCase( Locale.ROOT );
- }
-
- private void validateJavaIdentifier(String identifier, String role) {
- if ( identifier == null || identifier.isEmpty() ) {
- throw new GradleException( "A " + role + " name must not be empty" );
- }
- if ( JAVA_KEYWORDS.contains( identifier ) ) {
- throw new GradleException( "`" + identifier + "` is not a legal Java identifier for a " + role );
- }
- if ( !isJavaIdentifierStart( identifier.charAt( 0 ) ) ) {
- throw new GradleException( "`" + identifier + "` is not a legal Java identifier for a " + role );
- }
- for ( int i = 1; i < identifier.length(); i++ ) {
- if ( !isJavaIdentifierPart( identifier.charAt( i ) ) ) {
- throw new GradleException( "`" + identifier + "` is not a legal Java identifier for a " + role );
+ catch (IOException e) {
+ getLogger().warn( "Unable to close classloader", e );
}
}
}
@@ -982,115 +275,7 @@ URL[] resolveProjectClassPath() {
return urls;
}
catch (MalformedURLException e) {
- getLogger().error( "MalformedURLException while resolving project runtime classpath" );
throw new BuildException( e );
}
}
-
- private Driver registerDriver(ClassLoader classLoader, String driverClassName) {
- getLogger().lifecycle( "Registering the database driver: " + driverClassName );
- try {
- final var driverClass = classLoader.loadClass( driverClassName );
- final var constructor = driverClass.getDeclaredConstructor();
- final var driver = createDelegatingDriver( (Driver) constructor.newInstance() );
- DriverManager.registerDriver( driver );
- return driver;
- }
- catch (Exception e) {
- getLogger().error( "Exception while registering the database driver: " + e.getMessage() );
- throw new RuntimeException( e );
- }
- }
-
- private Driver createDelegatingDriver(Driver driver) {
- return (Driver) Proxy.newProxyInstance(
- DriverManager.class.getClassLoader(),
- new Class[] { Driver.class },
- (proxy, method, args) -> method.invoke( driver, args )
- );
- }
-
- private void deregisterDriver(Driver driver) {
- if ( driver == null ) {
- return;
- }
- try {
- DriverManager.deregisterDriver( driver );
- }
- catch (SQLException e) {
- getLogger().warn( "Unable to deregister JDBC driver", e );
- }
- }
-
- private void closeClassLoader(URLClassLoader classLoader) {
- if ( classLoader != null ) {
- try {
- classLoader.close();
- }
- catch (IOException e) {
- getLogger().warn( "Unable to close JDBC classloader", e );
- }
- }
- }
-
- private record TaskConfiguration(
- String jdbcDriver,
- String jdbcUrl,
- String username,
- String password,
- String packageName,
- String catalogName,
- String schemaName,
- String tableNamePattern,
- String revengFile) {
- }
-
- private static final class Table {
- private final String catalog;
- private final String schema;
- private final String name;
- private final List columns = new ArrayList<>();
-
- private Table(String catalog, String schema, String name) {
- this.catalog = catalog;
- this.schema = schema;
- this.name = name;
- }
-
- private TableIdentifier identifier() {
- return TableIdentifier.create( catalog, schema, name );
- }
-
- private List identifiers() {
- final var identifier = identifier();
- final var unqualifiedIdentifier = TableIdentifier.create( null, null, name );
- return identifier.equals( unqualifiedIdentifier )
- ? List.of( identifier )
- : List.of( identifier, unqualifiedIdentifier );
- }
-
- private boolean matches(TableIdentifier identifier) {
- return Objects.equals( name, identifier.getName() )
- && ( identifier.getCatalog() == null || Objects.equals( catalog, identifier.getCatalog() ) )
- && ( identifier.getSchema() == null || Objects.equals( schema, identifier.getSchema() ) );
- }
-
- @Override
- public boolean equals(Object object) {
- return object instanceof Table table
- && identifier().equals( table.identifier() );
- }
-
- @Override
- public int hashCode() {
- return identifier().hashCode();
- }
- }
-
- private record Column(String name, boolean nullable, boolean unique, int length, int precision, int scale,
- ForeignKeyColumn foreignKeyColumn, int position) {
- }
-
- private record ForeignKeyColumn(String referencedTableName, String referencedColumnName) {
- }
}
diff --git a/tooling/hibernate-gradle-plugin/src/main/java/org/hibernate/orm/tooling/gradle/reveng/RevengSpec.java b/tooling/hibernate-gradle-plugin/src/main/java/org/hibernate/orm/tooling/gradle/reveng/RevengSpec.java
index 9e5be5300095..2d69435d6790 100644
--- a/tooling/hibernate-gradle-plugin/src/main/java/org/hibernate/orm/tooling/gradle/reveng/RevengSpec.java
+++ b/tooling/hibernate-gradle-plugin/src/main/java/org/hibernate/orm/tooling/gradle/reveng/RevengSpec.java
@@ -20,5 +20,7 @@ public class RevengSpec {
public Boolean generateAnnotations = true;
public Boolean useGenerics = true;
public String templatePath = null;
+ public Boolean useSchemaAnnotations = false;
+ public String schemaPackage = null;
}
diff --git a/tooling/hibernate-maven-plugin/src/main/java/org/hibernate/orm/tooling/maven/reveng/GenerateJavaMojo.java b/tooling/hibernate-maven-plugin/src/main/java/org/hibernate/orm/tooling/maven/reveng/GenerateJavaMojo.java
index d1e2e7ba36ce..fddb9c7f33e5 100644
--- a/tooling/hibernate-maven-plugin/src/main/java/org/hibernate/orm/tooling/maven/reveng/GenerateJavaMojo.java
+++ b/tooling/hibernate-maven-plugin/src/main/java/org/hibernate/orm/tooling/maven/reveng/GenerateJavaMojo.java
@@ -45,6 +45,17 @@ public class GenerateJavaMojo extends AbstractGenerationMojo {
@Parameter
private String templatePath;
+ /** If true, generated entities use schema annotation types instead of
+ * inline {@code @Table}, {@code @Column}, and {@code @JoinColumn} annotations.
+ * Requires {@code schemaPackage} to be set. */
+ @Parameter(defaultValue = "false")
+ private boolean useSchemaAnnotations;
+
+ /** The Java package for generated schema annotation types.
+ * Required when {@code useSchemaAnnotations} is true. */
+ @Parameter
+ private String schemaPackage;
+
protected void executeExporter(MetadataDescriptor metadataDescriptor) {
Exporter pojoExporter = ExporterFactory.createExporter(ExporterType.JAVA);
pojoExporter.getProperties().put(ExporterConstants.METADATA_DESCRIPTOR, metadataDescriptor);
@@ -55,6 +66,10 @@ protected void executeExporter(MetadataDescriptor metadataDescriptor) {
}
pojoExporter.getProperties().setProperty("ejb3", String.valueOf(ejb3));
pojoExporter.getProperties().setProperty("jdk5", String.valueOf(jdk5));
+ pojoExporter.getProperties().setProperty("useSchemaAnnotations", String.valueOf(useSchemaAnnotations));
+ if (schemaPackage != null) {
+ pojoExporter.getProperties().put("schemaPackage", schemaPackage);
+ }
getLog().info("Starting POJO export to directory: " + outputDirectory + "...");
pojoExporter.start();
}
diff --git a/tooling/hibernate-maven-plugin/src/main/java/org/hibernate/orm/tooling/maven/reveng/GenerateSchemaAnnotationsMojo.java b/tooling/hibernate-maven-plugin/src/main/java/org/hibernate/orm/tooling/maven/reveng/GenerateSchemaAnnotationsMojo.java
new file mode 100644
index 000000000000..2b2642b8480e
--- /dev/null
+++ b/tooling/hibernate-maven-plugin/src/main/java/org/hibernate/orm/tooling/maven/reveng/GenerateSchemaAnnotationsMojo.java
@@ -0,0 +1,62 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.orm.tooling.maven.reveng;
+
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.hibernate.tool.reveng.api.export.Exporter;
+import org.hibernate.tool.reveng.api.export.ExporterConstants;
+import org.hibernate.tool.reveng.api.export.ExporterFactory;
+import org.hibernate.tool.reveng.api.export.ExporterType;
+import org.hibernate.tool.reveng.api.metadata.MetadataDescriptor;
+
+import java.io.File;
+
+import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES;
+
+/**
+ * Mojo to generate static schema annotation types from an existing database.
+ *
+ * For each table, the generated top-level annotation type is meta-annotated with {@code @TableMapping}
+ * holding a {@code @Table}. For each non-foreign key column, the generated nested annotation type is
+ * meta-annotated with {@code @ColumnMapping} holding a {@code @Column}. For each foreign key column,
+ * the generated nested annotation type is meta-annotated with {@code @JoinColumnMapping} holding a
+ * {@code @JoinColumn}.
+ */
+@Mojo(
+ name = "generateSchemaAnnotations",
+ defaultPhase = GENERATE_SOURCES,
+ requiresDependencyResolution = ResolutionScope.RUNTIME)
+public class GenerateSchemaAnnotationsMojo extends AbstractGenerationMojo {
+
+ /** The directory into which the schema annotations will be generated. */
+ @Parameter(defaultValue = "${project.build.directory}/generated-sources/")
+ private File outputDirectory;
+
+ /** A path used for looking up user-edited templates. */
+ @Parameter
+ private String templatePath;
+
+ /** The Java package for generated annotation types. */
+ @Parameter
+ private String schemaPackage;
+
+ @Override
+ protected void executeExporter(MetadataDescriptor metadataDescriptor) {
+ Exporter exporter = ExporterFactory.createExporter(ExporterType.SCHEMA);
+ exporter.getProperties().put(ExporterConstants.METADATA_DESCRIPTOR, metadataDescriptor);
+ exporter.getProperties().put(ExporterConstants.DESTINATION_FOLDER, outputDirectory);
+ if (templatePath != null) {
+ getLog().info("Setting template path to: " + templatePath);
+ exporter.getProperties().put(ExporterConstants.TEMPLATE_PATH, new String[] {templatePath});
+ }
+ if (schemaPackage != null) {
+ exporter.getProperties().put("schemaPackage", schemaPackage);
+ }
+ getLog().info("Starting schema annotation export to directory: " + outputDirectory + "...");
+ exporter.start();
+ }
+}
diff --git a/tooling/hibernate-maven-plugin/src/test/java/org/hibernate/orm/tooling/maven/reveng/GenerateSchemaAnnotationsMojoTest.java b/tooling/hibernate-maven-plugin/src/test/java/org/hibernate/orm/tooling/maven/reveng/GenerateSchemaAnnotationsMojoTest.java
new file mode 100644
index 000000000000..7560b2079834
--- /dev/null
+++ b/tooling/hibernate-maven-plugin/src/test/java/org/hibernate/orm/tooling/maven/reveng/GenerateSchemaAnnotationsMojoTest.java
@@ -0,0 +1,149 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.orm.tooling.maven.reveng;
+
+import org.apache.maven.project.MavenProject;
+import org.hibernate.tool.reveng.api.metadata.MetadataDescriptor;
+import org.hibernate.tool.reveng.api.metadata.MetadataDescriptorFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GenerateSchemaAnnotationsMojoTest {
+
+ private static final String CREATE_PERSON_TABLE =
+ "create table PERSON (ID int not null, NAME varchar(20), primary key (ID))";
+ private static final String CREATE_ITEM_TABLE =
+ "create table ITEM (ID int not null, NAME varchar(20), OWNER_ID int not null, primary key (ID), foreign key (OWNER_ID) references PERSON(ID))";
+ private static final String DROP_ITEM_TABLE =
+ "drop table ITEM";
+ private static final String DROP_PERSON_TABLE =
+ "drop table PERSON";
+
+ @TempDir
+ private File tempDir;
+
+ private File outputDirectory;
+ private GenerateSchemaAnnotationsMojo mojo;
+
+ @BeforeEach
+ public void beforeEach() throws Exception {
+ createDatabase();
+ createOutputDirectory();
+ createMojo();
+ }
+
+ @AfterEach
+ public void afterEach() throws Exception {
+ dropDatabase();
+ }
+
+ @Test
+ public void testGenerateSchemaAnnotations() throws Exception {
+ File personFile = new File(outputDirectory, "PERSON.java");
+ File itemFile = new File(outputDirectory, "ITEM.java");
+ assertFalse(personFile.exists());
+ assertFalse(itemFile.exists());
+ mojo.executeExporter(createMetadataDescriptor());
+ assertTrue(personFile.exists());
+ assertTrue(itemFile.exists());
+ String personContent = new String(Files.readAllBytes(personFile.toPath()));
+ assertTrue(personContent.contains("@TableMapping"));
+ assertTrue(personContent.contains("@ColumnMapping"));
+ String itemContent = new String(Files.readAllBytes(itemFile.toPath()));
+ assertTrue(itemContent.contains("@JoinColumnMapping"));
+ }
+
+ @Test
+ public void testGenerateSchemaAnnotationsWithPackage() throws Exception {
+ Field schemaPackageField = GenerateSchemaAnnotationsMojo.class.getDeclaredField("schemaPackage");
+ schemaPackageField.setAccessible(true);
+ schemaPackageField.set(mojo, "org.example.schema");
+ File personFile = new File(outputDirectory, "org/example/schema/PERSON.java");
+ assertFalse(personFile.exists());
+ mojo.executeExporter(createMetadataDescriptor());
+ assertTrue(personFile.exists());
+ String personContent = new String(Files.readAllBytes(personFile.toPath()));
+ assertTrue(personContent.contains("package org.example.schema;"));
+ assertTrue(personContent.contains("@TableMapping"));
+ }
+
+ @Test
+ public void testGenerateSchemaAnnotationsWithoutPackage() throws Exception {
+ File personFile = new File(outputDirectory, "PERSON.java");
+ assertFalse(personFile.exists());
+ mojo.executeExporter(createMetadataDescriptor());
+ assertTrue(personFile.exists());
+ String personContent = new String(Files.readAllBytes(personFile.toPath()));
+ assertFalse(personContent.contains("package "));
+ }
+
+ private void createDatabase() throws Exception {
+ Connection connection = DriverManager.getConnection(constructJdbcConnectionString());
+ Statement statement = connection.createStatement();
+ statement.execute(CREATE_PERSON_TABLE);
+ statement.execute(CREATE_ITEM_TABLE);
+ statement.close();
+ connection.close();
+ }
+
+ private void dropDatabase() throws Exception {
+ Connection connection = DriverManager.getConnection(constructJdbcConnectionString());
+ Statement statement = connection.createStatement();
+ statement.execute(DROP_ITEM_TABLE);
+ statement.execute(DROP_PERSON_TABLE);
+ statement.close();
+ connection.close();
+ }
+
+ private void createMojo() throws Exception {
+ mojo = new GenerateSchemaAnnotationsMojo();
+ Field projectField = AbstractGenerationMojo.class.getDeclaredField("project");
+ projectField.setAccessible(true);
+ projectField.set(mojo, new MavenProject());
+ Field propertyFileField = AbstractGenerationMojo.class.getDeclaredField("propertyFile");
+ propertyFileField.setAccessible(true);
+ propertyFileField.set(mojo, new File(tempDir, "hibernate.properties"));
+ Field outputDirectoryField = GenerateSchemaAnnotationsMojo.class.getDeclaredField("outputDirectory");
+ outputDirectoryField.setAccessible(true);
+ outputDirectoryField.set(mojo, outputDirectory);
+ }
+
+ private void createOutputDirectory() {
+ outputDirectory = new File(tempDir, "generated");
+ if (!outputDirectory.mkdir()) throw new RuntimeException("Unable to create output directory: " + outputDirectory);
+ }
+
+ private MetadataDescriptor createMetadataDescriptor() {
+ return MetadataDescriptorFactory.createReverseEngineeringDescriptor(
+ null,
+ createProperties());
+ }
+
+ private Properties createProperties() {
+ Properties result = new Properties();
+ result.put("hibernate.connection.url", constructJdbcConnectionString());
+ result.put("hibernate.default_catalog", "TEST");
+ result.put("hibernate.default_schema", "PUBLIC");
+ return result;
+ }
+
+ private String constructJdbcConnectionString() {
+ return "jdbc:h2:" + tempDir.getAbsolutePath() + "/database/test;AUTO_SERVER=TRUE";
+ }
+
+}
diff --git a/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/api/export/ExporterType.java b/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/api/export/ExporterType.java
index d82762ae4c68..00380d976f0e 100644
--- a/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/api/export/ExporterType.java
+++ b/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/api/export/ExporterType.java
@@ -14,7 +14,8 @@ public enum ExporterType {
HBM ("org.hibernate.tool.reveng.internal.export.hbm.HbmExporter"),
HBM_LINT ("org.hibernate.tool.reveng.internal.export.lint.HbmLintExporter"),
JAVA ("org.hibernate.tool.reveng.internal.export.java.JavaExporter"),
- QUERY ("org.hibernate.tool.reveng.internal.export.query.QueryExporter");
+ QUERY ("org.hibernate.tool.reveng.internal.export.query.QueryExporter"),
+ SCHEMA ("org.hibernate.tool.reveng.internal.export.schema.SchemaAnnotationExporter");
private String className;
diff --git a/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/java/JavaExporter.java b/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/java/JavaExporter.java
index 15bac62dc937..a52432234add 100644
--- a/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/java/JavaExporter.java
+++ b/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/java/JavaExporter.java
@@ -4,6 +4,10 @@
*/
package org.hibernate.tool.reveng.internal.export.java;
+import org.hibernate.tool.reveng.api.export.Exporter;
+import org.hibernate.tool.reveng.api.export.ExporterConstants;
+import org.hibernate.tool.reveng.api.export.ExporterFactory;
+import org.hibernate.tool.reveng.api.export.ExporterType;
import org.hibernate.tool.reveng.internal.export.common.GenericExporter;
/**
@@ -34,6 +38,26 @@ protected void setupContext() {
if(!getProperties().containsKey("jdk5")) {
getProperties().put("jdk5", "false");
}
+ if(!getProperties().containsKey("useSchemaAnnotations")) {
+ getProperties().put("useSchemaAnnotations", "false");
+ }
super.setupContext();
}
+
+ @Override
+ protected void doStart() {
+ if ("true".equals(getProperties().get("useSchemaAnnotations"))) {
+ String schemaPackage = (String) getProperties().get("schemaPackage");
+ if (schemaPackage == null || schemaPackage.isBlank()) {
+ throw new RuntimeException(
+ "schemaPackage must be set when useSchemaAnnotations is true");
+ }
+ Exporter schemaExporter = ExporterFactory.createExporter(ExporterType.SCHEMA);
+ schemaExporter.getProperties().put(ExporterConstants.METADATA_DESCRIPTOR, getProperties().get(ExporterConstants.METADATA_DESCRIPTOR));
+ schemaExporter.getProperties().put(ExporterConstants.DESTINATION_FOLDER, getOutputDirectory());
+ schemaExporter.getProperties().put("schemaPackage", schemaPackage);
+ schemaExporter.start();
+ }
+ super.doStart();
+ }
}
diff --git a/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/schema/SchemaAnnotationExporter.java b/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/schema/SchemaAnnotationExporter.java
new file mode 100644
index 000000000000..e77767321bfc
--- /dev/null
+++ b/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/schema/SchemaAnnotationExporter.java
@@ -0,0 +1,56 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.tool.reveng.internal.export.schema;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.hibernate.mapping.Table;
+import org.hibernate.tool.reveng.internal.export.common.AbstractExporter;
+import org.hibernate.tool.reveng.internal.export.common.TemplateProducer;
+
+public class SchemaAnnotationExporter extends AbstractExporter {
+
+ private static final String SCHEMA_ANNOTATION_FTL = "schema/SchemaAnnotation.ftl";
+
+ @Override
+ protected void setupContext() {
+ super.setupContext();
+ getTemplateHelper().putInContext( "schemaHelper", new SchemaAnnotationHelper() );
+ String pkg = (String) getProperties().get( "schemaPackage" );
+ if ( pkg != null ) {
+ getTemplateHelper().putInContext( "schemaPackage", pkg );
+ }
+ }
+
+ @Override
+ protected void doStart() {
+ TemplateProducer producer = new TemplateProducer( getTemplateHelper(), getArtifactCollector() );
+ String schemaPackage = (String) getProperties().get( "schemaPackage" );
+ for ( Table table : getMetadata().collectTableMappings() ) {
+ if ( table.isPhysicalTable() ) {
+ Map context = new HashMap<>();
+ context.put( "table", table );
+ String annotationName = table.getName().toUpperCase( Locale.ROOT );
+ File output = new File( getOutputDirectory(), resolveFilename( annotationName, schemaPackage ) );
+ producer.produce( context, SCHEMA_ANNOTATION_FTL, output, annotationName );
+ }
+ }
+ }
+
+ private String resolveFilename(String annotationName, String schemaPackage) {
+ String packagePath = schemaPackage != null ? schemaPackage.replace( '.', '/' ) : "";
+ if ( packagePath.isEmpty() ) {
+ return annotationName + ".java";
+ }
+ return packagePath + "/" + annotationName + ".java";
+ }
+
+ public String getName() {
+ return "schemaAnnotationsExporter";
+ }
+}
diff --git a/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/schema/SchemaAnnotationHelper.java b/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/schema/SchemaAnnotationHelper.java
new file mode 100644
index 000000000000..63cf235aedd9
--- /dev/null
+++ b/tooling/hibernate-reveng/src/main/java/org/hibernate/tool/reveng/internal/export/schema/SchemaAnnotationHelper.java
@@ -0,0 +1,74 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.tool.reveng.internal.export.schema;
+
+import java.util.Iterator;
+import java.util.Locale;
+
+import org.hibernate.mapping.Column;
+import org.hibernate.mapping.ForeignKey;
+import org.hibernate.mapping.Table;
+
+public class SchemaAnnotationHelper {
+
+ public boolean isForeignKeyColumn(Table table, Column column) {
+ for ( ForeignKey fk : table.getForeignKeys().values() ) {
+ if ( fk.containsColumn( column ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public String getReferencedTableName(Table table, Column column) {
+ for ( ForeignKey fk : table.getForeignKeys().values() ) {
+ if ( fk.containsColumn( column ) ) {
+ return fk.getReferencedTable().getName();
+ }
+ }
+ return null;
+ }
+
+ public String getReferencedColumnName(Table table, Column column) {
+ for ( ForeignKey fk : table.getForeignKeys().values() ) {
+ if ( fk.containsColumn( column ) ) {
+ Iterator fkColumns = fk.getColumns().iterator();
+ Iterator refColumns = fk.getReferencedColumns().iterator();
+ while ( fkColumns.hasNext() && refColumns.hasNext() ) {
+ Column fkCol = fkColumns.next();
+ Column refCol = refColumns.next();
+ if ( fkCol.getName().equals( column.getName() ) ) {
+ return refCol.getName();
+ }
+ }
+ if ( fk.getReferencedColumns().isEmpty() ) {
+ return fk.getReferencedTable().getPrimaryKey()
+ .getColumns().get( fk.getColumns().indexOf( column ) ).getName();
+ }
+ }
+ }
+ return null;
+ }
+
+ public String toUpperCase(String name) {
+ return name.toUpperCase( Locale.ROOT );
+ }
+
+ public int columnLength(Column column) {
+ Long length = column.getLength();
+ return length != null ? length.intValue() : 255;
+ }
+
+ public int columnPrecision(Column column) {
+ Integer precision = column.getPrecision();
+ return precision != null ? precision : 0;
+ }
+
+ public int columnScale(Column column) {
+ Integer scale = column.getScale();
+ return scale != null ? scale : 0;
+ }
+
+}
diff --git a/tooling/hibernate-reveng/src/main/resources/pojo/Ejb3PropertyGetAnnotation.ftl b/tooling/hibernate-reveng/src/main/resources/pojo/Ejb3PropertyGetAnnotation.ftl
index 125012a00adb..ffc605d6b6a4 100644
--- a/tooling/hibernate-reveng/src/main/resources/pojo/Ejb3PropertyGetAnnotation.ftl
+++ b/tooling/hibernate-reveng/src/main/resources/pojo/Ejb3PropertyGetAnnotation.ftl
@@ -22,11 +22,26 @@
#if>
#if>
+<#if useSchemaAnnotations?if_exists && !pojo.isComponent() && property.columnSpan == 1 && !property.columns[0].formula>
+<#assign schemaAnnotation = pojo.importType(schemaPackage + "." + clazz.table.name?upper_case)>
<#if c2h.isOneToOne(property)>
${pojo.generateOneToOneAnnotation(property, md)}
+ @${schemaAnnotation}.${property.columns[0].name?upper_case}
<#elseif c2h.isManyToOne(property)>
${pojo.generateManyToOneAnnotation(property)}
-<#--TODO support optional and targetEntity-->
+ @${schemaAnnotation}.${property.columns[0].name?upper_case}
+<#elseif c2h.isCollection(property)>
+${pojo.generateCollectionAnnotation(property, md)}
+<#else>
+${pojo.generateBasicAnnotation(property)}
+ @${schemaAnnotation}.${property.columns[0].name?upper_case}
+#if>
+<#else>
+<#if c2h.isOneToOne(property)>
+${pojo.generateOneToOneAnnotation(property, md)}
+<#elseif c2h.isManyToOne(property)>
+${pojo.generateManyToOneAnnotation(property)}
+<#--TODO support optional and targetEntity-->
${pojo.generateJoinColumnsAnnotation(property, md)}
<#elseif c2h.isCollection(property)>
${pojo.generateCollectionAnnotation(property, md)}
@@ -34,4 +49,5 @@ ${pojo.generateCollectionAnnotation(property, md)}
${pojo.generateBasicAnnotation(property)}
${pojo.generateAnnColumnAnnotation(property)}
#if>
+#if>
#if>
\ No newline at end of file
diff --git a/tooling/hibernate-reveng/src/main/resources/pojo/Ejb3TypeDeclaration.ftl b/tooling/hibernate-reveng/src/main/resources/pojo/Ejb3TypeDeclaration.ftl
index 396928ccb89b..b1c1f0c7c077 100644
--- a/tooling/hibernate-reveng/src/main/resources/pojo/Ejb3TypeDeclaration.ftl
+++ b/tooling/hibernate-reveng/src/main/resources/pojo/Ejb3TypeDeclaration.ftl
@@ -18,6 +18,9 @@
@${pojo.importType("jakarta.persistence.Embeddable")}
<#else>
@${pojo.importType("jakarta.persistence.Entity")}
+<#if useSchemaAnnotations?if_exists>
+@${pojo.importType(schemaPackage + "." + clazz.table.name?upper_case)}
+<#else>
@${pojo.importType("jakarta.persistence.Table")}(name="${clazz.table.name}"
<#if clazz.table.schema?exists>
,schema="${clazz.table.schema}"
@@ -29,4 +32,5 @@
, uniqueConstraints = ${uniqueConstraint}
#if>)
#if>
-#if>
\ No newline at end of file
+#if>
+#if>
diff --git a/tooling/hibernate-reveng/src/main/resources/schema/SchemaAnnotation.ftl b/tooling/hibernate-reveng/src/main/resources/schema/SchemaAnnotation.ftl
new file mode 100644
index 000000000000..df67eee94e49
--- /dev/null
+++ b/tooling/hibernate-reveng/src/main/resources/schema/SchemaAnnotation.ftl
@@ -0,0 +1,39 @@
+<#--
+~ SPDX-License-Identifier: Apache-2.0
+~ Copyright Red Hat Inc. and Hibernate Authors
+-->
+<#assign pkg = schemaPackage!"">
+<#if pkg?has_content>
+package ${pkg};
+
+#if>
+import java.lang.annotation.Retention;
+
+import org.hibernate.annotations.schema.ColumnMapping;
+import org.hibernate.annotations.schema.JoinColumnMapping;
+import org.hibernate.annotations.schema.TableMapping;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.Table;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Retention(RUNTIME)
+@TableMapping(@Table(name = "${table.name}"))
+public @interface ${schemaHelper.toUpperCase(table.name)} {
+<#list table.columns as column>
+
+<#if schemaHelper.isForeignKeyColumn(table, column)>
+ @Retention(RUNTIME)
+ @JoinColumnMapping(@JoinColumn(name = "${column.name}", referencedColumnName = "${schemaHelper.getReferencedColumnName(table, column)}", nullable = ${column.nullable?c}))
+ @interface ${schemaHelper.toUpperCase(column.name)} {
+ }
+<#else>
+ @Retention(RUNTIME)
+ @ColumnMapping(@Column(name = "${column.name}", nullable = ${column.nullable?c}, unique = ${column.unique?c}, length = ${schemaHelper.columnLength(column)}, precision = ${schemaHelper.columnPrecision(column)}, scale = ${schemaHelper.columnScale(column)}))
+ @interface ${schemaHelper.toUpperCase(column.name)} {
+ }
+#if>
+#list>
+}
diff --git a/tooling/hibernate-reveng/src/test/java/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/TestCase.java b/tooling/hibernate-reveng/src/test/java/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/TestCase.java
new file mode 100644
index 000000000000..38a4133b920d
--- /dev/null
+++ b/tooling/hibernate-reveng/src/test/java/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/TestCase.java
@@ -0,0 +1,101 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.tool.reveng.hbm2x.JdbcHbm2JavaSchemaAnnotations;
+
+import org.hibernate.tool.reveng.api.export.Exporter;
+import org.hibernate.tool.reveng.api.export.ExporterConstants;
+import org.hibernate.tool.reveng.api.export.ExporterFactory;
+import org.hibernate.tool.reveng.api.export.ExporterType;
+import org.hibernate.tool.reveng.api.metadata.MetadataDescriptorFactory;
+import org.hibernate.tool.reveng.test.utils.JUnitUtil;
+import org.hibernate.tool.reveng.test.utils.JdbcUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.nio.file.Files;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestCase {
+
+ @TempDir
+ public File outputDir = new File("output");
+
+ @BeforeEach
+ public void setUp() {
+ JdbcUtil.createDatabase(this);
+ Exporter exporter = ExporterFactory.createExporter(ExporterType.JAVA);
+ exporter.getProperties().put(
+ ExporterConstants.METADATA_DESCRIPTOR,
+ MetadataDescriptorFactory.createReverseEngineeringDescriptor(null, null));
+ exporter.getProperties().put(ExporterConstants.DESTINATION_FOLDER, outputDir);
+ exporter.getProperties().put(ExporterConstants.TEMPLATE_PATH, new String[0]);
+ exporter.getProperties().setProperty("ejb3", "true");
+ exporter.getProperties().setProperty("jdk5", "true");
+ exporter.getProperties().setProperty("useSchemaAnnotations", "true");
+ exporter.getProperties().setProperty("schemaPackage", "org.example.schema");
+ exporter.start();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ JdbcUtil.dropDatabase(this);
+ }
+
+ @Test
+ public void testEntityFilesExist() {
+ JUnitUtil.assertIsNonEmptyFile(new File(outputDir, "Person.java"));
+ JUnitUtil.assertIsNonEmptyFile(new File(outputDir, "Item.java"));
+ }
+
+ @Test
+ public void testSchemaAnnotationFilesExist() {
+ JUnitUtil.assertIsNonEmptyFile(new File(outputDir, "org/example/schema/PERSON.java"));
+ JUnitUtil.assertIsNonEmptyFile(new File(outputDir, "org/example/schema/ITEM.java"));
+ }
+
+ @Test
+ public void testEntityUsesTableSchemaAnnotation() throws Exception {
+ String personContent = Files.readString(new File(outputDir, "Person.java").toPath());
+ assertTrue(personContent.contains("@PERSON"), "Entity should use @PERSON schema annotation");
+ assertFalse(personContent.contains("@Table"), "Entity should not use @Table");
+ }
+
+ @Test
+ public void testEntityUsesColumnSchemaAnnotation() throws Exception {
+ String personContent = Files.readString(new File(outputDir, "Person.java").toPath());
+ assertTrue(personContent.contains("@PERSON.NAME"), "Property should use @PERSON.NAME schema annotation");
+ assertFalse(personContent.contains("@Column"), "Property should not use @Column");
+ }
+
+ @Test
+ public void testEntityUsesJoinColumnSchemaAnnotation() throws Exception {
+ String itemContent = Files.readString(new File(outputDir, "Item.java").toPath());
+ assertNotNull(itemContent);
+ assertTrue(itemContent.contains("@ITEM.OWNER_ID"), "FK property should use @ITEM.OWNER_ID schema annotation");
+ assertFalse(itemContent.contains("@JoinColumn"), "FK property should not use @JoinColumn");
+ }
+
+ @Test
+ public void testEntityKeepsRelationshipAnnotations() throws Exception {
+ String itemContent = Files.readString(new File(outputDir, "Item.java").toPath());
+ assertTrue(itemContent.contains("@ManyToOne"), "FK property should keep @ManyToOne");
+ String personContent = Files.readString(new File(outputDir, "Person.java").toPath());
+ assertTrue(personContent.contains("@Entity"), "Entity should keep @Entity");
+ assertTrue(personContent.contains("@Id"), "Id property should keep @Id");
+ }
+
+ @Test
+ public void testEntityImportsSchemaAnnotation() throws Exception {
+ String personContent = Files.readString(new File(outputDir, "Person.java").toPath());
+ assertTrue(personContent.contains("import org.example.schema.PERSON"), "Entity should import schema annotation");
+ }
+
+}
diff --git a/tooling/hibernate-reveng/src/test/resources/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/create.sql b/tooling/hibernate-reveng/src/test/resources/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/create.sql
new file mode 100644
index 000000000000..93f62ce6abc8
--- /dev/null
+++ b/tooling/hibernate-reveng/src/test/resources/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/create.sql
@@ -0,0 +1,2 @@
+CREATE TABLE PERSON (ID INT NOT NULL, NAME VARCHAR(20), PRIMARY KEY (ID))
+CREATE TABLE ITEM (ID INT NOT NULL, NAME VARCHAR(20), OWNER_ID INT NOT NULL, PRIMARY KEY (ID), FOREIGN KEY (OWNER_ID) REFERENCES PERSON(ID))
diff --git a/tooling/hibernate-reveng/src/test/resources/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/drop.sql b/tooling/hibernate-reveng/src/test/resources/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/drop.sql
new file mode 100644
index 000000000000..fc8bc89f9d7c
--- /dev/null
+++ b/tooling/hibernate-reveng/src/test/resources/org/hibernate/tool/reveng/hbm2x/JdbcHbm2JavaSchemaAnnotations/drop.sql
@@ -0,0 +1,2 @@
+DROP TABLE ITEM
+DROP TABLE PERSON