/****************************************************************************** * Product: Adempiere ERP & CRM Smart Business Solution * * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. 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., * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * * For the text or an alternative of this public license, you may reach us * * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * * or via info@compiere.org or http://www.compiere.org/license.html * *****************************************************************************/ package org.compiere.dbPort; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Paths; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.Map; import java.util.Vector; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.adempiere.exceptions.AdempiereException; import org.compiere.Adempiere; import org.compiere.db.Database; import org.compiere.model.I_AD_UserPreference; import org.compiere.util.CLogger; import org.compiere.util.DisplayType; import org.compiere.util.Env; import org.compiere.util.Ini; import org.compiere.util.Util; /** * Convert SQL from Oracle syntax to Target DB syntax * * @author Jorg Janke, Victor Perez * @version $Id: Convert.java,v 1.3 2006/07/30 00:55:04 jjanke Exp $ * * @author Teo Sarca, www.arhipac.ro *
  • BF [ 2782095 ] Do not log *Access records * https://sourceforge.net/p/adempiere/bugs/1867/ *
  • TODO: BF [ 2782611 ] Migration scripts are not UTF8 * https://sourceforge.net/p/adempiere/bugs/1869/ * @author Teo Sarca *
  • BF [ 3137355 ] PG query not valid when contains quotes and backslashes * https://sourceforge.net/p/adempiere/bugs/2560/ */ public abstract class Convert { /** RegEx: insensitive and dot to include line end characters */ public static final int REGEX_FLAGS = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; /** Statement used */ protected Statement m_stmt = null; /** Last Conversion Error */ protected String m_conversionError = null; /** Last Execution Error */ protected Exception m_exception = null; /** Verbose Messages */ protected boolean m_verbose = true; /** Logger */ private static final CLogger log = CLogger.getCLogger (Convert.class); private static File fileOr = null; private static FileOutputStream fosScriptOr = null; private static Writer writerOr = null; private static File filePg = null; private static FileOutputStream fosScriptPg = null; private static Writer writerPg = null; /** * Set Verbose * @param verbose */ public void setVerbose (boolean verbose) { m_verbose = verbose; } // setVerbose /** * Execute SQL Statements (stops at first error).
    * If an error occur, hadError() returns true.
    * You can get error details via getConversionError() or getException(). * @param sqlStatements * @param conn connection * @return true if success * @throws IllegalStateException if no connection */ public boolean execute (String sqlStatements, Connection conn) { if (conn == null) throw new IllegalStateException ("Require connection"); // String[] sql = convert (sqlStatements); m_exception = null; if (m_conversionError != null || sql == null) return false; boolean ok = true; int i = 0; String statement = null; try { if (m_stmt == null) m_stmt = conn.createStatement(); // for (i = 0; ok && i < sql.length; i++) { statement = sql[i]; if (statement.length() == 0) { if (m_verbose) if (log.isLoggable(Level.FINER)) log.finer("Skipping empty (" + i + ")"); } else { if (m_verbose) { if (log.isLoggable(Level.INFO)) log.info("Executing (" + i + ") <<" + statement + ">>"); } else { if (log.isLoggable(Level.INFO)) log.info("Executing " + i); } try { m_stmt.clearWarnings(); int no = m_stmt.executeUpdate(statement); SQLWarning warn = m_stmt.getWarnings(); if (warn != null) { if (m_verbose) { if (log.isLoggable(Level.INFO)) log.info("- " + warn); } else { if (log.isLoggable(Level.INFO)) { log.info("Executing (" + i + ") <<" + statement + ">>"); log.info("- " + warn); } } } if (m_verbose) if (log.isLoggable(Level.FINE)) log.fine("- ok " + no); } catch (SQLException ex) { // Ignore Drop Errors if (!statement.startsWith("DROP ")) { ok = false; m_exception = ex; } if (!m_verbose) if (log.isLoggable(Level.INFO)) log.info("Executing (" + i + ") <<" + statement + ">>"); if (log.isLoggable(Level.INFO)) log.info("Error executing " + i + "/" + sql.length + " = " + ex); } } } // for all statements } catch (SQLException e) { m_exception = e; if (!m_verbose) if (log.isLoggable(Level.INFO)) log.info("Executing (" + i + ") <<" + statement + ">>"); if (log.isLoggable(Level.INFO)) log.info("Error executing " + i + "/" + sql.length + " = " + e); return false; } return ok; } // execute /** * Return last execution exception * @return execution exception */ public Exception getException() { return m_exception; } // getException /** * Returns true if a conversion or execution error had occurred. * Get more details via getConversionError() or getException(). * @return true if error had occurred */ public boolean hasError() { return (m_exception != null) | (m_conversionError != null); } // hasError /** * Convert SQL Statements (stops at first error).
    * Statements are delimited by /.
    * If an error occurred, hadError() returns true. * You can get details via getConversionError(). * @param sqlStatements * @return converted statement as a string */ public String convertAll (String sqlStatements) { String[] sql = convert (sqlStatements); StringBuilder sb = new StringBuilder (sqlStatements.length() + 10); for (int i = 0; i < sql.length; i++) { // line.separator sb.append(sql[i]).append("\n/\n"); if (m_verbose) if (log.isLoggable(Level.INFO)) log.info("Statement " + i + ": " + sql[i]); } return sb.toString(); } // convertAll /** * Convert SQL Statements (stops at first error).
    * If an error occurred, hadError() returns true. * You can get details via getConversionError(). * @param sqlStatements * @return Array of converted Statements */ public String[] convert (String sqlStatements) { m_conversionError = null; if (sqlStatements == null || sqlStatements.length() == 0) { m_conversionError = "SQL_Statement is null or has zero length"; log.info(m_conversionError); return null; } // return convertIt (sqlStatements); } // convert /** * Return last conversion error or null. * @return last conversion error */ public String getConversionError() { return m_conversionError; } // getConversionError /** * Conversion routine (stops at first error). *
    	 *  - convertStatement
    	 *      - convertWithConvertMap
    	 *      - convertComplexStatement
    	 *      - decode, sequence, exception
    	 *  
    * @param sqlStatements * @return array of converted statements */ protected String[] convertIt (String sqlStatements) { ArrayList result = new ArrayList (); result.addAll(convertStatement(sqlStatements)); // may return more than one target statement // convert to array String[] sql = new String[result.size()]; result.toArray(sql); return sql; } // convertIt /** * Clean up Statement. Remove trailing spaces, carriage return and tab * * @param statement * @return sql statement */ protected String cleanUpStatement(String statement) { String clean = statement.trim(); // Convert cr/lf/tab to single space Matcher m = Pattern.compile("\\s+").matcher(clean); clean = m.replaceAll(" "); clean = clean.trim(); return clean; } // cleanUpStatement /** * Utility method to replace quoted string with a predefined marker. * @param inputValue * @param retVars * @param nonce * @return string */ protected String replaceQuotedStrings(String inputValue, VectorretVars, String nonce) { // save every value // Carlos Ruiz - globalqss - better matching regexp retVars.clear(); // First we need to replace double quotes to not be matched by regexp - Teo Sarca BF [3137355 ] final String quoteMarker = "<--QUOTE"+nonce+"-->"; inputValue = inputValue.replace("''", quoteMarker); Pattern p = Pattern.compile("'[[^']*]*'"); Matcher m = p.matcher(inputValue); int i = 0; StringBuilder retValue = new StringBuilder(inputValue.length()); while (m.find()) { String var = inputValue.substring(m.start(), m.end()).replace(quoteMarker, "''"); // Put back quotes, if any retVars.addElement(var); m.appendReplacement(retValue, "<--QS" + i + "QS" + nonce + "-->"); i++; } m.appendTail(retValue); return retValue.toString() .replace(quoteMarker, "''") // Put back quotes, if any ; } /** * Utility method to recover quoted string store in retVars * @param retValue * @param retVars * @param nonce * @return string */ protected String recoverQuotedStrings(String retValue, VectorretVars, String nonce) { for (int i = 0; i < retVars.size(); i++) { //hengsin, special character in replacement can cause exception String replacement = (String) retVars.get(i); replacement = escapeQuotedString(replacement); retValue = retValue.replace("<--QS" + i + "QS" + nonce + "-->", replacement); } return retValue; } /** * hook for database specific escape of quoted string ( if needed ) * @param in * @return string */ protected String escapeQuotedString(String in) { return in; } /** * Convert simple SQL Statement. Based on ConvertMap. * * @param sqlStatement * @return converted Statement */ private String applyConvertMap(String sqlStatement) { // Error Checks if (sqlStatement.toUpperCase().indexOf("EXCEPTION WHEN") != -1) { String error = "Exception clause needs to be converted: " + sqlStatement; if (log.isLoggable(Level.INFO)) log.info(error); m_conversionError = error; return sqlStatement; } // Carlos Ruiz - globalqss // Standard Statement -- change the keys in ConvertMap String retValue = sqlStatement; Pattern p; Matcher m; // for each iteration in the conversion map Map convertMap = getConvertMap(); if (convertMap != null) { Iterator iter = convertMap.keySet().iterator(); while (iter.hasNext()) { // replace the key on convertmap (i.e.: number by numeric) String regex = (String) iter.next(); String replacement = (String) convertMap.get(regex); try { p = Pattern.compile(regex, REGEX_FLAGS); m = p.matcher(retValue); retValue = m.replaceAll(replacement); } catch (Exception e) { String error = "Error expression: " + regex + " - " + e; log.warning(error); m_conversionError = error; } } } return retValue; } // convertSimpleStatement /** * do convert map base conversion * @param sqlStatement * @return converted sql statement */ protected String convertWithConvertMap(String sqlStatement) { try { sqlStatement = applyConvertMap(cleanUpStatement(sqlStatement)); } catch (RuntimeException e) { log.warning(e.getLocalizedMessage()); m_exception = e; } return sqlStatement; } /** * Get convert map for use in sql convertion * @return map */ protected Map getConvertMap() { return null; } /** * Convert single Statements. * - remove comments * - process FUNCTION/TRIGGER/PROCEDURE * - process Statement * @param sqlStatement * @return converted statement */ protected abstract ArrayList convertStatement (String sqlStatement); /** * True if the database support native oracle dialect, false otherwise. * @return boolean */ public abstract boolean isOracle(); /** * Log oraStatement and pgStatement to SQL migration script file. * @param oraStatement * @param pgStatement */ public synchronized static void logMigrationScript(String oraStatement, String pgStatement) { // Check AdempiereSys // check property Log migration script boolean logMigrationScript = isLogMigrationScript(); if (logMigrationScript) { if (dontLog(oraStatement)) return; // Log oracle and postgres migration scripts in migration/iD[version]/[oracle|postgresql] directory // [timestamp]_[ticket].sql String fileName = null; String folderOr = null; String folderPg = null; String prm_COMMENT = null; try { if (fosScriptOr == null || fosScriptPg == null) { prm_COMMENT = Env.getContext(Env.getCtx(), I_AD_UserPreference.COLUMNNAME_MigrationScriptComment); fileName = getMigrationScriptFileName(prm_COMMENT); folderOr = getMigrationScriptFolder("oracle"); folderPg = getMigrationScriptFolder("postgresql"); Files.createDirectories(Paths.get(folderOr)); Files.createDirectories(Paths.get(folderPg)); } if (fosScriptOr == null) { fileOr = new File(folderOr + fileName); fosScriptOr = new FileOutputStream(fileOr, true); writerOr = new BufferedWriter(new OutputStreamWriter(fosScriptOr, "UTF8")); writerOr.append("-- "); writerOr.append(prm_COMMENT); writerOr.append("\nSELECT register_migration_script('").append(fileName).append("') FROM dual;\n\n"); // adding these in prevention because some oracle scripts have multiline strings or @ that are misinterpreted by sqlplus writerOr.append("SET SQLBLANKLINES ON\nSET DEFINE OFF\n\n"); } writeLogMigrationScript(writerOr, oraStatement); if (pgStatement == null) { // if oracle call convert for postgres before logging Convert convert = Database.getDatabase(Database.DB_POSTGRESQL).getConvert(); String[] r = convert.convert(oraStatement); pgStatement = r[0]; } if (fosScriptPg == null) { filePg = new File(folderPg + fileName); fosScriptPg = new FileOutputStream(filePg, true); writerPg = new BufferedWriter(new OutputStreamWriter(fosScriptPg, "UTF8")); writerPg.append("-- "); writerPg.append(prm_COMMENT); writerPg.append("\nSELECT register_migration_script('").append(fileName).append("') FROM dual;\n\n"); } writeLogMigrationScript(writerPg, pgStatement); } catch (IOException e) { throw new AdempiereException(e); } } } /** * @param ticketComment * @return migration script file name */ public static String getMigrationScriptFileName(String ticketComment) { // [timestamp]_[ticket].sql String fileName; String now = new SimpleDateFormat("yyyyMMddHHmm").format(new Date()); String pattern = "(IDEMPIERE-[0-9]*)"; Pattern p = Pattern.compile(pattern); Matcher m = p.matcher(ticketComment); String ticket = null; if (m.find()) ticket = m.group(1); if (ticket == null) ticket = "PlaceholderForTicket"; fileName = now + "_" + ticket + ".sql"; return fileName; } /** * @param dbtype oracle or postgresql * @return absolute migration script folder path for dbtype */ public static String getMigrationScriptFolder(String dbtype) { // migration/iD[version]/[oracle|postgresql] directory String version = Adempiere.MAIN_VERSION.substring(8); String homeScript; if (Util.isDeveloperMode()) homeScript = Adempiere.getAdempiereHome() + File.separator; else homeScript = System.getProperty("java.io.tmpdir") + File.separator; return homeScript + "migration" + File.separator + "iD" + version + File.separator + dbtype + File.separator; } /** * @return true if it is in log migration script mode */ public static boolean isLogMigrationScript() { boolean logMigrationScript = false; if (Ini.isClient()) { logMigrationScript = Ini.isPropertyBool(Ini.P_LOGMIGRATIONSCRIPT); } else { String sysProperty = Env.getCtx().getProperty(Ini.P_LOGMIGRATIONSCRIPT, "N"); logMigrationScript = "y".equalsIgnoreCase(sysProperty) || "true".equalsIgnoreCase(sysProperty); } return logMigrationScript; } /** List of tables to skip log migration script */ private static String [] dontLogTables = new String[] { "AD_ACCESSLOG", "AD_ALERTPROCESSORLOG", "AD_CHANGELOG", "AD_DOCUMENT_ACTION_ACCESS", "AD_FORM_ACCESS", "AD_INFOWINDOW_ACCESS", "AD_ISSUE", "AD_LDAPPROCESSORLOG", "AD_PACKAGE_IMP", "AD_PACKAGE_IMP_BACKUP", "AD_PACKAGE_IMP_DETAIL", "AD_PACKAGE_IMP_INST", "AD_PACKAGE_IMP_PROC", "AD_PINSTANCE", "AD_PINSTANCE_LOG", "AD_PINSTANCE_PARA", "AD_PREFERENCE", "AD_PROCESS_ACCESS", "AD_RECENTITEM", "AD_REPLICATION_LOG", "AD_SCHEDULERLOG", "AD_SESSION", "AD_WINDOW_ACCESS", "AD_WLISTBOX_CUSTOMIZATION", "AD_WORKFLOW_ACCESS", "AD_WORKFLOWPROCESSORLOG", "AD_USERPREFERENCE", "CM_WEBACCESSLOG", "C_ACCTPROCESSORLOG", "K_INDEXLOG", "R_REQUESTPROCESSORLOG", "T_AGING", "T_ALTER_COLUMN", "T_DISTRIBUTIONRUNDETAIL", "T_INVENTORYVALUE", "T_INVOICEGL", "T_REPLENISH", "T_REPORT", "T_REPORTSTATEMENT", "T_SELECTION", "T_SELECTION2", "T_SPOOL", "T_TRANSACTION", "T_TRIALBALANCE" }; /** * @param tableName * @return true if log migration script should ignore tableName */ public static boolean isDontLogTable(String tableName) { if (Util.isEmpty(tableName)) return false; // Don't log trl - those will be created/maintained using synchronize terminology if (tableName.endsWith("_TRL")) return true; for (String t : dontLogTables) { if (t.equalsIgnoreCase(tableName)) return true; } return false; } private static boolean dontLog(String statement) { // Do not log *Access records - teo_Sarca BF [ 2782095 ] // IDEMPIERE-323 Migration script log AD_Document_Action_Access (nmicoud / CarlosRuiz_globalqss) String uppStmt = statement.toUpperCase().trim(); // don't log selects if (uppStmt.startsWith("SELECT ")) return true; // don't log update to statistic process if (uppStmt.startsWith("UPDATE AD_PROCESS SET STATISTIC_")) return true; if (uppStmt.startsWith("UPDATE C_ACCTPROCESSOR SET DATENEXTRUN")) return true; if (uppStmt.startsWith("UPDATE R_REQUESTPROCESSOR SET DATELASTRUN")) return true; // don't log sequence updates if (uppStmt.startsWith("UPDATE AD_SEQUENCE SET CURRENTNEXT")) return true; // Don't log DELETE FROM Some_Table WHERE AD_Table_ID=? AND Record_ID=? if (uppStmt.startsWith("DELETE FROM ") && uppStmt.endsWith(" WHERE AD_TABLE_ID=? AND RECORD_ID=?")) return true; // Don't log trl related statements - those will be created/maintained using synchronize terminology if (uppStmt.matches("UPDATE .*_TRL SET .*")) return true; if (uppStmt.matches("INSERT INTO .*_TRL .*")) return true; if (uppStmt.matches("DELETE FROM .*_TRL .*")) return true; // Don't log tree custom table statements (not present in core) if (uppStmt.startsWith("INSERT INTO AD_TREENODE ") && uppStmt.contains(" AND T.TREETYPE='TL' AND T.AD_TABLE_ID=")) return true; for (int i = 0; i < dontLogTables.length; i++) { if (uppStmt.startsWith("INSERT INTO " + dontLogTables[i] + " ")) return true; if (uppStmt.startsWith("DELETE FROM " + dontLogTables[i] + " ")) return true; if (uppStmt.startsWith("DELETE " + dontLogTables[i] + " ")) return true; if (uppStmt.startsWith("UPDATE " + dontLogTables[i] + " ")) return true; if (uppStmt.startsWith("INSERT INTO " + dontLogTables[i] + "(")) return true; } // don't log selects or insert/update for exception tables (i.e. AD_Issue, AD_ChangeLog) return false; } /** * Use writer to append SQL statement to an output media (usually file). * @param w {@link Writer} * @param statement SQL statement * @throws IOException */ private static void writeLogMigrationScript(Writer w, String statement) throws IOException { // log time and date SimpleDateFormat format = DisplayType.getDateFormat(DisplayType.DateTime); String dateTimeText = format.format(new Timestamp(System.currentTimeMillis())); w.append("-- "); w.append(dateTimeText); w.append("\n"); // log statement w.append(statement); // close statement w.append("\n;\n\n"); // flush stream - teo_sarca BF [ 1894474 ] w.flush(); } /** * Close the files for migration scripts, used just on Tests */ public static void closeLogMigrationScript() { try { if (writerOr != null) { writerOr.flush(); writerOr.close(); writerOr = null; } if (writerPg != null) { writerPg.flush(); writerPg.close(); writerPg = null; } if (fosScriptOr != null) { fosScriptOr.flush(); fosScriptOr.close(); fosScriptOr = null; } if (fosScriptPg != null) { fosScriptPg.flush(); fosScriptPg.close(); fosScriptPg = null; } fileOr = null; filePg = null; } catch (IOException e) { // ignore e.printStackTrace(); } } /** * Get the name of the migration script file * @return */ public static String getGeneratedMigrationScriptFileName() { if (filePg != null) { return filePg.getName(); } return null; } } // Convert