/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * 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 version 2 of the License. * * 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., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.core.db.setup; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import mazz.i18n.Logger; import mazz.i18n.Msg; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.rhq.core.db.DatabaseType; import org.rhq.core.db.DatabaseTypeFactory; import org.rhq.core.db.DbUtil; import org.rhq.core.db.DbUtilsI18NFactory; import org.rhq.core.db.DbUtilsI18NResourceKeys; import org.rhq.core.db.ExtendedSQLException; import org.rhq.core.db.TypeMap; /** * Performs XML parsing of the database schema and data files and sets up the database accordingly. This has the ability * to not only set up the database, but also to purge the database of the schemas and data. */ public class DBSetup { private static final Logger LOG = DbUtilsI18NFactory.getLogger(DBSetup.class); private static final Msg MSG = DbUtilsI18NFactory.getMsg(); private static final String DBSETUP_ROOT_ELEMENT_NAME = "dbsetup"; private String m_jdbcUrl; private String m_username; private String m_password; private boolean m_consoleMode; private Connection m_connection; private DatabaseType m_databaseType; private boolean m_doDisconnect = true; /** * Creates a new {@link DBSetup} object where this object is not in console mode (meaning messages will not be * emitted to the console). * * @param jdbc_url the JDBC URL used to connect to the database * @param username the user that will be logged into the database * @param password user's credentials */ public DBSetup(String jdbc_url, String username, String password) { m_jdbcUrl = jdbc_url; m_username = username; m_password = password; m_consoleMode = false; } /** * Creates a new {@link DBSetup} object where this object is in console mode (meaning messages will be emitted to * the console). * * @param jdbc_url the JDBC URL used to connect to the database * @param username the user that will be logged into the database * @param password user's credentials * @param consoleMode indicates if messages should be emitted to the console */ public DBSetup(String jdbc_url, String username, String password, boolean consoleMode) { m_jdbcUrl = jdbc_url; m_username = username; m_password = password; m_consoleMode = consoleMode; } /** * Creates a new instance with an already established connection. * @param connection * @throws Exception */ public DBSetup(Connection connection) throws Exception { m_connection = connection; m_databaseType = DatabaseTypeFactory.getDatabaseType(connection); m_consoleMode = false; // MySQL complains if autocomit is true and you try to commit. // DDL operations are not transactional anyhow. m_connection.setAutoCommit(false); m_doDisconnect = false; } /** * A console application that can be used to run the DBSetup from a command line. The arguments are as follows: * * <pre> * DBSetup -op=setup|clear|uninstall|uninstallsetup * -jdbcurl=<db-url> [-jdbcuser=<username>] [-jdbcpassword=<password>] * -file=<dbsetup-xml-file> * </pre> * * where: * * <ul> * <li>-op: Defines what operation to perform: * * <ul> * <li>export: exports an existing database schema and its data to an XML file</li> * <li>setup: creates the new database schema and inserts all data</li> * <li>clear: deletes all data from the schema but leaves the schema in the database</li> * <li>uninstall: deletes all data and the schema itself from the database</li> * <li>uninstallsetup: performs an uninstall first, and then a setup</li> * </ul> * </li> * <li>-jdbcurl: JDBC URL used to connect to the database</li> * <li>-jdbcuser: username that is used to connect to the database</li> * <li>-jdbcpassword: credentials of the user</li> * <li>-file=<dbsetup-xml-file>: specifies the path to the DBSetup XML file that is read or exported to</li> * </ul> * * @param args see description */ public static void main(String[] args) { boolean do_export = false; boolean do_setup = false; boolean do_clear = false; boolean do_uninstall = false; boolean do_uninstallsetup = false; String op_requested = ""; String jdbc_url = null; String jdbc_user = null; String jdbc_password = null; String dbsetup_file = null; try { if (args.length >= 2) { for (String arg : args) { if (arg.startsWith("-op=")) { op_requested = arg.substring(arg.indexOf('=') + 1); do_export = op_requested.equals("export"); do_setup = op_requested.equals("setup"); do_clear = op_requested.equals("clear"); do_uninstall = op_requested.equals("uninstall"); do_uninstallsetup = op_requested.equals("uninstallsetup"); } else if (arg.startsWith("-jdbcurl=")) { jdbc_url = arg.substring(arg.indexOf('=') + 1); } else if (arg.startsWith("-jdbcuser=")) { jdbc_user = arg.substring(arg.indexOf('=') + 1); } else if (arg.startsWith("-jdbcpassword=")) { jdbc_password = arg.substring(arg.indexOf('=') + 1); } else if (arg.startsWith("-file=")) { dbsetup_file = arg.substring(arg.indexOf('=') + 1); } else { throw new IllegalArgumentException(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_BAD_ARG, arg)); } } } else { throw new IllegalArgumentException(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_HELP)); } if (!do_export && !do_setup && !do_clear && !do_uninstall && !do_uninstallsetup) { throw new IllegalArgumentException(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_BAD_OP, op_requested)); } // if we are exporting, the file is the file we are writing to; otherwise, it should be an existing XML file if (do_export) { if ((dbsetup_file == null) || (dbsetup_file.trim().length() == 0)) { throw new IllegalArgumentException(MSG.getMsg( DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_BAD_DBSETUPFILE, dbsetup_file)); } } else { File f = new File(dbsetup_file); if (!f.exists()) { throw new IllegalArgumentException(MSG.getMsg( DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_BAD_DBSETUPFILE, dbsetup_file)); } dbsetup_file = f.getAbsolutePath(); } if (jdbc_url == null) { throw new IllegalArgumentException(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_BAD_JDBCURL)); } if (dbsetup_file == null) { throw new IllegalArgumentException(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_BAD_DBSETUPFILE, "")); } } catch (Exception iae) { System.out.println(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_USAGE, iae.getMessage())); return; } DBSetup dbsetup = new DBSetup(jdbc_url, jdbc_user, jdbc_password); try { boolean ok = true; if (do_export) { dbsetup.export(dbsetup_file); ok = true; // all failures will be in form of exceptions } if (do_setup) { dbsetup.setup(dbsetup_file); ok = true; // all failures will be in form of exceptions } else if (do_clear) { ok = dbsetup.clear(dbsetup_file); } else if (do_uninstall) { ok = dbsetup.uninstall(dbsetup_file); } else if (do_uninstallsetup) { ok = dbsetup.uninstall(dbsetup_file); if (ok) { dbsetup.setup(dbsetup_file); } } if (ok) { System.out.println(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_OK)); } else { System.out.println(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_NOT_OK)); } } catch (Exception e) { System.out.println(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_CMDLINE_ERROR, e.getMessage())); e.printStackTrace(System.out); } return; } /** * Creates a node via the target document which is a duplicate of the <code>source</code> node. The returned node is * not placed in the target document, the caller should place the returned node in the target document where it * deems appropriate. If <code>deep</code> is <code>true</code>, any child nodes of <code>source</code> are also * deeply copied. * * @param target the document used to create the node * @param source the node to copy * @param deep if <code>true</code>, all child nodes of <code>source</code> are also deeply copied * * @return the new node that was duplicated */ private Node copyNode(Document target, Node source, boolean deep) { Node ret_new_node; String node_name = source.getNodeName(); String node_value = source.getNodeValue(); switch (source.getNodeType()) { case Node.ELEMENT_NODE: { Element new_elem = target.createElement(node_name); ret_new_node = new_elem; NamedNodeMap map = source.getAttributes(); for (int iMap = 0; iMap < map.getLength(); iMap++) { Node attr = map.item(iMap); new_elem.setAttribute(attr.getNodeName(), attr.getNodeValue()); } break; } case Node.COMMENT_NODE: { ret_new_node = target.createComment(node_name); break; } case Node.TEXT_NODE: { ret_new_node = target.createTextNode(node_name); break; } default: { // we don't care about any other type of node, don't copy it ret_new_node = null; break; } } if (ret_new_node != null) { ret_new_node.setNodeValue(node_value); if (deep) { importChildNodes(ret_new_node, source, deep); } } return ret_new_node; } /** * Copies the source node and places it after the <code>after</code> node. Setting <code>deep</code> to <code> * true</code> means you want to also copy the child nodes of <code>source</code>. * * @param after * @param source * @param deep */ private void importNodeAfter(Node after, Node source, boolean deep) { Node nodeNew = copyNode(after.getOwnerDocument(), source, deep); // Append the node to the target Node nodeNext = after.getNextSibling(); if (nodeNext != null) { after.getParentNode().insertBefore(nodeNew, after); } else { after.getParentNode().appendChild(nodeNew); } } /** * All child nodes of <code>source</code> are copied and appended to the parent node. * * @param parent * @param source * @param deep if <code>true</code>, deeply copies all child nodes of <code>source</code> */ private void importChildNodes(Node parent, Node source, boolean deep) { NodeList listChildren = source.getChildNodes(); for (int i = 0; i < listChildren.getLength(); i++) { parent.appendChild(copyNode(parent.getOwnerDocument(), listChildren.item(i), deep)); } } /** * Reads in a DBSetup XML file. The file can be found either in this object's classloader or on the file system. * * @param file * * @return the DOM document of the DBSetup XML file * * @throws IOException * @throws SAXException */ private Document readDocument(String file) throws IOException, SAXException { Document docResult; InputStream stream = this.getClass().getClassLoader().getResourceAsStream(file); if (stream == null) { docResult = readDocument(file, null); } else { docResult = readDocument(stream, null); } return docResult; } /** * Reads in either a file or stream that contains DBSetup XML content. <code>source</code> can be either a String (a * file name) or an InputStream. If <code>after</code> is not <code>null</code>, the XML content that is read in * will have its top-most node appended to that <code>after</code> node. * * @param source either a String or InputStream * @param after the node where to append the new XML data (may be <code>null</code>) * * @return the document that was read in * * @throws IOException * @throws SAXException */ private Document readDocument(Object source, Node after) throws IOException, SAXException { Document ret_document_result = null; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setErrorHandler(new ErrorHandler()); File source_file = null; if (source instanceof String) { source_file = new File((String) source); ret_document_result = builder.parse(source_file); } else if (source instanceof InputStream) { ret_document_result = builder.parse((InputStream) source); } else { throw new IOException("source=" + source.getClass()); } Node node_root = ret_document_result.getDocumentElement(); // Make sure we have a DBSetup XML file. if (node_root.getNodeName().equalsIgnoreCase(DBSETUP_ROOT_ELEMENT_NAME) == false) { if (source instanceof String) { throw new IOException(LOG.getMsgString(DbUtilsI18NResourceKeys.DBSETUP_SOURCE_NOT_VALID, source.toString())); } throw new IOException(LOG.getMsgString(DbUtilsI18NResourceKeys.DBSETUP_SOURCE_NOT_VALID, "<stream>")); } // Look for include tags NodeList listNodes = node_root.getChildNodes(); for (int iNode = 0; iNode < listNodes.getLength(); iNode++) { Node node = listNodes.item(iNode); if (node.getNodeName().equalsIgnoreCase("include") == true) { NamedNodeMap map = node.getAttributes(); for (int iAttr = 0; iAttr < map.getLength(); iAttr++) { Node nodeMap = map.item(iAttr); if (nodeMap.getNodeName().equalsIgnoreCase("file") == true) { File fileInclude = new File(nodeMap.getNodeValue()); if (fileInclude.isAbsolute() == false) { if (!(source instanceof String)) { throw new IOException( LOG.getMsgString(DbUtilsI18NResourceKeys.DBSETUP_PATHS_NOT_RELATIVE_TO_STREAM)); } fileInclude = new File(source_file.getParentFile(), nodeMap.getNodeValue()); } readDocument(fileInclude.getAbsolutePath(), node); node_root.removeChild(node); } } } else if (after != null) { importNodeAfter(after, node, true); } } } catch (ParserConfigurationException e) { throw new SAXException(e); } return ret_document_result; } /** * Performs the full database setup. * * @param file_name the path to the database setup XML file * * @throws Exception * * @see #setup(String, String, boolean, boolean) */ public void setup(String file_name) throws Exception { setup(file_name, null, false, false); } /** * Performs the actual database setup. This will read the source XML file and will perform the necessary SQL to * create the schema and insert the data. * * @param file_name the file that contains the DBSetup XML content. * @param table_name if not <code>null</code>, only this table will be setup * @param data_only if <code>true</code> does not create the schema; only inserts data into an existing schema * @param do_delete if <code>true</code>, and <code>table_name</code> was specified, this will delete all rows * from that table before inserting the new data. The table must exist. It only makes sense to * use this with <code>data_only</code> set to <code>true</code>. * * @throws Exception */ public void setup(String file_name, String table_name, boolean data_only, boolean do_delete) throws Exception { int created_views = 0; int created_tables = 0; int created_indexes = 0; try { Document doc = readDocument(file_name); Node root_node = doc.getDocumentElement(); // Make sure we can connect to the database connect(); log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CONNECTED_TO_DB, m_jdbcUrl, m_username); // let's get the known types of all our supported databases Collection<TypeMap> column_types_map = TypeMap.loadKnownTypeMaps(); // process the tables List<Table> tables = Table.getTables(root_node, getDatabaseType(), this); for (Table table : tables) { if (table.isObsolete()) { log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_IGNORING_OBSOLETE_TABLE, table.getName()); continue; } if (table_name != null) { if (!table_name.equalsIgnoreCase(table.getName())) { continue; } if (do_delete) { table.clear(); } } log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_SETTING_UP_TABLE, table.getName()); if (!data_only) { // we were told that we can create the table try { // Only attempt to create the table if the table tag has columns if (table.getColumns().size() > 0) { table.create(column_types_map); created_tables++; log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CREATED_TABLE, table.getName()); } } catch (SQLException e) { handleFatalSQLException(e, DbUtilsI18NResourceKeys.DBSETUP_CREATED_TABLE_ERROR, table.getName()); } // create the indexes Collection<Index> indexes = table.getIndexes(); for (Index index : indexes) { try { index.create(); log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CREATED_INDEX, index.getName(), index.getTable().getName()); created_indexes++; } catch (SQLException e) { handleFatalSQLException(e, DbUtilsI18NResourceKeys.DBSETUP_CREATED_INDEX_ERROR, index.getName(), index.getTable().getName()); } } } // Create the Data try { DataSet dataset = table.getDataSet(); int rows_created = dataset.create(); log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CREATED_ROWS, rows_created, table.getName()); } catch (SQLException e) { handleFatalSQLException(e, DbUtilsI18NResourceKeys.DBSETUP_CREATED_ROWS_ERROR, table.getName()); } } // process views Collection<View> views = View.getViews(root_node, getDatabaseType(), this); for (View view : views) { log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_SETTING_UP_VIEW, view.getName()); if (!data_only) { // create the view try { view.create(column_types_map); log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CREATED_VIEW, view.getName()); created_views++; } catch (SQLException e) { handleFatalSQLException(e, DbUtilsI18NResourceKeys.DBSETUP_CREATED_VIEW_ERROR, view.getName()); } } } // log results log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_SETUP_TABLES, created_tables); log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_SETUP_INDEXES, created_indexes); log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_SETUP_VIEWS, created_views); } finally { disconnect(); } return; } /** * Throws an SQL exception that wraps the given exception, with the new exception having a descriptive error message * consisting of the <code>msg_key</code> bundle message plus all messages of the given exception. * * @param e the original exception that occurred * @param msg_key the message key from the resource bundle * @param params the parameters to replace the resource bundle message's placeholders * * @throws SQLException */ private void handleFatalSQLException(SQLException e, String msg_key, Object... params) throws SQLException { String err_msg; if (m_consoleMode) { err_msg = MSG.getMsg(msg_key, params); } else { err_msg = LOG.getMsgString(msg_key, params); } String exception_string = DbUtil.getSQLExceptionString(e); String full_error_msg = err_msg + " [" + exception_string + ']'; SQLException wrapper_exception = new SQLException(full_error_msg, e.getSQLState(), e.getErrorCode()); wrapper_exception.initCause(e); log(LogPriority.FATAL, e, DbUtilsI18NResourceKeys.DBSETUP_FATAL_SQL_EXCEPTION, full_error_msg); throw wrapper_exception; } /** * Purges the entire schema of all existing data. The schema (tables, views, etc) are left intact. * * @param file_name the path to the database setup XML file * * @return <code>true</code> if all data was successfully deleted; <code>false</code> if at least one failure * occurred when trying remove data from the tables * * @throws Exception * * @see #clear(String, String) * @see #uninstall(String) */ public boolean clear(String file_name) throws Exception { return this.clear(file_name, null); } /** * Purges data from the schema. If <code>table</code> is not <code>null</code>, only that table will be cleared of * data; the remaining tables will have their data remain as-is. * * @param file_name the path to the database setup XML file * @param table_name the table to clear, if <code>null</code>, then all tables are cleared * * @return <code>true</code> if all data was successfully deleted; <code>false</code> if at least one failure * occurred when trying remove data from the tables * * @throws Exception * * @see #uninstall(String) */ public boolean clear(String file_name, String table_name) throws Exception { boolean ret_ok = false; // assume a failure will occur int modified_tables_count = 0; int failed_tables_count = 0; try { Document doc = readDocument(file_name); Node root_node = doc.getDocumentElement(); // Make sure we can connect to the database connect(); log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CONNECTED_TO_DB, m_jdbcUrl, m_username); // remove data from tables in reverse order of their creation // to bypass dependency constraints. This and our two-passes we make should be able to clear // out all data - I hope :) List<Table> tables = Table.getTables(root_node, getDatabaseType(), this); List<Table> failed_tables = new ArrayList<Table>(); Collections.reverse(tables); // our first pass for (Table table : tables) { if ((table_name != null) && (table.getName().compareToIgnoreCase(table_name) != 0)) { continue; } try { table.clear(); modified_tables_count++; log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CLEARED_TABLE, table.getName()); } catch (SQLException e) { failed_tables_count++; failed_tables.add(table); // add it to the list so we try to clear it again in our second pass log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CLEARED_TABLE_ERROR_FIRST_PASS, table.getName(), DbUtil.getSQLExceptionString(e)); } } // our second pass - hopefully, we've cleared out data that caused constraint errors in the first pass if (failed_tables.size() > 0) { log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CLEAR_SECOND_PASS, failed_tables.size()); for (Table table : failed_tables) { if ((table_name != null) && (table.getName().compareToIgnoreCase(table_name) != 0)) { continue; } try { table.clear(); // hooray! we were able to finally clear out all the data modified_tables_count++; failed_tables_count--; log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CLEARED_TABLE, table.getName()); } catch (SQLException e) { // crap - there is still a problem causing us to be unable to clear the data log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CLEARED_TABLE_ERROR_SECOND_PASS, table.getName(), DbUtil.getSQLExceptionString(e)); } } } // log Results log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_CLEAR_CLEARED_TABLES, modified_tables_count); log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_CLEAR_FAILED_TABLES, failed_tables_count); ret_ok = (failed_tables_count == 0); } finally { disconnect(); } return ret_ok; } /** * This is the reverse of setup - that is, it removes a schema from a database, deleting all data with it. * * @param file_name the file that contains the DBSetup XML content. * * @return <code>true</code> if all views and tables were successfully removed; <code>false</code> if at least one * failure occurred when trying remove views and tables * * @throws Exception */ public boolean uninstall(String file_name) throws Exception { boolean ret_ok = false; // assume a failure will occur int uninstalled_views = 0; int failed_views = 0; int modified_tables = 0; int failed_tables = 0; try { Document doc = readDocument(file_name); Node root_node = doc.getDocumentElement(); // Make sure we can connect to the database connect(); log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CONNECTED_TO_DB, m_jdbcUrl, m_username); // drop views first Collection<View> views = View.getViews(root_node, getDatabaseType(), this); for (View view : views) { try { String viewName = view.getName(); if (null == viewName || viewName.trim().isEmpty()) { continue; } view.drop(); uninstalled_views++; log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_DROPPED_VIEW, view.getName()); } catch (SQLException e) { failed_views++; logDropFailureIfNecessary(DbUtilsI18NResourceKeys.DBSETUP_DROPPED_VIEW_ERROR, view.getName(), e); } } View.uninstallCleanup(this); // drop tables - do so in reverse order of their creation to bypass dependency constraints List<Table> tables = Table.getTables(root_node, getDatabaseType(), this); Collections.reverse(tables); for (Table table : tables) { try { String tableName = table.getName(); if (null == tableName || tableName.trim().isEmpty()) { continue; } table.drop(); modified_tables++; log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_DROPPED_TABLE, table.getName()); } catch (SQLException e) { failed_tables++; logDropFailureIfNecessary(DbUtilsI18NResourceKeys.DBSETUP_DROPPED_TABLE_ERROR, table.getName(), e); } } Table.uninstallCleanup(this); // log results log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_UNINSTALL_DROPPED_VIEWS, uninstalled_views); log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_UNINSTALL_DROPPED_TABLES, modified_tables); log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_UNINSTALL_FAILED_VIEWS, failed_views); log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_UNINSTALL_FAILED_TABLES, failed_tables); ret_ok = ((failed_views + failed_tables) == 0); } finally { disconnect(); } return ret_ok; } // skip logging an error if the drop failure is due to the object not existing private void logDropFailureIfNecessary(String I18NKey, String objectName, SQLException e) { String sqlExceptionString = DbUtil.getSQLExceptionString(e); if (null != sqlExceptionString && sqlExceptionString.toLowerCase().contains("does not exist")) { return; } log(LogPriority.ERROR, I18NKey, objectName, sqlExceptionString); } /** * Exports an existing schema to a DBSetup XML file. * * <p><b>NOTE:</b> you cannot use the generated XML file for input into DBSetup. This exported XML file is only for * reference purposes; it is not generated in a way that can be used to recreate the DB. Use your database vendor's * backup/restore utilities to export databases for backup and recovery.</p> * * @param file the XML file that will contain the exported schema * * @throws Exception */ public void export(String file) throws Exception { int created_tables = 0; try { // Make sure we can connect to the database connect(); log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_CONNECTED_TO_DB, m_jdbcUrl, m_username); // Create the DBSetup XML file Document doc = createNewDBSetupXmlDocument(); Node warning = doc.createComment(MSG.getMsg(DbUtilsI18NResourceKeys.DBSETUP_EXPORT_WARNING_NOTICE, new Date())); Element root_element = doc.createElement(DBSETUP_ROOT_ELEMENT_NAME + "-export"); doc.appendChild(warning); root_element.setAttribute("name", file); doc.appendChild(root_element); // Find Tables Collection<Table> tables = Table.getTables(getDatabaseType(), this, m_username); for (Table table : tables) { log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_EXPORT_FOUND_TABLE, table.getName()); Element elemTab = doc.createElement("table"); elemTab.setAttribute("name", table.getName()); root_element.appendChild(elemTab); // Get Columns Iterator iterCols = table.getColumns().iterator(); while (iterCols.hasNext() == true) { Column col = (Column) iterCols.next(); log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_EXPORT_FOUND_COLUMN, table.getName(), col.getName()); Element elemChild = doc.createElement("column"); elemChild.setAttribute("name", col.getName()); elemChild.setAttribute("type", col.getType()); elemChild.setAttribute("size", String.valueOf(col.getSize())); if (col.isRequired() == true) { elemChild.setAttribute("required", String.valueOf(col.isRequired())); } elemTab.appendChild(elemChild); } // Get Data DataSet dataset = table.getDataSet(); while (dataset.next()) { Element elemChild = doc.createElement("data"); int iCols = table.getColumns().size(); for (int i = 0; i < iCols; i++) { Data data = dataset.getData(i); elemChild.setAttribute(data.getColumnName(), data.getValue()); } elemTab.appendChild(elemChild); } created_tables++; } writeDBSetupXmlDocument(doc, file); // log results log(LogPriority.INFO, DbUtilsI18NResourceKeys.DBSETUP_EXPORT_CREATED_TABLES, created_tables, file); } finally { disconnect(); } return; } /** * Creates a new XML document that will contain DBSetup content. * * @return the new document * * @throws Exception */ private Document createNewDBSetupXmlDocument() throws Exception { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setErrorHandler(new ErrorHandler()); Document docResult = builder.newDocument(); return docResult; } /** * Given a document node, writes its content into the given file. * * @param doc * @param file * * @throws Exception */ private void writeDBSetupXmlDocument(Document doc, String file) throws Exception { TransformerFactory factory = TransformerFactory.newInstance(); Transformer trans = factory.newTransformer(); trans.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes"); trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); DOMSource src = new DOMSource(doc); StreamResult result = new StreamResult(new File(file)); trans.transform(src, result); return; } /** * SAX parser error handler that will log XML problems. */ private class ErrorHandler implements org.xml.sax.ErrorHandler { /** * @see ErrorHandler#fatalError(SAXParseException) */ public void fatalError(SAXParseException e) throws SAXException { log(LogPriority.FATAL, e, DbUtilsI18NResourceKeys.DBSETUP_SAX_FATAL, e.getLineNumber(), e.getColumnNumber(), e.getMessage()); } /** * @see ErrorHandler#error(SAXParseException) */ public void error(SAXParseException e) throws SAXException { log(LogPriority.ERROR, e, DbUtilsI18NResourceKeys.DBSETUP_SAX_ERROR, e.getLineNumber(), e.getColumnNumber(), e.getMessage()); } /** * @see ErrorHandler#warning(SAXParseException) */ public void warning(SAXParseException e) throws SAXException { log(LogPriority.WARN, e, DbUtilsI18NResourceKeys.DBSETUP_SAX_WARNING, e.getLineNumber(), e.getColumnNumber(), e.getMessage()); } } /** * Executes the given SQL. * * @param sql * * @throws SQLException */ protected void doSQL(String sql) throws SQLException { this.doSQL(sql, false); } /** * This method either executes the given SQL (if <code>returnPreparedStatement</code> is <code>false</code>) or it * just prepares it in a statement and expects the caller to execute the statement. The caller must close the * returned prepared statement (which occurs if <code>returnPreparedStatement</code> is <code>true</code>) * * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>, <code>UPDATE</code> * or <code>DELETE</code>; or an SQL statement that returns nothing, * such as a DDL statement * @param returnPreparedStatement if <code>true</code>, the SQL isn't executed; it is just prepared * * @return the statement (which may be a prepared statement or may be the statement that was executed) * * @throws SQLException */ protected Statement doSQL(String sql, boolean returnPreparedStatement) throws SQLException { Statement stmt; log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_DO_SQL, sql); // Cache the original commit option boolean committing = this.getConnection().getAutoCommit(); if (committing) { this.getConnection().setAutoCommit(false); } // Currently, returnPreparedStatement is always false (this method is never called directly, just by the above // overloaded method), so we can rely on this method to track performance. // Aug 04, 2008 long start = System.currentTimeMillis(); if (returnPreparedStatement) { stmt = this.getConnection().prepareStatement(sql); } else { stmt = this.getConnection().createStatement(); try { stmt.executeUpdate(sql); this.getConnection().commit(); } catch (SQLException e) { try { this.getConnection().rollback(); } catch (Exception e2) { // Log this? } throw new ExtendedSQLException(e, sql); } finally { if (stmt != null) { stmt.close(); } } } long duration = System.currentTimeMillis() - start; // log(LogPriority.DEBUG, DbUtilsI18NResourceKeys.DBSETUP_DURATION, duration); // Reset the commit option this.getConnection().setAutoCommit(committing); return stmt; } /** * Gets the live connection that this object is using. If one hasn't been created yet, <code>null</code> is * returned. * * @return live connection */ protected Connection getConnection() { return m_connection; } /** * Gets the database type information of the database this object is currently {@link #getConnection() connected} * to. If there is not a live connection to a database, <code>null</code> is returned. * * @return database type */ protected DatabaseType getDatabaseType() { return m_databaseType; } /** * Returns the username that this object will use when connecting to the database. * * @return username */ protected String getJdbcUser() { return m_username; } /** * Returns a connection to the database. * A new connection is established if none existed before * @return the connection * * @throws Exception if failed to connect or determine the type of database that was connected to */ private Connection connect() throws Exception { if (m_connection != null) { return m_connection; } m_connection = DbUtil.getConnection(m_jdbcUrl, m_username, m_password); try { m_databaseType = DatabaseTypeFactory.getDatabaseType(m_connection); // MySQL complains if autocomit is true and you try to commit. // DDL operations are not transactional anyhow. m_connection.setAutoCommit(false); } catch (Exception e) { // what probably happened was we connected to a database that we do not support // let's close the connection and throw this exception back out try { m_connection.close(); } catch (Exception ignore) { } m_connection = null; m_databaseType = null; throw e; } return m_connection; } /** * If this object is currently connected, this will close that live connection. */ private void disconnect() { if (!m_doDisconnect) { return; } try { m_connection.close(); } catch (Exception e) { // ignore } finally { m_connection = null; m_databaseType = null; } return; } /** * Logs the given message. This will divert the message to the logger if we are not in console mode. If we are in * console mode, the message goes to stdout in the user's locale. * * @param priority the priority of the message * @param msg_key the bundle message * @param params the parameters to the bundle message placeholders */ private void log(LogPriority priority, String msg_key, Object... params) { if (m_consoleMode) { // use MSG to get the message in the user's locale System.out.println(priority.toString() + ": " + MSG.getMsg(msg_key, params)); } else { switch (priority) { case DEBUG: { LOG.debug(msg_key, params); break; } case INFO: { LOG.info(msg_key, params); break; } case WARN: { LOG.warn(msg_key, params); break; } case ERROR: { LOG.error(msg_key, params); break; } case FATAL: { LOG.fatal(msg_key, params); break; } } } return; } /** * Logs the given message with the exception. This will divert the message to the logger if we are not in console * mode. If we are in console mode, the message goes to stdout in the user's locale. * * @param priority the priority of the message * @param exception the exception to log with the message * @param msg_key the bundle message * @param params the parameters to the bundle message placeholders */ private void log(LogPriority priority, Exception exception, String msg_key, Object... params) { if (m_consoleMode) { // use MSG to get the message in the user's locale System.out.println(priority.toString() + ": " + MSG.getMsg(msg_key, params)); exception.printStackTrace(System.out); } else { switch (priority) { case DEBUG: { LOG.debug(exception, msg_key, params); break; } case INFO: { LOG.info(exception, msg_key, params); break; } case WARN: { LOG.warn(exception, msg_key, params); break; } case ERROR: { LOG.error(exception, msg_key, params); break; } case FATAL: { LOG.fatal(exception, msg_key, params); break; } } } return; } /** * Used to determine how to log a message (used for both logging and console output). */ private enum LogPriority { DEBUG, INFO, WARN, ERROR, FATAL } }