/**********************************************************************
Copyright (c) 2005 Andy Jefferson and others. All rights reserved.
Licensed 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.
Contributors:
...
**********************************************************************/
package org.jpox.store.rdbms.scostore;
import java.lang.reflect.Array;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.jpox.ClassLoaderResolver;
import org.jpox.ManagedConnection;
import org.jpox.ObjectManager;
import org.jpox.StateManager;
import org.jpox.exceptions.JPOXDataStoreException;
import org.jpox.exceptions.JPOXUserException;
import org.jpox.metadata.AbstractMemberMetaData;
import org.jpox.metadata.ArrayMetaData;
import org.jpox.store.mapped.DatastoreClass;
import org.jpox.store.mapped.DatastoreContainerObject;
import org.jpox.store.mapped.DatastoreIdentifier;
import org.jpox.store.mapped.expression.LogicSetExpression;
import org.jpox.store.mapped.expression.QueryExpression;
import org.jpox.store.mapped.expression.ScalarExpression;
import org.jpox.store.mapped.mapping.JavaTypeMapping;
import org.jpox.store.mapped.mapping.MappingConsumer;
import org.jpox.store.mapped.mapping.Mappings;
import org.jpox.store.query.IncompatibleQueryElementTypeException;
import org.jpox.store.rdbms.RDBMSManager;
import org.jpox.store.rdbms.SQLController;
import org.jpox.store.rdbms.mapping.RDBMSMapping;
import org.jpox.store.rdbms.query.UnionIteratorStatement;
import org.jpox.util.JPOXLogger;
/**
* Backing store for an array that is formed by a foreign key in the table of the
* element type. Only supported when the element is a PersistenceCapable type (since
* that has its own element table, capable of having a FK!)
*
* @version $Revision: 1.33 $
*/
public class FKArrayStore extends AbstractArrayStore
{
/** Statement for updating a foreign key in a 1-N unidirectional */
private String updateFkStmt;
/** Statement for nullifying a FK in the element. */
private String clearNullifyStmt;
/**
* Constructor.
* @param fmd Field MetaData for the field that this represents
* @param storeMgr The Store Manager in use
* @param clr The ClassLoaderResolver
*/
public FKArrayStore(AbstractMemberMetaData fmd, RDBMSManager storeMgr, ClassLoaderResolver clr)
{
super(storeMgr, clr);
setOwnerMemberMetaData(fmd);
ArrayMetaData arrmd = fmd.getArray();
if (arrmd == null)
{
throw new JPOXUserException(LOCALISER.msg("056000", fmd.getFullFieldName()));
}
// Load the element class
elementType = fmd.getType().getComponentType().getName();
Class element_class = clr.classForName(elementType);
if (storeMgr.getOMFContext().getTypeManager().isReferenceType(element_class))
{
// Take the metadata for the first implementation of the reference type
emd = storeMgr.getOMFContext().getMetaDataManager().getMetaDataForImplementationOfReference(element_class,null,clr);
if (emd != null)
{
// Pretend we have a relationship with this one implementation
elementType = emd.getFullClassName();
}
}
else
{
// Check that the element class has MetaData
emd = storeMgr.getOMFContext().getMetaDataManager().getMetaDataForClass(element_class, clr);
}
if (emd == null)
{
throw new JPOXUserException(LOCALISER.msg("056003", element_class.getName(), fmd.getFullFieldName()));
}
elementInfo = getElementInformationForClass();
if (elementInfo != null && elementInfo.length > 1)
{
throw new JPOXUserException(LOCALISER.msg("056045",
ownerMemberMetaData.getFullFieldName()));
}
elementMapping = elementInfo[0].getDatastoreClass().getIDMapping(); // Just use the first element type as the guide for the element mapping
elementsAreEmbedded = false; // Can't embed element when using FK relation
elementsAreSerialised = false; // Can't serialise element when using FK relation
// Get the field in the element table (if any)
String mappedByFieldName = fmd.getMappedBy();
if (mappedByFieldName != null)
{
// 1-N FK bidirectional
// The element class has a field for the owner.
AbstractMemberMetaData eofmd = storeMgr.getOMFContext().getMetaDataManager().getMetaDataForMember(element_class, clr, mappedByFieldName);
if (eofmd == null)
{
throw new JPOXUserException(LOCALISER.msg("056024", fmd.getFullFieldName(),
mappedByFieldName, element_class.getName()));
}
// Check that the type of the element "mapped-by" field is consistent with the owner type
if (!clr.isAssignableFrom(eofmd.getType(), fmd.getAbstractClassMetaData().getFullClassName()))
{
throw new JPOXUserException(LOCALISER.msg("056025", fmd.getFullFieldName(),
eofmd.getFullFieldName(), eofmd.getTypeName(), fmd.getAbstractClassMetaData().getFullClassName()));
}
String ownerFieldName = eofmd.getName();
ownerMapping = elementInfo[0].getDatastoreClass().getFieldMapping(eofmd);
if (ownerMapping == null)
{
throw new JPOXUserException(LOCALISER.msg("056046",
fmd.getAbstractClassMetaData().getFullClassName(), fmd.getName(), elementType, ownerFieldName));
}
if (isEmbeddedMapping(ownerMapping))
{
throw new JPOXUserException(LOCALISER.msg("056026",
ownerFieldName, elementType, eofmd.getTypeName(), fmd.getClassName()));
}
}
else
{
// 1-N FK unidirectional
// The element class knows nothing about the owner (but the table has external mappings)
ownerMapping = elementInfo[0].getDatastoreClass().getExternalMapping(fmd, MappingConsumer.MAPPING_TYPE_EXTERNAL_FK);
if (ownerMapping == null)
{
throw new JPOXUserException(LOCALISER.msg("056047",
fmd.getAbstractClassMetaData().getFullClassName(), fmd.getName(), elementType));
}
}
orderMapping = elementInfo[0].getDatastoreClass().getExternalMapping(fmd, MappingConsumer.MAPPING_TYPE_EXTERNAL_INDEX);
if (orderMapping == null)
{
throw new JPOXUserException(LOCALISER.msg("056048",
fmd.getAbstractClassMetaData().getFullClassName(), fmd.getName(), elementType));
}
relationDiscriminatorMapping = elementInfo[0].getDatastoreClass().getExternalMapping(fmd, MappingConsumer.MAPPING_TYPE_EXTERNAL_FK_DISCRIM);
if (relationDiscriminatorMapping != null)
{
relationDiscriminatorValue = fmd.getValueForExtension("relation-discriminator-value");
if (relationDiscriminatorValue == null)
{
// No value defined so just use the field name
relationDiscriminatorValue = fmd.getFullFieldName();
}
}
// TODO Cater for multiple element tables
containerTable = elementInfo[0].getDatastoreClass();
if (fmd.getMappedBy() != null && ownerMapping.getDatastoreContainer() != containerTable)
{
// Element and owner don't have consistent tables so use the one with the mapping
// e.g collection is of subclass, yet superclass has the link back to the owner
containerTable = ownerMapping.getDatastoreContainer();
}
}
/**
* Generates the statement for clearing items by nulling the owner link out.
* The statement will be
* <PRE>
* UPDATE ARRAYTABLE SET OWNERCOL=NULL, INDEXCOL=NULL [,DISTINGUISHER=NULL]
* WHERE OWNERCOL=? [AND DISTINGUISHER=?]
* </PRE>
* when there is only one element table, and will be
* <PRE>
* UPDATE ? SET OWNERCOL=NULL, INDEXCOL=NULL [,DISTINGUISHER=NULL]
* WHERE OWNERCOL=? [AND DISTINGUISHER=?]
* </PRE>
* when there is more than 1 element table.
* @return The Statement for clearing items for the owner.
*/
protected String getClearNullifyStmt()
{
if (clearNullifyStmt == null)
{
StringBuffer stmt = new StringBuffer();
stmt.append("UPDATE ");
if (elementInfo.length > 1)
{
stmt.append("?");
}
else
{
stmt.append(elementInfo[0].getDatastoreClass().toString());
}
stmt.append(" SET ");
for (int i=0; i<ownerMapping.getNumberOfDatastoreFields(); i++)
{
if (i > 0)
{
stmt.append(", ");
}
stmt.append(ownerMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString() + " = NULL");
}
for (int i=0; i<orderMapping.getNumberOfDatastoreFields(); i++)
{
stmt.append(", ");
stmt.append(orderMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString() + " = NULL");
}
if (relationDiscriminatorMapping != null)
{
for (int i=0; i<relationDiscriminatorMapping.getNumberOfDatastoreFields(); i++)
{
stmt.append(", ");
stmt.append(relationDiscriminatorMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString() + " = NULL");
}
}
stmt.append(" WHERE ");
for (int i=0; i<ownerMapping.getNumberOfDatastoreFields(); i++)
{
if (i > 0)
{
stmt.append(" AND ");
}
stmt.append(ownerMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString());
stmt.append(" = ");
stmt.append(((RDBMSMapping)ownerMapping.getDataStoreMapping(i)).getUpdateInputParameter());
}
if (relationDiscriminatorMapping != null)
{
for (int i=0; i<relationDiscriminatorMapping.getNumberOfDatastoreFields(); i++)
{
stmt.append(" AND ");
stmt.append(relationDiscriminatorMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString());
stmt.append(" = ");
stmt.append(((RDBMSMapping)relationDiscriminatorMapping.getDataStoreMapping(i)).getUpdateInputParameter());
}
}
clearNullifyStmt = stmt.toString();
}
return clearNullifyStmt;
}
/**
* Generate statement for updating the owner, index columns in an inverse 1-N.
* Will result in the statement
* <PRE>
* UPDATE ELEMENTTABLE SET FK_COL_1=?, FK_COL_2=?, FK_IDX=? [,DISTINGUISHER=?]
* WHERE ELEMENT_ID=?
* </PRE>
* when we have a single element table, and
* <PRE>
* UPDATE ? SET FK_COL_1=?, FK_COL_2=?, FK_IDX=? [,DISTINGUISHER=?]
* WHERE ELEMENT_ID=?
* </PRE>
* when we have multiple element tables possible.
* @return Statement for updating the owner/index of an element in an inverse 1-N
*/
private String getUpdateFkStmt()
{
if (updateFkStmt == null)
{
StringBuffer stmt = new StringBuffer();
stmt.append("UPDATE ");
if (elementInfo.length > 1)
{
stmt.append("?");
}
else
{
stmt.append(elementInfo[0].getDatastoreClass().toString());
}
stmt.append(" SET ");
for (int i=0; i<ownerMapping.getNumberOfDatastoreFields(); i++)
{
if (i > 0)
{
stmt.append(",");
}
stmt.append(ownerMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString());
stmt.append(" = ");
stmt.append(((RDBMSMapping)ownerMapping.getDataStoreMapping(i)).getUpdateInputParameter());
}
for (int i=0; i<orderMapping.getNumberOfDatastoreFields(); i++)
{
stmt.append(",");
stmt.append(orderMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString());
stmt.append(" = ");
stmt.append(((RDBMSMapping)orderMapping.getDataStoreMapping(i)).getUpdateInputParameter());
}
if (relationDiscriminatorMapping != null)
{
for (int i=0; i<relationDiscriminatorMapping.getNumberOfDatastoreFields(); i++)
{
stmt.append(",");
stmt.append(relationDiscriminatorMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString());
stmt.append(" = ");
stmt.append(((RDBMSMapping)relationDiscriminatorMapping.getDataStoreMapping(i)).getUpdateInputParameter());
}
}
stmt.append(" WHERE ");
for (int i=0; i<elementMapping.getNumberOfDatastoreFields(); i++)
{
if (i > 0)
{
stmt.append(" AND ");
}
stmt.append(elementMapping.getDataStoreMapping(i).getDatastoreField().getIdentifier().toString());
stmt.append(" = ");
stmt.append(((RDBMSMapping)elementMapping.getDataStoreMapping(i)).getUpdateInputParameter());
}
updateFkStmt = stmt.toString();
}
return updateFkStmt;
}
/**
* Update a FK and element position in the element.
* @param sm StateManager for the owner
* @param element The element to update
* @param owner The owner object to set in the FK
* @param index The index position (or -1 if not known)
* @return Whether it was performed successfully
*/
private boolean updateElementFk(StateManager sm, Object element, Object owner, int index)
{
if (element == null)
{
return false;
}
boolean retval;
ObjectManager om = sm.getObjectManager();
String updateFkStmt = getUpdateFkStmt();
try
{
ManagedConnection mconn = storeMgr.getConnection(om);
SQLController sqlControl = storeMgr.getSQLController();
try
{
PreparedStatement ps = sqlControl.getStatementForUpdate(mconn, updateFkStmt, false);
try
{
int jdbcPosition = 1;
if (elementInfo.length > 1)
{
DatastoreClass table = storeMgr.getDatastoreClass(element.getClass().getName(), clr);
if (table != null)
{
ps.setString(jdbcPosition++, table.toString());
}
else
{
JPOXLogger.PERSISTENCE.info(">> InverseArrayStore.updateElementFK : need to set table in statement but dont know table where to store " + element);
}
}
if (owner == null)
{
ownerMapping.setObject(om, ps, Mappings.getParametersIndex(jdbcPosition, ownerMapping), null);
jdbcPosition += ownerMapping.getNumberOfDatastoreFields();
}
else
{
jdbcPosition = populateOwnerInStatement(sm, om, ps, jdbcPosition);
}
jdbcPosition = populateOrderInStatement(om, ps, index, jdbcPosition);
if (relationDiscriminatorMapping != null)
{
jdbcPosition = populateRelationDiscriminatorInStatement(om, ps, jdbcPosition);
}
jdbcPosition = populateElementInStatement(om, ps, element, jdbcPosition);
sqlControl.executeStatementUpdate(mconn, updateFkStmt, ps, true);
retval = true;
}
finally
{
sqlControl.closeStatement(mconn, ps);
}
}
finally
{
mconn.release();
}
}
catch (SQLException e)
{
throw new JPOXDataStoreException(LOCALISER.msg("056027", updateFkStmt),e);
}
return retval;
}
/**
* Method to clear the Array.
* This is called when the container object is being deleted and the elements are to be removed (maybe for dependent field).
* @param ownerSM The state manager
**/
public void clear(StateManager ownerSM)
{
boolean deleteElements = false;
if (ownerMemberMetaData.getArray().isDependentElement())
{
// Elements are dependent and can't exist on their own, so delete them all
JPOXLogger.DATASTORE.debug(LOCALISER.msg("056034"));
deleteElements = true;
}
else
{
if (ownerMapping.isNullable() && orderMapping.isNullable())
{
// Field is not dependent, and nullable so we null the FK
JPOXLogger.DATASTORE.debug(LOCALISER.msg("056036"));
deleteElements = false;
}
else
{
// Field is not dependent, and not nullable so we just delete the elements
JPOXLogger.DATASTORE.debug(LOCALISER.msg("056035"));
deleteElements = true;
}
}
if (deleteElements)
{
ownerSM.getObjectManager().getApiAdapter().isLoaded(ownerSM, ownerMemberMetaData.getAbsoluteFieldNumber()); // Make sure the field is loaded
Object[] value = (Object[]) ownerSM.provideField(ownerMemberMetaData.getAbsoluteFieldNumber());
if (value != null && value.length > 0)
{
ownerSM.getObjectManager().deleteObjects(value);
}
}
else
{
// TODO If the relation is bidirectional we need to clear the owner in the element
String clearNullifyStmt = getClearNullifyStmt();
try
{
ObjectManager om = ownerSM.getObjectManager();
ManagedConnection mconn = storeMgr.getConnection(om);
SQLController sqlControl = storeMgr.getSQLController();
try
{
PreparedStatement ps = sqlControl.getStatementForUpdate(mconn, clearNullifyStmt, false);
try
{
int jdbcPosition = 1;
jdbcPosition = populateOwnerInStatement(ownerSM, om, ps, jdbcPosition);
if (relationDiscriminatorMapping != null)
{
jdbcPosition = populateRelationDiscriminatorInStatement(om, ps, jdbcPosition);
}
sqlControl.executeStatementUpdate(mconn, clearNullifyStmt, ps, true);
}
finally
{
sqlControl.closeStatement(mconn, ps);
}
}
finally
{
mconn.release();
}
}
catch (SQLException e)
{
throw new JPOXDataStoreException(LOCALISER.msg("056013",clearNullifyStmt),e);
}
}
}
/**
* Method to set the array for the specified owner to the passed value.
* @param ownerSM State Manager for the owner
* @param array the array
* @return Whether the array was updated successfully
*/
public boolean set(StateManager ownerSM, Object array)
{
if (array == null)
{
return true;
}
// Check that all elements are inserted
for (int i=0;i<Array.getLength(array);i++)
{
validateElementForWriting(ownerSM, Array.get(array, i), null);
}
// Update the FK and position of all elements
int length = Array.getLength(array);
for (int i=0;i<length;i++)
{
Object obj = Array.get(array, i);
updateElementFk(ownerSM, obj, ownerSM.getObject(), i);
}
return true;
}
/**
* Accessor for the iterator statement to retrieve the element(s) for the array.
* @param ownerSM The StateManager
* @return The Query Statement.
**/
protected QueryExpression getIteratorStatement(StateManager ownerSM)
{
if (elementInfo == null)
{
return null;
}
final ClassLoaderResolver clr = ownerSM.getObjectManager().getClassLoaderResolver();
QueryExpression stmt = null;
for (int i=0;i<elementInfo.length;i++)
{
final int elementNo = i;
Class elementCls = clr.classForName(elementType);
QueryExpression subStmt = new UnionIteratorStatement(
clr, elementCls, true, this.storeMgr,
elementCls, elementMapping, elementInfo[elementNo].getDatastoreClass(), false,
null, true, false).getQueryStatement(null);
if (stmt == null)
{
stmt = subStmt;
}
else
{
stmt.union(subStmt);
}
}
// Apply condition on join-table owner field to filter by owner
ScalarExpression ownerExpr = ownerMapping.newScalarExpression(stmt, stmt.getMainTableExpression());
ScalarExpression ownerVal = ownerMapping.newLiteral(stmt, ownerSM.getObject());
stmt.andCondition(ownerExpr.eq(ownerVal), true);
// Apply condition on distinguisher field to filter by distinguisher (when present)
if (relationDiscriminatorMapping != null)
{
ScalarExpression distinguisherExpr = relationDiscriminatorMapping.newScalarExpression(stmt, stmt.getMainTableExpression());
ScalarExpression distinguisherVal = relationDiscriminatorMapping.newLiteral(stmt, relationDiscriminatorValue);
stmt.andCondition(distinguisherExpr.eq(distinguisherVal), true);
}
// Order elements by their position in the array
ScalarExpression exprIndex[] = new ScalarExpression[orderMapping.getNumberOfDatastoreFields()];
boolean descendingOrder[] = new boolean[orderMapping.getNumberOfDatastoreFields()];
exprIndex = orderMapping.newScalarExpression(stmt, stmt.getMainTableExpression()).getExpressionList().toArray();
stmt.setOrdering(exprIndex, descendingOrder);
return stmt;
}
/**
* Method used in queries when contains() has been invoked.
* @param stmt The Query Statement
* @param parentStmt the parent Query Statement. If there is no parent, <code>parentStmt</code> must be equals to <code>stmt</code>
* @param ownerMapping the mapping for the owner.
* @param ownerTe Table Expression for the owner
* @param listTableAlias alias for the "List" table.
* @param filteredElementType The Class Type for the filtered element
* @param elmExpr The Expression for the element
* @param elementTableAlias The SQL alias to assign to the expression for the element table.
* @return expression to the join
**/
public ScalarExpression joinElementsTo(
QueryExpression stmt,
QueryExpression parentStmt,
JavaTypeMapping ownerMapping,
LogicSetExpression ownerTe,
DatastoreIdentifier listTableAlias,
Class filteredElementType,
ScalarExpression elmExpr,
DatastoreIdentifier elementTableAlias)
{
ClassLoaderResolver clr=stmt.getClassLoaderResolver();
if (!clr.isAssignableFrom(elementType,filteredElementType) &&
!clr.isAssignableFrom(filteredElementType,elementType))
{
throw new IncompatibleQueryElementTypeException(elementType, filteredElementType.getName());
}
// Join the element table on the owner ID column.
DatastoreContainerObject filteredElementTable = storeMgr.getDatastoreClass(filteredElementType.getName(), stmt.getClassLoaderResolver());
stmt.newTableExpression(filteredElementTable, elementTableAlias);
ScalarExpression ownerExpr = ownerMapping.newScalarExpression(stmt,ownerTe);
DatastoreIdentifier containerRangeVar = listTableAlias;
if (stmt.getTableExpression(containerRangeVar) == null)
{
containerRangeVar = elementTableAlias;
}
ScalarExpression ownerSetExpr = this.ownerMapping.newScalarExpression(stmt,stmt.getTableExpression(containerRangeVar));
stmt.newTableExpression(containerTable, containerRangeVar);
stmt.andCondition(ownerExpr.eq(ownerSetExpr),true);
JavaTypeMapping elementTableID = filteredElementTable.getIDMapping();
return elementTableID.newScalarExpression(stmt,stmt.getTableExpression(containerRangeVar));
}
}