/*********************************************************************** * This file is part of iDempiere ERP Open Source * * http://www.idempiere.org * * * * Copyright (C) Contributors * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * * MA 02110-1301, USA. * * * * Contributors: * * - Carlos Ruiz - globalqss - bxservice * * Sponsored by FH * **********************************************************************/ package org.idempiere.process; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.adempiere.exceptions.DBException; import org.compiere.model.MChangeLog; import org.compiere.model.MColumn; import org.compiere.model.MPInstance; import org.compiere.model.MProcessPara; import org.compiere.model.MTable; import org.compiere.model.MUser; import org.compiere.model.MVerifyMigration; import org.compiere.model.MViewColumn; import org.compiere.model.MViewComponent; import org.compiere.model.PO; import org.compiere.model.Query; import org.compiere.process.ProcessInfoParameter; import org.compiere.process.SvrProcess; import org.compiere.util.DB; import org.compiere.util.Msg; /** * Process to help verifying after a migration, check if customizations were overwritten */ @org.adempiere.base.annotation.Process public class VerifyMigration extends SvrProcess { /* Date To */ private Timestamp p_DateTo = null; /* SeqNo to insert into AD_VerifyMigration stepping 10 */ private int m_SeqNo = 0; /* Counter of records inserted in AD_VerifyMigration */ int m_cnt = 0; @Override protected void prepare() { for (ProcessInfoParameter para : getParameter()) { String name = para.getParameterName(); switch (name) { case "DateTo": p_DateTo = para.getParameterAsTimestamp(); break; default: MProcessPara.validateUnknownParameter(getProcessInfo().getAD_Process_ID(), para); break; } } } @Override protected String doIt() throws Exception { if (log.isLoggable(Level.INFO)) log.info("DateTo" + p_DateTo); verifyCustomizationsInChangeLog(); verifyViewColumns(); addLog(getAD_PInstance_ID(), null, null, Msg.getElement(getCtx(), MPInstance.COLUMNNAME_AD_PInstance_ID) + " " + getAD_PInstance_ID(), MPInstance.Table_ID, getAD_PInstance_ID()); return "@Inserted@ " + m_cnt; } /** * Verify if the customizations registered in change log were modified with migration scripts */ private void verifyCustomizationsInChangeLog() { StringBuilder sql = new StringBuilder(); sql.append("SELECT * "); sql.append("FROM AD_ChangeLog cl "); sql.append("WHERE cl.AD_Client_ID = 0 "); sql.append(" AND cl.AD_Table_ID<=").append(MTable.MAX_OFFICIAL_ID); sql.append(" AND cl.Record_ID<=").append(MTable.MAX_OFFICIAL_ID); sql.append(" AND cl.EventChangeLog = 'U' "); sql.append(" AND cl.AD_ChangeLog_ID = (SELECT MAX(clm.AD_ChangeLog_ID) "); sql.append(" FROM AD_ChangeLog clm "); sql.append(" WHERE cl.AD_Client_ID = clm.AD_Client_ID "); sql.append(" AND cl.AD_Table_ID = clm.AD_Table_ID "); sql.append(" AND cl.Record_ID = clm.Record_ID "); sql.append(" AND cl.AD_Column_ID = clm.AD_Column_ID "); sql.append(" AND cl.EventChangeLog = clm.EventChangeLog) "); sql.append("ORDER BY cl.AD_Table_ID, "); sql.append(" cl.AD_Column_ID, "); sql.append(" cl.Record_ID"); try (PreparedStatement pstmt = DB.prepareStatement(sql.toString(), get_TrxName())) { ResultSet rs = pstmt.executeQuery(); // For each LAST customization change log record (changes to official records in official tables) while (rs.next()) { MChangeLog cl = new MChangeLog(getCtx(), rs, get_TrxName()); MTable table = MTable.get(cl.getAD_Table_ID()); MColumn column = MColumn.get(cl.getAD_Column_ID()); if (!column.isAllowLogging() || !table.isChangeLog()) continue; // do not process as the table or column was marked as not logging String tabcol = table.getTableName() + "." + column.getColumnName(); MUser user = MUser.get(cl.getCreatedBy()); PO po = table.getPO(cl.getRecord_ID(), get_TrxName()); if (po == null) { // the customized record does not exist in the database String msg = Msg.getMsg(getCtx(), "VM_CustomizedRecordDoesNotExist", // A customized record does not exist in the database. The column {0} was customized on {1,date,long} by {2} in record {3} that no longer exists new Object[] { tabcol, cl.getCreated(), user.getName(), cl.getRecord_ID() }); addVerifyMigration( cl.getAD_Table_ID(), cl.getAD_Column_ID(), cl.getRecord_ID(), cl.getAD_ChangeLog_ID(), msg, null, null, MVerifyMigration.PRIORITYRULE_Medium); continue; } Object currentValue = po.get_Value(column.getColumnName()); String expectedValue = cl.getNewValue(); // Report if there is a difference between the actual value in the database and the value changed by customization if ( (currentValue == null && expectedValue != null) || (currentValue != null && expectedValue == null) || (currentValue != null && ! currentValue.toString().equals(expectedValue))) { if (! MVerifyMigration.isIgnored(cl.getAD_ChangeLog_ID(), cl.getAD_Table_ID(), cl.getAD_Column_ID(), cl.getRecord_ID(), get_TrxName())) { String msg = Msg.getMsg(getCtx(), "VM_ExpectedValueDiffersFromCurrentValue", // Expected Value differs from Current Value. The column {0} in record {1} was customized on {2,date,long} by {3} new Object[] { tabcol, po.toString(), cl.getCreated(), user.getName() }); addVerifyMigration( cl.getAD_Table_ID(), cl.getAD_Column_ID(), cl.getRecord_ID(), cl.getAD_ChangeLog_ID(), msg, currentValue, expectedValue, MVerifyMigration.PRIORITYRULE_High); } } } } catch (SQLException e) { throw new DBException(e, sql.toString()); } } /** * Verify if views and columns exist in database * @throws SQLException */ private void verifyViewColumns() throws SQLException { List tables = new Query(getCtx(), MTable.Table_Name, "IsView='Y'", get_TrxName()) .setOnlyActiveRecords(true) .list(); for (MTable table : tables) { // Find columns in Dictionary MViewComponent component = new Query(getCtx(), MViewComponent.Table_Name, "AD_Table_ID=?", get_TrxName()) .setOnlyActiveRecords(true) .setParameters(table.getAD_Table_ID()) .setOrderBy(MViewComponent.COLUMNNAME_SeqNo) .first(); if (component == null) { log.warning("View not defined in dictionary " + table.getTableName()); continue; } List listDict = new ArrayList(); Map mapDict = new HashMap(); for (MViewColumn vcol : component.getColumns(true)) { String columnName = vcol.getColumnName(); if (columnName.startsWith("\"") && columnName.endsWith("\"")) columnName = columnName.substring(1, columnName.length()-1); listDict.add(columnName.toUpperCase()); mapDict.put(columnName.toUpperCase(), vcol); } if (listDict.size() == 0) { // ignore, view not defined in dictionary log.warning("View not defined in dictionary " + table.getTableName()); continue; } // Find columns in Database Connection conn = null; ResultSet rs = null; List listDB = new ArrayList(); try { conn = DB.getConnection(); DatabaseMetaData md = conn.getMetaData(); String catalog = DB.getDatabase().getCatalog(); String schema = DB.getDatabase().getSchema(); String tableName = table.getTableName(); if (md.storesUpperCaseIdentifiers()) tableName = tableName.toUpperCase(); else if (md.storesLowerCaseIdentifiers()) tableName = tableName.toLowerCase(); rs = md.getColumns(catalog, schema, tableName, null); while (rs.next()) { String columnName = rs.getString ("COLUMN_NAME"); listDB.add(columnName.toUpperCase()); } } finally { DB.close(rs); rs = null; if (conn != null) { try { conn.close(); } catch (Exception e) {} } } if (listDB.size() == 0) { // view not in database if (! MVerifyMigration.isIgnored(-1, MTable.Table_ID, -1, table.getAD_Table_ID(), get_TrxName())) { MUser user = MUser.get(table.getCreatedBy()); String msg = Msg.getMsg(getCtx(), "VM_ViewNotInDB", // View does not exist in database. The view {0} was created on {1,date,long} by {2} new Object[] { table.getTableName(), table.getCreated(), user.getName() }); addVerifyMigration( MTable.Table_ID, -1, table.getAD_Table_ID(), -1, msg, null, null, MVerifyMigration.PRIORITYRULE_High); } continue; } Collections.sort(listDB); Collections.sort(listDict); List inDictNotDB = new ArrayList<>(listDict); inDictNotDB.removeAll(listDB); for (String colDictNotDB : inDictNotDB) { MViewColumn viewColumn = mapDict.get(colDictNotDB); if (! MVerifyMigration.isIgnored(-1, MViewColumn.Table_ID, -1, viewColumn.getAD_ViewColumn_ID(), get_TrxName())) { String tabcol = table.getTableName() + "." + viewColumn.getColumnName(); MUser user = MUser.get(viewColumn.getCreatedBy()); String msg = Msg.getMsg(getCtx(), "VM_CustomViewColumnNotInDB", // Column View does not exist in database. The custom view column {0} was created on {1,date,long} by {2} new Object[] { tabcol, viewColumn.getCreated(), user.getName() }); addVerifyMigration( MViewColumn.Table_ID, -1, viewColumn.getAD_ViewColumn_ID(), -1, msg, null, null, MVerifyMigration.PRIORITYRULE_High); } } List inDBNotInDict = new ArrayList<>(listDB); inDBNotInDict.removeAll(listDict); for (String colDBNotInDict : inDBNotInDict) { // At this moment this cannot be ignored String tabcol = table.getTableName() + "." + colDBNotInDict; String msg = Msg.getMsg(getCtx(), "VM_ViewColumnNotInDict", // Column View does not exist in dictionary. The view column {0} exists in database but is not defined in dictionary new Object[] { tabcol }); addVerifyMigration( MViewColumn.Table_ID, -1, -1, -1, msg, null, null, MVerifyMigration.PRIORITYRULE_High); } } } /** * Adds a record to verify migration * @param tableId * @param columnId * @param recordId * @param changeLogId * @param msg * @param currentValue * @param expectedValue * @param priorityRule */ private void addVerifyMigration(int tableId, int columnId, int recordId, int changeLogId, String msg, Object currentValue, String expectedValue, String priorityRule) { m_SeqNo += 10; MVerifyMigration vm = new MVerifyMigration( getCtx(), getAD_PInstance_ID(), tableId, columnId, recordId, changeLogId, msg, m_SeqNo, currentValue, expectedValue, priorityRule, get_TrxName()); vm.saveEx(); m_cnt++; } }