/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ /* * MappingPolicy.java * * Created on Jan 14, 2003 */ package com.sun.jdo.spi.persistence.generator.database; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import java.security.AccessController; import java.security.PrivilegedAction; import java.sql.Types; import org.glassfish.persistence.common.I18NHelper; import com.sun.jdo.spi.persistence.utility.StringHelper; import com.sun.jdo.spi.persistence.utility.logging.Logger; import org.glassfish.persistence.common.database.DBVendorTypeHelper; import org.glassfish.persistence.common.DatabaseConstants; // XXX Capitalization of acronyms such as Jdbc vs. JDBC is inconsistent // throught out this package. /** * Describes how Java classes and their fields are to be mapped to database * tables and columns */ public class MappingPolicy implements Cloneable { // // The names of many properties in our .properties files are composed of // different pieces, here called "bases", followed by "indicators". The // idea is that a base may have multiple indicators. In a way, they are // kind of like instances of structs, with each base naming an instance // and each indicator naming an accessor to a value in the struct. // // The concatenation of bases and indicators is just another String, // which you can look up in a Properties object to get a value. // // Note that some property names have more than one base concatenated. // See SQL92.properties for examples. // /** @see DatabaseGenerationConstants#DOT */ static final char DOT = DatabaseGenerationConstants.DOT; // // Base names of properties which describe how class and field names are // to be represented in a database. // /** Base name to denote a class. */ private static final String CLASS_BASE = "{class-name}"; //NOI18N /** Base name to denote a field. */ private static final String FIELD_BASE = "{field-name}"; //NOI18N /** Base name to denote a relationship field. */ private static final String RELATIONSHIP_BASE = "{relationship-field-name}"; //NOI18N /** Represents a '.' in a regular expression */ private static String REGEXP_DOT = "\\."; // NOI18N /** Synonym for DatabaseGenerationConstants.INDICATOR_JDBC_PREFIX. */ private static final String INDICATOR_JDBC_PREFIX = DatabaseGenerationConstants.INDICATOR_JDBC_PREFIX; /** * Base name to denote maximum length of a name in a database. We * support different maximum lengths for table, column, and constraint * names. */ private static final String INDICATOR_MAXIMUM_LENGTH = DatabaseGenerationConstants.INDICATOR_MAXIMUM_LENGTH; // // Indicator names for properties which describe how class and field // names are to be represented in a database. // /** Indicator that property is for a table name. */ private static final String INDICATOR_TABLE_NAME = "table-name"; //NOI18N /** Indicator that property is for a column name. */ private static final String INDICATOR_COLUMN_NAME = "column-name"; //NOI18N /** Indicator that property is for a join table name. */ private static final String INDICATOR_JOIN_TABLE_NAME = "join-table-name"; //NOI18N /** Indicator that property is for a constraint name. */ private static final String INDICATOR_CONSTRAINT_NAME = "constraint-name"; //NOI18N // // These are complete property names composed of bases and indicators. // /** Prefix of properties that denote classes. */ private static final String CLASS_PREFIX = CLASS_BASE + DOT; /** Prefix of properties that denote classes. */ private static final String RELATIONSHIP_PREFIX = CLASS_PREFIX + RELATIONSHIP_BASE + DOT; /** Name of property that provides default field-to-column name mapping. */ private static final String DEFAULT_COLUMN_KEY = CLASS_PREFIX + FIELD_BASE + DOT + INDICATOR_COLUMN_NAME; /** Name of property that provides default jointable name mapping. */ private static final String DEFAULT_JOIN_TABLE_KEY = RELATIONSHIP_PREFIX + INDICATOR_JOIN_TABLE_NAME; /** Name of property that provides default constraint name mapping. */ private static final String DEFAULT_CONSTRAINT_KEY = RELATIONSHIP_PREFIX + INDICATOR_CONSTRAINT_NAME; /** Name of property that provides default class-to-table name mapping. */ private static final String DEFAULT_TABLE_KEY = CLASS_PREFIX + INDICATOR_TABLE_NAME; // // Now, here are values of properties which indicate how table and column // names are to be generated. I.e., in our .properties files, these are // potential values for some property names composed of the above bases // and indicators. // /** Property value indicating table name must be same as class name. */ private static final String TABLE_NAME_AS_CLASSNAME = "{className}"; //NOI18N /** Property value indicating table name must be upper case. */ private static final String TABLE_NAME_UPPERCASE = TABLE_NAME_AS_CLASSNAME.toUpperCase(); /** Property value indicating table name must be uppercase and unique. */ private static final String TABLE_NAME_HASH_UPPERCASE = "{HASH-CLASSNAME}"; //NOI18N /** Property value indicating colum name must be same as field name. */ private static final String COLUMN_NAME_AS_FIELDNAME = "{fieldName}"; //NOI18N /** Property value indicating column name must be uppercase. */ private static final String COLUMN_NAME_UPPERCASE = COLUMN_NAME_AS_FIELDNAME.toUpperCase(); /** Property value indicating join table name must be uppercase. */ private static final String JOIN_TABLE_NAME_UPPERCASE = "{CLASSNAMES}"; //NOI18N /** Property value indicating constraint name must be uppercase. */ private static final String CONSTRAINT_NAME_UPPERCASE = "{FIELDNAMES}"; //NOI18N // // Here are indicators for properties that direct how vendor-dependent // SQL is generated. // /** Indicator that property is for formatting SQL */ private static final String INDICATOR_SQL_FORMAT = "sql-format"; //NOI18N /** The indicator for a statement separator. */ private static final String STATEMENT_SEPARATOR_INDICATOR = "statementSeparator"; // NOI18N /** The indicator for starting a "create table". */ private static final String CREATE_TABLE_START_INDICATOR = "createTableStart"; // NOI18N /** The indicator for ending a "create table". */ private static final String CREATE_TABLE_END_INDICATOR = "createTableEnd"; // NOI18N /** The indicator for "create index". Added for Symfoware support as */ /** indexes on primary keys are mandatory */ private static final String CREATE_INDEX_INDICATOR = "createIndex"; // NOI18N /** The indicator for starting a "drop table". */ private static final String DROP_TABLE_INDICATOR = "dropTable"; // NOI18N /** The indicator for "add constraint". */ private static final String ALTER_TABLE_ADD_CONSTRAINT_START_INDICATOR = "alterTableAddConstraintStart"; // NOI18N /** The indicator for "drop constraint". */ private static final String ALTER_TABLE_DROP_CONSTRAINT_INDICATOR = "alterTableDropConstraint"; // NOI18N /** The indicator for adding a primary key constraint. */ private static final String PRIMARY_KEY_CONSTRAINT_INDICATOR = "primaryKeyConstraint"; // NOI18N /** The indicator for adding a foreign key constraint. */ private static final String FOREIGN_KEY_CONSTRAINT_INDICATOR = "foreignKeyConstraint"; // NOI18N /** The indicator for verbose nullability. */ private static final String COLUMN_NULLABILITY_INDICATOR = "columnNullability"; // NOI18N /** The indicator for information used with LOB columns. */ private static final String LOB_LOGGING_INDICATOR = "LOBLogging"; // NOI18N // // The remaining constants are neither bases nor indicators. // /** Prefix of column names which are primary key columns. */ private static final String PK_PREFIX = "PK_"; //NOI18N /** Prefix of column names which are foreign key columns. */ private static final String FK_PREFIX = "FK_"; //NOI18N /** Name of the "global" namespace. */ private static final String GLOBAL_NAMING_SPACE = "GLOBAL"; //NOI18N /** Property name which indicates unique table names should be generated. */ public static final String USE_UNIQUE_TABLE_NAMES = "use-unique-table-names"; // NOI18N /** Property name which indicates reserved words. */ private static final String RESERVED_WORDS = "reserved-words";// NOI18N /** When appended to a reserved word, causes it to be not-reserved. */ private static final String RESERVED_WORD_UNRESERVER = "9"; // NOI18N /** * Maximum length of the counter used to create unique names with a * numeric id. Note that this length includes a NAME_SEPARATOR, so that * we allow for 3 digits total. */ private static final int MAX_LEN_COUNTER = 4; /** Number of chars to change a reserved word into * unreserved. */ private static final int MAX_LEN_RESERVED = 1; /** * Name of subdirectory in which db vendor - specific properties files * are located. */ private static final String PROPERTY_FILE_DIR = "com/sun/jdo/spi/persistence/generator/database/"; // NOI18N /** Extension used by properties files. */ private static final String PROPERTY_FILE_EXT = ".properties"; // NOI18N // // The above are all constants; below things get "interesting". // /** This is the set of all default properties. */ private static final Properties defaultProps = new Properties(); // // XXX Consider getting these String-Integer and Integer-String maps into // SQLTypeUtil, which is in the dbschema module. Or not: we support only a // subset of java.sql.types, and (presumably) the dbschema module would // provide mappings for all types. // // Note also that is code in MappingGenerator of a "SQLTypeUtil" nature. // /** * Map from String names to the Integer-boxed values from * java.sql.Types. */ private static final Map jdbcTypes = new HashMap(); /** * Maps from Integer-boxed values from java.sql.Types to String names. */ private static final Map jdbcTypeNames = new HashMap(); /** * Global counter for creating unique names in each of the namespaces. * <em>Note that a single counter is used across all namespaces.</em> */ private int counter = 0; /** * Map from namespaces to Set of names defined in each namespace. Used * to ensure uniqueness within namespaces. */ private Map namespaces = new HashMap(); /** * Indicates whether or not generated table names should include a * unique value as part of their names. */ private boolean uniqueTableName = false; /** * Set of reserved words for a particular policy. */ private final Set reservedWords = new TreeSet(); /** * Set of reserved words for the default database. */ private static Set defaultReservedWords; /** * Map from the string names of the java types (e.g. "java.lang.String") * to a JDBCInfo of information about the corresponding java.sql.Types * type. Different for different dbvendor types, but the same instance, * per dbvendor, is shared by all MappingPolicy instances. */ private final Map dbJdbcInfoMap = new HashMap(); /** * Similar to {@link #dbJdbcInfoMap}, but is reinitialized by each * clone(). Contains user-provided overrides of the information in * dbjdbcInfoMap. */ private Map userJdbcInfoMap = new HashMap(); /** * Map from a boxed value based on fields in java.sql.Types to the String * name of a SQL type. */ private final Map sqlInfo = new HashMap(); // // Maximum lengths for table, column, and constraint names are // vendor-specific. // /** Maximum length of the name of a table. */ private int tableNameMaxLength; /** Maximum length of the name of a column. */ private int columnNameMaxLength; /** Maximum length of the name of a constraint. */ private int constraintNameMaxLength; // // Strings to represent vendor-specific SQL that starts a table, // separates statements, etc. // /** The SQL for a statement separator. */ private String statementSeparator; /** The SQL for starting a "create table". */ private String createTableStart; /** The SQL for ending a "create table". */ private String createTableEnd; /** The SQL for "create index". */ private String createIndex; /** The SQL for "drop table". */ private String dropTable; /** The SQL for "add constraint". */ private String alterTableAddConstraintStart; /** The SQL for "drop constraint". */ private String alterTableDropConstraint; /** The SQL for adding a primary key constraint. */ private String primaryKeyConstraint; /** The SQL for adding a foreign key constraint. */ private String foreignKeyConstraint; /** The SQL for indicating column nullability */ private String columnNullability; /** The SQL for indicating LOB column logging */ private String lobLogging = ""; /** * Map from the encoded name of a policy to its value. For example, a * class name's naming policy would be encoded as * "<classname>.table-name". */ // XXX Consider renaming this. private final Map namingPolicy = new HashMap(); /** Map from database vendor names to instances of MappingPolicy. */ private static final Map instances = new HashMap(); /** Logger for warning & error messages */ private static final Logger logger = LogHelperDatabaseGenerator.getLogger(); /** I18N message handler */ private final static ResourceBundle messages = I18NHelper.loadBundle(MappingPolicy.class); // // Initialize the JDBC String to Integer map and the default (SQL92) // MappingPolicy. // // XXX Why initialize the SQL policy, when there's a good chance it won't // ever be used? Do we really want to support unrecognized databases? // See comment in getMappingPolicy. The default properties, on the other // hand, *do* need to be loaded. // XXX We need to decide what happens when an unrecognized dbvendorname // is given: Error? Warning, continue running? static { // Initialize jdbcType map. jdbcTypes.put("BIGINT", new Integer(Types.BIGINT)); // NOI18N jdbcTypes.put("BIT", new Integer(Types.BIT)); // NOI18N jdbcTypes.put("BLOB", new Integer(Types.BLOB)); // NOI18N jdbcTypes.put("CHAR", new Integer(Types.CHAR)); // NOI18N jdbcTypes.put("CLOB", new Integer(Types.CLOB)); // NOI18N jdbcTypes.put("DATE", new Integer(Types.DATE)); // NOI18N jdbcTypes.put("DECIMAL", new Integer(Types.DECIMAL)); // NOI18N jdbcTypes.put("DOUBLE", new Integer(Types.DOUBLE)); // NOI18N jdbcTypes.put("INTEGER", new Integer(Types.INTEGER)); // NOI18N jdbcTypes.put("LONGVARBINARY", new Integer(Types.LONGVARBINARY)); // NOI18N jdbcTypes.put("LONGVARCHAR", new Integer(Types.LONGVARCHAR)); // NOI18N jdbcTypes.put("NULL", new Integer(Types.NULL)); // NOI18N jdbcTypes.put("REAL", new Integer(Types.REAL)); // NOI18N jdbcTypes.put("SMALLINT", new Integer(Types.SMALLINT)); // NOI18N jdbcTypes.put("TIME", new Integer(Types.TIME)); // NOI18N jdbcTypes.put("TIMESTAMP", new Integer(Types.TIMESTAMP)); // NOI18N jdbcTypes.put("TINYINT", new Integer(Types.TINYINT)); // NOI18N jdbcTypes.put("VARCHAR", new Integer(Types.VARCHAR)); // NOI18N jdbcTypeNames.put(new Integer(Types.BIGINT), "BIGINT"); // NOI18N jdbcTypeNames.put(new Integer(Types.BIT), "BIT"); // NOI18N jdbcTypeNames.put(new Integer(Types.BLOB), "BLOB"); // NOI18N jdbcTypeNames.put(new Integer(Types.CHAR), "CHAR"); // NOI18N jdbcTypeNames.put(new Integer(Types.CLOB), "CLOB"); // NOI18N jdbcTypeNames.put(new Integer(Types.DATE), "DATE"); // NOI18N jdbcTypeNames.put(new Integer(Types.DECIMAL), "DECIMAL"); // NOI18N jdbcTypeNames.put(new Integer(Types.DOUBLE), "DOUBLE"); // NOI18N jdbcTypeNames.put(new Integer(Types.INTEGER), "INTEGER"); // NOI18N jdbcTypeNames.put(new Integer(Types.LONGVARBINARY), "LONGVARBINARY"); // NOI18N jdbcTypeNames.put(new Integer(Types.LONGVARCHAR), "LONGVARCHAR"); // NOI18N jdbcTypeNames.put(new Integer(Types.NULL), "NULL"); // NOI18N jdbcTypeNames.put(new Integer(Types.REAL), "REAL"); // NOI18N jdbcTypeNames.put(new Integer(Types.SMALLINT), "SMALLINT"); // NOI18N jdbcTypeNames.put(new Integer(Types.TIME), "TIME"); // NOI18N jdbcTypeNames.put(new Integer(Types.TIMESTAMP), "TIMESTAMP"); // NOI18N jdbcTypeNames.put(new Integer(Types.TINYINT), "TINYINT"); // NOI18N jdbcTypeNames.put(new Integer(Types.VARCHAR), "VARCHAR"); // NOI18N try { // Create and load the default mapping policy. new MappingPolicy(); } catch (Throwable ex) { logger.log(Logger.SEVERE, I18NHelper.getMessage( messages, "EXC_MappingPolicyNotFound", //NOI18N DBVendorTypeHelper.DEFAULT_DB)); } } /** * Create the default MappingPolicy instance. */ // This should be invoked only once per JVM. See the class static // block of code above. private MappingPolicy() throws IOException { load(getPropertyFileName(DBVendorTypeHelper.DEFAULT_DB), defaultProps, false); init(defaultProps); // The DEFAULT_DB has reserved words for the default database type. // Other databases can access those same values through the // defaultReservedWords set. defaultReservedWords = reservedWords; instances.put(DBVendorTypeHelper.DEFAULT_DB, this); if (logger.isLoggable(Logger.FINEST)) { logger.finest("new MappingPolicy():\n" + toString()); // NOI18N } } /** * Create a MappingPolicy for the named database type. * @param databaseType Name of the database for which a MappingPolicy is * created. The name must conform to one of the .properties files in * this package. */ // This should be invoked only once per databaseType per JVM. See // {@link #getMappingPolicy}. private MappingPolicy(String databaseType) throws IOException { Properties mergedProp = new Properties(defaultProps); load(getPropertyFileName(databaseType), mergedProp, false); init(mergedProp); instances.put(databaseType, this); if (logger.isLoggable(Logger.FINEST)) { logger.finest("new MappingPolicy(" // NOI18N + databaseType + "):\n" + toString()); // NOI18N } } /** * Returns a vendor-specifc MappingPolicy instance. This method always * returns a copy (clone) of the known MappingPolicy to allow for * user-specific overrides. * @param databaseType a database vendor name as a String. * @return MappingPolicy instance corresponding to the provided * database vendor name. * @throws IOException if there are problems reading the vendor- * specific mappinng policy file */ public synchronized static MappingPolicy getMappingPolicy( String databaseType) throws IOException { if (logger.isLoggable(Logger.FINE)) { logger.fine("get MappingPolicy"+databaseType); // NOI18N } MappingPolicy mappingPolicy = null; try { if (databaseType == null) { databaseType = DBVendorTypeHelper.DEFAULT_DB; // XXX FIXME Need to log a warning and report to user that we // are *not* using databaseType given, that we are using // SQL92 instead, and provide list of recognized names. } mappingPolicy = (MappingPolicy) instances.get(databaseType); if (mappingPolicy == null) { mappingPolicy = new MappingPolicy(databaseType); } mappingPolicy = (MappingPolicy) mappingPolicy.clone(); } catch (CloneNotSupportedException ec) { // ignore it because it will not happen } return mappingPolicy; } /** * Clones the vendor-specific policy for generator session. Replace the * namespaces map in the clone, so that each instance has its own. * @return clone of this MappingPolicy * @throws CloneNotSupportedException never thrown */ protected Object clone() throws CloneNotSupportedException { MappingPolicy mappingPolicyClone = (MappingPolicy) super.clone(); mappingPolicyClone.namespaces = new HashMap(); mappingPolicyClone.uniqueTableName = false; mappingPolicyClone.userJdbcInfoMap = new HashMap(); return mappingPolicyClone; } /** * Initializes the given properties from the given resourceName. * @param resourceName Name of the resource to open, expected to contain * properties in text form. * @param properties Properties that are to be loaded. * @param override If <code>true</code>, treat resourceName as a filename * from which to read; if <code>false</code>, treat resourceName as the * name of a resource accessed via the class loader which loaded the * MappingPolicy class. */ private synchronized void load( final String resourceName, Properties properties, boolean override) throws IOException { if (logger.isLoggable(Logger.FINE)) { logger.fine("load resource:" + resourceName); // NOI18N } InputStream bin = null; InputStream in = null; try { if (override) { in = new FileInputStream(resourceName); } else { final ClassLoader loader = MappingPolicy.class.getClassLoader(); in = (InputStream) AccessController.doPrivileged( new PrivilegedAction() { public Object run() { Object rc = null; if (loader != null) { rc =loader.getResourceAsStream( resourceName); } else { rc = ClassLoader.getSystemResourceAsStream( resourceName); } return rc; } }); if (in == null) { throw new IOException(I18NHelper.getMessage(messages, "EXC_ResourceNotFound", resourceName));// NOI18N } } bin = new BufferedInputStream(in); properties.load(bin); if (logger.isLoggable(Logger.FINE)) { logger.fine("load "+resourceName + " successfuly"); // NOI18N } } finally { try { bin.close(); // XXX Need to close both streams in.close(); } catch (Exception e) { // ignore } } } /** * Resets the namespaces and counter. */ // XXX Consider renaming to "reset". void resetCounter() { namespaces.clear(); userJdbcInfoMap.clear(); counter = 0; } /** * Sets user-provided policy to that provided in the given properties. * @param props Properties which override built in defaults. */ public void setUserPolicy(Properties props) { if (null != props) { // Look for and set JDBCInfo entries. Use Enumeration instead of // iterator because former gets default values while latter does // not. for (Enumeration e = props.propertyNames(); e.hasMoreElements();) { String name = (String) e.nextElement(); String value = props.getProperty(name); if (name.equals(USE_UNIQUE_TABLE_NAMES)) { if (! StringHelper.isEmpty(value)) { uniqueTableName = Boolean.valueOf(value).booleanValue(); } continue; } StringTokenizer nameParser = new StringTokenizer(name, String.valueOf(DOT)); // Get the last element from key which is separated by DOT. String indicator = null; while (nameParser.hasMoreTokens()) { indicator = nameParser.nextToken(); } if (indicator.startsWith(INDICATOR_JDBC_PREFIX)) { setJDBCInfoEntry(userJdbcInfoMap, name, value, indicator); } else { if (logger.isLoggable(Logger.INFO)) { logger.info( I18NHelper.getMessage( messages, "MSG_UnexpectedUserProp", // NOI18N name, value)); // NOI18N } } } } } /** * Sets whether or not unique table names should be generated. * @param uniqueTableName If <code>true</code>, tables names will be * unique. */ public void setUniqueTableName(boolean uniqueTableName) { this.uniqueTableName = uniqueTableName; } /** * Returns the String name of the SQL type for a given JDBC type. * @param jdbcType One of the values from java.sql.Types. * @return Name of SQL type corresponding to given jdbcType. */ public String getSQLTypeName(int jdbcType) { String rc = null; // The name is in sqlInfo if it was loaded from one of our // vendor-specific properties files. Object o = sqlInfo.get(new Integer(jdbcType)); if (null != o) { rc = (String) o; } else { // Otherwise, user has overriden, e.g. java.lang.String -> CLOB. rc = getJdbcTypeName(jdbcType); } return rc; } /** * Returns JDBC type information corresponding to the given field name * and type. If fieldName is <code>null</code> or there is no * fieldName - specific information already existing, the default for the * given fieldType is returned. Otherwise, information specific to the * fieldName is returned. <em>Note:</em> It is possible to have a field * in a class have the same name as another class; this mechanism is not * robust in that case: * <pre> * class Foo { } * * class MyFoo { * Foo Foo = new Foo(); * } * </pre> * We think this obfuscation unlikely to occur. * @param fieldName Name of field for which information is needed. May * be null, in which case only fieldType is used to determine returned * information. * @param fieldType Name of a Java type. * @return JDBCInfo representing the field or type. */ public JDBCInfo getJDBCInfo(String fieldName, String fieldType) { JDBCInfo rc = null; if (logger.isLoggable(Logger.FINEST)) { logger.finest("Entering MappingPolicy.getJDBCInfo: " // NOI18N + fieldName + ", " + fieldType); // NOI18N } if (null != fieldName) { // If fieldName is given, try to find a JDBCInfo using that name. // Looking up fieldName only makes sense in userJdbcInfoMap // which contains the user's overrides. rc = (JDBCInfo) userJdbcInfoMap.get(fieldName); if (null != rc && (! rc.isComplete())) { // There is an override for the field named fieldName, but // it is not complete, i.e., not all possible information // about the field was provided in the user override. // // Choose a JDBCInfo to use to complete the information in rc. // If the user override specifies a type and there is // information about that type for the database, use that. // Otherwise, use the given fieldType. JDBCInfo ji = null; if (rc.hasJdbcType()) { ji = getdbJDBCInfo(rc.getJdbcType()); } if (null == ji) { ji = getdbJDBCInfo(fieldType); } // Fill in the rest of the fields in rc with values from ji. rc.complete(ji); } } if (null == rc) { // Either fieldName is null, or there is no JDBCInfo specific to // fieldName, so use fieldType. rc = getdbJDBCInfo(fieldType); } // If dbJdbcInfoMap has an entry for rc's jdbc type, replace rc's jdbc // type with the result of the mapping. This allows, for example, a // user to specify that a field should be represented by a CLOB, when // the database and/or driver do not support that but do support // LONGVARCHAR (e.g. Sybase). JDBCInfo ji = getdbJDBCInfo(rc.getJdbcType()); rc.override(ji); if (logger.isLoggable(Logger.FINEST)) { logger.finest("Leaving MappingPolicy.getJDBCInfo: " // NOI18N + fieldName + ", " + fieldType // NOI18N + " => " + rc); // NOI18N } return rc; } /** * Gets the JDBCInfo corresponding to the type in the given JDBCInfo. * I.e., it maps from one JDBCInfo to another on the basis of their * types. * @param ji JDBCInfo * @return a JDBCInfo * @throws IllegalArgumentException if <code>type</code> is not recognized * as being a valid member of java.sql.Types. Note that only a subset of * the types in java.sql.Types are recognized. */ private JDBCInfo getdbJDBCInfo(int jdbcType) { String typename = getJdbcTypeName(jdbcType); return (JDBCInfo) dbJdbcInfoMap.get(typename); } /** * Gets the JDBCInfo for the given fieldType * @param fieldType Name of the type of a field * @return a JDBCInfo for the given fieldType */ private JDBCInfo getdbJDBCInfo(String fieldType) { JDBCInfo rc = (JDBCInfo) dbJdbcInfoMap.get(fieldType); if (null == rc) { // There is also nothing provided for the field's // type, so use a BLOB. rc = (JDBCInfo) dbJdbcInfoMap.get("BLOB"); // NOI18N } return rc; } /** * Returns the boxed form of the java.sql.Types value corresponding to the * given name. * @param jdbcStringType Name of the value to return. * @return Value from java.sql.Types, wrapped into an Integer, or null if * jdbcTypeName is not that of a recognized JDBC type. */ static Integer getJdbcType(String jdbcTypeName) { return (Integer) jdbcTypes.get(jdbcTypeName.toUpperCase()); } /** * Provides a String that can be recognized as a policy to override the * default length of a field. * @param className name of a class * @param fieldName name of a field in that class * @return a String that can be used as the name of a length override * for a field in a class. */ public static String getOverrideForLength( String className, String fieldName) { return className + DOT + fieldName + DOT + DatabaseGenerationConstants.INDICATOR_JDBC_LENGTH; } /** * Provides a String that can be recognized as a policy to override the * default nullability of a field. * @param className name of a class * @param fieldName name of a field in that class * @return a String that can be used as the name of a nullability override * for a field in a class. */ public static String getOverrideForNullability( String className, String fieldName) { return className + DOT + fieldName + DOT + DatabaseGenerationConstants.INDICATOR_JDBC_NULLABLE; } /** * Provides a String that can be recognized as a policy to override the * default precision of a field. * @param className name of a class * @param fieldName name of a field in that class * @return a String that can be used as the name of a precision override * for a field in a class. */ public static String getOverrideForPrecision( String className, String fieldName) { return className + DOT + fieldName + DOT + DatabaseGenerationConstants.INDICATOR_JDBC_PRECISION; } /** * Provides a String that can be recognized as a policy to override the * default scale of a field. * @param className name of a class * @param fieldName name of a field in that class * @return a String that can be used as the name of a scale override * for a field in a class. */ public static String getOverrideForScale( String className, String fieldName) { return className + DOT + fieldName + DOT + DatabaseGenerationConstants.INDICATOR_JDBC_SCALE; } /** * Provides a String that can be recognized as a policy to override the * default type of a field. * @param className name of a class * @param fieldName name of a field in that class * @return a String that can be used as the name of a type override * for a field in a class. */ public static String getOverrideForType( String className, String fieldName) { return className + DOT + fieldName + DOT + DatabaseGenerationConstants.INDICATOR_JDBC_TYPE; } /** * Provide the String name of a JDBC type, as per java.sql.Types. * @param type A value from java.sql.Types * @return the String name corresponding to <code>type</code> * @throws IllegalArgumentException if <code>type</code> is not recognized * as being a valid member of java.sql.Types. Note that only a subset of * the types in java.sql.Types are recognized. */ public static String getJdbcTypeName(int type) throws IllegalArgumentException { String rc = (String) jdbcTypeNames.get(new Integer(type)); if (null == rc) { throw new IllegalArgumentException(); } return rc; } // // In the getZZZName methods below, we lookup a policy and determine a // name based on that policy. Note that the keys used for lookup are // created from the given name(s), so that the result of looking up a // policy might actually be the name that is returned. // /** * Returns the name of a table for a given class, as per current policy. * @param name Basis for what the returned table should be named, for * example the unqualified name of a class. * @param uniqueName Used if the current policy is to return a unique * name. Client must provide a name that is unique to them. * @return Name to be used for table. Regardless of the current policy, * the name is different from other names returned during the current run * of {@link DatabaseGenerator#generate}. */ // XXX FIXME: If the user needs to provide a unique name, why do we // invoke getUniqueGlobalName on it? public String getTableName(String name, String uniqueName) { StringBuffer key = new StringBuffer(name).append(DOT).append(INDICATOR_TABLE_NAME); String rc = (String)namingPolicy.get(key.toString()); if (rc == null) { rc = (String)namingPolicy.get(DEFAULT_TABLE_KEY); } if (uniqueTableName) { rc = TABLE_NAME_HASH_UPPERCASE; } if (rc.equals(TABLE_NAME_UPPERCASE)) { rc = name.toUpperCase(); } else if (rc.equals(TABLE_NAME_AS_CLASSNAME)) { rc = name; } else if (rc.equals(TABLE_NAME_HASH_UPPERCASE)) { rc = uniqueName.toUpperCase(); } return getUniqueGlobalName(rc, tableNameMaxLength); } /** * Returns the name of a column for a given field in a given class. The * column name will be unique within the table. * @param className Name of the class containing the field. * @param fieldName Name of the field for which a column name is returned. * @param tableName Name of the table in which the column name is created. * @return Name of a column that is unique within the named table. */ public String getColumnName(String className, String fieldName, String tableName) { // Get column naming policy based on className and fieldName StringBuffer key = new StringBuffer(className) .append(DOT).append(fieldName) .append(DOT).append(INDICATOR_COLUMN_NAME); String rc = (String)namingPolicy.get(key.toString()); if (rc == null) { // No fieldName specific policy, so use default for className key = new StringBuffer(className) .append(DOT).append(FIELD_BASE) .append(DOT).append(INDICATOR_COLUMN_NAME); rc = (String)namingPolicy.get(key.toString()); } if (rc == null) { // No overriding policy, so use overall default. rc = (String)namingPolicy.get(DEFAULT_COLUMN_KEY); } if (rc.equals(COLUMN_NAME_UPPERCASE)) { rc = fieldName.toUpperCase(); } else if (rc.equals(COLUMN_NAME_AS_FIELDNAME)) { rc = fieldName; } return getUniqueLocalName(rc, tableName, columnNameMaxLength); } /** * Returns the name of the column which represents a foreign key in the * named table. * @param tableName Name of the table in which the FK column will be * created. * @param columnName Name of PK column in referenced table. * @return Name of the FK column in the named table. */ // XXX Does this really need to be public? // XXX Rename to getFKColumnName public String getConstraintColumnName(String tableName, String columnName) { return getUniqueLocalName( new StringBuffer(tableName) .append(DatabaseConstants.NAME_SEPARATOR) .append(columnName).toString(), tableName, columnNameMaxLength); } /** * Returns the name of a constraint corresponding to the named * relationship. * @param relName Name of a relationship. * @param uniqueId Id that can be appened to relName to distinguish it * from other relNames in the database. Will be appended only if * {@link #uniqueTableName} is true. * @return Name of a constraint. */ public String getConstraintName(String relName, String uniqueId) { String rc = (String)namingPolicy.get(DEFAULT_CONSTRAINT_KEY); if (rc.equals(CONSTRAINT_NAME_UPPERCASE)) { rc = FK_PREFIX + relName.toUpperCase(); } if (uniqueTableName) { rc += uniqueId; } rc = getUniqueGlobalName(rc, constraintNameMaxLength); if (logger.isLoggable(Logger.FINER)) { logger.finer("MappingPolicy.getConstraintName: " // NOI8N + relName + " -> " + rc); // NOI18N } return rc; } /** * Returns the name of a PK constraint, unique-ified as required. * @param tableName Name of a table on which a constraint is to be placed. * @return Name of a constraint on named table. */ public String getPrimaryKeyConstraintName(String tableName) { return getUniqueGlobalName(PK_PREFIX + tableName, constraintNameMaxLength); } /** * Returns the name of a join table which joins the tables that correspond * to the two named classes. * @param className1 Name of one class to join. * @param className2 Name of the other class to join. * @return Name of a join table. */ public String getJoinTableName(String className1, String className2) { String rc = (String)namingPolicy.get(DEFAULT_JOIN_TABLE_KEY); if (rc.equals(JOIN_TABLE_NAME_UPPERCASE)) { rc = (className1 + className2).toUpperCase(); } return getUniqueGlobalName(rc, tableNameMaxLength); } /** * Return a unique name for a column. The column will be unique within * the named table. * @param colName Name of the column * @param tableName Name of the table. * @return A unique name for colName within tableName. */ private String getUniqueLocalName( String colName, String tableName, int maxLen) { return getUniqueName(colName, tableName, maxLen); } /** * Return a unique name for the given name. It will be "globally" unique * between invocations of method {@link #resetCounter}; the first use of * a MappingPolicy instance is "reset". * @param name Name for which a unique name is returned. * @return A unique name for given name. */ private String getUniqueGlobalName(String name, int maxLen) { return getUniqueName(name, GLOBAL_NAMING_SPACE, maxLen); } /** * Return a unique name for name. It will be unique within the given * namespace. * @param name Name for which a unique name is returned. * @param space Namespace in which the returned name is unique. * @return A unique name for given name. */ private String getUniqueName(String name, String namespace, int maxLen) { String rc = name; // Reserve MAX_LEN_COUNTER characters for unique-ifying digits. maxLen -= MAX_LEN_COUNTER; // Name cannot be more than maxLen chars long. if (name.length() > maxLen) { rc = name.substring(0, maxLen); } // Convert to upper case for uniqueing comparisons below. String nameUpper = rc.toUpperCase(); // Ensure the name we create is not a reserved word by comparing // nameUpper against reserved words. if (defaultReservedWords.contains(nameUpper) || reservedWords.contains(nameUpper)) { // Append a character that is not used as the last char of any // existing reserved words. Make sure we have space for this plus // for any uniqueing below. Length-limit both rc and nameUpper, so // that the value in the namespace and the end result have the same // length. maxLen -= MAX_LEN_RESERVED; if (rc.length() > maxLen) { // Limit nameUpper as well as rc because we need to do uniqueing // in a case-insensitve fashion. nameUpper = nameUpper.substring(0, maxLen); rc = rc.substring(0, maxLen); } nameUpper += RESERVED_WORD_UNRESERVER; rc += RESERVED_WORD_UNRESERVER; } Set names = (Set) namespaces.get(namespace); if (names == null) { // Name is first entry in namespace, therefore already unique, no // need to append counter. names = new HashSet(); names.add(nameUpper); namespaces.put(namespace, names); } else if (names.contains(nameUpper)) { // Name is already in namespace, so make a different name by // appending a count to given name. counter++; rc += DatabaseConstants.NAME_SEPARATOR + counter; } else { // Add new name to namespace. names.add(nameUpper); } return rc; } // // Accessors for SQL formatting Strings. // /** * @return the SQL for a statement separator. */ String getStatementSeparator() { return statementSeparator; } /** * @return the SQL for starting a "create table". */ String getCreateTableStart() { return createTableStart; } /** * @return the SQL for ending a "create table". */ String getCreateTableEnd() { return createTableEnd; } /** * @return the SQL for "create index". */ String getCreateIndex() { return createIndex; } /** * @return the SQL for a "drop table". */ String getDropTable() { return dropTable; } /** * @return the SQL for "add constraint". */ String getAlterTableAddConstraintStart() { return alterTableAddConstraintStart; } /** * @return the SQL for "drop constraint". */ String getAlterTableDropConstraint() { return alterTableDropConstraint; } /** * @return the SQL for adding a primary key constraint. */ String getPrimaryKeyConstraint() { return primaryKeyConstraint; } /** * @return the SQL for adding a foreign key constraint. */ String getForeignKeyConstraint() { return foreignKeyConstraint; } /** * @return the SQL for indicating column nullability */ String getColumnNullability() { return columnNullability; } /** * @return the SQL for indicating LOB column logging */ String getLobLogging() { return lobLogging; } // // This method and the 4 subsequent ones initialize a MappingPolicy // instance from a give Properties object. // /** * Initialize this MappingPolicy as per the values in the given * properties. * @param props Properties for initializing this MappingPolicy */ private void init(Properties props) { // Use Enumeration instead of iterator because former gets default // values while latter does not. for (Enumeration e = props.propertyNames(); e.hasMoreElements();) { String name = (String) e.nextElement(); String value = props.getProperty(name); if (name.equals(RESERVED_WORDS)) { initReservedWords(value); continue; } // The indicator is the last DOT-separated substring in name. String indicator = null; StringTokenizer nameParser = new StringTokenizer(name, String.valueOf(DOT)); while (nameParser.hasMoreTokens()) { indicator = nameParser.nextToken(); } if (indicator.equals(INDICATOR_SQL_FORMAT)) { setSqlFormatEntry(name, value); } else if (indicator.startsWith(INDICATOR_JDBC_PREFIX)) { setJDBCInfoEntry(dbJdbcInfoMap, name, value, indicator); } else if (indicator.equals(INDICATOR_MAXIMUM_LENGTH)) { setLengthEntry(name, value); } else if (indicator.equals(INDICATOR_TABLE_NAME) || indicator.equals(INDICATOR_COLUMN_NAME) || indicator.equals(INDICATOR_JOIN_TABLE_NAME) || indicator.equals(INDICATOR_CONSTRAINT_NAME)) { setNamingEntry(name, value); } else { setSQLInfoEntry(name, value); } } } /** * Initializes the <code>reservedWords</code> field. * @param res Comma-separated list of reserved words. */ private void initReservedWords(String res) { StringTokenizer st = new StringTokenizer(res, ","); while (st.hasMoreTokens()) { reservedWords.add(st.nextToken().trim()); } } // // These methods each set a value in one of our maps. // /** * Sets a SQL formatting property in this MappingPolicy. * @param name Name of the policy property, including its indicator. * @param indicator The indicator, alone, which should start with * "sql-format". * @param value Value to be bound to that property. */ private void setSqlFormatEntry(String name, String value) { if (value != null) { if (name.startsWith(STATEMENT_SEPARATOR_INDICATOR)) { statementSeparator = value; } else if (name.startsWith(CREATE_TABLE_START_INDICATOR)) { createTableStart = value; } else if (name.startsWith(CREATE_TABLE_END_INDICATOR)) { createTableEnd = value; } else if (name.startsWith(CREATE_INDEX_INDICATOR)) { createIndex = value; } else if (name.startsWith(DROP_TABLE_INDICATOR)) { dropTable = value; } else if (name.startsWith(ALTER_TABLE_ADD_CONSTRAINT_START_INDICATOR)) { alterTableAddConstraintStart = value; } else if (name.startsWith(ALTER_TABLE_DROP_CONSTRAINT_INDICATOR)) { alterTableDropConstraint = value; } else if (name.startsWith(PRIMARY_KEY_CONSTRAINT_INDICATOR)) { primaryKeyConstraint = value; } else if (name.startsWith(FOREIGN_KEY_CONSTRAINT_INDICATOR)) { foreignKeyConstraint = value; } else if (name.startsWith(COLUMN_NULLABILITY_INDICATOR)) { columnNullability = value; } else if (name.startsWith(LOB_LOGGING_INDICATOR)) { lobLogging = value; } } } /** * Sets a JDBC property in this MappingPolicy. * @param Map into which property is set. * @param name Name of the policy property, including its indicator. * @param indicator The indicator, alone, which should start with * "jdbc-". * @param value Value to be bound to that property. */ private void setJDBCInfoEntry( Map jdbcInfoMap, String name, String value, String indicator) { if (value != null) { // Get substring that is before the indicator, which is the name // of a type or of a particular field. String fieldOrType = name; int i = name.indexOf(DOT + indicator); if (i > 0) { fieldOrType = name.substring(0, i); } JDBCInfo ji = (JDBCInfo) jdbcInfoMap.get(fieldOrType); try { if (null != ji) { ji.setValue(value, indicator); } else { ji = new JDBCInfo(); ji.setValue(value, indicator); jdbcInfoMap.put(fieldOrType, ji); } } catch (JDBCInfo.IllegalJDBCTypeException ex) { String msg = I18NHelper.getMessage( messages, "EXC_InvalidJDBCTypeName", // NOI18N value, fieldOrType); logger.log(Logger.SEVERE, msg); throw new IllegalArgumentException(msg); } } } /** * Sets a name length attribute. * @param name Name of the attribute to set. Should be * INDICATOR_TABLE_NAME, INDICATOR_COLUMN_NAME, or * INDICATOR_CONSTRAINT_NAME. * @param value Value to which attribute is set. */ private void setLengthEntry(String name, String value) { if (value != null) { int val = Integer.valueOf(value).intValue(); if (name.startsWith(INDICATOR_TABLE_NAME)) { tableNameMaxLength = val; } else if (name.startsWith(INDICATOR_COLUMN_NAME)) { columnNameMaxLength = val; } else if (name.startsWith(INDICATOR_CONSTRAINT_NAME)) { constraintNameMaxLength = val; } } } /** * Set a naming property in this MappingPolicy. * @param name Name of the policy property. * @param value Value to be bound to that property. */ private void setNamingEntry(String name, String value) { namingPolicy.put(name, value); } /** * Sets a JDBC-to-SQL mapping property, that is, a mapping from a JDBC * type to a SQL type. * @param name Name of a JDBC type (see {@link java.sql.Types}). * @param value SQL type to be used to represent given JDBC type in * database. */ private void setSQLInfoEntry(String name, String value) { sqlInfo.put(getJdbcType(name), value); } /** * @param databaseType Name of a type of database. * @return Name of a file containing a description of properties for * named database type. */ private static String getPropertyFileName(String databaseType) { return PROPERTY_FILE_DIR + databaseType + PROPERTY_FILE_EXT; } /** * Debug support. * @return A description of this MappingPolicy in string form. * Basically, all it's "interesting" values. */ public String toString() { StringBuffer rc = new StringBuffer( "statementSeparator=" + statementSeparator // NOI18N + "\ncreateTableStart=" + createTableStart // NOI18N + "\ncreateTableEnd=" + createTableEnd // NOI18N + "\ncreateIndex=" + createIndex // NOI18N + "\ndropTable=" + dropTable // NOI18N + "\nalterTableAddConstraintStart=" + alterTableAddConstraintStart // NOI18N + "\nalterTableDropConstraint=" + alterTableDropConstraint // NOI18N + "\nprimaryKeyConstraint=" + primaryKeyConstraint // NOI18N + "\nforeignKeyConstraint=" + foreignKeyConstraint // NOI18N + "\ncolumnNullability=" + columnNullability // NOI18N + "\nlobLogging=" + lobLogging // NOI18N + "\ntableNameMaxLength=" + tableNameMaxLength // NOI18N + "\ncolumnNameMaxLength=" + columnNameMaxLength // NOI18N + "\nconstraintNameMaxLength=" + constraintNameMaxLength // NOI18N + "\nuniqueTableName=" + uniqueTableName // NOI18N + "\ncounter=" + counter // NOI18N + "\n\n"); // NOI18N rc.append(" dbJdbcInfoMap:\n").append(stringifyMap(dbJdbcInfoMap)); // NOI18N rc.append(" userJdbcInfoMap:\n").append(stringifyMap(userJdbcInfoMap)); // NOI18N rc.append(" sqlInfo:\n").append(stringifyMap(sqlInfo)); // NOI18N rc.append(" namingPolicy:\n").append(stringifyMap(namingPolicy)); // NOI18N rc.append(" namespaces:\n").append(stringifyMap(namespaces)); // NOI18N rc.append(" reservedWords:\n").append(stringifySet(reservedWords)); // NOI18N return rc.toString(); } /** * Debug support. * @param m Map whose keys & values are to be returned in a string. * @return The string form of m's keys and values, each pair separated * from the next by a newline, with keys separated from values by '='. */ private String stringifyMap(Map m) { StringBuffer rc = new StringBuffer(); for (Iterator i = m.entrySet().iterator(); i.hasNext();) { Map.Entry e = (Map.Entry) i.next(); rc.append(e.getKey()).append("=") // NOI18N .append(e.getValue()).append("\n"); // NOI18N } return rc.toString(); } /** * Debug support * @param s Set whose values are to be returned in a string * @return values from the given set, separated by spaces, up to 6 per * line. */ private String stringifySet(Set s) { StringBuffer rc = new StringBuffer(); int count = 0; for (Iterator i = s.iterator(); i.hasNext();) { rc.append(i.next()).append(" "); // NOI18N if (count++ > 6) { rc.append("\n"); // NOI18N count = 0; } } return rc.toString(); } }