/*
* 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.reflect.Field;
import javax.ejb.EJBException;
import org.jboss.deployment.DeploymentException;
import org.jboss.ejb.EntityEnterpriseContext;
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.CMPFieldStateFactory;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCCMPFieldMetaData;
/**
* JDBCCMP2xFieldBridge is a concrete implementation of JDBCCMPFieldBridge for
* CMP version 2.x. Instance data is stored in the entity persistence context.
* Whenever a field is changed it is compared to the current value and sets
* a dirty flag if the value has changed.
*
* Life-cycle:
* Tied to the EntityBridge.
*
* Multiplicity:
* One for each entity bean cmp field.
*
* @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
* @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a>
* @version $Revision: 81030 $
*/
public class JDBCCMP2xFieldBridge extends JDBCAbstractCMPFieldBridge
{
/** column name (used only at deployment time to check whether fields mapped to the same column) */
private final String columnName;
/** CMP field this foreign key field is mapped to */
private final JDBCCMP2xFieldBridge cmpFieldIAmMappedTo;
/** this is used for foreign key fields mapped to CMP fields (check ChainLink) */
private ChainLink cmrChainLink;
// Constructors
public JDBCCMP2xFieldBridge(JDBCStoreManager manager,
JDBCCMPFieldMetaData metadata)
throws DeploymentException
{
super(manager, metadata);
cmpFieldIAmMappedTo = null;
columnName = metadata.getColumnName();
}
public JDBCCMP2xFieldBridge(JDBCStoreManager manager,
JDBCCMPFieldMetaData metadata,
CMPFieldStateFactory stateFactory,
boolean checkDirtyAfterGet)
throws DeploymentException
{
this(manager, metadata);
this.stateFactory = stateFactory;
this.checkDirtyAfterGet = checkDirtyAfterGet;
}
public JDBCCMP2xFieldBridge(JDBCCMP2xFieldBridge cmpField,
CMPFieldStateFactory stateFactory,
boolean checkDirtyAfterGet)
throws DeploymentException
{
this(
(JDBCStoreManager) cmpField.getManager(),
cmpField.getFieldName(),
cmpField.getFieldType(),
cmpField.getJDBCType(),
cmpField.isReadOnly(), // should always be false?
cmpField.getReadTimeOut(),
cmpField.getPrimaryKeyClass(),
cmpField.getPrimaryKeyField(),
cmpField,
null, // it should not be a foreign key
cmpField.getColumnName()
);
this.stateFactory = stateFactory;
this.checkDirtyAfterGet = checkDirtyAfterGet;
}
/**
* This constructor creates a foreign key field.
*/
public JDBCCMP2xFieldBridge(JDBCStoreManager manager,
JDBCCMPFieldMetaData metadata,
JDBCType jdbcType)
throws DeploymentException
{
super(manager, metadata, jdbcType);
cmpFieldIAmMappedTo = null;
columnName = metadata.getColumnName();
}
/**
* This constructor is used to create a foreign key field instance that is
* a part of primary key field. See JDBCCMRFieldBridge.
*/
public JDBCCMP2xFieldBridge(JDBCStoreManager manager,
String fieldName,
Class fieldType,
JDBCType jdbcType,
boolean readOnly,
long readTimeOut,
Class primaryKeyClass,
Field primaryKeyField,
JDBCCMP2xFieldBridge cmpFieldIAmMappedTo,
JDBCCMRFieldBridge myCMRField,
String columnName)
throws DeploymentException
{
super(
manager,
fieldName,
fieldType,
jdbcType,
readOnly,
readTimeOut,
primaryKeyClass,
primaryKeyField,
cmpFieldIAmMappedTo.getFieldIndex(),
cmpFieldIAmMappedTo.getTableIndex(),
cmpFieldIAmMappedTo.checkDirtyAfterGet,
cmpFieldIAmMappedTo.stateFactory
);
this.cmpFieldIAmMappedTo = cmpFieldIAmMappedTo;
if(myCMRField != null)
{
cmrChainLink = new CMRChainLink(myCMRField);
cmpFieldIAmMappedTo.addCMRChainLink(cmrChainLink);
}
this.columnName = columnName;
}
// Public
public JDBCCMP2xFieldBridge getCmpFieldIAmMappedTo()
{
return cmpFieldIAmMappedTo;
}
public ChainLink getCmrChainLink()
{
return cmrChainLink;
}
public boolean isFKFieldMappedToCMPField()
{
return cmpFieldIAmMappedTo != null && this.cmrChainLink != null;
}
public String getColumnName()
{
return columnName;
}
// JDBCFieldBridge implementation
public Object getInstanceValue(EntityEnterpriseContext ctx)
{
FieldState fieldState = getLoadedState(ctx);
return fieldState.getValue();
}
public void setInstanceValue(EntityEnterpriseContext ctx, Object value)
{
FieldState fieldState = getFieldState(ctx);
// update current value
if(cmpFieldIAmMappedTo != null && cmpFieldIAmMappedTo.isPrimaryKeyMember())
{
// if this field shares the column with the primary key field and new value
// changes the primary key then we are in an illegal state.
if(value != null)
{
if(fieldState.isLoaded() && fieldState.isValueChanged(value))
{
throw new IllegalStateException(
"New value [" + value + "] of a foreign key field "
+ getFieldName()
+ " changed the value of a primary key field "
+ cmpFieldIAmMappedTo.getFieldName()
+ "[" + fieldState.value + "]"
);
}
else
{
fieldState.setValue(value);
}
}
}
else
{
if(cmrChainLink != null
&& JDBCEntityBridge.isEjbCreateDone(ctx)
&& fieldState.isLoaded()
&& fieldState.isValueChanged(value))
{
cmrChainLink.execute(ctx, fieldState, value);
}
fieldState.setValue(value);
}
// we are loading the field right now so it isLoaded
fieldState.setLoaded();
}
public void lockInstanceValue(EntityEnterpriseContext ctx)
{
getFieldState(ctx).lockValue();
}
public boolean isLoaded(EntityEnterpriseContext ctx)
{
return getFieldState(ctx).isLoaded();
}
/**
* Has the value of this field changes since the last time clean was called.
*/
public boolean isDirty(EntityEnterpriseContext ctx)
{
return !primaryKeyMember
&& !readOnly
&& getFieldState(ctx).isDirty();
}
/**
* Mark this field as clean. Saves the current state in context, so it
* can be compared when isDirty is called.
*/
public void setClean(EntityEnterpriseContext ctx)
{
FieldState fieldState = getFieldState(ctx);
fieldState.setClean();
// update last read time
if(readOnly && readTimeOut != -1)
fieldState.lastRead = System.currentTimeMillis();
}
public void resetPersistenceContext(EntityEnterpriseContext ctx)
{
if(isReadTimedOut(ctx))
{
JDBCContext jdbcCtx = (JDBCContext)ctx.getPersistenceContext();
FieldState fieldState = (FieldState)jdbcCtx.getFieldState(jdbcContextIndex);
if(fieldState != null)
fieldState.reset();
}
}
public boolean isReadTimedOut(EntityEnterpriseContext ctx)
{
// if we are read/write then we are always timed out
if(!readOnly)
return true;
// if read-time-out is -1 then we never time out.
if(readTimeOut == -1)
return false;
long readInterval = System.currentTimeMillis() - getFieldState(ctx).lastRead;
return readInterval >= readTimeOut;
}
public Object getLockedValue(EntityEnterpriseContext ctx)
{
return getLoadedState(ctx).getLockedValue();
}
public void updateState(EntityEnterpriseContext ctx, Object value)
{
getFieldState(ctx).updateState(value);
}
protected void setDirtyAfterGet(EntityEnterpriseContext ctx)
{
getFieldState(ctx).setCheckDirty();
}
// Private
private FieldState getLoadedState(EntityEnterpriseContext ctx)
{
FieldState fieldState = getFieldState(ctx);
if(!fieldState.isLoaded())
{
manager.loadField(this, ctx);
if(!fieldState.isLoaded())
throw new EJBException("Could not load field value: " + getFieldName());
}
return fieldState;
}
private void addCMRChainLink(ChainLink nextCMRChainLink)
{
if(cmrChainLink == null)
{
cmrChainLink = new DummyChainLink();
}
cmrChainLink.setNextLink(nextCMRChainLink);
}
private FieldState getFieldState(EntityEnterpriseContext ctx)
{
JDBCContext jdbcCtx = (JDBCContext)ctx.getPersistenceContext();
FieldState fieldState = (FieldState)jdbcCtx.getFieldState(jdbcContextIndex);
if(fieldState == null)
{
fieldState = new FieldState(jdbcCtx);
jdbcCtx.setFieldState(jdbcContextIndex, fieldState);
}
return fieldState;
}
// Inner
private class FieldState
{
/** entity's state this field state belongs to */
private JDBCEntityBridge.EntityState entityState;
/** current field value */
private Object value;
/** previous field state. NOTE: it might not be the same as previous field value */
private Object state;
/** locked field value */
private Object lockedValue;
/** last time the field was read */
private long lastRead = -1;
public FieldState(JDBCContext jdbcCtx)
{
this.entityState = jdbcCtx.getEntityState();
}
/**
* Reads current field value.
* @return current field value.
*/
public Object getValue()
{
//if(checkDirtyAfterGet)
// setCheckDirty();
return value;
}
/**
* Sets new field value and sets the flag that setter was called on the field
* @param newValue new field value.
*/
public void setValue(Object newValue)
{
this.value = newValue;
setCheckDirty();
}
private void setCheckDirty()
{
entityState.setCheckDirty(tableIndex);
}
/**
* @return true if the field is loaded.
*/
public boolean isLoaded()
{
return entityState.isLoaded(tableIndex);
}
/**
* Marks the field as loaded.
*/
public void setLoaded()
{
entityState.setLoaded(tableIndex);
}
/**
* @return true if the field is dirty.
*/
public boolean isDirty()
{
return isLoaded() && !stateFactory.isStateValid(state, value);
}
/**
* Compares current value to a new value. Note, it does not compare
* field states, just values.
* @param newValue new field value
* @return true if field values are not equal.
*/
public boolean isValueChanged(Object newValue)
{
return value == null ? newValue != null : !value.equals(newValue);
}
/**
* Resets masks and updates the state.
*/
public void setClean()
{
entityState.setClean(tableIndex);
updateState(value);
}
/**
* Updates the state to some specific value that might be different from the current
* field's value. This trick is needed for foreign key fields because they can be
* changed while not being loaded. When the owning CMR field is loaded this method is
* called with the loaded from the database value. Thus, we have correct state and locked value.
* @param value the value loaded from the database.
*/
private void updateState(Object value)
{
state = stateFactory.getFieldState(value);
lockedValue = value;
}
/**
* Resets everything.
*/
public void reset()
{
value = null;
state = null;
lastRead = -1;
entityState.resetFlags(tableIndex);
}
public void lockValue()
{
if(entityState.lockValue(tableIndex))
{
//log.debug("locking> " + fieldName + "=" + value);
lockedValue = value;
}
}
public Object getLockedValue()
{
return lockedValue;
}
}
/**
* Represents a link in the chain. The execute method will doExecute each link
* in the chain except for the link (originator) execute() was called on.
*/
private abstract static class ChainLink
{
private ChainLink nextLink;
public ChainLink()
{
nextLink = this;
}
public void setNextLink(ChainLink nextLink)
{
nextLink.nextLink = this.nextLink;
this.nextLink = nextLink;
}
public ChainLink getNextLink()
{
return nextLink;
}
public void execute(EntityEnterpriseContext ctx,
FieldState fieldState,
Object newValue)
{
nextLink.doExecute(this, ctx, fieldState, newValue);
}
protected abstract void doExecute(ChainLink originator,
EntityEnterpriseContext ctx,
FieldState fieldState,
Object newValue);
}
/**
* This chain link contains a CMR field a foreign key of which is mapped to a CMP field.
*/
private static class CMRChainLink
extends ChainLink
{
private final JDBCCMRFieldBridge cmrField;
public CMRChainLink(JDBCCMRFieldBridge cmrField)
{
this.cmrField = cmrField;
}
/**
* Going down the chain current related id is calculated and stored in oldRelatedId.
* When the next link is originator, the flow is going backward:
* - field state is updated with new vaue;
* - new related id is calculated;
* - old relationship is destroyed (if there is one);
* - new relationship is established (if it is valid).
*
* @param originator ChainLink that started execution.
* @param ctx EnterpriseEntityContext of the entity.
* @param fieldState field's state.
* @param newValue new field value.
*/
public void doExecute(ChainLink originator,
EntityEnterpriseContext ctx,
FieldState fieldState,
Object newValue)
{
// get old related id
Object oldRelatedId = cmrField.getRelatedIdFromContext(ctx);
// invoke down the cmrChain
if(originator != getNextLink())
{
getNextLink().doExecute(originator, ctx, fieldState, newValue);
}
// update field state
fieldState.setValue(newValue);
// get new related id
Object newRelatedId = cmrField.getRelatedIdFromContext(ctx);
// destroy old relationship
if(oldRelatedId != null)
destroyRelations(oldRelatedId, ctx);
// establish new relationship
if(newRelatedId != null)
createRelations(newRelatedId, ctx);
}
private void createRelations(Object newRelatedId, EntityEnterpriseContext ctx)
{
try
{
if(cmrField.isForeignKeyValid(newRelatedId))
{
cmrField.createRelationLinks(ctx, newRelatedId, false);
}
else
{
// set foreign key to a new value
cmrField.setForeignKey(ctx, newRelatedId);
// put calculated relatedId to the waiting list
if(ctx.getId() != null)
{
JDBCCMRFieldBridge relatedCMRField = (JDBCCMRFieldBridge)cmrField.getRelatedCMRField();
relatedCMRField.addRelatedPKWaitingForMyPK(newRelatedId, ctx.getId());
}
}
}
catch(Exception e)
{
// no such object
}
}
private void destroyRelations(Object oldRelatedId, EntityEnterpriseContext ctx)
{
JDBCCMRFieldBridge relatedCMRField = (JDBCCMRFieldBridge)cmrField.getRelatedCMRField();
relatedCMRField.removeRelatedPKWaitingForMyPK(oldRelatedId, ctx.getId());
try
{
if(cmrField.isForeignKeyValid(oldRelatedId))
{
cmrField.destroyRelationLinks(ctx, oldRelatedId, true, false);
}
}
catch(Exception e)
{
// no such object
}
}
}
private static class DummyChainLink
extends ChainLink
{
public void doExecute(ChainLink originator,
EntityEnterpriseContext ctx,
FieldState fieldState,
Object newValue)
{
// invoke down the cmrChain
if(originator != getNextLink())
{
getNextLink().doExecute(originator, ctx, fieldState, newValue);
}
// update field state
fieldState.setValue(newValue);
}
}
}