/* * Copyright (C) 2005-2009 Alfresco Software Limited. * * 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. * As a special exception to the terms and conditions of version 2.0 of * the GPL, you may redistribute this Program in connection with Free/Libre * and Open Source Software ("FLOSS") applications as described in Alfresco's * FLOSS exception. You should have received a copy of the text describing * the FLOSS exception, and it is also available here: * http://www.alfresco.com/legal/licensing" */ package org.alfresco.util.schemadump; import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.Types; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import javax.sql.DataSource; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.sax.TransformerHandler; import javax.xml.transform.stream.StreamResult; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.Oracle8iDialect; import org.hibernate.dialect.TypeNames; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; /** * Simple command line utility to help with database schema comparisons and upgrades. Dumps a database schema via JDBC * to a normalised XML form. * * @author dward */ public class Main { /** Reusable empty SAX attribute list. */ private static final Attributes EMPTY_ATTRIBUTES = new AttributesImpl(); /** Reverse map from database types to JDBC types (loaded from a Hibernate dialect). */ private final Map reverseTypeMap = new TreeMap(); /** Should we scale down string field widths (assuming 4 bytes to one character?). */ private boolean scaleCharacters; /** The JDBC connection. */ private Connection con; /** * The main method. * * @param args * the args: <context.xml> <output.xml> */ public static void main(final String[] args) throws Exception { if (args.length != 2) { System.out.println("Usage:"); System.out.println("java " + Main.class.getName() + " "); System.exit(1); } final File outputFile = new File(args[1]); new Main(args[0]).execute(outputFile); } /** * Creates a new instance of this tool by starting up a full context. */ @SuppressWarnings("unchecked") public Main(final String contextPath) throws Exception { final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "file:" + contextPath }); final DataSource datasource = (DataSource) context.getBean("dataSource"); this.con = datasource.getConnection(); // Use Java reflection to bypass accessibility rules and get hold of hibernate's type mapping! final Properties hibProps = (Properties) context.getBean("hibernateConfigProperties"); final Dialect dialect = (Dialect) Class.forName(hibProps.getProperty("hibernate.dialect")).newInstance(); // Initialize init(dialect); } /** * Create a new instance of the tool within the context of an existing database connection * * @param connection the database connection to use for metadata queries * @param dialect the Hibernate dialect */ public Main(final Connection connection, final Dialect dialect) throws Exception { this.con = connection; // Initialize init(dialect); } /** * Initializes the fields ready to perform the database metadata reading * @param dialect the Hibernate dialect */ @SuppressWarnings("unchecked") private void init(final Dialect dialect) throws Exception { this.scaleCharacters = dialect instanceof Oracle8iDialect; final Field typeNamesField = Dialect.class.getDeclaredField("typeNames"); typeNamesField.setAccessible(true); final TypeNames typeNames = (TypeNames) typeNamesField.get(dialect); final Field defaultsField = TypeNames.class.getDeclaredField("defaults"); defaultsField.setAccessible(true); final Map forwardMap2 = (Map) defaultsField.get(typeNames); for (final Map.Entry e : forwardMap2.entrySet()) { this.reverseTypeMap.put(e.getValue(), e.getKey()); } final Field weightedField = TypeNames.class.getDeclaredField("weighted"); weightedField.setAccessible(true); final Map> forwardMap1 = (Map>) weightedField .get(typeNames); for (final Map.Entry> e : forwardMap1.entrySet()) { for (final String type : e.getValue().values()) { this.reverseTypeMap.put(type, e.getKey()); } } } /** * Execute, writing the result to the given file. * * @param outputFile the file to write to */ public void execute(File outputFile) throws Exception { final NamedElementCollection result = execute(); // Set up a SAX TransformerHandler for outputting XML final SAXTransformerFactory stf = (SAXTransformerFactory) TransformerFactory.newInstance(); final TransformerHandler xmlOut = stf.newTransformerHandler(); final Transformer t = xmlOut.getTransformer(); try { t.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2"); } catch (final IllegalArgumentException e) { // It was worth a try } t.setOutputProperty(OutputKeys.INDENT, "yes"); xmlOut.setResult(new StreamResult(outputFile)); xmlOut.startDocument(); result.output(xmlOut); xmlOut.endDocument(); } /** * Execute. * * @return Returns the named XML elements */ public NamedElementCollection execute() throws Exception { final NamedElementCollection schemaCol = new NamedElementCollection("schema", "table"); final DatabaseMetaData dbmd = this.con.getMetaData(); // Assume that if there are schemas, we want the one named after the connection user or the one called "dbo" (MS // SQL hack) String schema = null; final ResultSet schemas = dbmd.getSchemas(); while (schemas.next()) { final String thisSchema = schemas.getString("TABLE_SCHEM"); if (thisSchema.equals(dbmd.getUserName()) || thisSchema.equalsIgnoreCase("dbo")) { schema = thisSchema; break; } } schemas.close(); final ResultSet tables = dbmd.getTables(null, schema, "%", new String[] { "TABLE", "VIEW" }); while (tables.next()) { final String tableName = tables.getString("TABLE_NAME"); final NamedElement tableEl = schemaCol.addNamedElement(tableName); final NamedElementCollection columnsCol = tableEl.addCollection("columns", "column"); final ResultSet columns = dbmd.getColumns(null, tables.getString("TABLE_SCHEM"), tableName, "%"); while (columns.next()) { final NamedElement columnEl = columnsCol.addNamedElement(columns.getString("COLUMN_NAME")); columnEl.addAttribute("seq", String.valueOf(columns.getInt("ORDINAL_POSITION"))); columnEl.addAttribute("type", convertToTypeName(columns.getString("TYPE_NAME"), columns .getInt("COLUMN_SIZE"), columns.getInt("DECIMAL_DIGITS"), columns.getInt("DATA_TYPE"))); columnEl.addAttribute("nullable", columns.getString("IS_NULLABLE")); } columns.close(); final ResultSet primarykeycols = dbmd.getPrimaryKeys(null, tables.getString("TABLE_SCHEM"), tableName); String primaryKeyName = null; NamedElementCollection primaryKey = null; while (primarykeycols.next()) { if (primaryKey == null) { primaryKeyName = primarykeycols.getString("PK_NAME"); primaryKey = tableEl.addCollection("primarykey", "column"); } final NamedElement pkCol = primaryKey.addNamedElement(primarykeycols.getString("COLUMN_NAME")); pkCol.addAttribute("seq", primarykeycols.getString("KEY_SEQ")); } primarykeycols.close(); final NamedElementCollection indexCol = tableEl.addCollection("indexes", "index"); final ResultSet indexes = dbmd.getIndexInfo(null, tables.getString("TABLE_SCHEM"), tableName, false, true); String lastIndexName = ""; NamedElementCollection indexCols = null; while (indexes.next()) { final String indexName = indexes.getString("INDEX_NAME"); if (indexName == null) { // Oracle seems to have some dummy index entries continue; } // Skip the index corresponding to the PK if it is mentioned else if (indexName.equals(primaryKeyName)) { continue; } if (!indexName.equals(lastIndexName)) { final NamedElement index = indexCol.addNamedElement(indexName); index.addAttribute("unique", String.valueOf(!indexes.getBoolean("NON_UNIQUE"))); indexCols = index.addCollection("columns", "column"); lastIndexName = indexName; } indexCols.addNamedElement(indexes.getString("COLUMN_NAME")); } indexes.close(); final NamedElementCollection foreignKeyCol = tableEl.addCollection("foreignkeys", "key"); final ResultSet foreignkeys = dbmd.getImportedKeys(null, tables.getString("TABLE_SCHEM"), tableName); String lastKeyName = ""; NamedElementCollection foreignKeyCols = null; while (foreignkeys.next()) { final String keyName = foreignkeys.getString("FK_NAME"); if (!keyName.equals(lastKeyName)) { final NamedElement key = foreignKeyCol.addNamedElement(keyName); foreignKeyCols = key.addCollection("columns", "column"); lastKeyName = keyName; } final NamedElement fkCol = foreignKeyCols.addNamedElement(foreignkeys.getString("FKCOLUMN_NAME")); fkCol.addAttribute("table", foreignkeys.getString("PKTABLE_NAME").toUpperCase()); fkCol.addAttribute("column", foreignkeys.getString("PKCOLUMN_NAME").toUpperCase()); } foreignkeys.close(); } tables.close(); return schemaCol; } /** * Chooses a JDBC type name, given database and JDBC type information. * * @param dbType * the db type * @param size * the size * @param digits * the number of digits * @param sqlType * the sql type * @return the string * @throws IllegalArgumentException * the illegal argument exception * @throws IllegalAccessException * the illegal access exception */ private String convertToTypeName(final String dbType, int size, final int digits, int sqlType) throws IllegalArgumentException, IllegalAccessException { // First see if the Hibernate dialect has a mapping to the database type String dbName = dbType.toLowerCase() + "(" + size + "," + digits + ")"; if (this.reverseTypeMap.containsKey(dbName)) { sqlType = this.reverseTypeMap.get(dbName); } else { dbName = dbType.toLowerCase(); if (this.reverseTypeMap.containsKey(dbName)) { sqlType = this.reverseTypeMap.get(dbName); } } final Field[] fields = Types.class.getFields(); final int modifiers = Modifier.PUBLIC | Modifier.STATIC; for (final Field field : fields) { if (field.getType().equals(int.class) && (field.getModifiers() & modifiers) == modifiers && field.getInt(null) == sqlType) { if (size == 0 || this.reverseTypeMap.containsKey(dbName) || sqlType == Types.TIMESTAMP || sqlType == Types.INTEGER) { return field.getName(); } else { // Hack to work around Oracle's byte semantics if (this.scaleCharacters && (sqlType == Types.CHAR || sqlType == Types.VARCHAR || sqlType == Types.CLOB)) { size /= 4; } return field.getName() + "(" + size + ")"; } } } return String.valueOf(sqlType); } /** * Represents a sorted collection of named elements in the XML model. */ private class NamedElementCollection { /** The collection name. */ private final String collectionName; /** The repeated element name. */ private final String elementName; /** The attributes of the collection. */ private final AttributesImpl attributes = new AttributesImpl(); /** The items in the collection. */ private final List items = new ArrayList(100); /** * The Constructor. * * @param collectionName * the collection name * @param elementName * the repeated element name */ public NamedElementCollection(final String collectionName, final String elementName) { this.collectionName = collectionName; this.elementName = elementName; } /** * Adds an attribute. * * @param name * the name * @param value * the value */ public void addAttribute(final String name, final String value) { this.attributes.addAttribute("", "", name, "CDATA", value); } /** * Adds a named element. * * @param name * the name * @return the named element */ public NamedElement addNamedElement(final String name) { final NamedElement retVal = new NamedElement(name); this.items.add(retVal); return retVal; } /** * Outputs to XML. * * @param xmlOut * the SAX content handler * @throws SAXException * the SAX exception */ public void output(final ContentHandler xmlOut) throws SAXException { xmlOut.startElement("", "", this.collectionName, this.attributes); Collections.sort(this.items); for (final NamedElement item : this.items) { item.output(xmlOut, this.elementName); } xmlOut.endElement("", "", this.collectionName); } } /** * Represents a named element in the XML model. */ private class NamedElement implements Comparable { /** The name. */ private final String name; /** The attributes. */ private final List attributes = new LinkedList(); /** The child collections. */ private final List collections = new LinkedList();; /** * Instantiates a new named element. * * @param name * the name */ public NamedElement(final String name) { this.name = name.toUpperCase(); } /** * Adds an attribute. * * @param name * the name * @param value * the value */ public void addAttribute(final String name, final String value) { this.attributes.add(new String[] { name, value }); } /** * Adds a child collection. * * @param collectionName * the collection name * @param elementName * the repeated element name * @return the named element collection */ public NamedElementCollection addCollection(final String collectionName, final String elementName) { final NamedElementCollection retVal = new NamedElementCollection(collectionName, elementName); this.collections.add(retVal); return retVal; } /** * Outputs to XML. * * @param xmlOut * the SAX content handler * @param elementName * the element name * @throws SAXException * the SAX exception */ public void output(final ContentHandler xmlOut, final String elementName) throws SAXException { final AttributesImpl attribs = new AttributesImpl(); attribs.addAttribute("", "", "name", "CDATA", this.name); xmlOut.startElement("", "", elementName, attribs); for (final String[] attrib : this.attributes) { xmlOut.startElement("", "", attrib[0], Main.EMPTY_ATTRIBUTES); final char[] chars = attrib[1].toCharArray(); xmlOut.characters(chars, 0, chars.length); xmlOut.endElement("", "", attrib[0]); } for (final NamedElementCollection coll : this.collections) { coll.output(xmlOut); } xmlOut.endElement("", "", elementName); } /* * (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(final NamedElement o) { return this.name.compareTo(o.name); } /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { if (!(obj instanceof NamedElement)) { return false; } return this.name.equals(((NamedElement) obj).name); } /* * (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return this.name.hashCode(); } } }