/* * 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 com.xpn.xwiki.store.migration.hibernate; import java.io.IOException; import java.util.Collection; import java.util.List; import javax.inject.Named; import javax.inject.Singleton; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.criterion.Projections; import org.xwiki.component.annotation.Component; import org.xwiki.component.manager.ComponentLookupException; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.store.XWikiHibernateBaseStore; import com.xpn.xwiki.store.XWikiHibernateBaseStore.HibernateCallback; import com.xpn.xwiki.store.XWikiStoreInterface; import com.xpn.xwiki.store.migration.AbstractDataMigrationManager; import com.xpn.xwiki.store.migration.DataMigration; import com.xpn.xwiki.store.migration.DataMigrationException; import com.xpn.xwiki.store.migration.XWikiDBVersion; import liquibase.Liquibase; import liquibase.database.Database; import liquibase.database.DatabaseFactory; import liquibase.database.jvm.JdbcConnection; import liquibase.exception.LiquibaseException; /** * Migration manager for hibernate store. * * @version $Id: b08828c82df63b147db0248648343f8f0d2084cf $ * @since 3.4M1 */ @Component @Named("hibernate") @Singleton public class HibernateDataMigrationManager extends AbstractDataMigrationManager { /** * Name of the liquibase resource used to include additional change logs XMLs. * If it exists, this resource should contains at least one valid liquibase XML definition. */ private static final String LIQUIBASE_RESOURCE = "liquibase-xwiki/"; /** * @return store system for execute store-specific actions. * @throws DataMigrationException if the store could not be reached */ public XWikiHibernateBaseStore getStore() throws DataMigrationException { try { return (XWikiHibernateBaseStore) this.componentManager.getInstance(XWikiStoreInterface.class, "hibernate"); } catch (ComponentLookupException e) { throw new DataMigrationException(String.format("Unable to reach the store for database %s", getXWikiContext().getWikiId()), e); } } @Override public XWikiDBVersion getDBVersionFromDatabase() throws DataMigrationException { XWikiDBVersion ver = getDBVersionFromConfig(); if (ver != null) { return ver; } final XWikiContext context = getXWikiContext(); final XWikiHibernateBaseStore store = getStore(); // Try retrieving a version from the database ver = store.failSafeExecuteRead(context, new HibernateCallback<XWikiDBVersion>() { @Override public XWikiDBVersion doInHibernate(Session session) throws HibernateException { // Retrieve the version from the database return (XWikiDBVersion) session.createCriteria(XWikiDBVersion.class).uniqueResult(); } }); // if it fails, return version 0 if there is some documents in the database, else null (empty db?) if (ver == null) { ver = store.failSafeExecuteRead(getXWikiContext(), new HibernateCallback<XWikiDBVersion>() { @Override public XWikiDBVersion doInHibernate(Session session) throws HibernateException { if (((Number) session.createCriteria(XWikiDocument.class) .setProjection(Projections.rowCount()) .uniqueResult()).longValue() > 0) { return new XWikiDBVersion(0); } return null; } }); } return ver; } @Override protected void initializeEmptyDB() throws DataMigrationException { final XWikiContext context = getXWikiContext(); final XWikiHibernateBaseStore store = getStore(); final Session originalSession = store.getSession(context); final Transaction originalTransaction = store.getTransaction(context); store.setSession(null, context); store.setTransaction(null, context); try { updateSchema(null); setDBVersion(getLatestVersion()); } finally { store.setSession(originalSession, context); store.setTransaction(originalTransaction, context); } } @Override protected void setDBVersionToDatabase(final XWikiDBVersion version) throws DataMigrationException { final XWikiContext context = getXWikiContext(); final XWikiHibernateBaseStore store = getStore(); final boolean bTransaction = store.getTransaction(context) == null; try { getStore().executeWrite(context, bTransaction, new HibernateCallback<Object>() { @Override public Object doInHibernate(Session session) throws HibernateException { session.createQuery("delete from " + XWikiDBVersion.class.getName()).executeUpdate(); session.save(version); return null; } }); } catch (Exception e) { throw new DataMigrationException(String.format("Unable to store new data version %d into database %s", version.getVersion(), context.getWikiId()), e); } } @Override protected void updateSchema(Collection<XWikiMigration> migrations) throws DataMigrationException { try { liquibaseUpdate(migrations, true); hibernateShemaUpdate(); liquibaseUpdate(migrations, false); } catch (Exception e) { throw new DataMigrationException(String.format("Unable to update schema of wiki [%s]", getXWikiContext().getWikiId()), e); } } /** * Run hibernate schema updates * * @throws DataMigrationException if the store is not accessible */ private void hibernateShemaUpdate() throws DataMigrationException { if (this.logger.isInfoEnabled()) { this.logger.info("Checking Hibernate mapping and updating schema if needed for wiki [{}]", getXWikiContext().getWikiId()); } getStore().updateSchema(getXWikiContext(), true); } /** * Get agregated liquibase change logs from a set of migration. * * @param migrations the set of migration to visit * @param preHibernate if true, get pre-hibernate schema update changelogs. * @return retrieved change logs * @throws DataMigrationException if an issue occurs in a migrator during retrieval of a change log * @since 4.3 */ private String getLiquibaseChangeLogs(Collection<XWikiMigration> migrations, boolean preHibernate) throws DataMigrationException { StringBuilder changeLogs = new StringBuilder(10000); if (migrations != null) { for (XWikiMigration migration : migrations) { if (migration.dataMigration instanceof HibernateDataMigration) { String changeLog; if (preHibernate) { changeLog = ((HibernateDataMigration) migration.dataMigration).getPreHibernateLiquibaseChangeLog(); } else { changeLog = ((HibernateDataMigration) migration.dataMigration).getLiquibaseChangeLog(); } if (changeLog != null) { changeLogs.append(changeLog); } } } } if (!preHibernate) { // Add liquibase changes from resources if any try { if (getClass().getClassLoader().getResources(LIQUIBASE_RESOURCE).hasMoreElements()) { changeLogs.append("<includeAll path=\"" + LIQUIBASE_RESOURCE + "\"/>"); } } catch (IOException ignored) { // ignored } } return changeLogs.toString(); } /** * Run liquibase for a given set of change logs * * @param migrations the set of migration to visit * @param preHibernate if true, use pre-hibernate schema update changelogs. * @throws XWikiException * @throws DataMigrationException * @since 4.3 */ private void liquibaseUpdate(Collection<XWikiMigration> migrations, boolean preHibernate) throws XWikiException, DataMigrationException { String liquibaseChangeLogs = getLiquibaseChangeLogs(migrations, preHibernate); if (liquibaseChangeLogs == null || liquibaseChangeLogs.length() == 0) { return; } final String database = getXWikiContext().getWikiId(); if (this.logger.isInfoEnabled()) { if (preHibernate) { this.logger.info("Running early schema updates (using liquibase) for database [{}]", database); } else { this.logger.info("Running additional schema updates (using liquibase) for database [{}]", database); } } final StringBuilder changeLogs = new StringBuilder(10000); changeLogs.append(getLiquibaseChangeLogHeader()); changeLogs.append(liquibaseChangeLogs); changeLogs.append(getLiquibaseChangeLogFooter()); final XWikiHibernateBaseStore store = getStore(); store.executeRead(getXWikiContext(), new HibernateCallback<Object>() { @Override @SuppressWarnings("unchecked") public Object doInHibernate(Session session) throws XWikiException { Liquibase lb; try { Database lbDatabase = DatabaseFactory.getInstance().findCorrectDatabaseImplementation( new JdbcConnection(session.connection())); // Precise the schema name to liquibase, since it does not usually determine it // properly (See XWIKI-8813). lbDatabase.setDefaultSchemaName(store.getSchemaFromWikiName(getXWikiContext())); lb = new Liquibase(MigrationResourceAccessor.CHANGELOG_NAME, new MigrationResourceAccessor(changeLogs.toString()), lbDatabase); } catch (LiquibaseException e) { throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.ERROR_XWIKI_STORE_MIGRATION, String.format("Unable to launch liquibase for database %s, schema update failed.", database), e); } try { lb.update(null); } catch (LiquibaseException e) { throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.ERROR_XWIKI_STORE_MIGRATION, String.format("Unable to update schema of database %s.", database), e); } return null; } }); } /** * @return the liquibase XML change log top level element opening with the XML declaration * @since 4.0M1 */ private String getLiquibaseChangeLogHeader() { return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<databaseChangeLog\n" + " xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n" + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + " xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog " + "http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd\">"; } /** * @return the liquibase change log top level element close tag * @since 4.0M1 */ private String getLiquibaseChangeLogFooter() { return "</databaseChangeLog>"; } @Override protected void startMigrations() throws DataMigrationException { XWikiContext context = getXWikiContext(); XWikiHibernateBaseStore store = getStore(); Session originalSession = store.getSession(context); Transaction originalTransaction = store.getTransaction(context); store.setSession(null, context); store.setTransaction(null, context); try { super.startMigrations(); } finally { store.setSession(originalSession, context); store.setTransaction(originalTransaction, context); } } @Override protected List<? extends DataMigration> getAllMigrations() throws DataMigrationException { try { return this.componentManager.getInstanceList(HibernateDataMigration.class); } catch (ComponentLookupException e) { throw new DataMigrationException("Unable to retrieve the list of hibernate data migrations", e); } } }