/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ejb.plugins.cmp.jdbc.metadata;
import java.util.ArrayList;
import java.util.Iterator;
import javax.ejb.EJBException;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.jboss.deployment.DeploymentException;
import org.jboss.metadata.MetaData;
import org.jboss.metadata.RelationMetaData;
import org.jboss.metadata.RelationshipRoleMetaData;
import org.w3c.dom.Element;
/**
* This class represents one ejb-relation element in the ejb-jar.xml file. Most
* properties of this class are immutable. The mutable properties have set
* methods.
*
* @author <a href="mailto:dain@daingroup.com">Dain Sundstrom </a>
* @author <a href="mailto:heiko.rupp@cellent.de">Heiko W. Rupp </a>
* @version $Revision: 81030 $
*/
public final class JDBCRelationMetaData
{
private final static int TABLE = 1;
private final static int FOREIGN_KEY = 2;
/** Name of the relation. Loaded from the ejb-relation-name element. */
private final String relationName;
/**
* The left jdbc relationship role. Loaded from an ejb-relationship-role.
* Left/right assignment is completely arbitrary.
*/
private final JDBCRelationshipRoleMetaData left;
/**
* The right relationship role. Loaded from an ejb-relationship-role.
* Left/right assignment is completely arbitrary.
*/
private final JDBCRelationshipRoleMetaData right;
/**
* The mapping style for this relation (i.e., TABLE or FOREIGN_KEY).
*/
private final int mappingStyle;
/** data source name in jndi */
private final String dataSourceName;
/** datasource type mapping name is defined in the deployment descriptor */
private final String datasourceMappingName;
/** This is a cache of the datasource object. */
private transient DataSource dataSource;
/** type mapping used for the relation table */
private final JDBCTypeMappingMetaData datasourceMapping;
/** the name of the table to use for this bean */
private final String tableName;
/** is table created */
private boolean tableCreated;
/** is table dropped */
private boolean tableDropped;
/** should we create the table when deployed */
private final boolean createTable;
/** should we drop the table when deployed */
private final boolean removeTable;
/** should we alter the table when deployed */
private final boolean alterTable;
/**
* What commands should be issued directly after creation of a table?
*/
private final ArrayList tablePostCreateCmd;
/** should we use 'SELECT ... FOR UPDATE' syntax? */
private final boolean rowLocking;
/** should the table have a primary key constraint? */
private final boolean primaryKeyConstraint;
/** is the relationship read-only? */
private final boolean readOnly;
/** how long is read valid */
private final int readTimeOut;
/**
* Constructs jdbc relation meta data with the data from the relation
* metadata loaded from the ejb-jar.xml file.
*
* @param jdbcApplication used to retrieve the entities of this relation
* @param relationMetaData relation meta data loaded from the ejb-jar.xml
* file
*/
public JDBCRelationMetaData(JDBCApplicationMetaData jdbcApplication, RelationMetaData relationMetaData)
throws DeploymentException
{
RelationshipRoleMetaData leftRole = relationMetaData.getLeftRelationshipRole();
RelationshipRoleMetaData rightRole = relationMetaData.getRightRelationshipRole();
// set the default mapping style
if (leftRole.isMultiplicityMany() && rightRole.isMultiplicityMany())
{
mappingStyle = TABLE;
}
else
{
mappingStyle = FOREIGN_KEY;
}
dataSourceName = null;
datasourceMappingName = null;
datasourceMapping = null;
createTable = false;
removeTable = false;
alterTable = false;
rowLocking = false;
primaryKeyConstraint = false;
readOnly = false;
readTimeOut = -1;
left = new JDBCRelationshipRoleMetaData(this, jdbcApplication, leftRole);
right = new JDBCRelationshipRoleMetaData(this, jdbcApplication, rightRole);
left.init(right);
right.init(left);
relationName = getNonNullRelationName(left, right, relationMetaData.getRelationName());
if (mappingStyle == TABLE)
{
tableName = createDefaultTableName();
tablePostCreateCmd = getDefaultTablePostCreateCmd();
}
else
{
tableName = null;
tablePostCreateCmd = null;
}
}
/**
* Constructs relation meta data with the data contained in the ejb-relation
* element or the defaults element from a jbosscmp-jdbc xml file. Optional
* values of the xml element that are not present are loaded from the
* defaultValues parameter.
*
* @param jdbcApplication used to retrieve type mappings in table mapping
* style
* @param element the xml Element which contains the metadata about this
* relation
* @param defaultValues the JDBCApplicationMetaData which contains the
* values for optional elements of the element
* @throws DeploymentException if the xml element is not semantically
* correct
*/
public JDBCRelationMetaData(JDBCApplicationMetaData jdbcApplication, Element element,
JDBCRelationMetaData defaultValues) throws DeploymentException
{
mappingStyle = loadMappingStyle(element, defaultValues);
// read-only
String readOnlyString = MetaData.getOptionalChildContent(element, "read-only");
if (readOnlyString != null)
{
readOnly = Boolean.valueOf(readOnlyString).booleanValue();
}
else
{
readOnly = defaultValues.isReadOnly();
}
// read-time-out
String readTimeOutString = MetaData.getOptionalChildContent(element, "read-time-out");
if (readTimeOutString != null)
{
try
{
readTimeOut = Integer.parseInt(readTimeOutString);
}
catch (NumberFormatException e)
{
throw new DeploymentException("Invalid number format in " + "read-time-out '" + readTimeOutString + "': "
+ e);
}
}
else
{
readTimeOut = defaultValues.getReadTimeOut();
}
//
// Load all of the table options. defaults and relation-table-mapping
// will have these elements, and foreign-key will get the default values.
//
Element mappingElement = getMappingElement(element);
// datasource name
String dataSourceNameString = MetaData.getOptionalChildContent(mappingElement, "datasource");
if (dataSourceNameString != null)
dataSourceName = dataSourceNameString;
else
dataSourceName = defaultValues.getDataSourceName();
// get the type mapping for this datasource (optional, but always
// set in standardjbosscmp-jdbc.xml)
String datasourceMappingString = MetaData.getOptionalChildContent(mappingElement, "datasource-mapping");
if (datasourceMappingString != null)
{
datasourceMappingName = datasourceMappingString;
datasourceMapping = jdbcApplication.getTypeMappingByName(datasourceMappingString);
if (datasourceMapping == null)
{
throw new DeploymentException("Error in jbosscmp-jdbc.xml : " + "datasource-mapping "
+ datasourceMappingString + " not found");
}
}
else if(defaultValues.datasourceMappingName != null && defaultValues.getTypeMapping() != null)
{
datasourceMappingName = null;
datasourceMapping = defaultValues.getTypeMapping();
}
else
{
datasourceMappingName = null;
datasourceMapping = JDBCEntityMetaData.obtainTypeMappingFromLibrary(dataSourceName);
}
// get table name
String tableNameString = MetaData.getOptionalChildContent(mappingElement, "table-name");
if (tableNameString == null)
{
tableNameString = defaultValues.getDefaultTableName();
if (tableNameString == null)
{
// use defaultValues to create default, because left/right
// have not been assigned yet, and values used to generate
// default table name never change
tableNameString = defaultValues.createDefaultTableName();
}
}
tableName = tableNameString;
// create table? If not provided, keep default.
String createString = MetaData.getOptionalChildContent(mappingElement, "create-table");
if (createString != null)
{
createTable = Boolean.valueOf(createString).booleanValue();
}
else
{
createTable = defaultValues.getCreateTable();
}
// remove table? If not provided, keep default.
String removeString = MetaData.getOptionalChildContent(mappingElement, "remove-table");
if (removeString != null)
{
removeTable = Boolean.valueOf(removeString).booleanValue();
}
else
{
removeTable = defaultValues.getRemoveTable();
}
// post-table-create commands
Element posttc = MetaData.getOptionalChild(mappingElement, "post-table-create");
if (posttc != null)
{
Iterator it = MetaData.getChildrenByTagName(posttc, "sql-statement");
tablePostCreateCmd = new ArrayList();
while (it.hasNext())
{
Element etmp = (Element) it.next();
tablePostCreateCmd.add(MetaData.getElementContent(etmp));
}
}
else
{
tablePostCreateCmd = defaultValues.getDefaultTablePostCreateCmd();
}
// alter table? If not provided, keep default.
String alterString = MetaData.getOptionalChildContent(mappingElement, "alter-table");
if (alterString != null)
{
alterTable = Boolean.valueOf(alterString).booleanValue();
}
else
{
alterTable = defaultValues.getAlterTable();
}
// select for update
String sForUpString = MetaData.getOptionalChildContent(mappingElement, "row-locking");
if (sForUpString != null)
{
rowLocking = !isReadOnly() && (Boolean.valueOf(sForUpString).booleanValue());
}
else
{
rowLocking = defaultValues.hasRowLocking();
}
// primary key constraint? If not provided, keep default.
String pkString = MetaData.getOptionalChildContent(mappingElement, "pk-constraint");
if (pkString != null)
{
primaryKeyConstraint = Boolean.valueOf(pkString).booleanValue();
}
else
{
primaryKeyConstraint = defaultValues.hasPrimaryKeyConstraint();
}
//
// load metadata for each specified role
//
JDBCRelationshipRoleMetaData defaultLeft = defaultValues.getLeftRelationshipRole();
JDBCRelationshipRoleMetaData defaultRight = defaultValues.getRightRelationshipRole();
if (!MetaData.getChildrenByTagName(element, "ejb-relationship-role").hasNext())
{
// no roles specified use the defaults
left = new JDBCRelationshipRoleMetaData(this, jdbcApplication, element, defaultLeft);
right = new JDBCRelationshipRoleMetaData(this, jdbcApplication, element, defaultRight);
left.init(right);
right.init(left);
}
else
{
Element leftElement = getEJBRelationshipRoleElement(element, defaultLeft);
left = new JDBCRelationshipRoleMetaData(this, jdbcApplication, leftElement, defaultLeft);
Element rightElement = getEJBRelationshipRoleElement(element, defaultRight);
right = new JDBCRelationshipRoleMetaData(this, jdbcApplication, rightElement, defaultRight);
left.init(right, leftElement);
right.init(left, rightElement);
}
this.relationName = getNonNullRelationName(left, right, defaultValues.getRelationName());
// at least one side of a fk relation must have keys
if (isForeignKeyMappingStyle() && left.getKeyFields().isEmpty() && right.getKeyFields().isEmpty())
{
throw new DeploymentException("Atleast one role of a foreign-key "
+ "mapped relationship must have key fields " + "(or <primkey-field> is missing from ejb-jar.xml): "
+ "ejb-relation-name=" + relationName);
}
// both sides of a table relation must have keys
if (isTableMappingStyle() && (left.getKeyFields().isEmpty() || right.getKeyFields().isEmpty()))
{
throw new DeploymentException("Both roles of a relation-table " + "mapped relationship must have key fields: "
+ "ejb-relation-name=" + relationName);
}
}
private int loadMappingStyle(Element element, JDBCRelationMetaData defaultValues) throws DeploymentException
{
// if defaults check for preferred-relation-mapping
if ("defaults".equals(element.getTagName()))
{
// set mapping style based on preferred-relation-mapping (if possible)
String perferredRelationMapping = MetaData.getOptionalChildContent(element, "preferred-relation-mapping");
if ("relation-table".equals(perferredRelationMapping) || defaultValues.isManyToMany())
{
return TABLE;
}
else
{
return FOREIGN_KEY;
}
}
// check for table mapping style
if (MetaData.getOptionalChild(element, "relation-table-mapping") != null)
{
return TABLE;
}
// check for foreign-key mapping style
if (MetaData.getOptionalChild(element, "foreign-key-mapping") != null)
{
if (defaultValues.isManyToMany())
{
throw new DeploymentException("Foreign key mapping-style "
+ "is not allowed for many-to-many relationsips.");
}
return FOREIGN_KEY;
}
// no mapping style element, will use defaultValues
return defaultValues.mappingStyle;
}
private static Element getMappingElement(Element element) throws DeploymentException
{
// if defaults check for preferred-relation-mapping
if ("defaults".equals(element.getTagName()))
{
return element;
}
// check for table mapping style
Element tableMappingElement = MetaData.getOptionalChild(element, "relation-table-mapping");
if (tableMappingElement != null)
{
return tableMappingElement;
}
// check for foreign-key mapping style
Element foreignKeyMappingElement = MetaData.getOptionalChild(element, "foreign-key-mapping");
if (foreignKeyMappingElement != null)
{
return foreignKeyMappingElement;
}
return null;
}
private static Element getEJBRelationshipRoleElement(Element element, JDBCRelationshipRoleMetaData defaultRole)
throws DeploymentException
{
String roleName = defaultRole.getRelationshipRoleName();
if (roleName == null)
throw new DeploymentException("No ejb-relationship-role-name element found");
Iterator iter = MetaData.getChildrenByTagName(element, "ejb-relationship-role");
if (!iter.hasNext())
{
throw new DeploymentException("No ejb-relationship-role " + "elements found");
}
Element roleElement = null;
for (int i = 0; iter.hasNext(); i++)
{
// only 2 roles are allowed
if (i > 1)
{
throw new DeploymentException("Expected only 2 " + "ejb-relationship-role but found more then 2");
}
Element tempElement = (Element) iter.next();
if (roleName.equals(MetaData.getUniqueChildContent(tempElement, "ejb-relationship-role-name")))
{
roleElement = tempElement;
}
}
if (roleElement == null)
{
throw new DeploymentException("An ejb-relationship-role element was " + "not found for role '" + roleName
+ "'");
}
return roleElement;
}
/**
* Gets the relation name. Relation name is loaded from the
* ejb-relation-name element.
*
* @return the name of this relation
*/
public String getRelationName()
{
return relationName;
}
/**
* Gets the left jdbc relationship role. The relationship role is loaded
* from an ejb-relationship-role. Left/right assignment is completely
* arbitrary.
*
* @return the left JDBCRelationshipRoleMetaData
*/
public JDBCRelationshipRoleMetaData getLeftRelationshipRole()
{
return left;
}
/**
* Gets the right jdbc relationship role. The relationship role is loaded
* from an ejb-relationship-role. Left/right assignment is completely
* arbitrary.
*
* @return the right JDBCRelationshipRoleMetaData
*/
public JDBCRelationshipRoleMetaData getRightRelationshipRole()
{
return right;
}
/**
* Gets the relationship role related to the specified role.
*
* @param role the relationship role that the related role is desired
* @return the relationship role related to the specified role. right role
* of this relation
*/
public JDBCRelationshipRoleMetaData getOtherRelationshipRole(JDBCRelationshipRoleMetaData role)
{
if (left == role)
{
return right;
}
else if (right == role)
{
return left;
}
else
{
throw new IllegalArgumentException("Specified role is not the left " + "or right role. role=" + role);
}
}
/**
* Should this relation be mapped to a relation table.
*
* @return true if this relation is mapped to a table
*/
public boolean isTableMappingStyle()
{
return mappingStyle == TABLE;
}
/**
* Should this relation use foreign keys for storage.
*
* @return true if this relation is mapped to foreign keys
*/
public boolean isForeignKeyMappingStyle()
{
return mappingStyle == FOREIGN_KEY;
}
/**
* Gets the name of the datasource in jndi for this entity
*
* @return the name of datasource in jndi
*/
private String getDataSourceName()
{
return dataSourceName;
}
/**
* Gets the jdbc type mapping for this entity
*
* @return the jdbc type mapping for this entity
*/
public JDBCTypeMappingMetaData getTypeMapping() throws DeploymentException
{
if(datasourceMapping == null)
{
throw new DeploymentException("type-mapping is not initialized: " + dataSourceName
+ " was not deployed or type-mapping was not configured.");
}
return datasourceMapping;
}
/**
* Gets the name of the relation table.
*
* @return the name of the relation table to which is relation is mapped
*/
public String getDefaultTableName()
{
return tableName;
}
/**
* Gets the (user-defined) SQL commands that should be issued to the db
* after table creation.
*
* @return the SQL command
*/
public ArrayList getDefaultTablePostCreateCmd()
{
return tablePostCreateCmd;
}
/**
* Does the table exist yet? This does not mean that table has been created
* by the appilcation, or the the database metadata has been checked for the
* existance of the table, but that at this point the table is assumed to
* exist.
*
* @return true if the table exists
*/
public boolean isTableCreated()
{
return tableCreated;
}
public void setTableCreated()
{
tableCreated = true;
}
/**
* Sets table dropped flag.
*/
public void setTableDropped()
{
this.tableDropped = true;
}
public boolean isTableDropped()
{
return tableDropped;
}
/**
* Should the relation table be created on startup.
*
* @return true if the store mananager should attempt to create the relation
* table
*/
public boolean getCreateTable()
{
return createTable;
}
/**
* Should the relation table be removed on shutdown.
*
* @return true if the store mananager should attempt to remove the relation
* table
*/
public boolean getRemoveTable()
{
return removeTable;
}
/**
* Should the relation table be altered on deploy.
*/
public boolean getAlterTable()
{
return alterTable;
}
/**
* When the relation table is created, should it have a primary key
* constraint.
*
* @return true if the store mananager should add a primary key constraint
* to the the create table sql statement
*/
public boolean hasPrimaryKeyConstraint()
{
return primaryKeyConstraint;
}
/**
* Is this relation read-only?
*/
public boolean isReadOnly()
{
return readOnly;
}
/**
* Gets the read time out length.
*/
public int getReadTimeOut()
{
return readTimeOut;
}
/**
* Should select queries do row locking
*/
public boolean hasRowLocking()
{
return rowLocking;
}
private String createDefaultTableName()
{
String defaultTableName = left.getEntity().getName();
if (left.getCMRFieldName() != null)
{
defaultTableName += "_" + left.getCMRFieldName();
}
defaultTableName += "_" + right.getEntity().getName();
if (right.getCMRFieldName() != null)
{
defaultTableName += "_" + right.getCMRFieldName();
}
return defaultTableName;
}
private boolean isManyToMany()
{
return left.isMultiplicityMany() && right.isMultiplicityMany();
}
public synchronized DataSource getDataSource()
{
if (dataSource == null)
{
try
{
InitialContext context = new InitialContext();
dataSource = (DataSource) context.lookup(dataSourceName);
}
catch (NamingException e)
{
throw new EJBException("Data source for relationship named " + relationName + " not found "
+ dataSourceName);
}
}
return dataSource;
}
private String getNonNullRelationName(JDBCRelationshipRoleMetaData left,
JDBCRelationshipRoleMetaData right,
String relationName)
{
// JBossCMP needs ejb-relation-name if jbosscmp-jdbc.xml is used to map relationships.
if(relationName == null)
{
// generate unique name, we can't rely on ejb-relationship-role-name being unique
relationName = left.getEntity().getName() +
(!left.isNavigable() ? "" : "_" + left.getCMRFieldName()) +
"-" +
right.getEntity().getName() +
(!right.isNavigable() ? "" : "_" + right.getCMRFieldName());
}
return relationName;
}
}