/* * 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.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import javax.inject.Named; import javax.inject.Singleton; import org.apache.commons.lang3.StringUtils; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.jdbc.Work; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.component.annotation.Component; import com.xpn.xwiki.XWikiException; 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; /** * Migration for XWIKI-7771: Fix the LOBs wrongly created by the 3.2-3.5 mapping files for PostgreSQL. * * @version $Id: 7ff50013745e08fd572847c4c949e7a5c131a040 $ * @since 3.5.1 */ @Component @Named("R35102XWIKI7771") @Singleton public class R35102XWIKI7771DataMigration extends AbstractHibernateDataMigration { @Override public String getDescription() { return "See https://jira.xwiki.org/browse/XWIKI-7771"; } @Override public XWikiDBVersion getVersion() { return new XWikiDBVersion(35102); } @Override public boolean shouldExecute(XWikiDBVersion startupVersion) { boolean shouldExecute = false; try { getStore().beginTransaction(getXWikiContext()); // Run this migration if the database isn't new shouldExecute = (startupVersion.getVersion() > 0 && getStore().getDatabaseProductName() == DatabaseProduct.POSTGRESQL); getStore().endTransaction(getXWikiContext(), false); } catch (XWikiException ex) { // Shouldn't happen, ignore } catch (DataMigrationException ex) { // Shouldn't happen, ignore } return shouldExecute; } @Override public void hibernateMigrate() throws DataMigrationException, XWikiException { getStore().executeWrite(getXWikiContext(), new HibernateCallback<Object>() { @Override public Object doInHibernate(Session session) throws HibernateException, XWikiException { session.doWork(new R35102Work("xwikircs", "xwr_patch", "xwr_docid", "History for document with ID")); session.doWork(new R35102Work("xwikirecyclebin", "xdd_xml", "xdd_id", "Deleted document with ID")); session.doWork(new R35102Work("xwikiattrecyclebin", "xda_xml", "xda_id", "Deleted attachment with ID")); return Boolean.TRUE; } }); } /** * Hibernate {@link Work} class reads back LOB data into Text fields. * * @version $Id: 7ff50013745e08fd572847c4c949e7a5c131a040 $ */ private static class R35102Work implements Work { /** The name of the table to fix. */ private String tableName; /** The name of the column to fix. */ private String columnName; /** The name of the column holding an identifier which can be printed in the logs when a migration fails. */ private String idColumnName; /** What kind of data is corrupt when a migration fails. */ private String dataType; /** Logging helper object. */ private Logger logger = LoggerFactory.getLogger(R35102Work.class); /** * Constructor specifying the table and columns to fix. * * @param tableName the name of the table to fix * @param columnName the name of the column to fix * @param idColumnName the name of the column containing an identifier that should be printed in the logs when a * migration fails * @param dataType the data type printed in the logs when a migration fails */ R35102Work(String tableName, String columnName, String idColumnName, String dataType) { this.tableName = tableName; this.columnName = columnName; this.idColumnName = idColumnName; this.dataType = dataType; } @Override public void execute(Connection connection) throws SQLException { Statement stmt = connection.createStatement(); ResultSet lobs = stmt.executeQuery("SELECT " + this.columnName + ", " + this.idColumnName + " FROM " + this.tableName + ";"); Map<String, Long> lobsToProcess = new HashMap<String, Long>(); while (lobs.next()) { // If we're not migrating data created by a version between 3.2 and 3.5, then the data is already OK if (StringUtils.isNumeric(lobs.getString(1))) { lobsToProcess.put(lobs.getString(1), lobs.getLong(2)); } } PreparedStatement inlineLob = connection.prepareStatement(MessageFormat.format("UPDATE {0} SET {1} =" + " convert_from(loread(lo_open({1}::int, 262144), 10000000), ''LATIN1'') WHERE {1} = ?", this.tableName, this.columnName)); PreparedStatement emptyLob = connection.prepareStatement(MessageFormat.format("UPDATE {0} SET {1} = ''''" + " WHERE {1} = ?", this.tableName, this.columnName)); PreparedStatement removeLob = connection.prepareStatement("select lo_unlink(?)"); for (Entry<String, Long> lob : lobsToProcess.entrySet()) { try { inlineLob.setString(1, lob.getKey()); inlineLob.executeUpdate(); // We commit early since any error will invalidate the whole transaction removeLob.setLong(1, Long.valueOf(lob.getKey())); removeLob.execute(); connection.commit(); } catch (SQLException ex) { if (ex.getMessage().contains("0x00")) { // The hibernate mapping file was broken between 3.2 and 3.5 for PostgreSQL, and any non-ASCII // characters written to the database got broken since for each character, only the last 8 bits // of the character's unicode value was sent to the database. There's no way of getting back the // missing bytes, we can just empty the value set in this row. // Start a new transaction connection.rollback(); this.logger.warn(this.dataType + " [{}] cannot be recovered", lob.getValue()); emptyLob.setString(1, lob.getKey()); emptyLob.executeUpdate(); removeLob.setLong(1, Long.valueOf(lob.getKey())); removeLob.execute(); connection.commit(); } else { throw ex; } } } } } }