/*
 * 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;
   }
}
