Java tutorial
package org.apache.ddlutils.platform; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.io.IOException; import java.io.Writer; import java.rmi.server.UID; import java.sql.Types; import java.text.DateFormat; import java.text.NumberFormat; import java.util.Iterator; import java.util.Locale; import java.util.Map; import org.apache.commons.collections.map.ListOrderedMap; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.ddlutils.DdlUtilsException; import org.apache.ddlutils.Platform; import org.apache.ddlutils.PlatformInfo; import org.apache.ddlutils.model.CascadeActionEnum; import org.apache.ddlutils.model.Column; import org.apache.ddlutils.model.Database; import org.apache.ddlutils.model.ForeignKey; import org.apache.ddlutils.model.Index; import org.apache.ddlutils.model.IndexColumn; import org.apache.ddlutils.model.ModelException; import org.apache.ddlutils.model.Table; import org.apache.ddlutils.model.TypeMap; import org.apache.ddlutils.util.StringUtilsExt; /** * This class is a collection of Strategy methods for creating the DDL required to create and drop * databases and tables. * * It is hoped that just a single implementation of this class, for each database should make creating DDL * for each physical database fairly straightforward. * * An implementation of this class can always delegate down to some templating technology such as Velocity if * it requires. Though often that can be quite complex when attempting to reuse code across many databases. * Hopefully only a small amount code needs to be changed on a per database basis. * * @version $Revision$ */ public abstract class SqlBuilder { /** The line separator for in between sql commands. */ private static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n"); /** The placeholder for the size value in the native type spec. */ protected static final String SIZE_PLACEHOLDER = "{0}"; /** The Log to which logging calls will be made. */ protected final Log _log = LogFactory.getLog(SqlBuilder.class); /** The platform that this builder belongs to. */ private Platform _platform; /** The current Writer used to output the SQL to. */ private Writer _writer; /** The indentation used to indent commands. */ private String _indent = " "; /** An optional locale specification for number and date formatting. */ private String _valueLocale; /** The date formatter. */ private DateFormat _valueDateFormat; /** The date time formatter. */ private DateFormat _valueTimeFormat; /** The number formatter. */ private NumberFormat _valueNumberFormat; /** Helper object for dealing with default values. */ private DefaultValueHelper _defaultValueHelper = new DefaultValueHelper(); /** The character sequences that need escaping. */ private Map _charSequencesToEscape = new ListOrderedMap(); // // Configuration // /** * Creates a new sql builder. * * @param platform The plaftform this builder belongs to */ public SqlBuilder(Platform platform) { _platform = platform; } /** * Returns the platform object. * * @return The platform */ public Platform getPlatform() { return _platform; } /** * Returns the platform info object. * * @return The info object */ public PlatformInfo getPlatformInfo() { return _platform.getPlatformInfo(); } /** * Returns the writer that the DDL is printed to. * * @return The writer */ public Writer getWriter() { return _writer; } /** * Sets the writer for printing the DDL to. * * @param writer The writer */ public void setWriter(Writer writer) { _writer = writer; } /** * Returns the default value helper. * * @return The default value helper */ public DefaultValueHelper getDefaultValueHelper() { return _defaultValueHelper; } /** * Returns the string used to indent the SQL. * * @return The indentation string */ public String getIndent() { return _indent; } /** * Sets the string used to indent the SQL. * * @param indent The indentation string */ public void setIndent(String indent) { _indent = indent; } /** * Returns the locale that is used for number and date formatting * (when printing default values and in generates insert/update/delete * statements). * * @return The locale or <code>null</code> if default formatting is used */ public String getValueLocale() { return _valueLocale; } /** * Sets the locale that is used for number and date formatting * (when printing default values and in generates insert/update/delete * statements). * * @param localeStr The new locale or <code>null</code> if default formatting * should be used; Format is "language[_country[_variant]]" */ public void setValueLocale(String localeStr) { if (localeStr != null) { int sepPos = localeStr.indexOf('_'); String language = null; String country = null; String variant = null; if (sepPos > 0) { language = localeStr.substring(0, sepPos); country = localeStr.substring(sepPos + 1); sepPos = country.indexOf('_'); if (sepPos > 0) { variant = country.substring(sepPos + 1); country = country.substring(0, sepPos); } } else { language = localeStr; } if (language != null) { Locale locale = null; if (variant != null) { locale = new Locale(language, country, variant); } else if (country != null) { locale = new Locale(language, country); } else { locale = new Locale(language); } _valueLocale = localeStr; setValueDateFormat(DateFormat.getDateInstance(DateFormat.SHORT, locale)); setValueTimeFormat(DateFormat.getTimeInstance(DateFormat.SHORT, locale)); setValueNumberFormat(NumberFormat.getNumberInstance(locale)); return; } } _valueLocale = null; setValueDateFormat(null); setValueTimeFormat(null); setValueNumberFormat(null); } /** * Returns the format object for formatting dates in the specified locale. * * @return The date format object or null if no locale is set */ protected DateFormat getValueDateFormat() { return _valueDateFormat; } /** * Sets the format object for formatting dates in the specified locale. * * @param format The date format object */ protected void setValueDateFormat(DateFormat format) { _valueDateFormat = format; } /** * Returns the format object for formatting times in the specified locale. * * @return The time format object or null if no locale is set */ protected DateFormat getValueTimeFormat() { return _valueTimeFormat; } /** * Sets the date format object for formatting times in the specified locale. * * @param format The time format object */ protected void setValueTimeFormat(DateFormat format) { _valueTimeFormat = format; } /** * Returns the format object for formatting numbers in the specified locale. * * @return The number format object or null if no locale is set */ protected NumberFormat getValueNumberFormat() { return _valueNumberFormat; } /** * Returns a new date format object for formatting numbers in the specified locale. * Platforms can override this if necessary. * * @param format The number format object */ protected void setValueNumberFormat(NumberFormat format) { _valueNumberFormat = format; } /** * Adds a char sequence that needs escaping, and its escaped version. * * @param charSequence The char sequence * @param escapedVersion The escaped version */ protected void addEscapedCharSequence(String charSequence, String escapedVersion) { _charSequencesToEscape.put(charSequence, escapedVersion); } /** * Returns the maximum number of characters that a table name can have. * This method is intended to give platform specific builder implementations * more control over the maximum length. * * @return The number of characters, or -1 if not limited */ public int getMaxTableNameLength() { return getPlatformInfo().getMaxTableNameLength(); } /** * Returns the maximum number of characters that a column name can have. * This method is intended to give platform specific builder implementations * more control over the maximum length. * * @return The number of characters, or -1 if not limited */ public int getMaxColumnNameLength() { return getPlatformInfo().getMaxColumnNameLength(); } /** * Returns the maximum number of characters that a constraint name can have. * This method is intended to give platform specific builder implementations * more control over the maximum length. * * @return The number of characters, or -1 if not limited */ public int getMaxConstraintNameLength() { return getPlatformInfo().getMaxConstraintNameLength(); } /** * Returns the maximum number of characters that a foreign key name can have. * This method is intended to give platform specific builder implementations * more control over the maximum length. * * @return The number of characters, or -1 if not limited */ public int getMaxForeignKeyNameLength() { return getPlatformInfo().getMaxForeignKeyNameLength(); } // // public interface // /** * Outputs the DDL required to drop and (re)create all tables in the database model. * * @param database The database model */ public void createTables(Database database) throws IOException { createTables(database, null, true); } /** * Outputs the DDL required to drop (if requested) and (re)create all tables in the database model. * * @param database The database * @param dropTables Whether to drop tables before creating them */ public void createTables(Database database, boolean dropTables) throws IOException { createTables(database, null, dropTables); } /** * Outputs the DDL required to drop (if requested) and (re)create all tables in the database model. * * @param database The database * @param params The parameters used in the creation * @param dropTables Whether to drop tables before creating them */ public void createTables(Database database, CreationParameters params, boolean dropTables) throws IOException { if (dropTables) { dropTables(database); } for (int idx = 0; idx < database.getTableCount(); idx++) { Table table = database.getTable(idx); writeTableComment(table); createTable(database, table, params == null ? null : params.getParametersFor(table)); } // we're writing the external foreignkeys last to ensure that all referenced tables are already defined createForeignKeys(database); } /** * Outputs the DDL to create the given temporary table. Per default this is simply * a call to {@link #createTable(Database, Table, Map)}. * * @param database The database model * @param table The table * @param parameters Additional platform-specific parameters for the table creation */ protected void createTemporaryTable(Database database, Table table, Map parameters) throws IOException { createTable(database, table, parameters); } /** * Outputs the DDL to drop the given temporary table. Per default this is simply * a call to {@link #dropTable(Table)}. * * @param database The database model * @param table The table */ protected void dropTemporaryTable(Database database, Table table) throws IOException { dropTable(table); } /** * Writes a statement that copies the data from the source to the target table. Note * that this copies only those columns that are in both tables. * Database-specific implementations might redefine this method though it usually * suffices to redefine the {@link #writeCastExpression(Column, Column)} method. * * @param sourceTable The source table * @param targetTable The target table */ protected void copyData(Table sourceTable, Table targetTable) throws IOException { ListOrderedMap columns = new ListOrderedMap(); for (int idx = 0; idx < sourceTable.getColumnCount(); idx++) { Column sourceColumn = sourceTable.getColumn(idx); Column targetColumn = targetTable.findColumn(sourceColumn.getName(), getPlatform().isDelimitedIdentifierModeOn()); if (targetColumn != null) { columns.put(sourceColumn, targetColumn); } } print("INSERT INTO "); printIdentifier(getTableName(targetTable)); print(" ("); for (Iterator columnIt = columns.keySet().iterator(); columnIt.hasNext();) { printIdentifier(getColumnName((Column) columnIt.next())); if (columnIt.hasNext()) { print(","); } } print(") SELECT "); for (Iterator columnsIt = columns.entrySet().iterator(); columnsIt.hasNext();) { Map.Entry entry = (Map.Entry) columnsIt.next(); writeCastExpression((Column) entry.getKey(), (Column) entry.getValue()); if (columnsIt.hasNext()) { print(","); } } print(" FROM "); printIdentifier(getTableName(sourceTable)); printEndOfStatement(); } /** * Writes a cast expression that converts the value of the source column to the data type * of the target column. Per default, simply the name of the source column is written * thereby assuming that any casts happen implicitly. * * @param sourceColumn The source column * @param targetColumn The target column */ protected void writeCastExpression(Column sourceColumn, Column targetColumn) throws IOException { printIdentifier(getColumnName(sourceColumn)); } /** * Compares the two strings. * * @param string1 The first string * @param string2 The second string * @param caseMatters Whether case matters in the comparison * @return <code>true</code> if the string are equal */ protected boolean areEqual(String string1, String string2, boolean caseMatters) { return (caseMatters && string1.equals(string2)) || (!caseMatters && string1.equalsIgnoreCase(string2)); } /** * Outputs the DDL to create the table along with any non-external constraints as well * as with external primary keys and indices (but not foreign keys). * * @param database The database model * @param table The table */ public void createTable(Database database, Table table) throws IOException { createTable(database, table, null); } /** * Outputs the DDL to create the table along with any non-external constraints as well * as with external primary keys and indices (but not foreign keys). * * @param database The database model * @param table The table * @param parameters Additional platform-specific parameters for the table creation */ public void createTable(Database database, Table table, Map parameters) throws IOException { writeTableCreationStmt(database, table, parameters); writeTableCreationStmtEnding(table, parameters); if (!getPlatformInfo().isPrimaryKeyEmbedded()) { createPrimaryKey(table, table.getPrimaryKeyColumns()); } if (!getPlatformInfo().isIndicesEmbedded()) { createIndexes(table); } } /** * Writes the primary key constraints of the table as alter table statements. * * @param table The table * @param primaryKeyColumns The primary key columns */ public void createPrimaryKey(Table table, Column[] primaryKeyColumns) throws IOException { if ((primaryKeyColumns.length > 0) && shouldGeneratePrimaryKeys(primaryKeyColumns)) { print("ALTER TABLE "); printlnIdentifier(getTableName(table)); printIndent(); print("ADD CONSTRAINT "); printIdentifier(getConstraintName(null, table, "PK", null)); print(" "); writePrimaryKeyStmt(table, primaryKeyColumns); printEndOfStatement(); } } /** * Writes the indexes for the given table using external index creation statements. * * @param table The table */ public void createIndexes(Table table) throws IOException { for (int idx = 0; idx < table.getIndexCount(); idx++) { Index index = table.getIndex(idx); if (!index.isUnique() && !getPlatformInfo().isIndicesSupported()) { throw new ModelException("Platform does not support non-unique indices"); } createIndex(table, index); } } /** * Writes the given index for the table using an external index creation statement. * * @param table The table * @param index The index */ public void createIndex(Table table, Index index) throws IOException { if (!getPlatformInfo().isIndicesSupported()) { throw new DdlUtilsException("This platform does not support indexes"); } else if (index.getName() == null) { _log.warn("Cannot write unnamed index " + index); } else { print("CREATE"); if (index.isUnique()) { print(" UNIQUE"); } print(" INDEX "); printIdentifier(getIndexName(index)); print(" ON "); printIdentifier(getTableName(table)); print(" ("); for (int idx = 0; idx < index.getColumnCount(); idx++) { IndexColumn idxColumn = index.getColumn(idx); Column col = table.findColumn(idxColumn.getName()); if (col == null) { // would get null pointer on next line anyway, so throw exception throw new ModelException("Invalid column '" + idxColumn.getName() + "' on index " + index.getName() + " for table " + table.getName()); } if (idx > 0) { print(", "); } printIdentifier(getColumnName(col)); } print(")"); printEndOfStatement(); } } /** * Creates the external foreignkey creation statements for all tables in the database. * * @param database The database */ public void createForeignKeys(Database database) throws IOException { for (int idx = 0; idx < database.getTableCount(); idx++) { createForeignKeys(database, database.getTable(idx)); } } /** * Creates external foreignkey creation statements if necessary. * * @param database The database model * @param table The table */ public void createForeignKeys(Database database, Table table) throws IOException { for (int idx = 0; idx < table.getForeignKeyCount(); idx++) { createForeignKey(database, table, table.getForeignKey(idx)); } } /** * Writes a single foreign key constraint using a alter table statement. * * @param database The database model * @param table The table * @param foreignKey The foreign key */ public void createForeignKey(Database database, Table table, ForeignKey foreignKey) throws IOException { if (getPlatformInfo().isForeignKeysEmbedded()) { throw new DdlUtilsException("This platform does not supported the external creation of foreign keys"); } else if (foreignKey.getForeignTableName() == null) { _log.warn("Foreign key table is null for key " + foreignKey); } else { writeTableAlterStmt(table); print("ADD CONSTRAINT "); printIdentifier(getForeignKeyName(table, foreignKey)); print(" FOREIGN KEY ("); writeLocalReferences(foreignKey); print(") REFERENCES "); printIdentifier(getTableName(database.findTable(foreignKey.getForeignTableName()))); print(" ("); writeForeignReferences(foreignKey); print(")"); writeForeignKeyOnDeleteAction(table, foreignKey); writeForeignKeyOnUpdateAction(table, foreignKey); printEndOfStatement(); } } /** * Prints the SQL for adding a column to a table. * * @param model The database model * @param table The table * @param newColumn The new column */ public void addColumn(Database model, Table table, Column newColumn) throws IOException { print("ALTER TABLE "); printlnIdentifier(getTableName(table)); printIndent(); print("ADD COLUMN "); writeColumn(table, newColumn); printEndOfStatement(); } /** * Outputs the DDL required to drop the database. * * @param database The database */ public void dropTables(Database database) throws IOException { // we're dropping the external foreignkeys first for (int idx = database.getTableCount() - 1; idx >= 0; idx--) { Table table = database.getTable(idx); if ((table.getName() != null) && (table.getName().length() > 0)) { dropForeignKeys(table); } } // Next we drop the tables in reverse order to avoid referencial problems // TODO: It might be more useful to either (or both) // * determine an order in which the tables can be dropped safely (via the foreignkeys) // * alter the tables first to drop the internal foreignkeys for (int idx = database.getTableCount() - 1; idx >= 0; idx--) { Table table = database.getTable(idx); if ((table.getName() != null) && (table.getName().length() > 0)) { writeTableComment(table); dropTable(table); } } } /** * Outputs the DDL required to drop the given table. This method also * drops foreign keys to the table. * * @param database The database * @param table The table */ public void dropTable(Database database, Table table) throws IOException { // we're dropping the foreignkeys to the table first for (int idx = database.getTableCount() - 1; idx >= 0; idx--) { Table otherTable = database.getTable(idx); ForeignKey[] fks = otherTable.getForeignKeys(); for (int fkIdx = 0; (fks != null) && (fkIdx < fks.length); fkIdx++) { if (fks[fkIdx].getForeignTable().equals(table)) { dropForeignKey(otherTable, fks[fkIdx]); } } } // and the foreign keys from the table dropForeignKeys(table); writeTableComment(table); dropTable(table); } /** * Outputs the DDL to drop the table. Note that this method does not drop * foreign keys to this table. Use {@link #dropTable(Database, Table)} * if you want that. * * @param table The table to drop */ public void dropTable(Table table) throws IOException { print("DROP TABLE "); printIdentifier(getTableName(table)); printEndOfStatement(); } /** * Creates external foreignkey drop statements. * * @param table The table */ public void dropForeignKeys(Table table) throws IOException { for (int idx = 0; idx < table.getForeignKeyCount(); idx++) { dropForeignKey(table, table.getForeignKey(idx)); } } /** * Generates the statement to drop a foreignkey constraint from the database using an * alter table statement. * * @param table The table * @param foreignKey The foreign key */ public void dropForeignKey(Table table, ForeignKey foreignKey) throws IOException { writeTableAlterStmt(table); print("DROP CONSTRAINT "); printIdentifier(getForeignKeyName(table, foreignKey)); printEndOfStatement(); } /** * Creates the SQL for inserting an object into the specified table. * If values are given then a concrete insert statement is created, otherwise an * insert statement usable in a prepared statement is build. * * @param table The table * @param columnValues The columns values indexed by the column names * @param genPlaceholders Whether to generate value placeholders for a * prepared statement * @return The insertion sql */ public String getInsertSql(Table table, Map columnValues, boolean genPlaceholders) { StringBuffer buffer = new StringBuffer("INSERT INTO "); boolean addComma = false; buffer.append(getDelimitedIdentifier(getTableName(table))); buffer.append(" ("); for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (columnValues.containsKey(column.getName())) { if (addComma) { buffer.append(", "); } buffer.append(getDelimitedIdentifier(column.getName())); addComma = true; } } buffer.append(") VALUES ("); if (genPlaceholders) { addComma = false; for (int idx = 0; idx < columnValues.size(); idx++) { if (addComma) { buffer.append(", "); } buffer.append("?"); addComma = true; } } else { addComma = false; for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (columnValues.containsKey(column.getName())) { if (addComma) { buffer.append(", "); } buffer.append(getValueAsString(column, columnValues.get(column.getName()))); addComma = true; } } } buffer.append(")"); return buffer.toString(); } /** * Creates the SQL for updating an object in the specified table. * If values are given then a concrete update statement is created, otherwise an * update statement usable in a prepared statement is build. * * @param table The table * @param columnValues Contains the values for the columns to update, and should also * contain the primary key values to identify the object to update * in case <code>genPlaceholders</code> is <code>false</code> * @param genPlaceholders Whether to generate value placeholders for a * prepared statement (both for the pk values and the object values) * @return The update sql */ public String getUpdateSql(Table table, Map columnValues, boolean genPlaceholders) { StringBuffer buffer = new StringBuffer("UPDATE "); boolean addSep = false; buffer.append(getDelimitedIdentifier(getTableName(table))); buffer.append(" SET "); for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (!column.isPrimaryKey() && columnValues.containsKey(column.getName())) { if (addSep) { buffer.append(", "); } buffer.append(getDelimitedIdentifier(column.getName())); buffer.append(" = "); if (genPlaceholders) { buffer.append("?"); } else { buffer.append(getValueAsString(column, columnValues.get(column.getName()))); } addSep = true; } } buffer.append(" WHERE "); addSep = false; for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (column.isPrimaryKey() && columnValues.containsKey(column.getName())) { if (addSep) { buffer.append(" AND "); } buffer.append(getDelimitedIdentifier(column.getName())); buffer.append(" = "); if (genPlaceholders) { buffer.append("?"); } else { buffer.append(getValueAsString(column, columnValues.get(column.getName()))); } addSep = true; } } return buffer.toString(); } /** * Creates the SQL for updating an object in the specified table. * If values are given then a concrete update statement is created, otherwise an * update statement usable in a prepared statement is build. * * @param table The table * @param oldColumnValues Contains the column values to identify the row to update * @param newColumnValues Contains the values for the columns to update * @param genPlaceholders Whether to generate value placeholders for a * prepared statement (both for the pk values and the object values) * @return The update sql */ public String getUpdateSql(Table table, Map oldColumnValues, Map newColumnValues, boolean genPlaceholders) { StringBuffer buffer = new StringBuffer("UPDATE "); boolean addSep = false; buffer.append(getDelimitedIdentifier(getTableName(table))); buffer.append(" SET "); for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (newColumnValues.containsKey(column.getName())) { if (addSep) { buffer.append(", "); } buffer.append(getDelimitedIdentifier(column.getName())); buffer.append(" = "); if (genPlaceholders) { buffer.append("?"); } else { buffer.append(getValueAsString(column, newColumnValues.get(column.getName()))); } addSep = true; } } buffer.append(" WHERE "); addSep = false; for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (oldColumnValues.containsKey(column.getName())) { if (addSep) { buffer.append(" AND "); } buffer.append(getDelimitedIdentifier(column.getName())); buffer.append(" = "); if (genPlaceholders) { buffer.append("?"); } else { buffer.append(getValueAsString(column, oldColumnValues.get(column.getName()))); } addSep = true; } } return buffer.toString(); } /** * Creates the SQL for deleting an object from the specified table. Depending on * the value of <code>genPlaceholders</code>, the generated SQL will contain * prepared statement place holders or concrete values. Only those primary key * columns wil be used that are present in the given map. If the map is null or * completely empty, then the SQL will not have a WHERE clause. The SQL will contain * the columns in the order defined in the table. * * @param table The table * @param pkValues The primary key columns to use, and optionally their values * @param genPlaceholders Whether to generate value placeholders for a * prepared statement * @return The delete sql */ public String getDeleteSql(Table table, Map pkValues, boolean genPlaceholders) { StringBuffer buffer = new StringBuffer("DELETE FROM "); boolean addSep = false; buffer.append(getDelimitedIdentifier(getTableName(table))); if ((pkValues != null) && !pkValues.isEmpty()) { buffer.append(" WHERE "); Column[] pkCols = table.getPrimaryKeyColumns(); for (int pkColIdx = 0; pkColIdx < pkCols.length; pkColIdx++) { Column column = pkCols[pkColIdx]; if (pkValues.containsKey(column.getName())) { if (addSep) { buffer.append(" AND "); } buffer.append(getDelimitedIdentifier(column.getName())); buffer.append(" = "); if (genPlaceholders) { buffer.append("?"); } else { buffer.append(getValueAsString(column, pkValues.get(column.getName()))); } addSep = true; } } } return buffer.toString(); } /** * Generates the string representation of the given value. * * @param column The column * @param value The value * @return The string representation */ protected String getValueAsString(Column column, Object value) { if (value == null) { return "NULL"; } StringBuffer result = new StringBuffer(); // TODO: Handle binary types (BINARY, VARBINARY, LONGVARBINARY, BLOB) switch (column.getTypeCode()) { case Types.DATE: result.append(getPlatformInfo().getValueQuoteToken()); if (!(value instanceof String) && (getValueDateFormat() != null)) { // TODO: Can the format method handle java.sql.Date properly ? result.append(getValueDateFormat().format(value)); } else { result.append(value.toString()); } result.append(getPlatformInfo().getValueQuoteToken()); break; case Types.TIME: result.append(getPlatformInfo().getValueQuoteToken()); if (!(value instanceof String) && (getValueTimeFormat() != null)) { // TODO: Can the format method handle java.sql.Date properly ? result.append(getValueTimeFormat().format(value)); } else { result.append(value.toString()); } result.append(getPlatformInfo().getValueQuoteToken()); break; case Types.TIMESTAMP: result.append(getPlatformInfo().getValueQuoteToken()); // TODO: SimpleDateFormat does not support nano seconds so we would // need a custom date formatter for timestamps result.append(value.toString()); result.append(getPlatformInfo().getValueQuoteToken()); break; case Types.REAL: case Types.NUMERIC: case Types.FLOAT: case Types.DOUBLE: case Types.DECIMAL: result.append(getPlatformInfo().getValueQuoteToken()); if (!(value instanceof String) && (getValueNumberFormat() != null)) { result.append(getValueNumberFormat().format(value)); } else { result.append(value.toString()); } result.append(getPlatformInfo().getValueQuoteToken()); break; default: result.append(getPlatformInfo().getValueQuoteToken()); result.append(escapeStringValue(value.toString())); result.append(getPlatformInfo().getValueQuoteToken()); break; } return result.toString(); } /** * Generates the SQL for querying the id that was created in the last insertion * operation. This is obviously only useful for pk fields that are auto-incrementing. * A database that does not support this, will return <code>null</code>. * * @param table The table * @return The sql, or <code>null</code> if the database does not support this */ public String getSelectLastIdentityValues(Table table) { // No default possible as the databases are quite different in this respect return null; } // // implementation methods that may be overridden by specific database builders // /** * Generates a version of the name that has at most the specified * length. * * @param name The original name * @param desiredLength The desired maximum length * @return The shortened version */ public String shortenName(String name, int desiredLength) { if (name == null) { return null; } // TODO: Find an algorithm that generates unique names int originalLength = name.length(); if ((desiredLength <= 0) || (originalLength <= desiredLength)) { return name; } int delta = originalLength - desiredLength; int startCut = desiredLength / 2; StringBuffer result = new StringBuffer(); result.append(name.substring(0, startCut)); if (((startCut == 0) || (name.charAt(startCut - 1) != '_')) && ((startCut + delta + 1 == originalLength) || (name.charAt(startCut + delta + 1) != '_'))) { // just to make sure that there isn't already a '_' right before or right // after the cutting place (which would look odd with an aditional one) result.append("_"); } result.append(name.substring(startCut + delta + 1, originalLength)); return result.toString(); } /** * Returns the table name. This method takes care of length limitations imposed by some databases. * * @param table The table * @return The table name */ public String getTableName(Table table) { return shortenName(table.getName(), getMaxTableNameLength()); } /** * Outputs a comment for the table. * * @param table The table */ protected void writeTableComment(Table table) throws IOException { printComment("-----------------------------------------------------------------------"); printComment(getTableName(table)); printComment("-----------------------------------------------------------------------"); println(); } /** * Generates the first part of the ALTER TABLE statement including the * table name. * * @param table The table being altered */ protected void writeTableAlterStmt(Table table) throws IOException { print("ALTER TABLE "); printlnIdentifier(getTableName(table)); printIndent(); } /** * Writes the table creation statement without the statement end. * * @param database The model * @param table The table * @param parameters Additional platform-specific parameters for the table creation */ protected void writeTableCreationStmt(Database database, Table table, Map parameters) throws IOException { print("CREATE TABLE "); printlnIdentifier(getTableName(table)); println("("); writeColumns(table); if (getPlatformInfo().isPrimaryKeyEmbedded()) { writeEmbeddedPrimaryKeysStmt(table); } if (getPlatformInfo().isForeignKeysEmbedded()) { writeEmbeddedForeignKeysStmt(database, table); } if (getPlatformInfo().isIndicesEmbedded()) { writeEmbeddedIndicesStmt(table); } println(); print(")"); } /** * Writes the end of the table creation statement. Per default, * only the end of the statement is written, but this can be changed * in subclasses. * * @param table The table * @param parameters Additional platform-specific parameters for the table creation */ protected void writeTableCreationStmtEnding(Table table, Map parameters) throws IOException { printEndOfStatement(); } /** * Writes the columns of the given table. * * @param table The table */ protected void writeColumns(Table table) throws IOException { for (int idx = 0; idx < table.getColumnCount(); idx++) { printIndent(); writeColumn(table, table.getColumn(idx)); if (idx < table.getColumnCount() - 1) { println(","); } } } /** * Returns the column name. This method takes care of length limitations imposed by some databases. * * @param column The column * @return The column name */ protected String getColumnName(Column column) throws IOException { return shortenName(column.getName(), getMaxColumnNameLength()); } /** * Outputs the DDL for the specified column. * * @param table The table containing the column * @param column The column */ protected void writeColumn(Table table, Column column) throws IOException { //see comments in columnsDiffer about null/"" defaults printIdentifier(getColumnName(column)); print(" "); print(getSqlType(column)); writeColumnDefaultValueStmt(table, column); if (column.isRequired()) { print(" "); writeColumnNotNullableStmt(); } else if (getPlatformInfo().isNullAsDefaultValueRequired() && getPlatformInfo().hasNullDefault(column.getTypeCode())) { print(" "); writeColumnNullableStmt(); } if (column.isAutoIncrement() && !getPlatformInfo().isDefaultValueUsedForIdentitySpec()) { if (!getPlatformInfo().isNonPrimaryKeyIdentityColumnsSupported() && !column.isPrimaryKey()) { throw new ModelException("Column " + column.getName() + " in table " + table.getName() + " is auto-incrementing but not a primary key column, which is not supported by the platform"); } print(" "); writeColumnAutoIncrementStmt(table, column); } } /** * Returns the full SQL type specification (including size and precision/scale) for the * given column. * * @param column The column * @return The full SQL type string including the size */ protected String getSqlType(Column column) { return getSqlType(column, getNativeType(column)); } /** * Returns the full SQL type specification (including size and precision/scale) for the * given column. * * @param column The column * @param nativeType Overrides the native type of the column; can include the size placeholder * @return The full SQL type string including the size */ protected String getSqlType(Column column, String nativeType) { int sizePos = nativeType.indexOf(SIZE_PLACEHOLDER); StringBuffer sqlType = new StringBuffer(); sqlType.append(sizePos >= 0 ? nativeType.substring(0, sizePos) : nativeType); String sizeSpec = getSizeSpec(column); if (!StringUtilsExt.isEmpty(sizeSpec)) { sqlType.append("("); sqlType.append(sizeSpec); sqlType.append(")"); } sqlType.append(sizePos >= 0 ? nativeType.substring(sizePos + SIZE_PLACEHOLDER.length()) : ""); return sqlType.toString(); } /** * Returns the database-native type for the given column. * * @param column The column * @return The native type */ protected String getNativeType(Column column) { String nativeType = (String) getPlatformInfo().getNativeType(column.getTypeCode()); return nativeType == null ? column.getType() : nativeType; } /** * Returns the bare database-native type for the given column without any size specifies. * * @param column The column * @return The native type */ protected String getBareNativeType(Column column) { String nativeType = getNativeType(column); int sizePos = nativeType.indexOf(SIZE_PLACEHOLDER); return sizePos >= 0 ? nativeType.substring(0, sizePos) : nativeType; } /** * Returns the size specification for the given column. If the column is of a type that has size * or precision and scale, and no size is defined for the column itself, then the default size * or precision/scale for that type and platform is used instead. * * @param column The column * @return The size spec */ protected String getSizeSpec(Column column) { StringBuffer result = new StringBuffer(); Object sizeSpec = column.getSize(); if (sizeSpec == null) { sizeSpec = getPlatformInfo().getDefaultSize(column.getTypeCode()); } if (sizeSpec != null) { if (getPlatformInfo().hasSize(column.getTypeCode())) { result.append(sizeSpec.toString()); } else if (getPlatformInfo().hasPrecisionAndScale(column.getTypeCode())) { result.append(column.getSizeAsInt()); result.append(","); result.append(column.getScale()); } } return result.toString(); } /** * Returns the native default value for the column. * * @param column The column * @return The native default value */ protected String getNativeDefaultValue(Column column) { return column.getDefaultValue(); } /** * Escapes the necessary characters in given string value. * * @param value The value * @return The corresponding string with the special characters properly escaped */ protected String escapeStringValue(String value) { String result = value; for (Iterator it = _charSequencesToEscape.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); result = StringUtils.replace(result, (String) entry.getKey(), (String) entry.getValue()); } return result; } /** * Determines whether the given default spec is a non-empty spec that shall be used in a DEFAULT * expression. E.g. if the spec is an empty string and the type is a numeric type, then it is * no valid default value whereas if it is a string type, then it is valid. * * @param defaultSpec The default value spec * @param typeCode The JDBC type code * @return <code>true</code> if the default value spec is valid */ protected boolean isValidDefaultValue(String defaultSpec, int typeCode) { return (defaultSpec != null) && ((defaultSpec.length() > 0) || (!TypeMap.isNumericType(typeCode) && !TypeMap.isDateTimeType(typeCode))); } /** * Prints the default value stmt part for the column. * * @param table The table * @param column The column */ protected void writeColumnDefaultValueStmt(Table table, Column column) throws IOException { Object parsedDefault = column.getParsedDefaultValue(); if (parsedDefault != null) { if (!getPlatformInfo().isDefaultValuesForLongTypesSupported() && ((column.getTypeCode() == Types.LONGVARBINARY) || (column.getTypeCode() == Types.LONGVARCHAR))) { throw new ModelException( "The platform does not support default values for LONGVARCHAR or LONGVARBINARY columns"); } // we write empty default value strings only if the type is not a numeric or date/time type if (isValidDefaultValue(column.getDefaultValue(), column.getTypeCode())) { print(" DEFAULT "); writeColumnDefaultValue(table, column); } } else if (getPlatformInfo().isDefaultValueUsedForIdentitySpec() && column.isAutoIncrement()) { print(" DEFAULT "); writeColumnDefaultValue(table, column); } } /** * Prints the default value of the column. * * @param table The table * @param column The column */ protected void writeColumnDefaultValue(Table table, Column column) throws IOException { printDefaultValue(getNativeDefaultValue(column), column.getTypeCode()); } /** * Prints the default value of the column. * * @param defaultValue The default value * @param typeCode The type code to write the default value for */ protected void printDefaultValue(Object defaultValue, int typeCode) throws IOException { if (defaultValue != null) { boolean shouldUseQuotes = !TypeMap.isNumericType(typeCode); if (shouldUseQuotes) { // characters are only escaped when within a string literal print(getPlatformInfo().getValueQuoteToken()); print(escapeStringValue(defaultValue.toString())); print(getPlatformInfo().getValueQuoteToken()); } else { print(defaultValue.toString()); } } } /** * Prints that the column is an auto increment column. * * @param table The table * @param column The column */ protected void writeColumnAutoIncrementStmt(Table table, Column column) throws IOException { print("IDENTITY"); } /** * Prints that a column is nullable. */ protected void writeColumnNullableStmt() throws IOException { print("NULL"); } /** * Prints that a column is not nullable. */ protected void writeColumnNotNullableStmt() throws IOException { print("NOT NULL"); } /** * Compares the current column in the database with the desired one. * Type, nullability, size, scale, default value, and precision radix are * the attributes checked. Currently default values are compared, and * null and empty string are considered equal. * * @param currentColumn The current column as it is in the database * @param desiredColumn The desired column * @return <code>true</code> if the column specifications differ */ protected boolean columnsDiffer(Column currentColumn, Column desiredColumn) { //The createColumn method leaves off the default clause if column.getDefaultValue() //is null. mySQL interprets this as a default of "" or 0, and thus the columns //are always different according to this method. alterDatabase will generate //an alter statement for the column, but it will be the exact same definition //as before. In order to avoid this situation I am ignoring the comparison //if the desired default is null. In order to "un-default" a column you'll //have to have a default="" or default="0" in the schema xml. //If this is bad for other databases, it is recommended that the createColumn //method use a "DEFAULT NULL" statement if that is what is needed. //A good way to get this would be to require a defaultValue="<NULL>" in the //schema xml if you really want null and not just unspecified. String desiredDefault = desiredColumn.getDefaultValue(); String currentDefault = currentColumn.getDefaultValue(); boolean defaultsEqual = (desiredDefault == null) || desiredDefault.equals(currentDefault); boolean sizeMatters = getPlatformInfo().hasSize(currentColumn.getTypeCode()) && (desiredColumn.getSize() != null); // We're comparing the jdbc type that corresponds to the native type for the // desired type, in order to avoid repeated altering of a perfectly valid column if ((getPlatformInfo().getTargetJdbcType(desiredColumn.getTypeCode()) != currentColumn.getTypeCode()) || (desiredColumn.isRequired() != currentColumn.isRequired()) || (sizeMatters && !StringUtils.equals(desiredColumn.getSize(), currentColumn.getSize())) || !defaultsEqual) { return true; } else { return false; } } /** * Returns the name to be used for the given foreign key. If the foreign key has no * specified name, this method determines a unique name for it. The name will also * be shortened to honor the maximum identifier length imposed by the platform. * * @param table The table for whith the foreign key is defined * @param fk The foreign key * @return The name */ public String getForeignKeyName(Table table, ForeignKey fk) { String fkName = fk.getName(); boolean needsName = (fkName == null) || (fkName.length() == 0); if (needsName) { StringBuffer name = new StringBuffer(); for (int idx = 0; idx < fk.getReferenceCount(); idx++) { name.append(fk.getReference(idx).getLocalColumnName()); name.append("_"); } name.append(fk.getForeignTableName()); fkName = getConstraintName(null, table, "FK", name.toString()); } fkName = shortenName(fkName, getMaxForeignKeyNameLength()); if (needsName) { _log.warn("Encountered a foreign key in table " + table.getName() + " that has no name. " + "DdlUtils will use the auto-generated and shortened name " + fkName + " instead."); } return fkName; } /** * Returns the constraint name. This method takes care of length limitations imposed by some databases. * * @param prefix The constraint prefix, can be <code>null</code> * @param table The table that the constraint belongs to * @param secondPart The second name part, e.g. the name of the constraint column * @param suffix The constraint suffix, e.g. a counter (can be <code>null</code>) * @return The constraint name */ public String getConstraintName(String prefix, Table table, String secondPart, String suffix) { StringBuffer result = new StringBuffer(); if (prefix != null) { result.append(prefix); result.append("_"); } result.append(table.getName()); result.append("_"); result.append(secondPart); if (suffix != null) { result.append("_"); result.append(suffix); } return shortenName(result.toString(), getMaxConstraintNameLength()); } /** * Writes the primary key constraints of the table inside its definition. * * @param table The table */ protected void writeEmbeddedPrimaryKeysStmt(Table table) throws IOException { Column[] primaryKeyColumns = table.getPrimaryKeyColumns(); if ((primaryKeyColumns.length > 0) && shouldGeneratePrimaryKeys(primaryKeyColumns)) { printStartOfEmbeddedStatement(); writePrimaryKeyStmt(table, primaryKeyColumns); } } /** * Determines whether we should generate a primary key constraint for the given * primary key columns. * * @param primaryKeyColumns The pk columns * @return <code>true</code> if a pk statement should be generated for the columns */ protected boolean shouldGeneratePrimaryKeys(Column[] primaryKeyColumns) { return true; } /** * Writes a primary key statement for the given columns. * * @param table The table * @param primaryKeyColumns The primary columns */ protected void writePrimaryKeyStmt(Table table, Column[] primaryKeyColumns) throws IOException { print("PRIMARY KEY ("); for (int idx = 0; idx < primaryKeyColumns.length; idx++) { printIdentifier(getColumnName(primaryKeyColumns[idx])); if (idx < primaryKeyColumns.length - 1) { print(", "); } } print(")"); } /** * Returns the index name. This method takes care of length limitations imposed by some databases. * * @param index The index * @return The index name */ public String getIndexName(Index index) { return shortenName(index.getName(), getMaxConstraintNameLength()); } /** * Writes the indexes embedded within the create table statement. * * @param table The table */ protected void writeEmbeddedIndicesStmt(Table table) throws IOException { if (getPlatformInfo().isIndicesSupported()) { for (int idx = 0; idx < table.getIndexCount(); idx++) { printStartOfEmbeddedStatement(); writeEmbeddedIndexCreateStmt(table, table.getIndex(idx)); } } } /** * Writes the given embedded index of the table. * * @param table The table * @param index The index */ protected void writeEmbeddedIndexCreateStmt(Table table, Index index) throws IOException { if ((index.getName() != null) && (index.getName().length() > 0)) { print(" CONSTRAINT "); printIdentifier(getIndexName(index)); } if (index.isUnique()) { print(" UNIQUE"); } else { print(" INDEX "); } print(" ("); for (int idx = 0; idx < index.getColumnCount(); idx++) { IndexColumn idxColumn = index.getColumn(idx); Column col = table.findColumn(idxColumn.getName()); if (col == null) { // would get null pointer on next line anyway, so throw exception throw new ModelException("Invalid column '" + idxColumn.getName() + "' on index " + index.getName() + " for table " + table.getName()); } if (idx > 0) { print(", "); } printIdentifier(getColumnName(col)); } print(")"); } /** * Generates the statement to drop a non-embedded index from the database. * * @param table The table the index is on * @param index The index to drop */ public void dropIndex(Table table, Index index) throws IOException { if (getPlatformInfo().isAlterTableForDropUsed()) { writeTableAlterStmt(table); } print("DROP INDEX "); printIdentifier(getIndexName(index)); if (!getPlatformInfo().isAlterTableForDropUsed()) { print(" ON "); printIdentifier(getTableName(table)); } printEndOfStatement(); } /** * Writes the foreign key constraints inside a create table () clause. * * @param database The database model * @param table The table */ protected void writeEmbeddedForeignKeysStmt(Database database, Table table) throws IOException { for (int idx = 0; idx < table.getForeignKeyCount(); idx++) { ForeignKey foreignKey = table.getForeignKey(idx); if (foreignKey.getForeignTableName() == null) { _log.warn("Foreign key table is null for key " + foreignKey); } else { printStartOfEmbeddedStatement(); if (getPlatformInfo().isEmbeddedForeignKeysNamed()) { print("CONSTRAINT "); printIdentifier(getForeignKeyName(table, foreignKey)); print(" "); } print("FOREIGN KEY ("); writeLocalReferences(foreignKey); print(") REFERENCES "); printIdentifier(getTableName(database.findTable(foreignKey.getForeignTableName()))); print(" ("); writeForeignReferences(foreignKey); print(")"); writeForeignKeyOnDeleteAction(table, foreignKey); writeForeignKeyOnUpdateAction(table, foreignKey); } } } /** * Writes a list of local references for the given foreign key. * * @param key The foreign key */ protected void writeLocalReferences(ForeignKey key) throws IOException { for (int idx = 0; idx < key.getReferenceCount(); idx++) { if (idx > 0) { print(", "); } printIdentifier(key.getReference(idx).getLocalColumnName()); } } /** * Writes a list of foreign references for the given foreign key. * * @param key The foreign key */ protected void writeForeignReferences(ForeignKey key) throws IOException { for (int idx = 0; idx < key.getReferenceCount(); idx++) { if (idx > 0) { print(", "); } printIdentifier(key.getReference(idx).getForeignColumnName()); } } /** * Writes the onDelete action for the given foreign key. * * @param table The table * @param foreignKey The foreignkey */ protected void writeForeignKeyOnDeleteAction(Table table, ForeignKey foreignKey) throws IOException { CascadeActionEnum action = foreignKey.getOnDelete(); if (!getPlatformInfo().isActionSupportedForOnDelete(action)) { if (getPlatform().isDefaultOnDeleteActionUsedIfUnsupported()) { _log.info("The platform does not support the " + action + " action for onDelete; using " + getPlatformInfo().getDefaultOnDeleteAction() + " instead"); action = getPlatformInfo().getDefaultOnDeleteAction(); } else { throw new ModelException("The platform does not support the action '" + action + "' for onDelete in foreign key in table " + table.getName()); } } if (action != getPlatformInfo().getDefaultOnDeleteAction()) { print(" ON DELETE "); switch (action.getValue()) { case CascadeActionEnum.VALUE_CASCADE: print("CASCADE"); break; case CascadeActionEnum.VALUE_SET_NULL: print("SET NULL"); break; case CascadeActionEnum.VALUE_SET_DEFAULT: print("SET DEFAULT"); break; case CascadeActionEnum.VALUE_RESTRICT: print("RESTRICT"); break; case CascadeActionEnum.VALUE_NONE: print("NO ACTION"); break; default: throw new ModelException("Unsupported cascade value '" + action + "' for onDelete in foreign key in table " + table.getName()); } } } /** * Writes the onDelete action for the given foreign key. * * @param table The table * @param foreignKey The foreignkey */ protected void writeForeignKeyOnUpdateAction(Table table, ForeignKey foreignKey) throws IOException { CascadeActionEnum action = foreignKey.getOnUpdate(); if (!getPlatformInfo().isActionSupportedForOnUpdate(action)) { if (getPlatform().isDefaultOnUpdateActionUsedIfUnsupported()) { _log.info("The platform does not support the " + action + " action for onUpdate; using " + getPlatformInfo().getDefaultOnUpdateAction() + " instead"); action = getPlatformInfo().getDefaultOnUpdateAction(); } else { throw new ModelException("The platform does not support the action '" + action + "' for onUpdate in foreign key in table " + table.getName()); } } if (action != getPlatformInfo().getDefaultOnUpdateAction()) { print(" ON UPDATE "); switch (action.getValue()) { case CascadeActionEnum.VALUE_CASCADE: print("CASCADE"); break; case CascadeActionEnum.VALUE_SET_NULL: print("SET NULL"); break; case CascadeActionEnum.VALUE_SET_DEFAULT: print("SET DEFAULT"); break; case CascadeActionEnum.VALUE_RESTRICT: print("RESTRICT"); break; case CascadeActionEnum.VALUE_NONE: print("NO ACTION"); break; default: throw new ModelException("Unsupported cascade value '" + action + "' for onUpdate in foreign key in table " + table.getName()); } } } // // Helper methods // /** * Prints an SQL comment to the current stream. * * @param text The comment text */ protected void printComment(String text) throws IOException { if (getPlatform().isSqlCommentsOn()) { print(getPlatformInfo().getCommentPrefix()); // Some databases insist on a space after the prefix print(" "); print(text); print(" "); print(getPlatformInfo().getCommentSuffix()); println(); } } /** * Prints the start of an embedded statement. */ protected void printStartOfEmbeddedStatement() throws IOException { println(","); printIndent(); } /** * Prints the end of statement text, which is typically a semi colon followed by * a carriage return. */ protected void printEndOfStatement() throws IOException { // TODO: It might make sense to use a special writer which stores the individual // statements separately (the end of a statement is identified by this method) println(getPlatformInfo().getSqlCommandDelimiter()); println(); } /** * Prints a newline. */ protected void println() throws IOException { print(LINE_SEPARATOR); } /** * Prints some text. * * @param text The text to print */ protected void print(String text) throws IOException { _writer.write(text); } /** * Returns the delimited version of the identifier (if configured). * * @param identifier The identifier * @return The delimited version of the identifier unless the platform is configured * to use undelimited identifiers; in that case, the identifier is returned unchanged */ protected String getDelimitedIdentifier(String identifier) { if (getPlatform().isDelimitedIdentifierModeOn()) { return getPlatformInfo().getDelimiterToken() + identifier + getPlatformInfo().getDelimiterToken(); } else { return identifier; } } /** * Prints the given identifier. For most databases, this will * be a delimited identifier. * * @param identifier The identifier */ protected void printIdentifier(String identifier) throws IOException { print(getDelimitedIdentifier(identifier)); } /** * Prints the given identifier followed by a newline. For most databases, this will * be a delimited identifier. * * @param identifier The identifier */ protected void printlnIdentifier(String identifier) throws IOException { println(getDelimitedIdentifier(identifier)); } /** * Prints some text followed by a newline. * * @param text The text to print */ protected void println(String text) throws IOException { print(text); println(); } /** * Prints the characters used to indent SQL. */ protected void printIndent() throws IOException { print(getIndent()); } /** * Creates a reasonably unique identifier only consisting of hexadecimal characters and underscores. * It looks like <code>d578271282b42fce__2955b56e_107df3fbc96__8000</code> and is 48 characters long. * * @return The identifier */ protected String createUniqueIdentifier() { return new UID().toString().replace(':', '_').replace('-', '_'); } }