/* * 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.bridge; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import javax.sql.DataSource; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.HashMap; import java.util.Arrays; import java.rmi.RemoteException; import javax.ejb.EJBException; import javax.ejb.EJBLocalObject; import javax.ejb.EJBLocalHome; import javax.ejb.RemoveException; import javax.ejb.NoSuchObjectLocalException; import javax.transaction.Status; import javax.transaction.Synchronization; import javax.transaction.SystemException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import javax.transaction.RollbackException; import org.jboss.deployment.DeploymentException; import org.jboss.ejb.EntityCache; import org.jboss.ejb.EntityContainer; import org.jboss.ejb.EntityEnterpriseContext; import org.jboss.ejb.LocalProxyFactory; import org.jboss.ejb.plugins.cmp.bridge.EntityBridge; import org.jboss.ejb.plugins.cmp.bridge.FieldBridge; import org.jboss.ejb.plugins.cmp.jdbc.JDBCContext; import org.jboss.ejb.plugins.cmp.jdbc.JDBCStoreManager; import org.jboss.ejb.plugins.cmp.jdbc.JDBCType; import org.jboss.ejb.plugins.cmp.jdbc.SQLUtil; import org.jboss.ejb.plugins.cmp.jdbc.CascadeDeleteStrategy; import org.jboss.ejb.plugins.cmp.jdbc.RelationData; import org.jboss.ejb.plugins.cmp.jdbc.JDBCEntityPersistenceStore; import org.jboss.ejb.plugins.cmp.jdbc.JDBCParameterSetter; import org.jboss.ejb.plugins.cmp.jdbc.JDBCResultSetReader; import org.jboss.tm.TransactionLocal; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCCMPFieldMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCReadAheadMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCRelationMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCRelationshipRoleMetaData; import org.jboss.ejb.plugins.cmp.ejbql.Catalog; import org.jboss.ejb.plugins.lock.Entrancy; import org.jboss.invocation.InvocationType; import org.jboss.logging.Logger; import org.jboss.security.SecurityContext; /** * JDBCCMRFieldBridge a bean relationship. This class only supports * relationships between entities managed by a JDBCStoreManager in the same * application. * <p/> * Life-cycle: * Tied to the EntityBridge. * <p/> * Multiplicity: * One for each role that entity has. * * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a> * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a> * @version $Revision: 81030 $ */ public final class JDBCCMRFieldBridge extends JDBCAbstractCMRFieldBridge { /** * The entity bridge to which this cmr field belongs. */ private final JDBCEntityBridge entity; /** * The manager of this entity. */ private final JDBCStoreManager manager; /** * Metadata of the relationship role that this field represents. */ private final JDBCRelationshipRoleMetaData metadata; /** * The data source used to acess the relation table if relevant. */ private DataSource dataSource; /** * The relation table name if relevent. */ private String qualifiedTableName; private String tableName; /** * The key fields that this entity maintains in the relation table. */ private JDBCCMP2xFieldBridge[] tableKeyFields; /** * JDBCType for the foreign key fields. Basically, this is an ordered * merge of the JDBCType of the foreign key field. */ private JDBCType jdbcType; /** * The related entity's container. */ private WeakReference relatedContainerRef; /** * The related entity's jdbc store manager */ private JDBCStoreManager relatedManager; /** * The related entity. */ private JDBCEntityBridge relatedEntity; /** * The related entity's cmr field for this relationship. */ private JDBCCMRFieldBridge relatedCMRField; /** * da log. */ private final Logger log; /** * Foreign key fields of this entity (i.e., related entities pk fields) */ private JDBCCMP2xFieldBridge[] foreignKeyFields; /** * Indicates whether all FK fields are mapped to PK fields */ private boolean allFKFieldsMappedToPKFields; /** * This map contains related PK fields that are mapped through FK fields to this entity's PK fields */ private final Map relatedPKFieldsByMyPKFields = new HashMap(); /** * This map contains related PK fields keyed by FK fields */ private final Map relatedPKFieldsByMyFKFields = new HashMap(); /** * Indicates whether there are foreign key fields mapped to CMP fields */ private boolean hasFKFieldsMappedToCMPFields; // Map for lists of related PK values keyed by this side's PK values. // The values are put/removed by related entities when its fields representing // foreign key are changed. When entity with this CMR is created, this map is checked // for waiting for it entities. Relationship with waiting entities is established, // removing waiting entities' primary keys from the map. // NOTE: this map is used only for foreign key fields mapped to CMP fields. private final TransactionLocal relatedPKValuesWaitingForMyPK = new TransactionLocal() { protected Object initialValue() { return new HashMap(); } }; /** * FindByPrimaryKey method used to find related instances in case when FK fields mapped to PK fields */ private Method relatedFindByPrimaryKey; /** * index of the field in the JDBCContext */ private final int jdbcContextIndex; /** * cascade-delete strategy */ private CascadeDeleteStrategy cascadeDeleteStrategy; /** * This CMR field and its related CMR field share the same RelationDataManager */ private RelationDataManager relationManager; /** * Creates a cmr field for the entity based on the metadata. */ public JDBCCMRFieldBridge(JDBCEntityBridge entity, JDBCStoreManager manager, JDBCRelationshipRoleMetaData metadata) throws DeploymentException { this.entity = entity; this.manager = manager; this.metadata = metadata; this.jdbcContextIndex = ((JDBCEntityBridge) manager.getEntityBridge()).getNextJDBCContextIndex(); // Creat the log String categoryName = this.getClass().getName() + "." + manager.getMetaData().getName() + "."; if(metadata.getCMRFieldName() != null) { categoryName += metadata.getCMRFieldName(); } else { categoryName += metadata.getRelatedRole().getEntity().getName() + "-" + metadata.getRelatedRole().getCMRFieldName(); } this.log = Logger.getLogger(categoryName); } public RelationDataManager getRelationDataManager() { return relationManager; } public void resolveRelationship() throws DeploymentException { // // Set handles to the related entity's container, cache, // manager, and invoker // // Related Entity Name String relatedEntityName = metadata.getRelatedRole().getEntity().getName(); // Related Entity Catalog catalog = (Catalog) manager.getApplicationData("CATALOG"); relatedEntity = (JDBCEntityBridge) catalog.getEntityByEJBName(relatedEntityName); if(relatedEntity == null) { throw new DeploymentException("Related entity not found: " + "entity=" + entity.getEntityName() + ", " + "cmrField=" + getFieldName() + ", " + "relatedEntity=" + relatedEntityName); } // Related CMR Field JDBCCMRFieldBridge[] cmrFields = (JDBCCMRFieldBridge[]) relatedEntity.getCMRFields(); for(int i = 0; i < cmrFields.length; ++i) { JDBCCMRFieldBridge cmrField = cmrFields[i]; if(metadata.getRelatedRole() == cmrField.getMetaData()) { relatedCMRField = cmrField; break; } } // if we didn't find the related CMR field throw an exception // with a detailed message if(relatedCMRField == null) { String message = "Related CMR field not found in " + relatedEntity.getEntityName() + " for relationship from"; message += entity.getEntityName() + "."; if(getFieldName() != null) { message += getFieldName(); } else { message += "<no-field>"; } message += " to "; message += relatedEntityName + "."; if(metadata.getRelatedRole().getCMRFieldName() != null) { message += metadata.getRelatedRole().getCMRFieldName(); } else { message += "<no-field>"; } throw new DeploymentException(message); } // Related Manager relatedManager = (JDBCStoreManager) relatedEntity.getManager(); // Related Container EntityContainer relatedContainer = relatedManager.getContainer(); this.relatedContainerRef = new WeakReference(relatedContainer); // related findByPrimaryKey Class homeClass = (relatedContainer.getLocalHomeClass() != null ? relatedContainer.getLocalHomeClass() : relatedContainer.getHomeClass()); try { relatedFindByPrimaryKey = homeClass.getMethod("findByPrimaryKey", new Class[]{relatedEntity.getPrimaryKeyClass()}); } catch(Exception e) { throw new DeploymentException("findByPrimaryKey(" + relatedEntity.getPrimaryKeyClass().getName() + " pk) was not found in " + homeClass.getName()); } // // Initialize the key fields // if(metadata.getRelationMetaData().isTableMappingStyle()) { // initialize relation table key fields Collection tableKeys = metadata.getKeyFields(); List keyFieldsList = new ArrayList(tableKeys.size()); // first phase is to create fk fields Map pkFieldsToFKFields = new HashMap(tableKeys.size()); for(Iterator i = tableKeys.iterator(); i.hasNext();) { JDBCCMPFieldMetaData cmpFieldMetaData = (JDBCCMPFieldMetaData) i.next(); FieldBridge pkField = entity.getFieldByName(cmpFieldMetaData.getFieldName()); if(pkField == null) { throw new DeploymentException("Primary key not found for key-field " + cmpFieldMetaData.getFieldName()); } pkFieldsToFKFields.put(pkField, new JDBCCMP2xFieldBridge(manager, cmpFieldMetaData)); } // second step is to order fk fields to match the order of pk fields JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields(); for(int i = 0; i < pkFields.length; ++i) { Object fkField = pkFieldsToFKFields.get(pkFields[i]); if(fkField == null) { throw new DeploymentException("Primary key " + pkFields[i].getFieldName() + " is not mapped."); } keyFieldsList.add(fkField); } tableKeyFields = (JDBCCMP2xFieldBridge[]) keyFieldsList.toArray( new JDBCCMP2xFieldBridge[keyFieldsList.size()]); dataSource = metadata.getRelationMetaData().getDataSource(); } else { initializeForeignKeyFields(); dataSource = hasForeignKey() ? entity.getDataSource() : relatedEntity.getDataSource(); } // Fix table name // // This code doesn't work here... The problem each side will generate // the table name and this will only work for simple generation. qualifiedTableName = SQLUtil.fixTableName(metadata.getRelationMetaData().getDefaultTableName(), dataSource); tableName = SQLUtil.getTableNameWithoutSchema(qualifiedTableName); relationManager = relatedCMRField.initRelationManager(this); } /** * The third phase of deployment. The method is called when relationships are already resolved. * * @throws DeploymentException */ public void start() throws DeploymentException { cascadeDeleteStrategy = CascadeDeleteStrategy.getCascadeDeleteStrategy(this); } public boolean removeFromRelations(EntityEnterpriseContext ctx, Object[] oldRelationsRef) { load(ctx); FieldState fieldState = getFieldState(ctx); List value = fieldState.getValue(); boolean removed = false; if(!value.isEmpty()) { if(hasFKFieldsMappedToCMPFields) { if(isForeignKeyValid(value.get(0))) { cascadeDeleteStrategy.removedIds(ctx, oldRelationsRef, value); removed = true; } } else { cascadeDeleteStrategy.removedIds(ctx, oldRelationsRef, value); removed = true; } } return removed; } public void cascadeDelete(EntityEnterpriseContext ctx, List oldValues) throws RemoveException, RemoteException { cascadeDeleteStrategy.cascadeDelete(ctx, oldValues); } public boolean isBatchCascadeDelete() { return (cascadeDeleteStrategy instanceof CascadeDeleteStrategy.BatchCascadeDeleteStrategy); } /** * Gets the manager of this entity. */ public JDBCStoreManager getJDBCStoreManager() { return manager; } /** * Gets bridge for this entity. */ public JDBCAbstractEntityBridge getEntity() { return entity; } /** * Gets the metadata of the relationship role that this field represents. */ public JDBCRelationshipRoleMetaData getMetaData() { return metadata; } /** * Gets the relation metadata. */ public JDBCRelationMetaData getRelationMetaData() { return metadata.getRelationMetaData(); } /** * Gets the name of this field. */ public String getFieldName() { return metadata.getCMRFieldName(); } /** * Gets the name of the relation table if relevent. */ public String getQualifiedTableName() { return qualifiedTableName; } public String getTableName() { return tableName; } /** * Gets the datasource of the relation table if relevent. */ public DataSource getDataSource() { return dataSource; } /** * Gets the read ahead meta data. */ public JDBCReadAheadMetaData getReadAhead() { return metadata.getReadAhead(); } public JDBCType getJDBCType() { return jdbcType; } public boolean isPrimaryKeyMember() { return false; } /** * Does this cmr field have foreign keys. */ public boolean hasForeignKey() { return foreignKeyFields != null; } /** * Returns true if all FK fields are mapped to PK fields */ public boolean allFkFieldsMappedToPkFields() { return allFKFieldsMappedToPKFields; } /** * Is this a collection valued field. */ public boolean isCollectionValued() { return metadata.getRelatedRole().isMultiplicityMany(); } /** * Is this a single valued field. */ public boolean isSingleValued() { return metadata.getRelatedRole().isMultiplicityOne(); } /** * Gets the key fields that this entity maintains in the relation table. */ public JDBCFieldBridge[] getTableKeyFields() { return tableKeyFields; } /** * Gets the foreign key fields of this entity (i.e., related entities pk fields) */ public JDBCFieldBridge[] getForeignKeyFields() { return foreignKeyFields; } /** * The related entity's cmr field for this relationship. */ public JDBCAbstractCMRFieldBridge getRelatedCMRField() { return relatedCMRField; } /** * The related manger. */ public JDBCStoreManager getRelatedManager() { return relatedManager; } /** * The related entity. */ public EntityBridge getRelatedEntity() { return relatedEntity; } /** * The related entity. */ public JDBCEntityBridge getRelatedJDBCEntity() { return relatedEntity; } /** * The related container */ private final EntityContainer getRelatedContainer() { return (EntityContainer) relatedContainerRef.get(); } /** * The related entity's local home interface. */ public final Class getRelatedLocalInterface() { return getRelatedContainer().getLocalClass(); } /** * The related entity's local container invoker. */ public final LocalProxyFactory getRelatedInvoker() { return getRelatedContainer().getLocalProxyFactory(); } /** * @param ctx - entity's context * @return true if entity is loaded, false - otherwise. */ public boolean isLoaded(EntityEnterpriseContext ctx) { return getFieldState(ctx).isLoaded; } /** * Establishes relationships with related entities waited for passed in context * to be created. * * @param ctx - entity's context. */ public void addRelatedPKsWaitedForMe(EntityEnterpriseContext ctx) { final Map relatedPKsMap = getRelatedPKsWaitingForMyPK(); synchronized(relatedPKsMap) { List relatedPKsWaitingForMe = (List) relatedPKsMap.get(ctx.getId()); if(relatedPKsWaitingForMe != null) { for(Iterator waitingPKsIter = relatedPKsWaitingForMe.iterator(); waitingPKsIter.hasNext();) { Object waitingPK = waitingPKsIter.next(); waitingPKsIter.remove(); if(isForeignKeyValid(waitingPK)) { createRelationLinks(ctx, waitingPK); } } } } } /** * Is this field readonly? */ public boolean isReadOnly() { return getRelationMetaData().isReadOnly(); } /** * Had the read time expired? */ public boolean isReadTimedOut(EntityEnterpriseContext ctx) { // if we are read/write then we are always timed out if(!isReadOnly()) { return true; } // if read-time-out is -1 then we never time out. if(getRelationMetaData().getReadTimeOut() == -1) { return false; } long readInterval = System.currentTimeMillis() - getFieldState(ctx).getLastRead(); return readInterval > getRelationMetaData().getReadTimeOut(); } /** * @param ctx - entity's context. * @return the value of this field. */ public Object getValue(EntityEnterpriseContext ctx) { // no user checks yet, but this is where they would go return getInstanceValue(ctx); } /** * Sets new value. * * @param ctx - entity's context; * @param value - new value. */ public void setValue(EntityEnterpriseContext ctx, Object value) { if(isReadOnly()) { throw new EJBException("Field is read-only: fieldName=" + getFieldName()); } if(!JDBCEntityBridge.isEjbCreateDone(ctx)) { throw new IllegalStateException("A CMR field cannot be set " + "in ejbCreate; this should be done in the ejbPostCreate " + "method instead [EJB 2.0 Spec. 10.5.2]."); } if(isCollectionValued() && value == null) { throw new IllegalArgumentException("null cannot be assigned to a " + "collection-valued cmr-field [EJB 2.0 Spec. 10.3.8]."); } /* if(allFKFieldsMappedToPKFields) { throw new IllegalStateException( "Can't modify relationship: CMR field " + entity.getEntityName() + "." + getFieldName() + " has foreign key fields mapped to the primary key columns." + " Primary key may only be set once in ejbCreate [EJB 2.0 Spec. 10.3.5]."); } */ setInstanceValue(ctx, value); } /** * Gets the value of the cmr field for the instance associated with * the context. */ public Object getInstanceValue(EntityEnterpriseContext myCtx) { load(myCtx); FieldState fieldState = getFieldState(myCtx); if(isCollectionValued()) { return fieldState.getRelationSet(); } // only return one try { List value = fieldState.getValue(); if(!value.isEmpty()) { Object fk = value.get(0); return getRelatedEntityByFK(fk); } else if(foreignKeyFields != null) { // for those completely mapped to CMP fields and created in this current tx !!! Object relatedId = getRelatedIdFromContext(myCtx); if(relatedId != null) { return getRelatedEntityByFK(relatedId); } } return null; } catch(EJBException e) { throw e; } catch(Exception e) { throw new EJBException(e); } } /** * Returns related entity's local interface. * If there are foreign key fields mapped to CMP fields, existence of related entity is checked * with findByPrimaryKey and if, in this case, related instance is not found, null is returned. * If foreign key fields mapped to its own columns then existence of related entity is not checked * and just its local object is returned. * * @param fk - foreign key value. * @return related local object instance. */ public EJBLocalObject getRelatedEntityByFK(Object fk) { EJBLocalObject relatedLocalObject = null; final EntityContainer relatedContainer = getRelatedContainer(); if(hasFKFieldsMappedToCMPFields && relatedManager.getReadAheadCache().getPreloadDataMap(fk, false) == null // not in preload cache ) { EJBLocalHome relatedHome = relatedContainer.getLocalProxyFactory().getEJBLocalHome(); try { relatedLocalObject = (EJBLocalObject) relatedFindByPrimaryKey.invoke(relatedHome, new Object[]{fk}); } catch(Exception ignore) { // no such entity. it is ok to ignore } } else { relatedLocalObject = relatedContainer.getLocalProxyFactory().getEntityEJBLocalObject(fk); } return relatedLocalObject; } /** * This method is called only for CMR fields with foreign key fields mapped to CMP fields * to check the validity of the foreign key value. * * @param fk the foreign key to check * @return true if there is related entity with the equal primary key */ public boolean isForeignKeyValid(Object fk) { boolean valid; if(relatedManager.getReadAheadCache().getPreloadDataMap(fk, false) != null) { valid = true; } else { EJBLocalHome relatedHome = getRelatedContainer().getLocalProxyFactory().getEJBLocalHome(); try { relatedFindByPrimaryKey.invoke(relatedHome, new Object[]{fk}); valid = true; } catch(Exception ignore) { // no such entity. it is ok to ignore valid = false; } } return valid; } /** * Sets the value of the cmr field for the instance associated with * the context. */ public void setInstanceValue(EntityEnterpriseContext myCtx, Object newValue) { // validate new value first List newPks; if(newValue instanceof Collection) { Collection col = (Collection) newValue; if(!col.isEmpty()) { newPks = new ArrayList(col.size()); for(Iterator iter = col.iterator(); iter.hasNext();) { Object localObject = iter.next(); if(localObject != null) { Object relatedId = getRelatedPrimaryKey(localObject); // check whether new value modifies the primary key if there are FK fields mapped to PK fields if(relatedPKFieldsByMyPKFields.size() > 0) { checkSetForeignKey(myCtx, relatedId); } newPks.add(relatedId); } } } else { newPks = Collections.EMPTY_LIST; } } else { if(newValue != null) { newPks = Collections.singletonList(getRelatedPrimaryKey(newValue)); } else { newPks = Collections.EMPTY_LIST; } } // load the current value load(myCtx); FieldState fieldState = getFieldState(myCtx); // is this just setting our own relation set back if(newValue == fieldState.getRelationSet()) { return; } try { // Remove old value(s) List value = fieldState.getValue(); if(!value.isEmpty()) { Object[] curPks = value.toArray(new Object[value.size()]); for(int i = 0; i < curPks.length; ++i) { destroyRelationLinks(myCtx, curPks[i]); } } // Add new value(s) for(int i = 0; i < newPks.size(); ++i) { createRelationLinks(myCtx, newPks.get(i)); } } catch(RuntimeException e) { throw e; } catch(Exception e) { throw new EJBException(e); } } /** * Checks whether new foreign key value conflicts with primary key value * in case of foreign key to primary key mapping. * * @param myCtx - entity's context; * @param newValue - new foreign key value. * @throws IllegalStateException - if new foreign key value changes * primary key value, otherwise returns silently. */ private void checkSetForeignKey(EntityEnterpriseContext myCtx, Object newValue) throws IllegalStateException { JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields(); for(int i = 0; i < pkFields.length; ++i) { JDBCCMP2xFieldBridge pkField = (JDBCCMP2xFieldBridge) pkFields[i]; JDBCCMP2xFieldBridge relatedPkField = (JDBCCMP2xFieldBridge) relatedPKFieldsByMyPKFields.get(pkField); if(relatedPkField != null) { Object comingValue = relatedPkField.getPrimaryKeyValue(newValue); Object currentValue = pkField.getInstanceValue(myCtx); // they shouldn't be null if(!comingValue.equals(currentValue)) { throw new IllegalStateException("Can't create relationship: CMR field " + entity.getEntityName() + "." + getFieldName() + " has foreign key fields mapped to the primary key columns." + " Primary key may only be set once in ejbCreate [EJB 2.0 Spec. 10.3.5]." + " primary key value is " + currentValue + " overriding value is " + comingValue); } } } } /** * Creates the relation links between the instance associated with the * context and the related instance (just the id is passed in). * <p/> * This method calls a.addRelation(b) and b.addRelation(a) */ public void createRelationLinks(EntityEnterpriseContext myCtx, Object relatedId) { createRelationLinks(myCtx, relatedId, true); } public void createRelationLinks(EntityEnterpriseContext myCtx, Object relatedId, boolean updateForeignKey) { if(isReadOnly()) { throw new EJBException("Field is read-only: " + getFieldName()); } // If my multiplicity is one, then we need to free the new related context // from its old relationship. Transaction tx = getTransaction(); if(metadata.isMultiplicityOne()) { Object oldRelatedId = relatedCMRField.invokeGetRelatedId(tx, relatedId); if(oldRelatedId != null) { invokeRemoveRelation(tx, oldRelatedId, relatedId); relatedCMRField.invokeRemoveRelation(tx, relatedId, oldRelatedId); } } addRelation(myCtx, relatedId, updateForeignKey); relatedCMRField.invokeAddRelation(tx, relatedId, myCtx.getId()); } /** * Destroys the relation links between the instance associated with the * context and the related instance (just the id is passed in). * <p/> * This method calls a.removeRelation(b) and b.removeRelation(a) */ public void destroyRelationLinks(EntityEnterpriseContext myCtx, Object relatedId) { destroyRelationLinks(myCtx, relatedId, true); } /** * Destroys the relation links between the instance associated with the * context and the related instance (just the id is passed in). * <p/> * This method calls a.removeRelation(b) and b.removeRelation(a) * <p/> * If updateValueCollection is false, the related id collection is not * updated. This form is only used by the RelationSet iterator. */ public void destroyRelationLinks(EntityEnterpriseContext myCtx, Object relatedId, boolean updateValueCollection) { destroyRelationLinks(myCtx, relatedId, updateValueCollection, true); } public void destroyRelationLinks(EntityEnterpriseContext myCtx, Object relatedId, boolean updateValueCollection, boolean updateForeignKey) { if(isReadOnly()) { throw new EJBException("Field is read-only: " + getFieldName()); } removeRelation(myCtx, relatedId, updateValueCollection, updateForeignKey); relatedCMRField.invokeRemoveRelation(getTransaction(), relatedId, myCtx.getId()); } /** * Schedules children for cascade delete. */ public void scheduleChildrenForCascadeDelete(EntityEnterpriseContext ctx) { load(ctx); FieldState fieldState = getFieldState(ctx); List value = fieldState.getValue(); if(!value.isEmpty()) { Transaction tx = getTransaction(); for(int i = 0; i < value.size(); ++i) { relatedCMRField.invokeScheduleForCascadeDelete(tx, value.get(i)); } } } /** * Schedules children for batch cascade delete. */ public void scheduleChildrenForBatchCascadeDelete(EntityEnterpriseContext ctx) { load(ctx); FieldState fieldState = getFieldState(ctx); List value = fieldState.getValue(); if(!value.isEmpty()) { Transaction tx = getTransaction(); for(int i = 0; i < value.size(); ++i) { relatedCMRField.invokeScheduleForBatchCascadeDelete(tx, value.get(i)); } } } /** * Schedules the instance with myId for cascade delete. */ private Object invokeScheduleForCascadeDelete(Transaction tx, Object myId) { try { EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache(); SecurityContext sc = SecurityActions.getSecurityContext(); CMRInvocation invocation = new CMRInvocation(); invocation.setCmrMessage(CMRMessage.SCHEDULE_FOR_CASCADE_DELETE); invocation.setEntrancy(Entrancy.NON_ENTRANT); invocation.setId(instanceCache.createCacheKey(myId)); invocation.setArguments(new Object[]{this}); invocation.setTransaction(tx); invocation.setPrincipal(sc.getUtil().getUserPrincipal()); invocation.setCredential(sc.getUtil().getCredential()); invocation.setType(InvocationType.LOCAL); return manager.getContainer().invoke(invocation); } catch(EJBException e) { throw e; } catch(Exception e) { throw new EJBException("Error in scheduleForCascadeDelete()", e); } } /** * Schedules the instance with myId for batch cascade delete. */ private Object invokeScheduleForBatchCascadeDelete(Transaction tx, Object myId) { try { EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache(); SecurityContext sc = SecurityActions.getSecurityContext(); CMRInvocation invocation = new CMRInvocation(); invocation.setCmrMessage(CMRMessage.SCHEDULE_FOR_BATCH_CASCADE_DELETE); invocation.setEntrancy(Entrancy.NON_ENTRANT); invocation.setId(instanceCache.createCacheKey(myId)); invocation.setArguments(new Object[]{this}); invocation.setTransaction(tx); invocation.setPrincipal(sc.getUtil().getUserPrincipal()); invocation.setCredential(sc.getUtil().getCredential()); invocation.setType(InvocationType.LOCAL); return manager.getContainer().invoke(invocation); } catch(EJBException e) { throw e; } catch(Exception e) { throw new EJBException("Error in scheduleForBatchCascadeDelete()", e); } } /** * Invokes the getRelatedId on the related CMR field via the container * invocation interceptor chain. */ private Object invokeGetRelatedId(Transaction tx, Object myId) { try { EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache(); SecurityContext sc = SecurityActions.getSecurityContext(); CMRInvocation invocation = new CMRInvocation(); invocation.setCmrMessage(CMRMessage.GET_RELATED_ID); invocation.setEntrancy(Entrancy.NON_ENTRANT); invocation.setId(instanceCache.createCacheKey(myId)); invocation.setArguments(new Object[]{this}); invocation.setTransaction(tx); invocation.setPrincipal(sc.getUtil().getUserPrincipal()); invocation.setCredential(sc.getUtil().getCredential()); invocation.setType(InvocationType.LOCAL); return manager.getContainer().invoke(invocation); } catch(EJBException e) { throw e; } catch(Exception e) { throw new EJBException("Error in getRelatedId", e); } } /** * Invokes the addRelation on the related CMR field via the container * invocation interceptor chain. */ private void invokeAddRelation(Transaction tx, Object myId, Object relatedId) { try { SecurityContext sc = SecurityActions.getSecurityContext(); EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache(); CMRInvocation invocation = new CMRInvocation(); invocation.setCmrMessage(CMRMessage.ADD_RELATION); invocation.setEntrancy(Entrancy.NON_ENTRANT); invocation.setId(instanceCache.createCacheKey(myId)); invocation.setArguments(new Object[]{this, relatedId}); invocation.setTransaction(tx); invocation.setPrincipal(sc.getUtil().getUserPrincipal()); invocation.setCredential(sc.getUtil().getCredential()); invocation.setType(InvocationType.LOCAL); manager.getContainer().invoke(invocation); } catch(EJBException e) { throw e; } catch(Exception e) { throw new EJBException("Error in addRelation", e); } } /** * Invokes the removeRelation on the related CMR field via the container * invocation interceptor chain. */ private void invokeRemoveRelation(Transaction tx, Object myId, Object relatedId) { try { EntityCache instanceCache = (EntityCache) manager.getContainer().getInstanceCache(); SecurityContext sc = SecurityActions.getSecurityContext(); CMRInvocation invocation = new CMRInvocation(); invocation.setCmrMessage(CMRMessage.REMOVE_RELATION); invocation.setEntrancy(Entrancy.NON_ENTRANT); invocation.setId(instanceCache.createCacheKey(myId)); invocation.setArguments(new Object[]{this, relatedId}); invocation.setTransaction(tx); invocation.setPrincipal(sc.getUtil().getUserPrincipal()); invocation.setCredential(sc.getUtil().getCredential()); invocation.setType(InvocationType.LOCAL); manager.getContainer().invoke(invocation); } catch(EJBException e) { throw e; } catch(Exception e) { throw new EJBException("Error in removeRelation", e); } } /** * Get the related entity's id. This only works on single valued cmr fields. */ public Object getRelatedId(EntityEnterpriseContext myCtx) { if(isCollectionValued()) { throw new EJBException("getRelatedId may only be called on a cmr-field with a multiplicity of one."); } load(myCtx); List value = getFieldState(myCtx).getValue(); return value.isEmpty() ? null : value.get(0); } /** * Creates a new instance of related id based on foreign key value in the context. * * @param ctx - entity's context. * @return related entity's id. */ public Object getRelatedIdFromContext(EntityEnterpriseContext ctx) { Object relatedId = null; Object fkFieldValue; for(int i = 0; i < foreignKeyFields.length; ++i) { JDBCCMP2xFieldBridge fkField = foreignKeyFields[i]; fkFieldValue = fkField.getInstanceValue(ctx); if(fkFieldValue == null) { return null; } JDBCCMP2xFieldBridge relatedPKField = (JDBCCMP2xFieldBridge) relatedPKFieldsByMyFKFields.get(fkField); relatedId = relatedPKField.setPrimaryKeyValue(relatedId, fkFieldValue); } return relatedId; } /** * Adds the foreign key to the set of related ids, and updates any foreign key fields. */ public void addRelation(EntityEnterpriseContext myCtx, Object fk) { addRelation(myCtx, fk, true); relationManager.addRelation(this, myCtx.getId(), relatedCMRField, fk); } private void addRelation(EntityEnterpriseContext myCtx, Object fk, boolean updateForeignKey) { checkSetForeignKey(myCtx, fk); if(isReadOnly()) { throw new EJBException("Field is read-only: " + getFieldName()); } if(!JDBCEntityBridge.isEjbCreateDone(myCtx)) { throw new IllegalStateException("A CMR field cannot be set or added " + "to a relationship in ejbCreate; this should be done in the " + "ejbPostCreate method instead [EJB 2.0 Spec. 10.5.2]."); } // add to current related set FieldState myState = getFieldState(myCtx); myState.addRelation(fk); // set the foreign key, if we have one. if(hasForeignKey() && updateForeignKey) { setForeignKey(myCtx, fk); } } /** * Removes the foreign key to the set of related ids, and updates any foreign key fields. */ public void removeRelation(EntityEnterpriseContext myCtx, Object fk) { removeRelation(myCtx, fk, true, true); relationManager.removeRelation(this, myCtx.getId(), relatedCMRField, fk); } private void removeRelation(EntityEnterpriseContext myCtx, Object fk, boolean updateValueCollection, boolean updateForeignKey) { if(isReadOnly()) { throw new EJBException("Field is read-only: " + getFieldName()); } // remove from current related set if(updateValueCollection) { FieldState myState = getFieldState(myCtx); myState.removeRelation(fk); } // set the foreign key to null, if we have one. if(hasForeignKey() && updateForeignKey) { setForeignKey(myCtx, null); } } /** * loads the collection of related ids * NOTE: after loading, the field might not be in a clean state as we support adding and removing * relations while the field is not loaded. The actual value of the field will be the value loaded * plus added relations and minus removed relations while the field was not loaded. */ private void load(EntityEnterpriseContext myCtx) { // if we are already loaded we're done FieldState fieldState = getFieldState(myCtx); if(fieldState.isLoaded()) { return; } // check the preload cache if(log.isTraceEnabled()) { log.trace("Read ahead cahce load: cmrField=" + getFieldName() + " pk=" + myCtx.getId()); } manager.getReadAheadCache().load(myCtx); if(fieldState.isLoaded()) { return; } // load the value from the database Collection values; if(hasForeignKey()) { // WARN: this method will load foreign keys if they are not yet loaded and // changes relationship lazy loading in advanced training labs. // i.e. it will load lazy cmp fields first of this entity and then will lazy load the related entity // instead of loading this entity JOIN related entity in one query. //Object fk = getRelatedIdFromContext(myCtx); boolean loadWithManager = false; Object fk = null; for(int i = 0; i < foreignKeyFields.length; ++i) { JDBCCMP2xFieldBridge fkField = foreignKeyFields[i]; // if the field is not loaded then load relationship with manager if(!fkField.isLoaded(myCtx)) { loadWithManager = true; break; } Object fkFieldValue = fkField.getInstanceValue(myCtx); // if one of the fk is null, the whole fk is considered to be null if(fkFieldValue == null) { fk = null; break; } JDBCCMP2xFieldBridge relatedPKField = (JDBCCMP2xFieldBridge) relatedPKFieldsByMyFKFields.get(fkField); fk = relatedPKField.setPrimaryKeyValue(fk, fkFieldValue); } if(loadWithManager) { values = manager.loadRelation(this, myCtx.getId()); } else { values = (fk == null ? Collections.EMPTY_LIST : Collections.singletonList(fk)); } } else { values = manager.loadRelation(this, myCtx.getId()); } load(myCtx, values); } public void load(EntityEnterpriseContext myCtx, Collection values) { // did we get more then one value for a single valued field if(isSingleValued() && values.size() > 1) { throw new EJBException("Data contains multiple values, but this cmr field is single valued: " + values); } // add the new values FieldState fieldState = getFieldState(myCtx); fieldState.loadRelations(values); // set the foreign key, if we have one. if(hasForeignKey()) { // update the states and locked values of FK fields if(!values.isEmpty()) { Object loadedValue = values.iterator().next(); for(int i = 0; i < foreignKeyFields.length; ++i) { JDBCCMP2xFieldBridge fkField = foreignKeyFields[i]; Object fieldValue = fkField.getPrimaryKeyValue(loadedValue); fkField.updateState(myCtx, fieldValue); } } // set the real FK value List realValue = fieldState.getValue(); Object fk = realValue.isEmpty() ? null : realValue.get(0); setForeignKey(myCtx, fk); } JDBCEntityBridge.setCreated(myCtx); } /** * Sets the foreign key field value. */ public void setForeignKey(EntityEnterpriseContext myCtx, Object fk) { if(!hasForeignKey()) { throw new EJBException(getFieldName() + " CMR field does not have a foreign key to set."); } for(int i = 0; i < foreignKeyFields.length; ++i) { JDBCCMP2xFieldBridge fkField = foreignKeyFields[i]; Object fieldValue = fkField.getPrimaryKeyValue(fk); fkField.setInstanceValue(myCtx, fieldValue); } } /** * Initialized the foreign key fields. */ public void initInstance(EntityEnterpriseContext ctx) { // mark this field as loaded getFieldState(ctx).loadRelations(Collections.EMPTY_SET); if(foreignKeyFields == null) { return; } for(int i = 0; i < foreignKeyFields.length; ++i) { JDBCCMP2xFieldBridge foreignKeyField = foreignKeyFields[i]; if(!foreignKeyField.isFKFieldMappedToCMPField()) { foreignKeyField.setInstanceValue(ctx, null); } } } /** * resets the persistence context of the foreign key fields */ public void resetPersistenceContext(EntityEnterpriseContext ctx) { // only resetStats if the read has timed out if(!isReadTimedOut(ctx)) { return; } // clear the field state JDBCContext jdbcCtx = (JDBCContext) ctx.getPersistenceContext(); // invalidate current field state /* FieldState currentFieldState = (FieldState) jdbcCtx.getFieldState(jdbcContextIndex); if(currentFieldState != null) currentFieldState.invalidate(); */ jdbcCtx.setFieldState(jdbcContextIndex, null); if(foreignKeyFields == null) { return; } for(int i = 0; i < foreignKeyFields.length; ++i) { JDBCCMP2xFieldBridge foreignKeyField = foreignKeyFields[i]; if(!foreignKeyField.isFKFieldMappedToCMPField()) { foreignKeyField.resetPersistenceContext(ctx); } } } public int setInstanceParameters(PreparedStatement ps, int parameterIndex, EntityEnterpriseContext ctx) { if(foreignKeyFields == null) { return parameterIndex; } List value = getFieldState(ctx).getValue(); Object fk = (value.isEmpty() ? null : value.get(0)); for(int i = 0; i < foreignKeyFields.length; ++i) { parameterIndex = foreignKeyFields[i].setPrimaryKeyParameters(ps, parameterIndex, fk); } return parameterIndex; } public int loadInstanceResults(ResultSet rs, int parameterIndex, EntityEnterpriseContext ctx) { if(!hasForeignKey()) { return parameterIndex; } // load the value from the database Object[] ref = new Object[1]; parameterIndex = loadArgumentResults(rs, parameterIndex, ref); // only actually set the value if the state is not already loaded FieldState fieldState = getFieldState(ctx); if(!fieldState.isLoaded()) { if(ref[0] != null) { load(ctx, Collections.singleton(ref[0])); } else { load(ctx, Collections.EMPTY_SET); } } return parameterIndex; } public int loadArgumentResults(ResultSet rs, int parameterIndex, Object[] fkRef) { if(foreignKeyFields == null) { return parameterIndex; } boolean fkIsNull = false; // value of this field, will be filled in below Object[] argumentRef = new Object[1]; for(int i = 0; i < foreignKeyFields.length; ++i) { JDBCCMPFieldBridge field = foreignKeyFields[i]; parameterIndex = field.loadArgumentResults(rs, parameterIndex, argumentRef); if(fkIsNull) { continue; } if(field.getPrimaryKeyField() != null) { // if there is a null field among FK fields, the whole FK field is considered null. // NOTE: don't throw exception in this case, it's ok if FK is partly mapped to a PK // NOTE2: we still need to iterate through foreign key fields and 'load' them to // return correct parameterIndex. if(argumentRef[0] == null) { fkRef[0] = null; fkIsNull = true; } else { // if we don't have a pk object yet create one if(fkRef[0] == null) { fkRef[0] = relatedEntity.createPrimaryKeyInstance(); } try { // Set this field's value into the primary key object. field.getPrimaryKeyField().set(fkRef[0], argumentRef[0]); } catch(Exception e) { // Non recoverable internal exception throw new EJBException("Internal error setting foreign-key field " + getFieldName(), e); } } } else { // This field is the primary key, so no extraction is necessary. fkRef[0] = argumentRef[0]; } } return parameterIndex; } /** * This method is never called. * In case of a CMR with foreign key fields, only the foreign key fields are asked for the dirty state. */ public boolean isDirty(EntityEnterpriseContext ctx) { return foreignKeyFields == null ? relationManager.isDirty() : false; } public boolean invalidateCache(EntityEnterpriseContext ctx) { JDBCContext jdbcCtx = (JDBCContext) ctx.getPersistenceContext(); FieldState fieldState = (FieldState) jdbcCtx.getFieldState(jdbcContextIndex); return fieldState == null ? false : fieldState.isChanged(); } /** * This method is never called. * In case of a CMR * - with foreign key fields, the foreign key fields are cleaned when necessary according to CMP fields' * behaviour. * - from m:m relationship, added/removed key pairs are cleared in application tx data map on sync. */ public void setClean(EntityEnterpriseContext ctx) { throw new UnsupportedOperationException(); } public boolean isCMPField() { return false; } public JDBCEntityPersistenceStore getManager() { return manager; } public boolean hasFKFieldsMappedToCMPFields() { return hasFKFieldsMappedToCMPFields; } public void addRelatedPKWaitingForMyPK(Object myPK, Object relatedPK) { Map relatedPKsWaitingForMyPK = getRelatedPKsWaitingForMyPK(); synchronized(relatedPKsWaitingForMyPK) { List relatedPKs = (List) relatedPKsWaitingForMyPK.get(myPK); if(relatedPKs == null) { relatedPKs = new ArrayList(1); relatedPKsWaitingForMyPK.put(myPK, relatedPKs); } relatedPKs.add(relatedPK); } } public void removeRelatedPKWaitingForMyPK(Object myPK, Object relatedPK) { final Map relatedPKMap = getRelatedPKsWaitingForMyPK(); synchronized(relatedPKMap) { List relatedPKs = (List) relatedPKMap.get(myPK); if(relatedPKs != null) { relatedPKs.remove(relatedPK); } } } /** * Gets the field state object from the persistence context. */ private FieldState getFieldState(EntityEnterpriseContext ctx) { JDBCContext jdbcCtx = (JDBCContext) ctx.getPersistenceContext(); FieldState fieldState = (FieldState) jdbcCtx.getFieldState(jdbcContextIndex); if(fieldState == null) { fieldState = new FieldState(ctx); jdbcCtx.setFieldState(jdbcContextIndex, fieldState); } return fieldState; } /** * Initializes foreign key fields * * @throws DeploymentException */ private void initializeForeignKeyFields() throws DeploymentException { Collection foreignKeys = metadata.getRelatedRole().getKeyFields(); // temporary map used later to write fk fields in special order Map fkFieldsByRelatedPKFields = new HashMap(); for(Iterator i = foreignKeys.iterator(); i.hasNext();) { JDBCCMPFieldMetaData fkFieldMetaData = (JDBCCMPFieldMetaData) i.next(); JDBCCMP2xFieldBridge relatedPKField = (JDBCCMP2xFieldBridge) relatedEntity.getFieldByName(fkFieldMetaData.getFieldName()); // now determine whether the fk is mapped to a pk column String fkColumnName = fkFieldMetaData.getColumnName(); JDBCCMP2xFieldBridge fkField = null; // look among the CMP fields for the field with the same column name JDBCFieldBridge[] tableFields = entity.getTableFields(); for(int tableInd = 0; tableInd < tableFields.length && fkField == null; ++tableInd) { JDBCCMP2xFieldBridge cmpField = (JDBCCMP2xFieldBridge) tableFields[tableInd]; if(fkColumnName.equals(cmpField.getColumnName())) { hasFKFieldsMappedToCMPFields = true; // construct the foreign key field fkField = new JDBCCMP2xFieldBridge((JDBCStoreManager) cmpField.getManager(), // this cmpField's manager relatedPKField.getFieldName(), relatedPKField.getFieldType(), cmpField.getJDBCType(), // this cmpField's jdbc type relatedPKField.isReadOnly(), relatedPKField.getReadTimeOut(), relatedPKField.getPrimaryKeyClass(), relatedPKField.getPrimaryKeyField(), cmpField, // CMP field I am mapped to this, fkColumnName); if(cmpField.isPrimaryKeyMember()) { relatedPKFieldsByMyPKFields.put(cmpField, relatedPKField); } } } // if the fk is not a part of pk then create a new field if(fkField == null) { fkField = new JDBCCMP2xFieldBridge(manager, fkFieldMetaData, manager.getJDBCTypeFactory().getJDBCType(fkFieldMetaData)); } fkFieldsByRelatedPKFields.put(relatedPKField, fkField); // temporary map relatedPKFieldsByMyFKFields.put(fkField, relatedPKField); } // Note: this important to order the foreign key fields so that their order matches // the order of related entity's pk fields in case of complex primary keys. // The order is important in fk-constraint generation and in SELECT when loading if(fkFieldsByRelatedPKFields.size() > 0) { JDBCFieldBridge[] relatedPKFields = relatedEntity.getPrimaryKeyFields(); List fkList = new ArrayList(relatedPKFields.length); for(int i = 0; i < relatedPKFields.length; ++i) { JDBCCMPFieldBridge fkField = (JDBCCMPFieldBridge) fkFieldsByRelatedPKFields.remove(relatedPKFields[i]); fkList.add(fkField); } foreignKeyFields = (JDBCCMP2xFieldBridge[]) fkList.toArray(new JDBCCMP2xFieldBridge[fkList.size()]); } else { foreignKeyFields = null; } // are all FK fields mapped to PK fields? allFKFieldsMappedToPKFields = relatedPKFieldsByMyPKFields.size() > 0 && relatedPKFieldsByMyPKFields.size() == foreignKeyFields.length; if(foreignKeyFields != null) { jdbcType = new CMRJDBCType(Arrays.asList(foreignKeyFields)); } } private Transaction getTransaction() { try { EntityContainer container = getJDBCStoreManager().getContainer(); TransactionManager tm = container.getTransactionManager(); return tm.getTransaction(); } catch(SystemException e) { throw new EJBException("Error getting transaction from the transaction manager", e); } } /** * @return Map of lists of waiting related PK values keyed by not yet created this side's PK value. */ private Map getRelatedPKsWaitingForMyPK() { return (Map) relatedPKValuesWaitingForMyPK.get(); } private RelationDataManager initRelationManager(JDBCCMRFieldBridge relatedField) { if(relationManager == null) { if(metadata.getRelationMetaData().isTableMappingStyle()) { relationManager = new M2MRelationManager(this, relatedField); } else { relationManager = EMPTY_RELATION_MANAGER; } } return relationManager; } private Object getRelatedPrimaryKey(Object localObject) { Object relatedId; if(relatedEntity.getLocalInterface().isAssignableFrom(localObject.getClass())) { EJBLocalObject local = (EJBLocalObject) localObject; try { relatedId = local.getPrimaryKey(); } catch(NoSuchObjectLocalException e) { throw new IllegalArgumentException(e.getMessage()); } /* if(relatedManager.wasCascadeDeleted(relatedId)) { throw new IllegalArgumentException("The instance was cascade-deleted: pk=" + relatedId); } */ } else { throw new IllegalArgumentException("The values of this field must be of type " + relatedEntity.getLocalInterface().getName()); } return relatedId; } public String toString() { return entity.getEntityName() + '.' + getFieldName(); } private final class FieldState { private final EntityEnterpriseContext ctx; private List[] setHandle = new List[1]; private Set addedRelations; private Set removedRelations; private Set relationSet; private boolean isLoaded = false; private final long lastRead = -1; private boolean changed; public FieldState(EntityEnterpriseContext ctx) { this.ctx = ctx; setHandle[0] = new ArrayList(); } /** * Get the current value (list of primary keys). */ public List getValue() { if(!isLoaded) { throw new EJBException("CMR field value not loaded yet"); } return Collections.unmodifiableList(setHandle[0]); } /** * Has this relation been loaded. */ public boolean isLoaded() { return isLoaded; } /** * When was this value last read from the datastore. */ public long getLastRead() { return lastRead; } /** * Add this foreign to the relationship. */ public void addRelation(Object fk) { if(isLoaded) { setHandle[0].add(fk); } else { if(removedRelations == null) { removedRelations = new HashSet(); addedRelations = new HashSet(); } removedRelations.remove(fk); addedRelations.add(fk); } changed = true; } /** * Remove this foreign to the relationship. */ public void removeRelation(Object fk) { if(isLoaded) { setHandle[0].remove(fk); } else { if(removedRelations == null) { removedRelations = new HashSet(); addedRelations = new HashSet(); } addedRelations.remove(fk); removedRelations.add(fk); } changed = true; } /** * loads the collection of related ids */ public void loadRelations(Collection values) { // check if we are aleready loaded if(isLoaded) { throw new EJBException("CMR field value is already loaded"); } // just in the case where there are lingering values setHandle[0].clear(); // add the new values setHandle[0].addAll(values); if(removedRelations != null) { // remove the already removed values setHandle[0].removeAll(removedRelations); removedRelations = null; } if(addedRelations != null) { // add the already added values // but remove FKs we are going to add to avoid duplication setHandle[0].removeAll(addedRelations); setHandle[0].addAll(addedRelations); addedRelations = null; } // mark the field loaded isLoaded = true; } /** * Get the current relation set or create a new one. */ public Set getRelationSet() { if(!isLoaded) { throw new EJBException("CMR field value not loaded yet"); } if(ctx.isReadOnly()) { // we are in a read-only invocation, so return a snapshot set return new RelationSet(JDBCCMRFieldBridge.this, ctx, new List[]{new ArrayList(setHandle[0])}, true); } // if we already have a relationset use it if(relationSet != null) { return relationSet; } // construct a new relationshet try { // get the curent transaction EntityContainer container = getJDBCStoreManager().getContainer(); TransactionManager tm = container.getTransactionManager(); Transaction tx = tm.getTransaction(); // if whe have a valid transaction... if(tx != null && (tx.getStatus() == Status.STATUS_ACTIVE || tx.getStatus() == Status.STATUS_PREPARING)) { // crete the relation set and register for a tx callback relationSet = new RelationSet(JDBCCMRFieldBridge.this, ctx, setHandle, false); TxSynchronization sync = new TxSynchronization(FieldState.this); tx.registerSynchronization(sync); } else { // if there is no transaction create a pre-failed list relationSet = new RelationSet(JDBCCMRFieldBridge.this, ctx, new List[1], false); } return relationSet; } catch(SystemException e) { throw new EJBException("Error while creating RelationSet", e); } catch(RollbackException e) { throw new EJBException("Error while creating RelationSet", e); } } /** * Invalidate the current relationship set. */ public void invalidate() { // make a new set handle and copy the currentList to the new handle // this will cause old references to the relationSet to throw an // IllegalStateException if accesses, but will not cause a reload // in Commit Option A List currentList = null; if(setHandle != null && setHandle.length > 0) { currentList = setHandle[0]; setHandle[0] = null; } setHandle = new List[1]; setHandle[0] = currentList; relationSet = null; changed = false; } public boolean isChanged() { return changed; } } private final static class CMRJDBCType implements JDBCType { private final String[] columnNames; private final Class[] javaTypes; private final int[] jdbcTypes; private final String[] sqlTypes; private final boolean[] notNull; private CMRJDBCType(List fields) { List columnNamesList = new ArrayList(); List javaTypesList = new ArrayList(); List jdbcTypesList = new ArrayList(); List sqlTypesList = new ArrayList(); List notNullList = new ArrayList(); for(Iterator iter = fields.iterator(); iter.hasNext();) { JDBCCMPFieldBridge field = (JDBCCMPFieldBridge) iter.next(); JDBCType type = field.getJDBCType(); for(int i = 0; i < type.getColumnNames().length; i++) { columnNamesList.add(type.getColumnNames()[i]); javaTypesList.add(type.getJavaTypes()[i]); jdbcTypesList.add(new Integer(type.getJDBCTypes()[i])); sqlTypesList.add(type.getSQLTypes()[i]); notNullList.add(new Boolean(type.getNotNull()[i])); } } columnNames = (String[]) columnNamesList.toArray(new String[columnNamesList.size()]); javaTypes = (Class[]) javaTypesList.toArray(new Class[javaTypesList.size()]); sqlTypes = (String[]) sqlTypesList.toArray(new String[sqlTypesList.size()]); jdbcTypes = new int[jdbcTypesList.size()]; for(int i = 0; i < jdbcTypes.length; i++) { jdbcTypes[i] = ((Integer) jdbcTypesList.get(i)).intValue(); } notNull = new boolean[notNullList.size()]; for(int i = 0; i < notNull.length; i++) { notNull[i] = ((Boolean) notNullList.get(i)).booleanValue(); } } public String[] getColumnNames() { return columnNames; } public Class[] getJavaTypes() { return javaTypes; } public int[] getJDBCTypes() { return jdbcTypes; } public String[] getSQLTypes() { return sqlTypes; } public boolean[] getNotNull() { return notNull; } public boolean[] getAutoIncrement() { return new boolean[]{false}; } public Object getColumnValue(int index, Object value) { throw new UnsupportedOperationException(); } public Object setColumnValue(int index, Object value, Object columnValue) { throw new UnsupportedOperationException(); } public boolean hasMapper() { throw new UnsupportedOperationException("hasMapper is not implemented."); } public boolean isSearchable() { throw new UnsupportedOperationException("isSearchable is not implemented."); } public JDBCResultSetReader[] getResultSetReaders() { // foreign key fields has their result set readers throw new UnsupportedOperationException(); } public JDBCParameterSetter[] getParameterSetter() { throw new UnsupportedOperationException(); } } private final static class TxSynchronization implements Synchronization { private final WeakReference fieldStateRef; private TxSynchronization(FieldState fieldState) { if(fieldState == null) { throw new IllegalArgumentException("fieldState is null"); } this.fieldStateRef = new WeakReference(fieldState); } public void beforeCompletion() { // REVIEW: THIS WILL NOT BE INVOKED ON A ROLLBACK // Be Careful where you put this invalidate // If you put it in afterCompletion, the beanlock will probably // be released before the invalidate and you will have a race FieldState fieldState = (FieldState) fieldStateRef.get(); if(fieldState != null) { fieldState.invalidate(); } } public void afterCompletion(int status) { } } public static interface RelationDataManager { void addRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId); void removeRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId); boolean isDirty(); RelationData getRelationData(); } private static final RelationDataManager EMPTY_RELATION_MANAGER = new RelationDataManager() { public void addRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId) { } public void removeRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId) { } public boolean isDirty() { return false; } public RelationData getRelationData() { throw new UnsupportedOperationException(); } }; public static class M2MRelationManager implements RelationDataManager { private final JDBCCMRFieldBridge leftField; private final JDBCCMRFieldBridge rightField; private final TransactionLocal relationData = new TransactionLocal() { protected Object initialValue() { return new RelationData(leftField, rightField); } }; public M2MRelationManager(JDBCCMRFieldBridge leftField, JDBCCMRFieldBridge rightField) { this.leftField = leftField; this.rightField = rightField; } public void addRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId) { final RelationData local = getRelationData(); local.addRelation(field, id, relatedField, relatedId); } public void removeRelation(JDBCCMRFieldBridge field, Object id, JDBCCMRFieldBridge relatedField, Object relatedId) { RelationData local = getRelationData(); local.removeRelation(field, id, relatedField, relatedId); } public boolean isDirty() { RelationData local = getRelationData(); return local.isDirty(); } public RelationData getRelationData() { final RelationData local = (RelationData) relationData.get(); return local; } } /*interface SecurityActions { class UTIL { static SecurityActions getSecurityActions() { return System.getSecurityManager() == null ? NON_PRIVILEGED : PRIVILEGED; } } SecurityActions NON_PRIVILEGED = new SecurityActions() { public Principal getPrincipal() { return SecurityAssociation.getPrincipal(); } public Object getCredential() { return SecurityAssociation.getCredential(); } }; SecurityActions PRIVILEGED = new SecurityActions() { private final PrivilegedAction getPrincipalAction = new PrivilegedAction() { public Object run() { return SecurityAssociation.getPrincipal(); } }; private final PrivilegedAction getCredentialAction = new PrivilegedAction() { public Object run() { return SecurityAssociation.getCredential(); } }; public Principal getPrincipal() { return (Principal) AccessController.doPrivileged(getPrincipalAction); } public Object getCredential() { return AccessController.doPrivileged(getCredentialAction); } }; Principal getPrincipal(); Object getCredential(); }*/ }