package pt.ist.fenixframework.pstm; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import jvstm.CommitException; import org.apache.ojb.broker.OptimisticLockException; import org.apache.ojb.broker.PersistenceBroker; import org.apache.ojb.broker.accesslayer.LookupException; import org.apache.ojb.broker.core.ValueContainer; import org.apache.ojb.broker.metadata.ClassDescriptor; import org.apache.ojb.broker.metadata.CollectionDescriptor; import org.apache.ojb.broker.metadata.JdbcType; import org.apache.ojb.broker.util.JdbcTypesHelper; import org.apache.ojb.broker.util.ObjectModificationDefaultImpl; import pt.ist.fenixframework.DomainObject; import pt.ist.fenixframework.FenixFramework; class DBChanges { private static final String SQL_CHANGE_LOGS_CMD_PREFIX = "INSERT INTO FF$TX_CHANGE_LOGS VALUES "; // The following value is the approximate length of each tuple to add after // the VALUES private static final int PER_RECORD_LENGTH = 60; private static final int MIN_BUFFER_CAPACITY = 256; private static final int MAX_BUFFER_CAPACITY = 10000; private static final int BUFFER_THRESHOLD = 256; private Set<AttrChangeLog> attrChangeLogs = null; private Set<DomainObject> newObjs = null; private Set objsToStore = null; private Set objsToDelete = null; private Map<RelationTupleInfo, RelationTupleInfo> mToNTuples = null; protected Set<AttrChangeLog> getAttrChangeLogs() { Set<AttrChangeLog> set = attrChangeLogs; if (set == null) { set = new HashSet<AttrChangeLog>(); } return Collections.unmodifiableSet(set); } protected Set<DomainObject> getNewObjects() { Set<DomainObject> set = newObjs; if (set == null) { set = new HashSet<DomainObject>(); } return Collections.unmodifiableSet(set); } public Set<DomainObject> getModifiedObjects() { Set<DomainObject> modified = new HashSet<DomainObject>(); if (attrChangeLogs != null) { for (AttrChangeLog log : attrChangeLogs) { if (!isNewObject(log.obj)) { modified.add(log.obj); } } } return modified; } public boolean isDeleted(Object obj) { return (objsToDelete != null) && objsToDelete.contains(obj); } public boolean needsWrite() { return ((newObjs != null) && (!newObjs.isEmpty())) || ((objsToStore != null) && (!objsToStore.isEmpty())) || ((objsToDelete != null) && (!objsToDelete.isEmpty())) || ((mToNTuples != null) && (!mToNTuples.isEmpty())); } public boolean isNewObject(DomainObject obj) { return (newObjs != null) && newObjs.contains(obj); } public void logAttrChange(DomainObject obj, String attrName) { if (attrChangeLogs == null) { attrChangeLogs = new HashSet<AttrChangeLog>(); } attrChangeLogs.add(new AttrChangeLog(obj, attrName)); } public void storeNewObject(DomainObject obj) { if (newObjs == null) { newObjs = new HashSet<DomainObject>(); } newObjs.add(obj); removeFromDeleted(obj); } public void storeObject(DomainObject obj, String attrName) { logAttrChange(obj, attrName); if (isNewObject(obj)) { // don't need to update new objects return; } if (objsToStore == null) { objsToStore = new HashSet(); } objsToStore.add(obj); removeFromDeleted(obj); } public void deleteObject(Object obj) { if (objsToDelete == null) { objsToDelete = new HashSet(); } objsToDelete.add(obj); if (newObjs != null) { newObjs.remove(obj); } if (objsToStore != null) { objsToStore.remove(obj); } } public void addRelationTuple(String relation, DomainObject obj1, String colNameOnObj1, DomainObject obj2, String colNameOnObj2) { setRelationTuple(relation, obj1, colNameOnObj1, obj2, colNameOnObj2, false); } public void removeRelationTuple(String relation, DomainObject obj1, String colNameOnObj1, DomainObject obj2, String colNameOnObj2) { setRelationTuple(relation, obj1, colNameOnObj1, obj2, colNameOnObj2, true); } private void removeFromDeleted(DomainObject obj) { if (objsToDelete != null) { if (objsToDelete.remove(obj)) { if (FenixFramework.getConfig().isErrorIfChangingDeletedObject()) { throw new Error("Changing object after it was deleted: " + obj); } else { System.err.println("WARNING: Changing object after it was deleted: " + obj); } } } } private void setRelationTuple(String relation, DomainObject obj1, String colNameOnObj1, DomainObject obj2, String colNameOnObj2, boolean remove) { if (mToNTuples == null) { mToNTuples = new HashMap<RelationTupleInfo, RelationTupleInfo>(); } RelationTupleInfo info = new RelationTupleInfo(relation, obj1, colNameOnObj1, obj2, colNameOnObj2, remove); mToNTuples.put(info, info); } void cache() { FenixCache cache = Transaction.getCache(); if (newObjs != null) { for (DomainObject obj : newObjs) { cache.cache(obj); } } } void makePersistent(PersistenceBroker pb, int txNumber) throws SQLException, LookupException { // store new objects if (newObjs != null) { for (Object obj : newObjs) { pb.store(obj, ObjectModificationDefaultImpl.INSERT); } } boolean foundOptimisticException = false; // update objects if (objsToStore != null) { for (Object obj : objsToStore) { try { pb.store(obj, ObjectModificationDefaultImpl.UPDATE); } catch (OptimisticLockException ole) { pb.removeFromCache(obj); foundOptimisticException = true; } } } if (foundOptimisticException) { throw new jvstm.CommitException(); } // delete objects if (objsToDelete != null) { for (Object obj : objsToDelete) { pb.delete(obj); } } // write m-to-n tuples if (mToNTuples != null) { for (RelationTupleInfo info : mToNTuples.values()) { updateMtoNRelation(pb, info); } } // write change logs Connection conn = pb.serviceConnectionManager().getConnection(); writeAttrChangeLogs(conn, txNumber); } private void writeAttrChangeLogs(Connection conn, int txNumber) throws SQLException { int numRecords = (attrChangeLogs == null) ? 0 : attrChangeLogs.size(); // allocate a large capacity StringBuilder to avoid reallocation int bufferCapacity = Math.min(MIN_BUFFER_CAPACITY + (numRecords * PER_RECORD_LENGTH), MAX_BUFFER_CAPACITY); StringBuilder sqlCmd = new StringBuilder(bufferCapacity); sqlCmd.append(SQL_CHANGE_LOGS_CMD_PREFIX); Statement stmt = null; try { stmt = conn.createStatement(); boolean addedRecord = false; if (attrChangeLogs == null) { // if no AttrChangeLog exists, then it means that we // only created objects, without changing any other // object // Still, we need to notify other servers of the tx // number, so create an empty changelog line... sqlCmd.append("(0,'',"); sqlCmd.append(txNumber); sqlCmd.append(")"); addedRecord = true; } else { for (AttrChangeLog log : attrChangeLogs) { if (isNewObject(log.obj)) { // don't need to warn others of changes to new objects continue; } if (addedRecord) { sqlCmd.append(","); } sqlCmd.append("("); sqlCmd.append(log.obj.getOID()); sqlCmd.append(",'"); sqlCmd.append(log.attr); sqlCmd.append("',"); sqlCmd.append(txNumber); sqlCmd.append(")"); addedRecord = true; if ((bufferCapacity - sqlCmd.length()) < BUFFER_THRESHOLD) { stmt.execute(sqlCmd.toString()); sqlCmd.setLength(0); sqlCmd.append(SQL_CHANGE_LOGS_CMD_PREFIX); addedRecord = false; } } } if (addedRecord) { try { stmt.execute(sqlCmd.toString()); } catch (SQLException ex) { System.out.println("SqlException: " + ex.getMessage()); System.out.println("Deadlock trying to insert: " + sqlCmd.toString()); throw new CommitException(); } } } finally { if (stmt != null) { stmt.close(); } } } private static JdbcType OID_JDBC_TYPE = JdbcTypesHelper.getJdbcTypeByName("BIGINT"); private final static String[] EMPTY_ARRAY = new String[0]; // copied and adapted from OJB's MtoNBroker protected void updateMtoNRelation(PersistenceBroker pb, RelationTupleInfo tupleInfo) { DomainObject obj1 = tupleInfo.obj1; DomainObject obj2 = tupleInfo.obj2; ClassDescriptor cld1 = pb.getDescriptorRepository().getDescriptorFor(obj1.getClass()); CollectionDescriptor cod = cld1.getCollectionDescriptorByName(tupleInfo.colNameOnObj1); if (cod == null) { // try the mapping on the other object cld1 = pb.getDescriptorRepository().getDescriptorFor(obj2.getClass()); cod = cld1.getCollectionDescriptorByName(tupleInfo.colNameOnObj2); // switch objects obj1 = tupleInfo.obj2; obj2 = tupleInfo.obj1; } String[] oidColumns = new String[2]; oidColumns[0] = cod.getFksToThisClass()[0]; oidColumns[1] = cod.getFksToItemClass()[0]; ValueContainer[] oidValues = new ValueContainer[2]; oidValues[0] = new ValueContainer(obj1.getOid(), OID_JDBC_TYPE); oidValues[1] = new ValueContainer(obj2.getOid(), OID_JDBC_TYPE); String table = cod.getIndirectionTable(); // always remove the tuple String sqlStmt = pb.serviceSqlGenerator().getDeleteMNStatement(table, oidColumns, null); pb.serviceJdbcAccess().executeUpdateSQL(sqlStmt, cld1, oidValues, null); // if it was not to remove but to add, then add it // this "delete-first, add-after" serves to ensure that we can add // multiple times // the same tuple to a relation and still have the Set semantics for the // relation. if (!tupleInfo.remove) { sqlStmt = pb.serviceSqlGenerator().getInsertMNStatement(table, oidColumns, EMPTY_ARRAY); pb.serviceJdbcAccess().executeUpdateSQL(sqlStmt, cld1, oidValues, null); } } static class RelationTupleInfo { final String relation; final DomainObject obj1; final String colNameOnObj1; final DomainObject obj2; final String colNameOnObj2; final boolean remove; RelationTupleInfo(String relation, DomainObject obj1, String colNameOnObj1, DomainObject obj2, String colNameOnObj2, boolean remove) { this.relation = relation; this.obj1 = obj1; this.colNameOnObj1 = colNameOnObj1; this.obj2 = obj2; this.colNameOnObj2 = colNameOnObj2; this.remove = remove; } @Override public int hashCode() { return relation.hashCode() + obj1.hashCode() + obj2.hashCode(); } @Override public boolean equals(Object obj) { if ((obj != null) && (obj.getClass() == this.getClass())) { RelationTupleInfo other = (RelationTupleInfo) obj; return this.relation.equals(other.relation) && this.obj1.equals(other.obj1) && this.obj2.equals(other.obj2); } else { return false; } } } static class AttrChangeLog { final DomainObject obj; final String attr; AttrChangeLog(DomainObject obj, String attr) { this.obj = obj; this.attr = attr; } @Override public int hashCode() { return System.identityHashCode(obj) + attr.hashCode(); } @Override public boolean equals(Object obj) { if ((obj != null) && (obj.getClass() == this.getClass())) { AttrChangeLog other = (AttrChangeLog) obj; return (this.obj == other.obj) && this.attr.equals(other.attr); } else { return false; } } } }