From 173c679a3f317da7c5dc61fde19308b98235447d Mon Sep 17 00:00:00 2001 From: Thomas Mortagne Date: Fri, 31 Jan 2025 11:07:17 +0100 Subject: [PATCH] XWIKI-22613: Updating the history can take a very long time when there are a lot of objects (#3788) XWIKI-22752: Allows indicating in the Hibernate hbm configuration files that an entity should be compressed XWIKI-22747: Introduce the concept of XWiki Hibernate adapter --- .../R140300001XWIKI19571DataMigration.java | 6 +- ...R140300001XWIKI19571DataMigrationTest.java | 14 +- .../xpn/xwiki/doc/XWikiDocumentArchive.java | 4 +- .../store/hibernate/HibernateStore.java | 278 +++++------------- .../com/xpn/xwiki/store/DatabaseProduct.java | 9 + .../xwiki/store/XWikiHibernateBaseStore.java | 39 ++- .../hibernate/AbstractResizeMigration.java | 194 ++++++------ .../R130200000XWIKI17200DataMigration.java | 18 +- .../hibernate/AbstractHibernateAdapter.java | 266 +++++++++++++++++ .../DatabaseProductNameResolver.java | 42 +++ .../store/hibernate/HibernateAdapter.java | 146 +++++++++ .../hibernate/HibernateAdapterFactory.java | 54 ++++ .../hibernate/HibernateStoreException.java | 55 ++++ .../internal/DefaultHibernateAdapter.java | 37 +++ .../internal/DerbyHibernateAdapter.java | 50 ++++ .../internal/H2HibernateAdapter.java | 57 ++++ .../internal/HSQLDBHibernateAdapter.java | 64 ++++ .../IDVersionHibernateAdapterFactory.java | 168 +++++++++++ .../LegacyDatabaseProductNameResolver.java | 63 ++++ .../MariaDBDatabaseProductNameResolver.java | 50 ++++ .../internal/MariaDBHibernateAdapter.java | 49 +++ .../internal/MySQL8HibernateAdapter.java | 50 ++++ .../internal/MySQLHibernateAdapter.java | 165 +++++++++++ .../internal/OracleHibernateAdapter.java | 133 +++++++++ .../internal/PostgreSQLHibernateAdapter.java | 64 ++++ .../main/resources/META-INF/components.txt | 12 + .../src/main/resources/xwiki.hbm.xml | 1 + .../src/main/resources/xwiki.oracle.hbm.xml | 1 + .../xwiki/doc/XWikiDocumentArchiveTest.java | 104 ++++++- .../xwiki/store/XWikiHibernateStoreTest.java | 9 +- .../IDVersionHibernateAdapterFactoryTest.java | 118 ++++++++ .../java/org/xwiki/store/StoreException.java | 54 ++++ .../src/main/resources/hibernate.cfg.xml.vm | 4 + 33 files changed, 2033 insertions(+), 345 deletions(-) create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/AbstractHibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/DatabaseProductNameResolver.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateAdapterFactory.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateStoreException.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/DefaultHibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/DerbyHibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/H2HibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/HSQLDBHibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/IDVersionHibernateAdapterFactory.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/LegacyDatabaseProductNameResolver.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MariaDBDatabaseProductNameResolver.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MariaDBHibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MySQL8HibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MySQLHibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/OracleHibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/PostgreSQLHibernateAdapter.java create mode 100644 xwiki-platform-core/xwiki-platform-oldcore/src/test/java/org/xwiki/store/hibernate/internal/IDVersionHibernateAdapterFactoryTest.java create mode 100644 xwiki-platform-core/xwiki-platform-store/xwiki-platform-store-api/src/main/java/org/xwiki/store/StoreException.java diff --git a/xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-default/src/main/java/org/xwiki/index/migration/R140300001XWIKI19571DataMigration.java b/xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-default/src/main/java/org/xwiki/index/migration/R140300001XWIKI19571DataMigration.java index e3c04d0d2336..da2edda6260b 100644 --- a/xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-default/src/main/java/org/xwiki/index/migration/R140300001XWIKI19571DataMigration.java +++ b/xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-default/src/main/java/org/xwiki/index/migration/R140300001XWIKI19571DataMigration.java @@ -100,7 +100,7 @@ public String getPreHibernateLiquibaseChangeLog() throws DataMigrationException PersistentClass persistentClass = this.hibernateStore.getConfigurationMetadata() .getEntityBinding(XWikiDocumentIndexingTask.class.getName()); - String tableName = this.hibernateStore.getConfiguredTableName(persistentClass); + String tableName = this.hibernateStore.getAdapter().getTableName(persistentClass); String changeSet = ""; if (exists(databaseMetaData, tableName)) { saveTasks(session, persistentClass, tableName); @@ -138,10 +138,10 @@ protected void hibernateMigrate() throws XWikiException */ private boolean exists(DatabaseMetaData databaseMetaData, String tableName) throws SQLException { - String databaseName = this.hibernateStore.getDatabaseFromWikiName(); + String databaseName = this.hibernateStore.getAdapter().getDatabaseFromWikiName(); ResultSet resultSet; - if (this.hibernateStore.isCatalog()) { + if (this.hibernateStore.getAdapter().isCatalog()) { resultSet = databaseMetaData.getTables(databaseName, null, tableName, null); } else { resultSet = databaseMetaData.getTables(null, databaseName, tableName, null); diff --git a/xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-default/src/test/java/org/xwiki/index/migration/R140300001XWIKI19571DataMigrationTest.java b/xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-default/src/test/java/org/xwiki/index/migration/R140300001XWIKI19571DataMigrationTest.java index 9f2cba81c356..a92e8ccbf0db 100644 --- a/xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-default/src/test/java/org/xwiki/index/migration/R140300001XWIKI19571DataMigrationTest.java +++ b/xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-default/src/test/java/org/xwiki/index/migration/R140300001XWIKI19571DataMigrationTest.java @@ -45,6 +45,7 @@ import org.xwiki.context.ExecutionContext; import org.xwiki.doc.tasks.XWikiDocumentIndexingTask; import org.xwiki.index.internal.TasksStore; +import org.xwiki.store.hibernate.HibernateAdapter; import org.xwiki.test.junit5.mockito.ComponentTest; import org.xwiki.test.junit5.mockito.InjectMockComponents; import org.xwiki.test.junit5.mockito.MockComponent; @@ -102,6 +103,9 @@ class R140300001XWIKI19571DataMigrationTest @Mock private Metadata metadata; + @Mock + private HibernateAdapter adapter; + @Mock private PersistentClass persistentClass; @@ -128,8 +132,10 @@ void setUp() throws Exception when(this.hibernateStore.getConfigurationMetadata()).thenReturn(this.metadata); when(this.metadata.getEntityBinding(XWikiDocumentIndexingTask.class.getName())) .thenReturn(this.persistentClass); - when(this.hibernateStore.getConfiguredTableName(this.persistentClass)).thenReturn("XWIKIDOCUMENTINDEXINGQUEUE"); - when(this.hibernateStore.getDatabaseFromWikiName()).thenReturn("dbname"); + when(this.hibernateStore.getAdapter()).thenReturn(this.adapter); + when(this.adapter.getTableName(this.persistentClass)) + .thenReturn("XWIKIDOCUMENTINDEXINGQUEUE"); + when(this.adapter.getDatabaseFromWikiName()).thenReturn("dbname"); when(this.connection.getMetaData()).thenReturn(this.databaseMetaData); when(this.hibernateStore.getConfiguredColumnName(this.persistentClass, "docId")).thenReturn("DOC_ID"); when(this.hibernateStore.getConfiguredColumnName(this.persistentClass, "version")).thenReturn("VERSION"); @@ -144,7 +150,7 @@ void setUp() throws Exception @Test void hibernateMigrateExistsIsCatalog() throws Exception { - when(this.hibernateStore.isCatalog()).thenReturn(true); + when(this.adapter.isCatalog()).thenReturn(true); ResultSet resultSet = mock(ResultSet.class); when(resultSet.next()).thenReturn(true); when(this.databaseMetaData.getTables("dbname", null, "XWIKIDOCUMENTINDEXINGQUEUE", null)).thenReturn(resultSet); @@ -182,7 +188,7 @@ void hibernateMigrateExistsIsCatalog() throws Exception @Test void hibernateMigrateExistsIsNotCatalog() throws Exception { - when(this.hibernateStore.isCatalog()).thenReturn(false); + when(this.adapter.isCatalog()).thenReturn(false); ResultSet resultSet = mock(ResultSet.class); when(resultSet.next()).thenReturn(true); when(this.databaseMetaData.getTables(null, "dbname", "XWIKIDOCUMENTINDEXINGQUEUE", null)).thenReturn(resultSet); diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocumentArchive.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocumentArchive.java index 07fa22e4ae06..754cd6bda274 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocumentArchive.java +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocumentArchive.java @@ -154,8 +154,8 @@ protected XWikiRCSNodeContent makePatch(XWikiRCSNodeInfo newnode, XWikiDocument XWikiRCSNodeInfo latestNode = getLatestNode(); if (latestNode != null) { int nodesCount = getNodes().size(); - int nodesPerFull = context.getWiki() == null ? 5 - : Integer.parseInt(context.getWiki().getConfig().getProperty("xwiki.store.rcs.nodesPerFull", "5")); + int nodesPerFull = context.getWiki() == null ? 1 + : Integer.parseInt(context.getWiki().getConfig().getProperty("xwiki.store.rcs.nodesPerFull", "1")); if (nodesPerFull <= 0 || (nodesCount % nodesPerFull) != 0) { XWikiRCSNodeContent latestContent = latestNode.getContent(context); latestContent.getPatch().setDiffVersion(latestContent.getPatch().getContent(), doc, context); diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/store/hibernate/HibernateStore.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/store/hibernate/HibernateStore.java index 790544c6e745..7c26ddf1549a 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/store/hibernate/HibernateStore.java +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/store/hibernate/HibernateStore.java @@ -30,11 +30,11 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.TimeZone; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; @@ -73,10 +73,7 @@ import org.hibernate.mapping.KeyValue; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; -import org.hibernate.mapping.Table; import org.hibernate.query.NativeQuery; -import org.hibernate.tool.hbm2ddl.SchemaUpdate; -import org.hibernate.tool.schema.TargetType; import org.hibernate.tool.schema.extract.spi.ExtractionContext; import org.hibernate.tool.schema.extract.spi.SequenceInformation; import org.slf4j.Logger; @@ -90,6 +87,9 @@ import org.xwiki.context.ExecutionContext; import org.xwiki.environment.Environment; import org.xwiki.logging.LoggerConfiguration; +import org.xwiki.store.hibernate.HibernateAdapter; +import org.xwiki.store.hibernate.HibernateAdapterFactory; +import org.xwiki.store.hibernate.HibernateStoreException; import org.xwiki.wiki.descriptor.WikiDescriptorManager; import org.xwiki.wiki.manager.WikiManagerException; @@ -111,11 +111,6 @@ @DisposePriority(10000) public class HibernateStore implements Disposable, Initializable { - /** - * @see #isConfiguredInSchemaMode() - */ - private static final String VIRTUAL_MODE_SCHEMA = "schema"; - private static final String CONTEXT_SESSION = "hibsession"; private static final String CONTEXT_TRANSACTION = "hibtransaction"; @@ -143,9 +138,6 @@ public class HibernateStore implements Disposable, Initializable @Inject private Execution execution; - @Inject - private WikiDescriptorManager wikis; - @Inject private HibernateConfiguration hibernateConfiguration; @@ -155,6 +147,15 @@ public class HibernateStore implements Disposable, Initializable @Inject private LoggerConfiguration loggerConfiguration; + @Inject + private List adapterFactories; + + @Inject + private WikiDescriptorManager wikis; + + @Inject + private Provider defaultHibernateAdapterProvider; + private DataMigrationManager dataMigrationManager; private MetadataSources metadataSources; @@ -169,6 +170,8 @@ public class HibernateStore implements Disposable, Initializable private DatabaseProduct databaseProductCache = DatabaseProduct.UNKNOWN; + private HibernateAdapter adapter; + private SessionFactory sessionFactory; private Metadata configuredMetadata; @@ -249,6 +252,8 @@ private void disposeSessionFactory() if (this.bootstrapServiceRegistry != null) { this.bootstrapServiceRegistry.close(); } + + this.adapter = null; } private void createSessionFactory() @@ -380,95 +385,6 @@ public Configuration getConfiguration() return this.configuration; } - /** - * @return true if the user has configured Hibernate to use XWiki in schema mode (vs database mode) - * @since 11.6RC1 - */ - public boolean isConfiguredInSchemaMode() - { - String virtualModePropertyValue = getConfiguration().getProperty("xwiki.virtual_mode"); - if (virtualModePropertyValue == null) { - virtualModePropertyValue = VIRTUAL_MODE_SCHEMA; - } - return StringUtils.equals(virtualModePropertyValue, VIRTUAL_MODE_SCHEMA); - } - - /** - * Convert wiki name in database/schema name. - * - * @param wikiId the wiki name to convert. - * @param product the database engine type. - * @return the database/schema name. - */ - public String getDatabaseFromWikiName(String wikiId, DatabaseProduct product) - { - if (wikiId == null) { - return null; - } - - String database = wikiId; - - // Some databases have special database for main wiki - // It's also possible to configure the name of the main wiki database - String mainWikiId = this.wikis.getMainWikiId(); - if (StringUtils.equalsIgnoreCase(wikiId, mainWikiId)) { - database = this.hibernateConfiguration.getDB(); - if (database == null) { - if (product == DatabaseProduct.DERBY) { - database = "APP"; - } else if (product == DatabaseProduct.HSQLDB || product == DatabaseProduct.H2) { - database = "PUBLIC"; - } else if (product == DatabaseProduct.POSTGRESQL && isConfiguredInSchemaMode()) { - database = "public"; - } else { - database = wikiId; - } - } - } - - // Minus (-) is not supported by many databases - database = database.replace('-', '_'); - - // In various places we need the canonical database name (which is upper case for HSQLDB, Oracle and H2) because - // the translation is not properly done by the Dialect - if (DatabaseProduct.HSQLDB == product || DatabaseProduct.ORACLE == product || DatabaseProduct.H2 == product) { - database = StringUtils.upperCase(database); - } - - // Apply prefix - String prefix = this.hibernateConfiguration.getDBPrefix(); - database = prefix + database; - - return database; - } - - /** - * Convert wiki name in database name. - *

- * Need Hibernate to be initialized. - * - * @param wikiId the wiki name to convert. - * @return the database name. - * @since 11.6RC1 - */ - public String getDatabaseFromWikiName(String wikiId) - { - return getDatabaseFromWikiName(wikiId, getDatabaseProductName()); - } - - /** - * Convert wiki name in database name. - *

- * Need hibernate to be initialized. - * - * @return the database name. - * @since 11.6RC1 - */ - public String getDatabaseFromWikiName() - { - return getDatabaseFromWikiName(this.wikis.getCurrentWikiId()); - } - /** * Retrieve the current database product name. *

@@ -506,6 +422,38 @@ public DatabaseProduct getDatabaseProductName() return this.databaseProductCache; } + /** + * @return the Hibernate adapter + * @throws HibernateException when failing to get the adapter + */ + public HibernateAdapter getAdapter() throws HibernateException + { + if (this.adapter == null) { + this.adapter = createHibernateAdapter(); + } + + return this.adapter; + } + + private synchronized HibernateAdapter createHibernateAdapter() + { + // Gather metadata about the database + DatabaseMetaData metadata = getDatabaseMetaData(); + + // Ask the various factories for adapters matching the metadata and configuration + for (HibernateAdapterFactory factory : this.adapterFactories) { + Optional newAdapter = factory.createHibernateAdapter(metadata, getConfiguration()); + + // If a matching adapter was found, stop searching + if (newAdapter.isPresent()) { + return newAdapter.get(); + } + } + + // Fallback on the default adapter if no specific one could be found + return this.defaultHibernateAdapterProvider.get(); + } + private String extractJDBCConnectionURLScheme(String fullConnectionURL) { // Format of a JDBC URL is always: "jdbc::..." @@ -546,30 +494,14 @@ public Metadata getConfigurationMetadata() return this.configuredMetadata; } - /** - * @return true if the current database product is catalog based, false for a schema based databases - * @since 11.6RC1 - */ - public boolean isCatalog() - { - DatabaseProduct product = getDatabaseProductName(); - if (DatabaseProduct.ORACLE == product - || (DatabaseProduct.POSTGRESQL == product && isConfiguredInSchemaMode())) { - return false; - } else { - return getDialect().canCreateCatalog(); - } - - } - /** * @since 11.5RC1 */ public void setWiki(MetadataBuilder builder, String wikiId) { - String databaseName = getDatabaseFromWikiName(wikiId); + String databaseName = getAdapter().getDatabaseFromWikiName(wikiId); - if (isCatalog()) { + if (getAdapter().isCatalog()) { builder.applyImplicitCatalogName(databaseName); } else { builder.applyImplicitSchemaName(databaseName); @@ -632,35 +564,6 @@ public String toDynamicMappingTableName(String className) return "xwikicustom_" + className.replaceAll("\\.", "_"); } - /** - * Escape database name depending of the database engine. - * - * @param databaseName the schema name to escape - * @return the escaped version - * @since 11.6RC1 - */ - public String escapeDatabaseName(String databaseName) - { - String escapedDatabaseName; - - // - Oracle converts user names in uppercase when no quotes is used. - // For example: "create user xwiki identified by xwiki;" creates a user named XWIKI (uppercase) - // - In Hibernate.cfg.xml we just specify: xwiki and - // Hibernate - // seems to be passing this username as is to Oracle which converts it to uppercase. - // - // Thus for Oracle we don't escape the schema. - if (DatabaseProduct.ORACLE == getDatabaseProductName()) { - escapedDatabaseName = databaseName; - } else { - String closeQuote = String.valueOf(getDialect().closeQuote()); - escapedDatabaseName = - getDialect().openQuote() + databaseName.replace(closeQuote, closeQuote + closeQuote) + closeQuote; - } - - return escapedDatabaseName; - } - /** * @return a singleton instance of the configured {@link Dialect} */ @@ -699,8 +602,8 @@ public void setWiki(Session session, String wikiId) throws XWikiException // Switch the database only if we did not switched on it last time if (wikiId != null) { - String databaseName = getDatabaseFromWikiName(wikiId); - String escapedDatabaseName = escapeDatabaseName(databaseName); + String databaseName = getAdapter().getDatabaseFromWikiName(wikiId); + String escapedDatabaseName = getAdapter().escapeDatabaseName(databaseName); DatabaseProduct product = getDatabaseProductName(); if (DatabaseProduct.ORACLE == product) { @@ -708,7 +611,7 @@ public void setWiki(Session session, String wikiId) throws XWikiException } else if (DatabaseProduct.DERBY == product || DatabaseProduct.HSQLDB == product || DatabaseProduct.DB2 == product || DatabaseProduct.H2 == product) { executeStatement("SET SCHEMA " + escapedDatabaseName, session); - } else if (DatabaseProduct.POSTGRESQL == product && isConfiguredInSchemaMode()) { + } else if (DatabaseProduct.POSTGRESQL == product && getAdapter().isConfiguredInSchemaMode()) { executeStatement("SET search_path TO " + escapedDatabaseName, session); } else { session.doWork(connection -> { @@ -832,7 +735,7 @@ public boolean beginTransaction(SessionFactory sfactory) throws XWikiException if (session != null) { String sessionDatabase = (String) session.getProperties().get("xwiki.database"); - String contextDatabase = getDatabaseFromWikiName(contextWikiId); + String contextDatabase = getAdapter().getDatabaseFromWikiName(contextWikiId); // The current context is trying to manipulate a database different from the one in the current session if (!Objects.equals(sessionDatabase, contextDatabase)) { @@ -1026,16 +929,17 @@ public void execute(Connection connection) throws SQLException /** * Automatically update the current database schema to contains what's defined in standard metadata. * + * @throws HibernateStoreException when failing to update the database * @since 11.6RC1 */ - public void updateDatabase(String wikiId) + public void updateDatabase(String wikiId) throws HibernateStoreException { MetadataBuilder metadataBuilder = this.metadataSources.getMetadataBuilder(); // Associate the metadata with a specific wiki setWiki(metadataBuilder, wikiId); - updateDatabase(metadataBuilder.build()); + getAdapter().updateDatabase(metadataBuilder.build()); // Workaround Hibernate bug which does not create the required sequence in some databases createSequenceIfMissing(wikiId); @@ -1105,8 +1009,8 @@ private List getOracleSequences(String schemaName) private void createSequenceIfMissing(String wikiId) { // There's no issue with catalog based databases, only with schemas. - if (!isCatalog() && getDialect().getNativeIdentifierGeneratorStrategy().equals("sequence")) { - String schemaName = getDatabaseFromWikiName(wikiId); + if (!getAdapter().isCatalog() && getDialect().getNativeIdentifierGeneratorStrategy().equals("sequence")) { + String schemaName = getAdapter().getDatabaseFromWikiName(wikiId); boolean ignoreError = false; @@ -1159,39 +1063,15 @@ private void createSequenceIfMissing(String wikiId) } } - /** - * Automatically update the current database schema to contains what's defined in standard metadata. - * - * @param metadata the metadata we want the current database to follow - * @since 11.6RC1 - */ - public void updateDatabase(Metadata metadata) - { - SchemaUpdate updater = new SchemaUpdate(); - updater.execute(EnumSet.of(TargetType.DATABASE), metadata); - - List exceptions = updater.getExceptions(); - - if (exceptions.isEmpty()) { - return; - } - - // Print the errors - for (Exception exception : exceptions) { - this.logger.error(exception.getMessage(), exception); - } - - throw new HibernateException("Failed to update the database. See the log for all errors", exceptions.get(0)); - } - /** * Allows to update the schema to match the Hibernate mapping * * @param wikiId the identifier of the wiki to update * @param force defines whether or not to force the update despite the xwiki.cfg settings + * @throws HibernateStoreException when failing to update the database * @since 11.6RC1 */ - public synchronized void updateDatabase(String wikiId, boolean force) + public synchronized void updateDatabase(String wikiId, boolean force) throws HibernateStoreException { // We don't update the database if the XWiki hibernate config parameter says not to update if (!force && !this.hibernateConfiguration.isUpdateSchema()) { @@ -1262,28 +1142,6 @@ public String getConfiguredColumnName(Column column) return columnName; } - /** - * @since 11.6RC1 - */ - public String getConfiguredTableName(PersistentClass persistentClass) - { - return getConfiguredTableName(persistentClass.getTable()); - } - - /** - * @since 13.2 - */ - public String getConfiguredTableName(Table table) - { - String tableName = table.getName(); - // HSQLDB and Oracle needs to use uppercase table name to retrieve the value. - if (getDatabaseProductName() == DatabaseProduct.HSQLDB || getDatabaseProductName() == DatabaseProduct.ORACLE) { - tableName = tableName.toUpperCase(); - } - - return tableName; - } - /** * Execute the passed function with the {@link DatabaseMetaData} {@link ResultSet} corresponding to the passed * entity and property. @@ -1299,11 +1157,11 @@ public String getConfiguredTableName(Table table) public R metadataTableOrColumn(Class entityType, String propertyName, R def, ResultSetFunction function) { // retrieve the database name from the context - final String databaseName = getDatabaseFromWikiName(); + final String databaseName = getAdapter().getDatabaseFromWikiName(); PersistentClass persistentClass = getConfigurationMetadata().getEntityBinding(entityType.getName()); - final String tableName = getConfiguredTableName(persistentClass); + final String tableName = getAdapter().getTableName(persistentClass); final String columnName = getConfiguredColumnName(persistentClass, propertyName); @@ -1313,13 +1171,13 @@ public R metadataTableOrColumn(Class entityType, String propertyName, R d String name = databaseName; if (columnName != null) { - if (isCatalog()) { + if (getAdapter().isCatalog()) { resultSet = databaseMetaData.getColumns(name, null, tableName, columnName); } else { resultSet = databaseMetaData.getColumns(null, name, tableName, columnName); } } else { - if (isCatalog()) { + if (getAdapter().isCatalog()) { resultSet = databaseMetaData.getTables(name, null, tableName, null); } else { resultSet = databaseMetaData.getTables(null, name, tableName, null); @@ -1397,9 +1255,9 @@ public boolean tableExists(Class entityClass) */ public boolean isWikiDatabaseExist(String wikiName) { - final String databaseName = getDatabaseFromWikiName(wikiName); + final String databaseName = getAdapter().getDatabaseFromWikiName(wikiName); - if (isCatalog()) { + if (getAdapter().isCatalog()) { return isCatalogExist(databaseName); } else { return isSchemaExist(databaseName); diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/DatabaseProduct.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/DatabaseProduct.java index 329669043d02..d01582670c1d 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/DatabaseProduct.java +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/DatabaseProduct.java @@ -119,6 +119,15 @@ public String getProductName() return this.productName; } + /** + * @return the JDBC scheme + * @since 17.1.0RC1 + */ + public String getJDBCScheme() + { + return this.jdbcScheme; + } + @Override public boolean equals(Object object) { diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateBaseStore.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateBaseStore.java index 7445e038ec1c..084d119a0f9b 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateBaseStore.java +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateBaseStore.java @@ -39,6 +39,9 @@ import org.slf4j.LoggerFactory; import org.xwiki.context.Execution; import org.xwiki.logging.LoggerManager; +import org.xwiki.stability.Unstable; +import org.xwiki.store.hibernate.HibernateAdapter; +import org.xwiki.store.hibernate.HibernateStoreException; import com.xpn.xwiki.XWiki; import com.xpn.xwiki.XWikiContext; @@ -126,6 +129,16 @@ public String getHint() return HINT; } + /** + * @return the {@link HibernateAdapter} + * @since 17.1.0RC1 + */ + @Unstable + public HibernateAdapter getHibernateAdapater() + { + return this.store.getAdapter(); + } + /** * Allows to get the current hibernate config file path */ @@ -262,6 +275,8 @@ public void updateSchema(XWikiContext inputxcontext, boolean force) throws Hiber try { this.store.updateDatabase(context.getWikiId(), force); + } catch (HibernateStoreException e) { + throw new HibernateException(e); } finally { restoreExecutionXContext(); } @@ -274,12 +289,14 @@ public void updateSchema(XWikiContext inputxcontext, boolean force) throws Hiber * @param databaseProduct the database engine type. * @param inputxcontext the XWiki context. * @return the database/schema name. + * @deprecated use {@link HibernateAdapter#getDatabaseFromWikiName(String)} instead * @since 1.1.2 * @since 1.2M2 */ + @Deprecated(since = "17.1.0RC1") protected String getSchemaFromWikiName(String wikiName, DatabaseProduct databaseProduct, XWikiContext inputxcontext) { - return this.store.getDatabaseFromWikiName(wikiName, databaseProduct); + return this.store.getAdapter().getDatabaseFromWikiName(wikiName); } /** @@ -290,12 +307,14 @@ protected String getSchemaFromWikiName(String wikiName, DatabaseProduct database * @param wikiId the wiki name to convert. * @param inputxcontext the XWiki context. * @return the database/schema name. + * @deprecated use {@link HibernateAdapter#getDatabaseFromWikiName(String)} instead * @since 1.1.2 * @since 1.2M2 */ + @Deprecated(since = "17.1.0RC1") protected String getSchemaFromWikiName(String wikiId, XWikiContext inputxcontext) { - return this.store.getDatabaseFromWikiName(wikiId); + return this.store.getAdapter().getDatabaseFromWikiName(wikiId); } /** @@ -305,12 +324,14 @@ protected String getSchemaFromWikiName(String wikiId, XWikiContext inputxcontext * * @param context the XWiki context. * @return the database/schema name. + * @deprecated use {@link HibernateAdapter#getDatabaseFromWikiName()} instead * @since 1.1.2 * @since 1.2M2 */ + @Deprecated(since = "17.1.0RC1") public String getSchemaFromWikiName(XWikiContext context) { - return this.store.getDatabaseFromWikiName(); + return this.store.getAdapter().getDatabaseFromWikiName(); } /** @@ -454,7 +475,9 @@ public void updateSchema(BaseClass bclass, XWikiContext inputxcontext) throws XW } Metadata metadata = this.store.getMetadata(bclass.getName(), custommapping, context.getWikiId()); - this.store.updateDatabase(metadata); + this.store.getAdapter().updateDatabase(metadata); + } catch (Exception e) { + throw new XWikiException("Failed to update the schema for class [" + bclass.getName() + "]", e); } finally { restoreExecutionXContext(); } @@ -513,10 +536,12 @@ public void setDatabase(Session session, XWikiContext inputxcontext) throws XWik * @param schema the schema name to escape * @param context the XWiki context to get database engine identifier * @return the escaped version + * @deprecated use {@link HibernateAdapter#escapeDatabaseName(String)} instead */ + @Deprecated(since = "17.1.0RC1") protected String escapeSchema(String schema, XWikiContext context) { - return this.store.escapeDatabaseName(schema); + return this.store.getAdapter().escapeDatabaseName(schema); } /** @@ -951,10 +976,12 @@ public T executeWrite(XWikiContext context, HibernateCallback cb) throws /** * @return true if the user has configured Hibernate to use XWiki in schema mode (vs database mode) * @since 4.5M1 + * @deprecated use {@link HibernateAdapter#isConfiguredInSchemaMode()} instead */ + @Deprecated(since = "17.1.0RC1") protected boolean isInSchemaMode() { - return this.store.isConfiguredInSchemaMode(); + return this.store.getAdapter().isConfiguredInSchemaMode(); } /** diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/migration/hibernate/AbstractResizeMigration.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/migration/hibernate/AbstractResizeMigration.java index fa949bea9bbf..108befb9b1e1 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/migration/hibernate/AbstractResizeMigration.java +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/migration/hibernate/AbstractResizeMigration.java @@ -27,13 +27,12 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import javax.inject.Inject; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.boot.Metadata; import org.hibernate.dialect.Dialect; @@ -51,13 +50,15 @@ import org.slf4j.Logger; import org.xwiki.extension.version.Version; import org.xwiki.extension.version.internal.DefaultVersion; +import org.xwiki.store.hibernate.HibernateAdapter; +import org.xwiki.store.hibernate.HibernateStoreException; +import org.xwiki.store.hibernate.internal.MySQLHibernateAdapter; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.DeletedAttachment; import com.xpn.xwiki.doc.XWikiDeletedDocument; import com.xpn.xwiki.internal.store.hibernate.HibernateStore; import com.xpn.xwiki.store.DatabaseProduct; -import com.xpn.xwiki.store.XWikiHibernateBaseStore.HibernateCallback; import com.xpn.xwiki.store.migration.DataMigrationException; import com.xpn.xwiki.store.migration.XWikiDBVersion; @@ -104,7 +105,7 @@ protected void hibernateMigrate() throws DataMigrationException, XWikiException // Nothing to do here, everything's done as part of Liquibase pre update } - private void warnDatabaTooOld(String databaseName, Version databaseVersion) + private void warnDatabaseTooOld(String databaseName, Version databaseVersion) { this.logger.warn( "The migration cannot run on {} versions lower than {}. The short String limitation will remain 255.", @@ -127,14 +128,14 @@ public boolean shouldExecute(XWikiDBVersion startupVersion) if (productName.equalsIgnoreCase("mariadb")) { // Impossible to apply this migration on MariaDB lower than 10.2 if (version.compareTo(MARIADB102) < 0) { - warnDatabaTooOld("MariaDB", MARIADB102); + warnDatabaseTooOld("MariaDB", MARIADB102); return false; } } else { // Impossible to apply this migration on MySQL lower than 5.7 if (version.compareTo(MYSQL57) < 0) { - warnDatabaTooOld("MySQL", MYSQL57); + warnDatabaseTooOld("MySQL", MYSQL57); return false; } @@ -162,7 +163,8 @@ private void appendXmlAttribute(String property, String value, StringBuilder str } private void updateColumn(Column column, DatabaseMetaData databaseMetaData, Set dynamicTables, - StringBuilder builder) throws SQLException, DataMigrationException + Map rowFormats, StringBuilder builder, Session session) + throws SQLException, HibernateStoreException { int expectedLenght = column.getLength(); @@ -171,19 +173,19 @@ private void updateColumn(Column column, DatabaseMetaData databaseMetaData, Set< // Skip the update if the column does not exist of if its size if greater or equals already if (databaseSize != null && databaseSize.intValue() < expectedLenght) { - update(column, dynamicTables, builder); + update(column, dynamicTables, rowFormats, builder, session); } } } private Integer getDatabaseSize(Column column, DatabaseMetaData databaseMetaData) throws SQLException { - String databaseName = this.hibernateStore.getDatabaseFromWikiName(); - String tableName = this.hibernateStore.getConfiguredTableName(column.getValue().getTable()); + String databaseName = this.hibernateStore.getAdapter().getDatabaseFromWikiName(); + String tableName = this.hibernateStore.getAdapter().getTableName(column.getValue().getTable()); String columnName = this.hibernateStore.getConfiguredColumnName(column); ResultSet resultSet; - if (this.hibernateStore.isCatalog()) { + if (this.hibernateStore.getAdapter().isCatalog()) { resultSet = databaseMetaData.getColumns(databaseName, null, tableName, columnName); } else { resultSet = databaseMetaData.getColumns(null, databaseName, tableName, columnName); @@ -197,27 +199,29 @@ private Integer getDatabaseSize(Column column, DatabaseMetaData databaseMetaData } private void updateProperty(Property property, DatabaseMetaData databaseMetaData, Set dynamicTables, - StringBuilder builder) throws SQLException, DataMigrationException + Map rowFormats, StringBuilder builder, Session session) + throws SQLException, HibernateStoreException { if (property != null) { - updateValue(property.getValue(), databaseMetaData, dynamicTables, builder); + updateValue(property.getValue(), databaseMetaData, dynamicTables, rowFormats, builder, session); } } private void updateValue(Value value, DatabaseMetaData databaseMetaData, Set dynamicTables, - StringBuilder builder) throws SQLException, DataMigrationException + Map rowFormats, StringBuilder builder, Session session) + throws SQLException, HibernateStoreException { - if (value instanceof Collection) { - Table collectionTable = ((Collection) value).getCollectionTable(); + if (value instanceof Collection collection) { + Table collectionTable = collection.getCollectionTable(); for (Iterator it = collectionTable.getColumnIterator(); it.hasNext();) { - updateColumn(it.next(), databaseMetaData, dynamicTables, builder); + updateColumn(it.next(), databaseMetaData, dynamicTables, rowFormats, builder, session); } } else if (value != null) { for (Iterator it = value.getColumnIterator(); it.hasNext();) { Selectable selectable = it.next(); - if (selectable instanceof Column) { - updateColumn((Column) selectable, databaseMetaData, dynamicTables, builder); + if (selectable instanceof Column column) { + updateColumn(column, databaseMetaData, dynamicTables, rowFormats, builder, session); } } } @@ -237,7 +241,7 @@ public String getPreHibernateLiquibaseChangeLog() throws DataMigrationException java.util.Collection bindings = this.hibernateStore.getConfigurationMetadata().getEntityBindings(); - Set dynamicTables = new HashSet<>(); + Set updatedTables = new HashSet<>(); // Gather existing tables List existingTables = new ArrayList<>(bindings.size()); @@ -248,23 +252,26 @@ public String getPreHibernateLiquibaseChangeLog() throws DataMigrationException } // Cleanup specific to MySQL/MariaDB - if (this.hibernateStore.getDatabaseProductName() == DatabaseProduct.MYSQL) { - // Make sure all MySQL/MariaDB tables use a DYNAMIC row format (required to support key prefix + Map rowFormats = null; + MySQLHibernateAdapter adapter = getMySQLAdapter(); + if (adapter != null) { + // Make sure all MySQL/MariaDB tables use the expected row format (required to support key prefix // length limit up to 3072 bytes) + rowFormats = adapter.getRowFormats(session); for (PersistentClass entity : existingTables) { - setTableDYNAMIC(entity, builder, dynamicTables); + setTableRowFormat(entity, rowFormats, builder, updatedTables, session); } // Remove combined UNIQUE KEY affecting xwikiattrecyclebin#XDA_FILENAME column since those are not // supposed to exist anymore and it can prevent the resize on MySQL/MariaDB - removeAttachmentRecycleFilenameMultiKey(builder); - removeRecycleFilenameMultiKey(builder); + removeAttachmentRecycleFilenameMultiKey(builder, session); + removeRecycleFilenameMultiKey(builder, session); } // Update columns for which the size changed - updateColumns(existingTables, databaseMetaData, builder, dynamicTables); + updateColumns(existingTables, databaseMetaData, builder, updatedTables, rowFormats, session); } - } catch (SQLException e) { + } catch (Exception e) { throw new DataMigrationException("Error while extracting metadata", e); } @@ -280,29 +287,29 @@ public String getPreHibernateLiquibaseChangeLog() throws DataMigrationException return null; } - private void removeAttachmentRecycleFilenameMultiKey(StringBuilder builder) throws DataMigrationException + private void removeAttachmentRecycleFilenameMultiKey(StringBuilder builder, Session session) { PersistentClass persistentClass = this.hibernateStore.getConfigurationMetadata().getEntityBinding(DeletedAttachment.class.getName()); - String tableName = this.hibernateStore.getConfiguredTableName(persistentClass); + String tableName = this.hibernateStore.getAdapter().getTableName(persistentClass); - removeFilenameMultiKey(tableName, builder); + removeFilenameMultiKey(tableName, builder, session); } - private void removeRecycleFilenameMultiKey(StringBuilder builder) throws DataMigrationException + private void removeRecycleFilenameMultiKey(StringBuilder builder, Session session) { PersistentClass persistentClass = this.hibernateStore.getConfigurationMetadata().getEntityBinding(XWikiDeletedDocument.class.getName()); - String tableName = this.hibernateStore.getConfiguredTableName(persistentClass); + String tableName = this.hibernateStore.getAdapter().getTableName(persistentClass); - removeFilenameMultiKey(tableName, builder); + removeFilenameMultiKey(tableName, builder, session); } - private void removeFilenameMultiKey(String tableName, StringBuilder builder) throws DataMigrationException + private void removeFilenameMultiKey(String tableName, StringBuilder builder, Session session) { - String databaseName = this.hibernateStore.getDatabaseFromWikiName(); + String databaseName = this.hibernateStore.getAdapter().getDatabaseFromWikiName(); - for (String key : getUniqueKeys(databaseName, tableName)) { + for (String key : getUniqueKeys(databaseName, tableName, session)) { builder.append(" getUniqueKeys(String databaseName, String tableName) throws DataMigrationException + private List getUniqueKeys(String databaseName, String tableName, Session session) { - try { - return getStore().executeRead(getXWikiContext(), new HibernateCallback>() - { - @Override - public List doInHibernate(Session session) throws HibernateException, XWikiException - { - NativeQuery query = session - .createNativeQuery("SELECT DISTINCT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " - + "WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table AND CONSTRAINT_TYPE = 'UNIQUE'"); - query.setParameter("schema", databaseName); - query.setParameter("table", tableName); - - return query.list(); - } - }); - } catch (XWikiException e) { - throw new DataMigrationException("Failed to get unique keys", e); - } + NativeQuery query = + session.createNativeQuery("SELECT DISTINCT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " + + "WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table AND CONSTRAINT_TYPE = 'UNIQUE'"); + query.setParameter("schema", databaseName); + query.setParameter("table", tableName); + + return query.list(); } private void updateColumns(List existingTables, DatabaseMetaData databaseMetaData, - StringBuilder builder, Set dynamicTables) throws SQLException, DataMigrationException + StringBuilder builder, Set dynamicTables, Map rowFormats, Session session) + throws SQLException, HibernateStoreException { for (PersistentClass entity : existingTables) { // Find properties to update for (Iterator it = entity.getPropertyIterator(); it.hasNext();) { - updateProperty(it.next(), databaseMetaData, dynamicTables, builder); + updateProperty(it.next(), databaseMetaData, dynamicTables, rowFormats, builder, session); } // Check the key - updateValue(entity.getKey(), databaseMetaData, dynamicTables, builder); + updateValue(entity.getKey(), databaseMetaData, dynamicTables, rowFormats, builder, session); } } - private void setTableDYNAMIC(PersistentClass entity, StringBuilder builder, Set dynamicTables) - throws DataMigrationException + private MySQLHibernateAdapter getMySQLAdapter() + { + HibernateAdapter adapter = this.hibernateStore.getAdapter(); + + return adapter instanceof MySQLHibernateAdapter mysqlApater ? mysqlApater : null; + } + + private void setTableRowFormat(PersistentClass entity, Map rowFormats, StringBuilder builder, + Set updatedTables, Session session) throws HibernateStoreException { - String tableName = this.hibernateStore.getConfiguredTableName(entity); + String tableName = getMySQLAdapter().getTableName(entity); + boolean compressed = entity.getMetaAttribute(HibernateAdapter.META_ATTRIBUTE_COMPRESSED) != null; - setTableDYNAMIC(tableName, builder, dynamicTables); + setTableRowFormat(tableName, compressed, rowFormats, builder, updatedTables, session); } - private void setTableDYNAMIC(String tableName, StringBuilder builder, Set dynamicTables) - throws DataMigrationException + private void setTableRowFormat(String tableName, boolean compressed, Map rowFormats, + StringBuilder builder, Set updatedTables, Session session) throws HibernateStoreException { - if (!dynamicTables.contains(tableName) && !StringUtils.equalsIgnoreCase(getRowFormat(tableName), "Dynamic")) { + MySQLHibernateAdapter adapater = (MySQLHibernateAdapter) this.hibernateStore.getAdapter(); + + String statement = adapater.getAlterRowFormatString(tableName, compressed, rowFormats, session); + + if (statement != null) { this.logger.debug("Converting raw format for table [{}]", tableName); builder.append(""); - builder.append("ALTER TABLE "); - builder.append(tableName); - builder.append(" ROW_FORMAT=DYNAMIC"); + builder.append(statement); builder.append(""); - dynamicTables.add(tableName); - } - } - - private String getRowFormat(String tableName) throws DataMigrationException - { - try { - return getStore().executeRead(getXWikiContext(), new HibernateCallback() - { - @Override - public String doInHibernate(Session session) throws HibernateException, XWikiException - { - NativeQuery query = - session.createNativeQuery("SELECT DISTINCT ROW_FORMAT FROM INFORMATION_SCHEMA.TABLES " - + "WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table"); - query.setParameter("schema", hibernateStore.getDatabaseFromWikiName()); - query.setParameter("table", tableName); - - String rawFormat = query.uniqueResult(); - - logger.debug("Row format for table [{}]: {}", tableName, rawFormat); - - return rawFormat; - } - }); - } catch (XWikiException e) { - throw new DataMigrationException("Failed to get unique keys", e); + updatedTables.add(tableName); } } private boolean exists(PersistentClass entity, DatabaseMetaData databaseMetaData) throws SQLException { - String databaseName = this.hibernateStore.getDatabaseFromWikiName(); - String tableName = this.hibernateStore.getConfiguredTableName(entity); + String databaseName = this.hibernateStore.getAdapter().getDatabaseFromWikiName(); + String tableName = this.hibernateStore.getAdapter().getTableName(entity); ResultSet resultSet; - if (this.hibernateStore.isCatalog()) { + if (this.hibernateStore.getAdapter().isCatalog()) { resultSet = databaseMetaData.getTables(databaseName, null, tableName, null); } else { resultSet = databaseMetaData.getTables(null, databaseName, tableName, null); @@ -411,13 +392,12 @@ private boolean exists(PersistentClass entity, DatabaseMetaData databaseMetaData return resultSet.next(); } - private void update(Column column, Set dynamicTables, StringBuilder builder) throws DataMigrationException + private void update(Column column, Set dynamicTables, Map rowFormats, StringBuilder builder, + Session session) throws HibernateStoreException { - DatabaseProduct productName = this.hibernateStore.getDatabaseProductName(); - - this.logger.debug("Database product name: {}", productName); + MySQLHibernateAdapter mysqlAdapater = getMySQLAdapter(); - if (productName == DatabaseProduct.MYSQL) { + if (mysqlAdapater != null) { JdbcEnvironment jdbcEnvironment = this.hibernateStore.getConfigurationMetadata().getDatabase().getJdbcEnvironment(); String tableName = jdbcEnvironment.getQualifiedObjectNameFormatter() @@ -427,10 +407,10 @@ private void update(Column column, Set dynamicTables, StringBuilder buil this.logger.debug("Updating column [{}] in table [{}]", quotedColumn, tableName); - // Make sure all MySQL/MariaDB tables use a DYNAMIC row format (required to support key prefix + // Make sure all MySQL/MariaDB tables use the expected row format (required to support key prefix // length limit up to 3072 bytes) - // Check again in case it's a special table which was missed in the previous pass - setTableDYNAMIC(tableName, builder, dynamicTables); + // Check again in case it's a special table which was missing in the previous pass + setTableRowFormat(tableName, false, rowFormats, builder, dynamicTables, session); // Not using here because Liquibase ignores attributes likes "NOT NULL" builder.append(""); @@ -442,7 +422,7 @@ private void update(Column column, Set dynamicTables, StringBuilder buil builder.append(""); } else { builder.append(" getCompressionAllowedConfiguration() + { + String configuration = this.hibernateStore.getConfiguration().getProperty("xwiki.compressionAllowed"); + if (configuration == null) { + return Optional.empty(); + } + + return Optional.of(Boolean.valueOf(configuration)); + } + + @Override + public boolean isCompressionAllowed() + { + return getCompressionAllowedConfiguration().orElse(false); + } + + // Global + + @Override + public SessionFactory getSessionFactory() + { + return this.hibernateStore.getSessionFactory(); + } + + @Override + public Dialect getDialect() + { + return this.hibernateStore.getDialect(); + } + + // Database operations + + @Override + public void updateDatabase(Metadata metadata) throws HibernateStoreException + { + try { + SessionFactory sessionFactory = getSessionFactory(); + + // Custom update before the Hibernate standard DDL + try (Session session = sessionFactory.openSession()) { + updateDatabaseBegin(metadata, session); + } + + // Hibernate standard DDL + updateDatabaseStandard(metadata); + + // Custom update after the Hibernate standard DDL + try (Session session = sessionFactory.openSession()) { + updateDatabaseAfter(metadata, session); + } + } catch (Exception e) { + throw new HibernateException("Failed to update the database", e); + } + } + + private void updateDatabaseStandard(Metadata metadata) throws HibernateStoreException + { + SchemaUpdate updater = new SchemaUpdate(); + updater.execute(EnumSet.of(TargetType.DATABASE), metadata); + List exceptions = updater.getExceptions(); + + if (!exceptions.isEmpty()) { + // Print the errors + for (Exception exception : exceptions) { + this.logger.error(exception.getMessage(), exception); + } + + throw new HibernateStoreException("Failed to update the database. See the previous log for all errors", + exceptions.get(0)); + } + } + + /** + * Execute everything not handled by the Hibernate DDL which needs to be done before. + * + * @param metadata the configuration of the database to apply + * @param session the session in which to execute the update + * @throws HibernateStoreException when failing to update the database + */ + protected void updateDatabaseBegin(Metadata metadata, Session session) throws HibernateStoreException + { + + } + + /** + * Execute everything not handled by the Hibernate DDL which needs to be done after. + * + * @param metadata the configuration of the database to apply + * @param session the session in which to execute the update + * @throws HibernateStoreException when failing to update the database + */ + protected void updateDatabaseAfter(Metadata metadata, Session session) throws HibernateStoreException + { + + } + + /** + * @return true if the current database product is catalog based, false for a schema based databases + */ + public boolean isCatalog() + { + return getDialect().canCreateCatalog(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/DatabaseProductNameResolver.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/DatabaseProductNameResolver.java new file mode 100644 index 000000000000..50d5ea34f7d4 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/DatabaseProductNameResolver.java @@ -0,0 +1,42 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate; + +import java.util.Optional; + +import org.xwiki.component.annotation.Role; +import org.xwiki.stability.Unstable; + +/** + * Provide a common identifier from the actual product name given by the database. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Unstable +@Role +public interface DatabaseProductNameResolver +{ + /** + * @param databaseProductName the database product name + * @return the identifier of the database, empty if the resolved does not match the database produce name + */ + Optional resolve(String databaseProductName); +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateAdapter.java new file mode 100644 index 000000000000..b550798016cb --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateAdapter.java @@ -0,0 +1,146 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate; + +import java.sql.Driver; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.Metadata; +import org.hibernate.dialect.Dialect; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Table; +import org.xwiki.component.annotation.Role; +import org.xwiki.stability.Unstable; + +/** + * Implement XWiki Hibernate features specific to a RDBMS. The goal is mainly to extend the Hibernate {@link Dialect} + * and JDBC {@link Driver} concepts with features they don't support (table compression, wiki -> database name, etc.). + *

+ * It's recommended to extend {@link AbstractHibernateAdapter}. + *

+ * A {@link HibernateAdapter} is supposed to be provided by a {@link HibernateAdapterFactory}, but if you need to + * associate the {@link HibernateAdapter} to a specific database engine (and optionally to a specific minimum version) + * you can use the following helper format: + *

    + *
  • "jdbc scheme/minimum version" (for example the "mysql/8" adapter will be selected for a MySQL 9.0.1 server, if no + * adapter is registered for version 9)
  • + *
  • "jdbc scheme" (for example the "mysql" adapter will be selected for a MySQL 5.7 server, if no adapter is + * registered for version 5.7 or lower)
  • + *
+ * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Unstable +@Role +public interface HibernateAdapter +{ + /** + * The name of the meta attribute indicating if a table should be compressed. + */ + String META_ATTRIBUTE_COMPRESSED = "xwiki-compressed"; + + // Configuration + + /** + * Get the native database/schema name for the current wiki. + * + * @return the native database/schema name + */ + String getDatabaseFromWikiName(); + + /** + * Get the native database/schema name for the passed wiki identifier. + * + * @param wikiId the identifier of the wiki + * @return the native database/schema name + */ + String getDatabaseFromWikiName(String wikiId); + + /** + * @return true if the user has configured Hibernate to use XWiki in schema mode (vs database mode) + */ + boolean isConfiguredInSchemaMode(); + + /** + * @param persistentClass the Hibernate entity + * @return the table name + */ + String getTableName(PersistentClass persistentClass); + + /** + * @param table the Hibernate table representation + * @return the table name + */ + String getTableName(Table table); + + /** + * @param tableName the name of the table + * @return the name of the table in the right case/format + */ + String getTableName(String tableName); + + /** + * @param entity the Hibernate entity for which to extract the configuration + * @return true if the table should be compressed + */ + boolean isCompressed(PersistentClass entity); + + /** + * Escape database name to be used in a query. + * + * @param databaseName the schema name to escape + * @return the escaped version + */ + String escapeDatabaseName(String databaseName); + + /** + * @return true if compression is enabled for this database. Its possible to force it using hibernate.cfg.xml + * {@code xwiki.compression} property, but the default may vary depending on the database. + */ + boolean isCompressionAllowed(); + + // Global + + /** + * @return the Hibernate {@link SessionFactory} + */ + SessionFactory getSessionFactory(); + + /** + * @return the Hibernate {@link Dialect} + */ + Dialect getDialect(); + + // Database operations + + /** + * Automatically update the current database schema to contains what's defined in provided metadata. + * + * @param metadata the metadata we want the current database to follow + * @throws HibernateStoreException when failing to update the database + */ + void updateDatabase(Metadata metadata) throws HibernateStoreException; + + /** + * @return true if the current database product is catalog based, false for a schema based databases + */ + boolean isCatalog(); +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateAdapterFactory.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateAdapterFactory.java new file mode 100644 index 000000000000..f0c36b08cd3f --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateAdapterFactory.java @@ -0,0 +1,54 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate; + +import java.sql.DatabaseMetaData; +import java.util.Optional; + +import org.hibernate.HibernateException; +import org.hibernate.cfg.Configuration; +import org.xwiki.component.annotation.Role; +import org.xwiki.stability.Unstable; + +/** + * A factory in charge of providing a specific {@link HibernateAdapter} instance, generally based on information found + * in the {@link DatabaseMetaData} (database server related metadata) and {@link Configuration} (Hibernate configuration + * properties). + *

+ * If no specific adapter is returned by the various factories, a default one is used. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Unstable +@Role +public interface HibernateAdapterFactory +{ + /** + * Create a new {@link HibernateAdapter} instance corresponding to the passed {@link DatabaseMetaData}. + * + * @param metaData the metadata gathered from the databases + * @param configuration the Hibernate configuration + * @return an {@link HibernateAdapter} instance if the this factory matches the passed metadata + * @throws HibernateException when failing to resolve the adapter + */ + Optional createHibernateAdapter(DatabaseMetaData metaData, Configuration configuration) + throws HibernateException; +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateStoreException.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateStoreException.java new file mode 100644 index 000000000000..937f61fbdb9e --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/HibernateStoreException.java @@ -0,0 +1,55 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate; + +import org.xwiki.stability.Unstable; +import org.xwiki.store.StoreException; + +/** + * Base exception for Hibernate store related APIs. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Unstable +public class HibernateStoreException extends StoreException +{ + /** + * Serialization identifier. + */ + private static final long serialVersionUID = 1L; + + /** + * @param message exception message + */ + public HibernateStoreException(String message) + { + super(message); + } + + /** + * @param message exception message + * @param cause nested exception + */ + public HibernateStoreException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/DefaultHibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/DefaultHibernateAdapter.java new file mode 100644 index 000000000000..550627376101 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/DefaultHibernateAdapter.java @@ -0,0 +1,37 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.AbstractHibernateAdapter; + +/** + * A fully generic adapter. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Singleton +public class DefaultHibernateAdapter extends AbstractHibernateAdapter +{ +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/DerbyHibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/DerbyHibernateAdapter.java new file mode 100644 index 000000000000..76378d590fc1 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/DerbyHibernateAdapter.java @@ -0,0 +1,50 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.AbstractHibernateAdapter; +import org.xwiki.store.hibernate.HibernateAdapter; + +/** + * The {@link HibernateAdapter} for Derby. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Named(DerbyHibernateAdapter.HINT) +@Singleton +public class DerbyHibernateAdapter extends AbstractHibernateAdapter +{ + /** + * The role hint of the component. + */ + public static final String HINT = "derby"; + + @Override + protected String getDefaultMainWikiDatabase(String wikiId) + { + return "APP"; + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/H2HibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/H2HibernateAdapter.java new file mode 100644 index 000000000000..f01161de90c9 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/H2HibernateAdapter.java @@ -0,0 +1,57 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.AbstractHibernateAdapter; +import org.xwiki.store.hibernate.HibernateAdapter; + +/** + * The {@link HibernateAdapter} for H2. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Named(H2HibernateAdapter.HINT) +@Singleton +public class H2HibernateAdapter extends AbstractHibernateAdapter +{ + /** + * The role hint of the component. + */ + public static final String HINT = "h2"; + + @Override + protected String getDefaultMainWikiDatabase(String wikiId) + { + return "PUBLIC"; + } + + @Override + protected String cleanDatabaseName(String name) + { + // H2 generally needs the schema name to be upper case + return super.cleanDatabaseName(name).toUpperCase(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/HSQLDBHibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/HSQLDBHibernateAdapter.java new file mode 100644 index 000000000000..f363a8bc99d8 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/HSQLDBHibernateAdapter.java @@ -0,0 +1,64 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.AbstractHibernateAdapter; +import org.xwiki.store.hibernate.HibernateAdapter; + +/** + * The {@link HibernateAdapter} for HSQLDB. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Named(HSQLDBHibernateAdapter.HINT) +@Singleton +public class HSQLDBHibernateAdapter extends AbstractHibernateAdapter +{ + /** + * The role hint of the component. + */ + public static final String HINT = "hsqldb"; + + @Override + protected String getDefaultMainWikiDatabase(String wikiId) + { + return "PUBLIC"; + } + + @Override + public String getTableName(String tableName) + { + // HSQLDB generally needs the table name to be upper case + return tableName != null ? tableName.toUpperCase() : null; + } + + @Override + protected String cleanDatabaseName(String name) + { + // HSQLDB generally needs the schema name to be upper case + return super.cleanDatabaseName(name).toUpperCase(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/IDVersionHibernateAdapterFactory.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/IDVersionHibernateAdapterFactory.java new file mode 100644 index 000000000000..c706b1114eff --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/IDVersionHibernateAdapterFactory.java @@ -0,0 +1,168 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import jakarta.inject.Named; + +import org.hibernate.HibernateException; +import org.hibernate.cfg.Configuration; +import org.xwiki.component.annotation.Component; +import org.xwiki.component.descriptor.ComponentDescriptor; +import org.xwiki.component.manager.ComponentLookupException; +import org.xwiki.component.manager.ComponentManager; +import org.xwiki.extension.version.Version; +import org.xwiki.extension.version.internal.DefaultVersion; +import org.xwiki.store.hibernate.DatabaseProductNameResolver; +import org.xwiki.store.hibernate.HibernateAdapter; +import org.xwiki.store.hibernate.HibernateAdapterFactory; + +/** + * A helper in charge of finding an adapter based on the database id and version. + *

+ * It search for an implementation of {@link HibernateAdapter} with the following role hints: + *

    + *
  • "jdbc scheme/minimum version" (for example the "mysql/8" adapter will be selected for a MySQL 9.0.1 server, if no + * adapter is registered for version 9)
  • + *
  • "jdbc scheme" (for example the "mysql" adapter will be selected for a MySQL 5.7 server, if no adapter is + * registered for version 5.7 or lower)
  • + *
+ *

+ * The version is supposed to mean that the adapter was designed for "this version of greater". + * + * @version $Id$ + */ +@Component +@Singleton +@Named("id/version") +public class IDVersionHibernateAdapterFactory implements HibernateAdapterFactory +{ + private static final Version DEFAULT_VERSION = new DefaultVersion("0"); + + @Inject + private ComponentManager componentManager; + + @Inject + private List resolvers; + + @Override + public Optional createHibernateAdapter(DatabaseMetaData metaData, + Configuration hibernateConfiguration) throws HibernateException + { + HibernateAdapter adapter = null; + + // Index all adapters + Map> mapping = getAdapterMapping(); + + // Gather adapter(s) associated to the specific database product id + Map versions; + try { + // Resolved the database product identifier + String databaseProductId = getDatabaseId(metaData.getDatabaseProductName()); + + versions = mapping.get(databaseProductId); + } catch (SQLException e) { + throw new HibernateException("Failed to access the database product name", e); + } + + if (versions != null) { + // Resolve the current database version + Version currentVersion; + try { + currentVersion = new DefaultVersion(metaData.getDatabaseProductVersion()); + } catch (SQLException e) { + throw new HibernateException("Failed to access the database product version", e); + } + + // Search if a registered adapter matched the current version + String roleHint = null; + for (Map.Entry entry : versions.entrySet()) { + if (entry.getKey().compareTo(currentVersion) <= 0) { + roleHint = entry.getValue(); + } else { + break; + } + } + + // Load the found adapter + if (roleHint != null) { + try { + adapter = this.componentManager.getInstance(HibernateAdapter.class, roleHint); + } catch (ComponentLookupException e) { + throw new HibernateException("Failed to initialize the adapater", e); + } + } + } + + return Optional.ofNullable(adapter); + } + + private String getDatabaseId(String databaseProductName) + { + for (DatabaseProductNameResolver resolver : this.resolvers) { + Optional id = resolver.resolve(databaseProductName); + + if (id.isPresent()) { + return id.get(); + } + } + + return databaseProductName.toLowerCase(); + } + + private Map> getAdapterMapping() + { + List> descriptors = + this.componentManager.getComponentDescriptorList(HibernateAdapter.class); + + Map> databases = new HashMap<>(); + for (ComponentDescriptor descriptor : descriptors) { + String roleHint = descriptor.getRoleHint(); + if (roleHint != null && !"default".equalsIgnoreCase(roleHint)) { + String databaseName = roleHint; + Version databaseVersion = DEFAULT_VERSION; + int index = roleHint.indexOf("/"); + if (index > 0) { + databaseName = roleHint.substring(0, index); + if (roleHint.length() > index) { + databaseVersion = new DefaultVersion(roleHint.substring(index + 1)); + } + } + + Map databaseVersions = + databases.computeIfAbsent(databaseName.toLowerCase(), n -> new TreeMap<>()); + + databaseVersions.put(databaseVersion, roleHint); + } + } + + return databases; + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/LegacyDatabaseProductNameResolver.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/LegacyDatabaseProductNameResolver.java new file mode 100644 index 000000000000..a56acfc2c5ce --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/LegacyDatabaseProductNameResolver.java @@ -0,0 +1,63 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import java.util.Optional; + +import javax.annotation.Priority; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.component.descriptor.ComponentDescriptor; +import org.xwiki.store.hibernate.DatabaseProductNameResolver; + +import com.xpn.xwiki.store.DatabaseProduct; + +/** + * Implementation of {@link DatabaseProductNameResolver} based on the old {@link DatabaseProduct} resolution, except for + * MariaDB (since it's identified as MySQL) which have a dedicated {@link DatabaseProductNameResolver} with a higher + * priority. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Singleton +// Make sure it's executed before other resolvers +@Priority(ComponentDescriptor.DEFAULT_PRIORITY + 1000) +// TODO: deprecated and move DatabaseProduct to legacy and use dedicated DatabaseProductNameResolver components to +// resolve all database identifiers +@Named("legacy") +public class LegacyDatabaseProductNameResolver implements DatabaseProductNameResolver +{ + @Override + public Optional resolve(String databaseProductName) + { + DatabaseProduct product = DatabaseProduct.toProduct(databaseProductName); + + if (product != DatabaseProduct.UNKNOWN) { + return Optional.of(product.getJDBCScheme()); + } + + return Optional.empty(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MariaDBDatabaseProductNameResolver.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MariaDBDatabaseProductNameResolver.java new file mode 100644 index 000000000000..f685faea46fa --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MariaDBDatabaseProductNameResolver.java @@ -0,0 +1,50 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import java.util.Optional; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.DatabaseProductNameResolver; + +/** + * Implementation of {@link DatabaseProductNameResolver} for MariaDB. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Singleton +@Named(MariaDBHibernateAdapter.HINT) +public class MariaDBDatabaseProductNameResolver implements DatabaseProductNameResolver +{ + @Override + public Optional resolve(String databaseProductName) + { + if (databaseProductName.equalsIgnoreCase(MariaDBHibernateAdapter.HINT)) { + return Optional.of(MariaDBHibernateAdapter.HINT); + } + + return Optional.empty(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MariaDBHibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MariaDBHibernateAdapter.java new file mode 100644 index 000000000000..5632f8a6f095 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MariaDBHibernateAdapter.java @@ -0,0 +1,49 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.HibernateAdapter; + +/** + * The {@link HibernateAdapter} for MariaDB. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Named(MariaDBHibernateAdapter.HINT) +@Singleton +public class MariaDBHibernateAdapter extends MySQLHibernateAdapter +{ + /** + * The role hint of the component. + */ + public static final String HINT = "mariadb"; + + @Override + public String getInformationShemaInnoDBTables() + { + return "information_schema.INNODB_SYS_TABLES"; + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MySQL8HibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MySQL8HibernateAdapter.java new file mode 100644 index 000000000000..fe02a00f1f03 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MySQL8HibernateAdapter.java @@ -0,0 +1,50 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.HibernateAdapter; + +/** + * The {@link HibernateAdapter} for MariaDB. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Named(MySQL8HibernateAdapter.HINT) +@Singleton +public class MySQL8HibernateAdapter extends MySQLHibernateAdapter +{ + /** + * The role hint of the component. + */ + public static final String HINT = "mysql/8"; + + @Override + public String getInformationShemaInnoDBTables() + { + // "INNODB_SYS_TABLES" was renamed to "INNODB_TABLES" in MySQL 8.0 + return "information_schema.INNODB_TABLES"; + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MySQLHibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MySQLHibernateAdapter.java new file mode 100644 index 000000000000..613dbbce471a --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/MySQLHibernateAdapter.java @@ -0,0 +1,165 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.apache.commons.lang3.StringUtils; +import org.hibernate.Session; +import org.hibernate.boot.Metadata; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.query.NativeQuery; +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.AbstractHibernateAdapter; +import org.xwiki.store.hibernate.HibernateAdapter; +import org.xwiki.store.hibernate.HibernateStoreException; + +/** + * The default {@link HibernateAdapter} for MySQL. Based on MySQL 5.7 specifications. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Singleton +@Named(MySQLHibernateAdapter.HINT) +public class MySQLHibernateAdapter extends AbstractHibernateAdapter +{ + /** + * The role hint of the component. + */ + public static final String HINT = "mysql"; + + @Override + public void updateDatabaseAfter(Metadata metadata, Session session) throws HibernateStoreException + { + updateRowFormats(metadata, session); + } + + protected void updateRowFormats(Metadata metadata, Session session) throws HibernateStoreException + { + // Gather tables row formats + Map rowFormats = getRowFormats(session); + + boolean compressionAllowed = isCompressionAllowed(); + + // Make sure each table row format matches the configuration + for (PersistentClass entity : metadata.getEntityBindings()) { + // Get the exact table name for the entity + String tableName = getTableName(entity); + + // Check if the table is configured to be compressed + boolean compressed = compressionAllowed && entity.getMetaAttribute(META_ATTRIBUTE_COMPRESSED) != null; + + // Compute the query statement + String statement = getAlterRowFormatString(tableName, compressed, rowFormats, session); + if (statement != null) { + // Create the query + NativeQuery query = session.createNativeQuery(statement); + + // Execute the query + session.getTransaction().begin(); + query.executeUpdate(); + session.getTransaction().commit(); + } + } + } + + /** + * @param tableName the name of the table to modify + * @param compressed true if the table should be compressed + * @param rowFormats the current row formats + * @param session the session in which to execute the query + * @return the SQL statement to execute if the table is not already compressed + * @throws HibernateStoreException when failing to check if the table is already compressed + */ + public String getAlterRowFormatString(String tableName, boolean compressed, Map rowFormats, + Session session) throws HibernateStoreException + { + String rowFormat = rowFormats.get(tableName); + + this.logger.debug("Row format for table [{}]: {}", tableName, rowFormat); + + String expectedRowFormat = compressed ? getCompressedRowFormat() : getDefaultRowFormat(); + + if (!StringUtils.equalsIgnoreCase(rowFormat, expectedRowFormat)) { + return "ALTER TABLE " + tableName + " ROW_FORMAT=" + expectedRowFormat; + } + + return null; + } + + /** + * @param session the session in which to execute the query + * @return the row format of each table in the current database + */ + public Map getRowFormats(Session session) + { + String currentDatabase = getDatabaseFromWikiName(); + NativeQuery query = session.createNativeQuery("SELECT DISTINCT NAME, ROW_FORMAT FROM " + + getInformationShemaInnoDBTables() + " WHERE LOWER(NAME) LIKE '" + currentDatabase.toLowerCase() + "/%'"); + + List results = query.list(); + + Map rowFormats = new HashMap<>(results.size()); + for (Object[] entry : results) { + String completeName = (String) entry[0]; + String tableName = completeName.substring(completeName.indexOf('/') + 1); + rowFormats.put(tableName, (String) entry[1]); + } + + return rowFormats; + } + + /** + * @return the table where to find the InnoDB tables metadata + */ + public String getInformationShemaInnoDBTables() + { + return "INFORMATION_SCHEMA.INNODB_SYS_TABLES"; + } + + /** + * @return the row format to use by default + */ + public String getDefaultRowFormat() + { + return "Dynamic"; + } + + /** + * @return the row format to use for compressed tables + */ + public String getCompressedRowFormat() + { + return "Compressed"; + } + + @Override + public boolean isCompressionAllowed() + { + return getCompressionAllowedConfiguration().orElse(true); + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/OracleHibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/OracleHibernateAdapter.java new file mode 100644 index 000000000000..d633f07e5887 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/OracleHibernateAdapter.java @@ -0,0 +1,133 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.hibernate.Session; +import org.hibernate.boot.Metadata; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.query.NativeQuery; +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.AbstractHibernateAdapter; +import org.xwiki.store.hibernate.HibernateAdapter; +import org.xwiki.store.hibernate.HibernateStoreException; + +/** + * The {@link HibernateAdapter} for Oracle. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Named(OracleHibernateAdapter.HINT) +@Singleton +public class OracleHibernateAdapter extends AbstractHibernateAdapter +{ + /** + * The role hint of the component. + */ + public static final String HINT = "oracle"; + + @Override + public void updateDatabaseAfter(Metadata metadata, Session session) throws HibernateStoreException + { + if (isCompressionAllowed()) { + // Make sure all the configured tables have the right compression configuration + updateTableCompression(metadata, session); + } + } + + private void updateTableCompression(Metadata metadata, Session session) + { + // Gather compressed tables + Set compressedTables = getCompressedTables(session); + + // Make sure each table compression status matches the configuration + for (PersistentClass entity : metadata.getEntityBindings()) { + // Get the exact table name for the entity + String tableName = getTableName(entity); + + // Check if the table is configured to be compressed + boolean compressed = isCompressed(entity); + + // Compute the query statement + if (compressedTables.contains(tableName) != compressed) { + // Create the query + NativeQuery query = session + .createNativeQuery("ALTER TABLE " + tableName + " " + (compressed ? "COMPRESS" : "NOCOMPRESS")); + + // Execute the query + session.getTransaction().begin(); + query.executeUpdate(); + session.getTransaction().commit(); + } + } + } + + /** + * @param session the session in which to execute the query + * @return the tables which are configured to be compressed + */ + public Set getCompressedTables(Session session) + { + NativeQuery query = + session.createNativeQuery("SELECT DISTINCT table_name FROM user_tables WHERE compression = 'ENABLED'"); + + return query.list().stream().map(String::toUpperCase).collect(Collectors.toSet()); + } + + @Override + public String getTableName(String tableName) + { + // Oracle generally needs the table name to be upper case + return tableName != null ? tableName.toUpperCase() : null; + } + + @Override + protected String cleanDatabaseName(String name) + { + // Oracle generally needs the schema name to be upper case + return super.cleanDatabaseName(name).toUpperCase(); + } + + @Override + public String escapeDatabaseName(String databaseName) + { + // - Oracle converts user names in uppercase when no quotes is used. + // For example: "create user xwiki identified by xwiki;" creates a user named XWIKI (uppercase) + // - In Hibernate.cfg.xml we just specify: xwiki and + // Hibernate + // seems to be passing this username as is to Oracle which converts it to uppercase. + // + // Thus for Oracle we don't escape the schema. + return databaseName; + } + + @Override + public boolean isCatalog() + { + return false; + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/PostgreSQLHibernateAdapter.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/PostgreSQLHibernateAdapter.java new file mode 100644 index 000000000000..932ded3b0fdb --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/org/xwiki/store/hibernate/internal/PostgreSQLHibernateAdapter.java @@ -0,0 +1,64 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.store.hibernate.AbstractHibernateAdapter; +import org.xwiki.store.hibernate.HibernateAdapter; + +/** + * The {@link HibernateAdapter} for PostgreSQL. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Component +@Named(PostgreSQLHibernateAdapter.HINT) +@Singleton +public class PostgreSQLHibernateAdapter extends AbstractHibernateAdapter +{ + /** + * The role hint of the component. + */ + public static final String HINT = "postgresql"; + + @Override + protected String getDefaultMainWikiDatabase(String wikiId) + { + if (isConfiguredInSchemaMode()) { + return "public"; + } + + return super.getDefaultMainWikiDatabase(wikiId); + } + + @Override + public boolean isCatalog() + { + if (isConfiguredInSchemaMode()) { + return false; + } + + return super.isCatalog(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/META-INF/components.txt index 9c58b84061e8..f296b3870cdd 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/META-INF/components.txt +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/META-INF/components.txt @@ -288,3 +288,15 @@ org.xwiki.internal.document.DefaultSimpleDocumentCache org.xwiki.query.hql.internal.ConfigurableHQLCompleteStatementValidator org.xwiki.query.hql.internal.DefaultHQLStatementValidator org.xwiki.query.hql.internal.StandardHQLCompleteStatementValidator +org.xwiki.store.hibernate.internal.DefaultHibernateAdapter +org.xwiki.store.hibernate.internal.DerbyHibernateAdapter +org.xwiki.store.hibernate.internal.H2HibernateAdapter +org.xwiki.store.hibernate.internal.HSQLDBHibernateAdapter +org.xwiki.store.hibernate.internal.IDVersionHibernateAdapterFactory +org.xwiki.store.hibernate.internal.LegacyDatabaseProductNameResolver +org.xwiki.store.hibernate.internal.MariaDBDatabaseProductNameResolver +org.xwiki.store.hibernate.internal.MariaDBHibernateAdapter +org.xwiki.store.hibernate.internal.MySQL8HibernateAdapter +org.xwiki.store.hibernate.internal.MySQLHibernateAdapter +org.xwiki.store.hibernate.internal.OracleHibernateAdapter +org.xwiki.store.hibernate.internal.PostgreSQLHibernateAdapter diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/xwiki.hbm.xml b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/xwiki.hbm.xml index 6d6e7bc0a4e9..ca80a048d66f 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/xwiki.hbm.xml +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/xwiki.hbm.xml @@ -103,6 +103,7 @@ + diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/xwiki.oracle.hbm.xml b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/xwiki.oracle.hbm.xml index 7785afa023e0..423d97a416c2 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/xwiki.oracle.hbm.xml +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/xwiki.oracle.hbm.xml @@ -118,6 +118,7 @@ + diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentArchiveTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentArchiveTest.java index 06aba6094559..bf06c485a022 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentArchiveTest.java +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentArchiveTest.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.suigeneris.jrcs.rcs.Version; +import org.xwiki.configuration.internal.MemoryConfigurationSource; import org.xwiki.model.reference.DocumentReference; import org.xwiki.test.annotation.AllComponents; @@ -52,12 +53,15 @@ class XWikiDocumentArchiveTest { private XWikiContext context; + private MemoryConfigurationSource xwikicfg; + @BeforeEach void setUp(MockitoOldcore mockitoOldcore) throws Exception { this.context = mockitoOldcore.getXWikiContext(); + this.xwikicfg = mockitoOldcore.registerMockXWikiCfg(); } - + /** * JRCS uses the user.name system property to set the author of a change. Verify that it * works if the user name has a space in its name. This used to fail and this test is here to @@ -290,8 +294,59 @@ void getNodes() throws XWikiException } @Test - void getNextFullVersions() throws XWikiException + void getNextFullVersions(MockitoOldcore mockitoOldcore) throws XWikiException + { + XWikiDocument doc = new XWikiDocument(new DocumentReference("Test", "Test", "Test")); + XWikiDocumentArchive archive = new XWikiDocumentArchive(doc.getId()); + doc.setDocumentArchive(archive); + String author = "XWiki.some author"; + + // We have a full version every 5 nodes, so we're injecting 15 versions + addRevisionToHistory(archive, doc, "content 1.1", author, "initial 1.1"); + + doc.setContent("content 2.1\nqwe @ "); + archive.updateArchive(doc, author, new Date(), "2.1", new Version(2,1), context); + + doc.setContent("content 2.2\nqweq@ "); + archive.updateArchive(doc, author, new Date(), "2.2", new Version(2,2), context); + + doc.setContent("content 2.3\nqweqe @@"); + archive.updateArchive(doc, author, new Date(), "2.3", new Version(2,3), context); + + doc.setContent("content 2.4\nqweqe @@"); + archive.updateArchive(doc, author, new Date(), "2.4", new Version(2,4), context); + + // 5 nodes so far, + // let's add 5 + + for (int i = 1; i <= 5; i++) { + String version = String.format("3.%s", i); + doc.setContent(version + "\nqweqe @@"); + archive.updateArchive(doc, author, new Date(), version, new Version(version), context); + } + + // let's add 7 + for (int i = 1; i <= 7; i++) { + String version = String.format("4.%s", i); + doc.setContent(version + "\nqweqe @@"); + archive.updateArchive(doc, author, new Date(), version, new Version(version), context); + } + assertEquals(new Version(4,7), archive.getLatestVersion()); + + assertEquals(new Version("2.3"), archive.getNextFullVersion(new Version("2.3"))); + assertEquals(new Version("1.1"), archive.getNextFullVersion(new Version("1.1"))); + assertEquals(new Version("2.1"), archive.getNextFullVersion(new Version("2.1"))); + assertEquals(new Version("3.1"), archive.getNextFullVersion(new Version("3.1"))); + assertEquals(new Version("4.6"), archive.getNextFullVersion(new Version("4.6"))); + assertEquals(new Version("4.4"), archive.getNextFullVersion(new Version("4.4"))); + } + + @Test + void getNextFullVersionsWhenFullEvery5Versions(MockitoOldcore mockitoOldcore) throws XWikiException { + // Change the configuration to create a full patch only every 5 versions + this.xwikicfg.setProperty("xwiki.store.rcs.nodesPerFull", 5); + XWikiDocument doc = new XWikiDocument(new DocumentReference("Test", "Test", "Test")); XWikiDocumentArchive archive = new XWikiDocumentArchive(doc.getId()); doc.setDocumentArchive(archive); @@ -374,6 +429,51 @@ void verifyDiffAndFullRevisionAlgorithm() throws Exception doc.setDocumentArchive(archive); String author = "XWiki.some author"; + addRevisionToHistory(archive, doc, "content 1.1", author, "1.1"); + assertFalse(archive.getNode(new Version(1, 1)).isDiff()); + + addRevisionToHistory(archive, doc, "content 2.1", author, "2.1"); + assertFalse(archive.getNode(new Version(1, 1)).isDiff()); + assertFalse(archive.getNode(new Version(2, 1)).isDiff()); + + addRevisionToHistory(archive, doc, "content 3.1", author, "3.1"); + assertFalse(archive.getNode(new Version(1, 1)).isDiff()); + assertFalse(archive.getNode(new Version(2, 1)).isDiff()); + assertFalse(archive.getNode(new Version(3, 1)).isDiff()); + + addRevisionToHistory(archive, doc, "content 4.1", author, "4.1"); + assertFalse(archive.getNode(new Version(1, 1)).isDiff()); + assertFalse(archive.getNode(new Version(2, 1)).isDiff()); + assertFalse(archive.getNode(new Version(3, 1)).isDiff()); + assertFalse(archive.getNode(new Version(4, 1)).isDiff()); + + addRevisionToHistory(archive, doc, "content 5.1", author, "5.1"); + assertFalse(archive.getNode(new Version(1, 1)).isDiff()); + assertFalse(archive.getNode(new Version(2, 1)).isDiff()); + assertFalse(archive.getNode(new Version(3, 1)).isDiff()); + assertFalse(archive.getNode(new Version(4, 1)).isDiff()); + assertFalse(archive.getNode(new Version(5, 1)).isDiff()); + + addRevisionToHistory(archive, doc, "content 6.1", author, "6.1"); + assertFalse(archive.getNode(new Version(1, 1)).isDiff()); + assertFalse(archive.getNode(new Version(2, 1)).isDiff()); + assertFalse(archive.getNode(new Version(3, 1)).isDiff()); + assertFalse(archive.getNode(new Version(4, 1)).isDiff()); + assertFalse(archive.getNode(new Version(5, 1)).isDiff()); + assertFalse(archive.getNode(new Version(6, 1)).isDiff()); + } + + @Test + void verifyDiffAndFullRevisionAlgorithmWhenFullEvery5Versions() throws Exception + { + // Change the configuration to create a full patch only every 5 versions + this.xwikicfg.setProperty("xwiki.store.rcs.nodesPerFull", 5); + + XWikiDocument doc = new XWikiDocument(new DocumentReference("Test", "Test", "Test")); + XWikiDocumentArchive archive = new XWikiDocumentArchive(doc.getId()); + doc.setDocumentArchive(archive); + String author = "XWiki.some author"; + // The first revision is always a full revision (not a diff) addRevisionToHistory(archive, doc, "content 1.1", author, "1.1"); assertFalse(archive.getNode(new Version(1, 1)).isDiff()); diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/store/XWikiHibernateStoreTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/store/XWikiHibernateStoreTest.java index 92136a116dcd..fd11cb91a767 100644 --- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/store/XWikiHibernateStoreTest.java +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/store/XWikiHibernateStoreTest.java @@ -51,6 +51,7 @@ import org.xwiki.observation.EventListener; import org.xwiki.observation.ObservationManager; import org.xwiki.query.QueryManager; +import org.xwiki.store.hibernate.HibernateAdapter; import org.xwiki.test.LogLevel; import org.xwiki.test.junit5.LogCaptureExtension; import org.xwiki.test.junit5.mockito.ComponentTest; @@ -106,6 +107,9 @@ public class XWikiHibernateStoreTest @Mock private Session session; + @Mock + private HibernateAdapter adapter; + /** * The Hibernate transaction. */ @@ -175,8 +179,9 @@ void setUp(MockitoComponentManager componentManager) throws Exception when(this.hibernateStore.getCurrentSession()).thenReturn(session); when(this.hibernateStore.getCurrentTransaction()).thenReturn(transaction); + when(this.hibernateStore.getAdapter()).thenReturn(this.adapter); // Default is schema mode - when(this.hibernateStore.isConfiguredInSchemaMode()).thenReturn(true); + when(this.adapter.isConfiguredInSchemaMode()).thenReturn(true); } @Test @@ -219,7 +224,7 @@ void executeDeleteWikiStatementForPostgreSQLWhenInSchemaMode() throws Exception @Test void executeDeleteWikiStatementForPostgreSQLWhenInDatabaseMode() throws Exception { - when(this.hibernateStore.isConfiguredInSchemaMode()).thenReturn(false); + when(this.adapter.isConfiguredInSchemaMode()).thenReturn(false); Statement statement = mock(Statement.class); DatabaseProduct databaseProduct = DatabaseProduct.POSTGRESQL; diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/org/xwiki/store/hibernate/internal/IDVersionHibernateAdapterFactoryTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/org/xwiki/store/hibernate/internal/IDVersionHibernateAdapterFactoryTest.java new file mode 100644 index 000000000000..d0c665e93b0a --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/org/xwiki/store/hibernate/internal/IDVersionHibernateAdapterFactoryTest.java @@ -0,0 +1,118 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store.hibernate.internal; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Optional; + +import jakarta.inject.Named; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xwiki.store.hibernate.DatabaseProductNameResolver; +import org.xwiki.store.hibernate.HibernateAdapter; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectComponentManager; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; +import org.xwiki.test.mockito.MockitoComponentManager; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Validate {@link IDVersionHibernateAdapterFactory}. + * + * @version $Id$ + */ +@ComponentTest +public class IDVersionHibernateAdapterFactoryTest +{ + @InjectMockComponents + private IDVersionHibernateAdapterFactory factory; + + @InjectComponentManager + private MockitoComponentManager componentManager; + + private DatabaseMetaData metaData = mock(DatabaseMetaData.class); + + @MockComponent + @Named("resolver") + private DatabaseProductNameResolver resolver; + + private Optional createHibernateAdapter(String databaseName, String databaseVersion) + throws SQLException + { + when(this.metaData.getDatabaseProductName()).thenReturn(databaseName); + when(this.metaData.getDatabaseProductVersion()).thenReturn(databaseVersion); + + return this.factory.createHibernateAdapter(this.metaData, null); + } + + private HibernateAdapter registerAdapter(String hint) throws Exception + { + return this.componentManager.registerMockComponent(HibernateAdapter.class, hint); + } + + @BeforeEach + void beforeEach() throws Exception + { + this.componentManager.registerComponent(DatabaseProductNameResolver.class, "resolver1", + new DatabaseProductNameResolver() + { + @Override + public Optional resolve(String databaseProductName) + { + return databaseProductName.equals("database") ? Optional.of("database1") : Optional.empty(); + } + }); + } + + @Test + void createHibernateAdapter() throws Exception + { + assertFalse(createHibernateAdapter("database", "version").isPresent()); + + HibernateAdapter adapter1 = registerAdapter("database1"); + HibernateAdapter adapter2 = registerAdapter("database2"); + + assertFalse(createHibernateAdapter("database", "version").isPresent()); + assertSame(adapter1, createHibernateAdapter("database1", "version").get()); + assertSame(adapter2, createHibernateAdapter("database2", "version").get()); + + HibernateAdapter adapter11 = registerAdapter("database1/1.0"); + HibernateAdapter adapter12 = registerAdapter("database1/2.0"); + + assertFalse(createHibernateAdapter("database", "version").isPresent()); + assertSame(adapter1, createHibernateAdapter("database1", "version").get()); + assertSame(adapter2, createHibernateAdapter("database2", "version").get()); + assertSame(adapter11, createHibernateAdapter("database1", "1.0").get()); + assertSame(adapter11, createHibernateAdapter("database1", "1.5").get()); + assertSame(adapter12, createHibernateAdapter("database1", "2.0").get()); + assertSame(adapter12, createHibernateAdapter("database1", "42").get()); + + when(this.resolver.resolve("database")).thenReturn(Optional.of("database1")); + + assertSame(adapter1, createHibernateAdapter("database", "version").get()); + } +} diff --git a/xwiki-platform-core/xwiki-platform-store/xwiki-platform-store-api/src/main/java/org/xwiki/store/StoreException.java b/xwiki-platform-core/xwiki-platform-store/xwiki-platform-store-api/src/main/java/org/xwiki/store/StoreException.java new file mode 100644 index 000000000000..ec72339bb1d3 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-store/xwiki-platform-store-api/src/main/java/org/xwiki/store/StoreException.java @@ -0,0 +1,54 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.store; + +import org.xwiki.stability.Unstable; + +/** + * Base exception for store related APIs. + * + * @version $Id$ + * @since 17.1.0RC1 + */ +@Unstable +public class StoreException extends Exception +{ + /** + * Serialization identifier. + */ + private static final long serialVersionUID = 1L; + + /** + * @param message exception message + */ + public StoreException(String message) + { + super(message); + } + + /** + * @param message exception message + * @param cause nested exception + */ + public StoreException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/hibernate.cfg.xml.vm b/xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/hibernate.cfg.xml.vm index ba28eb6d2fdd..da18a9359e40 100644 --- a/xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/hibernate.cfg.xml.vm +++ b/xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/hibernate.cfg.xml.vm @@ -304,6 +304,10 @@ jdbc:hsqldb:file:${environment.permanentDirectory}/database/xwiki_db;shutdown=tr true utf8 + + ## Note: This is starting the line in order not to put extra spaces when generated