/*
* Copyright (c) 2009, SQL Power Group Inc.
*
* This file is part of SQL Power Library.
*
* SQL Power Library is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* SQL Power Library 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.dao;
import java.beans.PropertyChangeEvent;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.log4j.Logger;
import ca.sqlpower.dao.helper.PersisterHelperFinder;
import ca.sqlpower.dao.helper.SPPersisterHelper;
import ca.sqlpower.dao.session.SessionPersisterSuperConverter;
import ca.sqlpower.object.ObjectDependentException;
import ca.sqlpower.object.SPChildEvent;
import ca.sqlpower.object.SPListener;
import ca.sqlpower.object.SPObject;
import ca.sqlpower.sqlobject.SQLCatalog;
import ca.sqlpower.sqlobject.SQLColumn;
import ca.sqlpower.sqlobject.SQLDatabase;
import ca.sqlpower.sqlobject.SQLIndex;
import ca.sqlpower.sqlobject.SQLObject;
import ca.sqlpower.sqlobject.SQLObjectUtils;
import ca.sqlpower.sqlobject.SQLRelationship;
import ca.sqlpower.sqlobject.SQLSchema;
import ca.sqlpower.sqlobject.SQLTable;
import ca.sqlpower.util.SQLPowerUtils;
import ca.sqlpower.util.TransactionEvent;
import ca.sqlpower.util.WorkspaceContainer;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
public abstract class SPSessionPersister implements SPPersister {
/**
* The god mode means that this listener will output
* events that are unconditional, always. This makes it the
* purveyor of the truth.
*/
private boolean godMode = false;
private static final Logger logger = Logger.getLogger(SPSessionPersister.class);
/**
* An {@link SPSession} to persist objects and properties onto.
*/
private WorkspaceContainer workspaceContainer;
/**
* A count of transactions, mainly to keep track of nested transactions.
*/
private int transactionCount = 0;
/**
* Persisted property buffer, mapping of {@link SPObject} UUIDs to each
* individual persisted property
*/
protected Multimap<String, PersistedSPOProperty> persistedProperties =
LinkedListMultimap.create();
/**
* This will be the list we will use to rollback persisted properties
*/
private List<PersistedPropertiesEntry> persistedPropertiesRollbackList =
new LinkedList<PersistedPropertiesEntry>();
/**
* Persisted {@link SPObject} buffer, contains all the data that was passed
* into the persistedObject call in the order of insertion. Note that this
* list must be kept consistent with persistedObjectsMap.
*/
protected List<PersistedSPObject> persistedObjects =
new LinkedList<PersistedSPObject>();
/**
* This map stores values looked up by findByUUID and allows them to be
* looked up faster to improve performance. This cache should be cleared
* regularly before and after each transaction so the values in it stay
* consistent with what actually exists in the client model.
*/
private final Map<String, SPObject> lookupCache = new HashMap<String, SPObject>();
/**
* This map allows for fast lookups of persisted objects by their UUID.
* <p>
* Note that the entries in this map must be kept consistent with those in
* persistedObjects.
* <p>
* Changing persistedObjects to be an ordered map may be more sensible then
* using this map in the future but we will see how much this improves
* performance.
*/
protected Map<String, PersistedSPObject> persistedObjectsMap =
new HashMap<String, PersistedSPObject>();
/**
* This will be the list we use to rollback persisted objects.
* It contains UUIDs of objects that were created.
*/
private List<PersistedObjectEntry> persistedObjectsRollbackList =
new LinkedList<PersistedObjectEntry>();
/**
* This comparator sorts buffered removeObject calls by each
* {@link SPObject} UUID. The UUIDs being compared must matchup with an
* existing SPObject in the {@link #root}. If it does not exist, it means
* that the SPObject has just been removed and this comparator is
* reshuffling the map.
* TODO We need a generic way of comparing {@link SPObject}s.
*/
protected final Comparator<String> removedObjectComparator = new Comparator<String>() {
public int compare(String uuid1, String uuid2) {
SPObject spo1 = findByUuid(root, uuid1, SPObject.class);
SPObject spo2 = findByUuid(root, uuid2, SPObject.class);
if (uuid1.equals(uuid2)) {
return 0;
} else if (spo1 == null && spo2 == null) {
return uuid2.compareTo(uuid1);
} else if (spo1 == null) {
return -1;
} else if (spo2 == null) {
return 1;
} else if (spo1.getParent() == null && spo2.getParent() == null) {
return 0;
} else if (spo1.getParent() == null) {
return 1;
} else if (spo2.getParent() == null) {
return -1;
} else if (spo1.equals(spo2)) {
return 0;
} else if (spo1.getParent().equals(spo2.getParent())) {
List<? extends SPObject> siblings;
if (spo1.getParent() instanceof SQLObject) {
siblings = ((SQLObject) spo1.getParent()).getChildrenWithoutPopulating();
} else {
siblings = spo1.getParent().getChildren();
}
return Integer.signum(siblings.indexOf(spo2) - siblings.indexOf(spo1));
}
List<SPObject> ancestorList1 = SQLPowerUtils.getAncestorList(spo1);
List<SPObject> ancestorList2 = SQLPowerUtils.getAncestorList(spo2);
SPObject previousAncestor = null;
SPObject ancestor1 = spo1;
SPObject ancestor2 = spo2;
boolean compareWithAncestor = false;
for (int i = 0, j = 0; i < ancestorList1.size() && j < ancestorList2.size(); i++, j++) {
ancestor1 = ancestorList1.get(i);
ancestor2 = ancestorList2.get(j);
if (previousAncestor != null && !ancestor1.equals(ancestor2)) {
compareWithAncestor = true;
break;
}
previousAncestor = ancestor1;
}
if (!compareWithAncestor) {
if (ancestorList1.size() < ancestorList2.size()) {
ancestor1 = spo1;
ancestor2 = ancestorList2.get(ancestorList1.size());
} else if (ancestorList1.size() > ancestorList2.size()) {
ancestor1 = ancestorList1.get(ancestorList2.size());
ancestor2 = spo2;
} else {
ancestor1 = spo1;
ancestor2 = spo2;
}
}
int c;
if (ancestor1.equals(ancestor2)) {
c = ancestorList2.size() - ancestorList1.size();
} else if (ancestor1.getClass() == ancestor2.getClass()) {
List<? extends SPObject> siblings;
if (previousAncestor instanceof SQLObject) {
siblings = ((SQLObject) previousAncestor).getChildrenWithoutPopulating();
} else {
siblings = previousAncestor.getChildren();
}
int index1 = siblings.indexOf(ancestor1);
int index2 = siblings.indexOf(ancestor2);
c = index2 - index1;
} else {
// XXX The comparator should really never reach
// this else block. However in the case that it does, compare by UUID.
c = uuid2.compareTo(uuid1);
}
return Integer.signum(c);
}
};
/**
* {@link SPObject} removal buffer, mapping of {@link SPObject} UUIDs
* to their parents
*/
protected Map<String, String> objectsToRemove = new HashMap<String, String>();
/**
* For some uses of the session persister we don't want to disable magic.
* By default we do want to disable magic.
*/
private boolean disableMagic = true;
/**
* In most cases we want the persister to rollback changes it made to the
* model as it is normally the responsibility of the persister to perform
* this action. However, in some cases we have another mechanism to undo the
* changes as there may be more changes in the transaction that is outside
* of this persister. For these cases we will disable the rollback for this
* class when committing fails.
*/
private boolean rollbackOnCommitError = true;
private void setPersistedProperties(
Multimap<String, PersistedSPOProperty> persistedProperties) {
this.persistedProperties = persistedProperties;
}
private void setPersistedObjects(List<PersistedSPObject> persistedObjects) {
this.persistedObjects = persistedObjects;
persistedObjectsMap.clear();
for (PersistedSPObject pso : persistedObjects) {
persistedObjectsMap.put(pso.getUUID(), pso);
}
}
private void setObjectsToRemove(Map<String, String> objectsToRemove) {
this.objectsToRemove = objectsToRemove;
}
/**
* This is the map we use to rollback object removal. The key is the UUID of the
* object removed for faster lookup if an object was moved instead of being
* directly removed. The map is ordered to keep the object order that they were
* removed in for rollback.
*/
private LinkedHashMap<String, RemovedObjectEntry> objectsToRemoveRollbackList =
new LinkedHashMap<String, RemovedObjectEntry>();
/**
* This root object is used to find other objects by UUID by walking the
* descendant tree when an object is required.
*/
protected final SPObject root;
/**
* Name of this persister (for debugging purposes).
*/
private final String name;
private Thread currentThread;
private boolean headingToWisconsin;
/**
* A class that can convert a complex object into a basic representation
* that can be placed in a string and can also convert the string
* representation back into the complex object.
*/
protected final SessionPersisterSuperConverter converter;
/**
* A long time ago we had to put a hack in this class to move the SQLImports
* at the end of the object list. However, now that we can extend the
* comparator the correct implementation is to extend the comparator and
* include this logic there. In some cases we need to disable this current
* hack due to performance issues.
*/
protected boolean correctSQLImportOrder = true;
/**
* Creates a session persister that can update an object at or a descendant
* of the given root now. If the persist call involves an object that is
* outside of the scope of the root node and its descendant tree an
* exception will be thrown depending on the method called as the object
* will not be found.
*
* @param name
* The name of the persister. Helps to tell which persister is
* performing actions.
* @param root
* The root of the tree of {@link SPObject}s. The tree rooted at
* this node will have objects added to it and properties changed
* on nodes in this tree.
* @param converter
* Used to convert objects given in strings to complex objects to
* update the session with.
*/
public SPSessionPersister(String name, SPObject root, SessionPersisterSuperConverter converter) {
this.name = name;
this.root = root;
this.converter = converter;
}
@Override
public String toString() {
return "SPSessionPersister \"" + name + "\"";
}
public void begin() throws SPPersistenceException {
synchronized (getWorkspaceContainer()) {
enforceThreadSafety();
if (transactionCount == 0) {
lookupCache.clear();
converter.setUUIDCache(lookupCache);
}
transactionCount++;
if (logger.isDebugEnabled()) {
logger.debug("spsp.begin(); - transaction count : " + transactionCount);
}
}
}
public void commit() throws SPPersistenceException {
synchronized (getWorkspaceContainer()) {
enforceThreadSafety();
if (logger.isDebugEnabled()) {
logger.debug("spsp.commit(); - transaction count : " + transactionCount);
}
final SPObject workspace = getWorkspaceContainer().getWorkspace();
synchronized (workspace) {
try {
if (disableMagic) {
workspace.setMagicEnabled(false);
}
if (transactionCount == 0) {
throw new SPPersistenceException(null,
"Commit attempted while not in a transaction");
}
// Make sure the rollback lists are empty.
objectsToRemoveRollbackList.clear();
persistedObjectsRollbackList.clear();
persistedPropertiesRollbackList.clear();
if (transactionCount == 1) {
if (logger.isDebugEnabled()) {
logger.debug("Begin of commit phase...");
logger.debug("Committing " + persistedObjects.size() +
" new objects, " + persistedProperties.size() +
" changes to different property names, and " +
objectsToRemove.size() + " objects are being removed.");
}
workspace.begin("Begin batch transaction...");
commitRemovals();
commitObjects();
commitProperties();
workspace.commit();
if (logger.isDebugEnabled()) {
logger.debug("...commit succeeded.");
}
}
} catch (Throwable t) {
logger.error("SPSessionPersister caught an exception while " +
"performing a commit operation. Will try to rollback...", t);
if (rollbackOnCommitError) {
rollback();
}
if (t instanceof SPPersistenceException) throw (SPPersistenceException) t;
if (t instanceof FriendlyRuntimeSPPersistenceException) throw (FriendlyRuntimeSPPersistenceException) t;
throw new SPPersistenceException(null, t);
} finally {
if (transactionCount > 0) {
transactionCount--;
if (transactionCount == 0) {
objectsToRemove.clear();
objectsToRemoveRollbackList.clear();
persistedObjects.clear();
persistedObjectsMap.clear();
persistedObjectsRollbackList.clear();
persistedProperties.clear();
persistedPropertiesRollbackList.clear();
lookupCache.clear();
converter.removeUUIDCache();
currentThread = null;
}
}
if (disableMagic) {
workspace.setMagicEnabled(true);
}
}
}
}
}
public void persistObject(String parentUUID, String type, String uuid,
int index) throws SPPersistenceException {
if (logger.isDebugEnabled()) logger.debug("Persisting object " + uuid + " of type " + type + " as a child to " + parentUUID);
synchronized (getWorkspaceContainer()) {
enforceThreadSafety();
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"spsp.persistObject(\"%s\", \"%s\", \"%s\", %d);", parentUUID,
type, uuid, index));
}
if (transactionCount == 0) {
rollback();
throw new SPPersistenceException("Cannot persist objects while outside " +
"a transaction.");
}
SPObject objectToPersist = findByUuid(root, uuid, SPObject.class);
boolean exists = exists(uuid);
if (exists && objectToPersist == null) {
throw new NullPointerException("The following object exists and doesn't exist: parent " +
parentUUID + ", id " + uuid + ", type " + type);
}
if (exists && objectToPersist.getClass() != root.getClass()) {
//If the object was already created by a different process we will continue
//if we are in god mode.
if (godMode) return;
rollback();
throw new SPPersistenceException(uuid,
"An SPObject with UUID " + uuid + " and type " + type
+ " under parent with UUID " + parentUUID
+ " already exists.\n"
+ " The object exists in the root already? " + (objectToPersist != null) + "\n"
+ " The persisted objects map contains keys: " + persistedObjectsMap.keySet() + "\n"
+ " The persisted properties map contains values for this object? "
+ (persistedProperties.get(uuid) != null && !persistedProperties.get(uuid).isEmpty()) + "\n"
+ " The removed set contains the object? " + (objectsToRemove.containsKey(uuid)));
}
PersistedSPObject pso = new PersistedSPObject(parentUUID,
type, uuid, index);
persistedObjects.add(pso);
persistedObjectsMap.put(pso.getUUID(), pso);
}
}
public void persistProperty(String uuid, String propertyName,
DataType propertyType, Object oldValue, Object newValue)
throws SPPersistenceException {
if (transactionCount <= 0) {
rollback();
throw new SPPersistenceException(null, "Cannot persist objects while outside " +
"a transaction.");
}
synchronized (getWorkspaceContainer()) {
enforceThreadSafety();
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"spsp.persistProperty(\"%s\", \"%s\", DataType.%s, %s, %s);",
uuid, propertyName, propertyType.name(), oldValue, newValue));
}
try {
persistPropertyHelper(uuid, propertyName, propertyType, oldValue,
newValue, godMode);
} catch (SPPersistenceException e) {
rollback();
throw e;
}
}
}
public void persistProperty(String uuid, String propertyName,
DataType propertyType, Object newValue)
throws SPPersistenceException {
if (logger.isDebugEnabled()) logger.debug("Persisting property " + propertyName + ", changing to " + newValue);
if (transactionCount <= 0) {
rollback();
throw new SPPersistenceException("Cannot persist objects while outside " +
"a transaction.");
}
synchronized (getWorkspaceContainer()) {
enforceThreadSafety();
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"spsp.persistProperty(\"%s\", \"%s\", DataType.%s, %s); // unconditional",
uuid, propertyName, propertyType.name(),
newValue));
}
try {
if (newValue instanceof InputStream && !((InputStream) newValue).markSupported()) {
newValue = new BufferedInputStream((InputStream) newValue);
}
persistPropertyHelper(uuid, propertyName, propertyType, newValue,
newValue, true);
} catch (SPPersistenceException e) {
rollback();
throw e;
}
}
}
/**
* Helper to persist a {@link SPObject} property given by its object
* UUID, property name, property type, expected old value, and new value.
* This can be done either conditionally or unconditionally based on which
* persistProperty method called this one.
*
* @param uuid
* The UUID of the {@link SPObject} to persist the property
* upon
* @param propertyName
* The property name
* @param propertyType
* The property type
* @param oldValue
* The expected old property value
* @param newValue
* The new property value to persist
* @throws SPPersistenceException
* Thrown if the property name is not known in this method.
*/
private void persistPropertyHelper(String uuid, String propertyName,
DataType propertyType, Object oldValue, Object newValue,
boolean unconditional) throws SPPersistenceException {
if (!exists(uuid)) {
throw new SPPersistenceException(uuid,
"SPObject with UUID " + uuid + " could not be found." +
" Was trying to set its property \"" + propertyName + "\" " +
"to value \"" + newValue + "\".");
}
Object lastPropertyValueFound = null;
for (PersistedSPOProperty persistedProperty : persistedProperties.get(uuid)) {
if (propertyName.equals(persistedProperty.getPropertyName())) {
lastPropertyValueFound = persistedProperty.getNewValue();
}
}
Object propertyValue = null;
SPObject spo = findByUuid(root, uuid,
SPObject.class);
if (lastPropertyValueFound != null) {
if (!unconditional && !lastPropertyValueFound.equals(oldValue)) {
throw new SPPersistenceException(uuid, "For property \""
+ propertyName + "\", the expected property value \""
+ oldValue
+ "\" does not match with the actual property value \""
+ lastPropertyValueFound + "\"");
}
} else {
if (!unconditional && spo != null) {
try {
propertyValue = PersisterHelperFinder.findPersister(spo.getClass()).findProperty(spo, propertyName, converter);
} catch (Exception e) {
throw new SPPersistenceException("Could not find the persister helper for " + spo.getClass(), e);
}
if (propertyValue != null && oldValue == null) {
throw new SPPersistenceException(uuid, "For property \""
+ propertyName + "\" on SPObject of type "
+ spo.getClass() + " and UUID + " + spo.getUUID()
+ ", the expected property value \"" + oldValue
+ "\" does not match with the actual property value \""
+ propertyValue + "\"");
}
// } else if (!unconditional) {
// throw new SPPersistenceException(uuid, "Could not find the object with id " +
// uuid + " to set property " + propertyValue);
}
}
if (spo != null) {
persistedProperties.put(uuid, new PersistedSPOProperty(uuid,
propertyName, propertyType, propertyValue, newValue, unconditional));
} else {
persistedProperties.put(uuid, new PersistedSPOProperty(uuid,
propertyName, propertyType, oldValue, newValue, unconditional));
}
}
public void removeObject(String parentUUID, String uuid)
throws SPPersistenceException {
synchronized (getWorkspaceContainer()) {
enforceThreadSafety();
if (logger.isDebugEnabled()) {
logger.debug(String.format("spsp.removeObject(\"%s\", \"%s\");",
parentUUID, uuid));
}
if (transactionCount == 0) {
logger.error("Remove Object attempted while not in a transaction. " +
"Rollback initiated.");
rollback();
throw new SPPersistenceException(uuid,"Remove Object attempted while " +
"not in a transaction. Rollback initiated.");
}
//Now that we allow remove persist calls on objects that are children of
//objects being removed the persister needs to handle them.
SPObject spo = findByUuid(root, uuid, SPObject.class);
if (spo == null) {
rollback();
throw new SPPersistenceException(uuid,
"Cannot remove the SPObject with UUID " + uuid
+ " from parent UUID " + parentUUID
+ " as it does not exist.");
}
objectsToRemove.put(uuid, parentUUID);
}
}
public void rollback() {
rollback(false);
}
public void rollback(boolean force) {
final SPObject workspace = getWorkspaceContainer().getWorkspace();
synchronized (workspace) {
if (headingToWisconsin) {
return;
}
headingToWisconsin = true;
if (!force) {
enforceThreadSafety();
}
try {
// We catch ANYTHING that comes out of here and rollback.
// Some exceptions are Runtimes, so we must catch those too.
if (disableMagic) {
workspace.setMagicEnabled(false);
}
workspace.begin("Rolling back changes.");
rollbackProperties();
rollbackCreations();
rollbackRemovals();
workspace.commit("Done Rolling back");
workspace.rollback("Rolling back all listeners on the workspace as they should have any transaction counters reset.");
} catch (Exception e) {
// This is a major fuck up. We could not rollback so now we must restore
// by whatever means
logger.fatal("First try at restore failed.", e);
// TODO Monitor this
} finally {
if (disableMagic) {
workspace.setMagicEnabled(true);
}
objectsToRemove.clear();
objectsToRemoveRollbackList.clear();
persistedObjects.clear();
persistedObjectsMap.clear();
persistedObjectsRollbackList.clear();
persistedProperties.clear();
persistedPropertiesRollbackList.clear();
lookupCache.clear();
converter.removeUUIDCache();
transactionCount = 0;
currentThread = null;
headingToWisconsin = false;
if (logger.isDebugEnabled()) {
logger.debug("spsp.rollback(); - Killed all current transactions.");
}
}
}
}
/**
* Commits the removal of persisted {@link SPObject}s
*
* @throws SPPersistenceException
* Thrown if an {@link SPObject} could not be removed from its
* parent.
*/
private void commitRemovals() throws SPPersistenceException {
Map<String, String> sortedObjectsToRemove =
new TreeMap<String, String>(removedObjectComparator);
sortedObjectsToRemove.putAll(objectsToRemove);
for (Map.Entry<String, String> removeEntry : sortedObjectsToRemove.entrySet()) {
SPObject spo = findByUuid(root, removeEntry.getKey(),
SPObject.class);
//The ancestor of this object has been deleted by this transaction
//already so we don't need to delete the object again.
if (spo == null) {
boolean descendantRemoved = false;
for (RemovedObjectEntry removedRollbackEntry : objectsToRemoveRollbackList.values()) {
//Can't use the cache as these objects are removed.
if (SQLPowerUtils.findByUuid(removedRollbackEntry.getRemovedChild(), removeEntry.getKey(), SPObject.class) != null) {
descendantRemoved = true;
break;
}
}
if (descendantRemoved) continue;
}
SPObject parent = findByUuid(root, removeEntry.getValue(),
SPObject.class);
try {
List<? extends SPObject> siblings;
if (parent instanceof SQLObject) {
siblings = ((SQLObject) parent).getChildrenWithoutPopulating();
} else {
if (parent == null) {
throw new NullPointerException("The parent with id " + removeEntry.getValue() +
" is missing. However, to get here the spo with id " + removeEntry.getKey() +
" must not have been null. The spo is " + spo);
}
siblings = parent.getChildren();
}
int index = siblings.indexOf(spo);
index -= parent.childPositionOffset(spo.getClass());
parent.removeChild(spo);
// Add spo and hierarchy of its children to the remove-roll-back-list
removeRollBackList(spo, parent, index);
Set<String> removedKeys = SQLPowerUtils.buildIdMap(spo).keySet();
for (String removedKey : removedKeys) {
lookupCache.remove(removedKey);
}
} catch (IllegalArgumentException e) {
throw new SPPersistenceException(removeEntry.getKey(), e);
} catch (ObjectDependentException e) {
throw new SPPersistenceException(removeEntry.getKey(), e);
}
}
if (objectsToRemoveRollbackList.size() != objectsToRemove.size()) {
logger.warn("Skipped some objects");
}
objectsToRemove.clear();
}
private void removeRollBackList(SPObject object, SPObject parent, int index) {
objectsToRemoveRollbackList.put(object.getUUID(),
new RemovedObjectEntry(parent.getUUID(), object, index));
}
/**
* We do this because we override this in MMSessionPersister which needs to do some work after sorting
* the list, then calling commitSortObjects
* @throws SPPersistenceException
*/
protected void commitObjects() throws SPPersistenceException {
Collections.sort(persistedObjects, getComparator());
if (correctSQLImportOrder) {
// importedKeys must be persisted after relationships. This is a bit of a ridiculous hack, so
// we may want to change it!
List<PersistedSPObject> rImportedKeys = new ArrayList<PersistedSPObject>();
for (int i = 0; i < persistedObjects.size(); i++) {
if (persistedObjects.get(i).getType().equals(SQLRelationship.SQLImportedKey.class.getName())) {
rImportedKeys.add(persistedObjects.get(i));
}
}
persistedObjects.removeAll(rImportedKeys);
persistedObjects.addAll(rImportedKeys);
// --------------------------------------------------------------------------------------------
}
commitSortedObjects();
}
/**
* Returns a comparator that can be used to sort the
* {@link PersistedSPObject} events so the objects in the tree are created
* in an order that does not cause them to cause exceptions.
*/
protected Comparator<? super PersistedSPObject> getComparator() {
return new PersistedObjectComparator(root, persistedObjectsMap);
}
/**
* Commits the persisted {@link SPObject}s
*
* @throws SPPersistenceException
*/
protected void commitSortedObjects() throws SPPersistenceException {
for (PersistedSPObject pso : persistedObjects) {
if (logger.isDebugEnabled()) logger.debug("Persisting " + pso);
if (pso.isLoaded())
continue;
SPObject parent = findByUuid(root, pso.getParentUUID(),
SPObject.class);
SPObject spo = null;
if (parent == null && pso.getType().equals(root.getClass().getName())) {
lookupCache.clear();
refreshRootNode(pso);
lookupCache.clear();
continue;
} else if (parent == null) {
throw new IllegalStateException("Missing parent with uuid " + pso.getParentUUID() +
" when trying to load " + pso);
}
//XXX This was initially part of the fix for bug 2326 but is a larger problem
//that is we need to get the children of a SQLObject without populating them
//for the persister layer but other than this case we should not need to let
//other places get at the children of SQLObjects in this manner.
Class<? extends SPObject> parentAllowedChildType;
try {
parentAllowedChildType = PersisterUtils.getParentAllowedChildType(pso.getType(),
parent.getClass().getName());
} catch (Exception e) {
throw new RuntimeException(e);
}
List<? extends SPObject> siblings;
if (parent instanceof SQLObject) {
siblings = ((SQLObject) parent).getChildrenWithoutPopulating(parentAllowedChildType);
} else {
siblings = parent.getChildren(parentAllowedChildType);
}
if (objectsToRemoveRollbackList.keySet().contains(parent.getUUID())) {
for (SPObject sibling : siblings) {
if (sibling.getUUID().equals(pso.getUUID())) {
spo = sibling;
break;
}
}
}
//Ancestor list does not contain the node passed in.
if (spo == null) {
for (SPObject ancestor : SQLPowerUtils.getAncestorList(parent)) {
if (objectsToRemoveRollbackList.keySet().contains(ancestor.getUUID())) {
for (SPObject sibling : siblings) {
if (sibling.getUUID().equals(pso.getUUID())) {
spo = sibling;
break;
}
}
break;
}
}
}
if (spo != null) {
//Case for the ancestor was moved and on adding the ancestor this object
//was essentially re-added as well as it was never technically removed.
//This is done above.
removeFinalPersistProperties(pso);
continue;
} else if (objectsToRemoveRollbackList.get(pso.getUUID()) != null) {
//Object was removed and added back in. This is the current way a 'move' of a
//child object is represented in the system. We may want to revisit this and
//look at creating a different command, or special property change of 'move'.
spo = objectsToRemoveRollbackList.get(pso.getUUID()).getRemovedChild();
removeFinalPersistProperties(pso);
} else if (parent instanceof SQLObject && populateSQLObject((SQLObject) parent)) {
continue;
} else {
try {
spo = PersisterHelperFinder.findPersister(pso.getType()).commitObject(pso, persistedProperties,
persistedObjects, converter);
} catch (Exception ex) {
throw new SPPersistenceException("Could not find the persister helper for " + pso.getType(), ex);
}
}
if (spo != null) {
SPListener removeChildOnAddListener = new SPListener() {
public void propertyChanged(PropertyChangeEvent arg0) {
//do nothing
}
public void childRemoved(SPChildEvent e) {
objectsToRemoveRollbackList.put(e.getChild().getUUID(),
new RemovedObjectEntry(e.getSource().getUUID(),
e.getChild(), e.getIndex()));
}
public void childAdded(SPChildEvent e) {
//do nothing
}
public void transactionStarted(TransactionEvent e) {
//do nothing
}
public void transactionRollback(TransactionEvent e) {
//do nothing
}
public void transactionEnded(TransactionEvent e) {
//do nothing
}
};
try {
parent.addSPListener(removeChildOnAddListener);
try {
// FIXME Terrible hack, see bug 2326
//XXX This appears to shuffle columns up which will cause columns to be rearranged
parent.addChild(spo, Math.min(pso.getIndex(), siblings.size()));
persistedObjectsRollbackList.add(
new PersistedObjectEntry(
parent.getUUID(),
spo.getUUID()));
lookupCache.putAll(SQLPowerUtils.buildIdMap(spo));
} catch (RuntimeException e) {
if (parent.getChildren().contains(spo)) {
try {
parent.removeChild(spo);
} catch (RuntimeException ex) {
logger.error("Failed to add the child and now cannot remove the child.", ex);
} catch (ObjectDependentException ex) {
logger.error("Failed to add the child and now cannot remove the child.", ex);
}
}
throw e;
}
} finally {
parent.removeSPListener(removeChildOnAddListener);
}
}
}
persistedObjects.clear();
persistedObjectsMap.clear();
}
/**
* Helper method for {@link #commitObjects()}. Removes the persist property
* calls that are final to the given object. For the case where the object
* was moved and is not actually being recreated but just moved in-tact.
*/
private void removeFinalPersistProperties(PersistedSPObject pso)
throws SPPersistenceException {
List<String> persistedPropNames;
try {
persistedPropNames = PersisterHelperFinder.findPersister(pso.getType()).getPersistedProperties();
} catch (Exception ex) {
throw new SPPersistenceException("Could not find the persister helper for " + pso.getType(), ex);
}
List<PersistedSPOProperty> propertiesToRemove = new ArrayList<PersistedSPOProperty>();
for (PersistedSPOProperty spoProperty : persistedProperties.get(pso.getUUID())) {
if (!persistedPropNames.contains(spoProperty.getPropertyName())) {
propertiesToRemove.add(spoProperty);
}
}
for (PersistedSPOProperty spoProperty : propertiesToRemove) {
persistedProperties.get(pso.getUUID()).remove(spoProperty);
}
}
/**
* This is a special corner case for populating SQLObjects. If an object is
* populating, the objects must be added in a special way so the populate
* flags are set when objects are notified that the population has happened.
* This prevents the problem of the DBTreeModel from calling populate when
* an object is added and an infinite loop occurring.
*
* @param parent
* The parent that is being added to the model. This may or may
* not have been populated.
* @return True if the object was populated and its children do not need to
* be added to the object again. False otherwise.
*/
private boolean populateSQLObject(SQLObject parent) throws SPPersistenceException {
//This gross chunk of code essentially simulates, and uses part of, the populate
//method of classes that have the populate method defined.
if (!(parent instanceof SQLTable || parent instanceof SQLDatabase ||
parent instanceof SQLSchema || parent instanceof SQLCatalog)) {
return false;
}
List<PersistedSPObject> childrenForPopulate = new ArrayList<PersistedSPObject>();
if (parent instanceof SQLTable) {
//For SQLTable
Boolean columnsPopulated = PersisterUtils.findPersistedBooleanProperty(persistedProperties, parent.getUUID(), "columnsPopulated");
Boolean indicesPopulated = PersisterUtils.findPersistedBooleanProperty(persistedProperties, parent.getUUID(), "indicesPopulated");
Boolean exportedKeysPopulated = PersisterUtils.findPersistedBooleanProperty(persistedProperties, parent.getUUID(), "exportedKeysPopulated");
Boolean importedKeysPopulated = PersisterUtils.findPersistedBooleanProperty(persistedProperties, parent.getUUID(), "importedKeysPopulated");
final SQLTable table = (SQLTable) parent;
if (!table.isColumnsPopulated() && columnsPopulated != null && columnsPopulated) {
List<PersistedSPObject> columnsForPopulate = new ArrayList<PersistedSPObject>();
for (PersistedSPObject spo : persistedObjects) {
if (!spo.isLoaded() && spo.getParentUUID().equals(parent.getUUID())
&& spo.getType().equals(SQLColumn.class.getName())) {
columnsForPopulate.add(spo);
}
}
List<SQLObject> children = new ArrayList<SQLObject>();
for (PersistedSPObject pso : columnsForPopulate) {
try {
SQLObject spo = (SQLObject) PersisterHelperFinder.findPersister(pso.getType()).commitObject(pso, persistedProperties,
persistedObjects, converter);
children.add(spo);
} catch (Exception ex) {
throw new SPPersistenceException("Could not find the persister helper for " + pso.getType(), ex);
}
}
SQLObjectUtils.populateChildrenWithList(parent, children);
childrenForPopulate.addAll(columnsForPopulate);
if (columnsForPopulate.isEmpty()) {
//Need to be set even if no columns exist so the indices and relationships
//don't explode
table.setColumnsPopulated(columnsPopulated);
}
}
if (!table.isIndicesPopulated() && indicesPopulated != null && indicesPopulated) {
List<PersistedSPObject> indicesForPopulate = new ArrayList<PersistedSPObject>();
for (PersistedSPObject spo : persistedObjects) {
if (!spo.isLoaded() && spo.getParentUUID().equals(parent.getUUID())
&& spo.getType().equals(SQLIndex.class.getName())) {
indicesForPopulate.add(spo);
}
}
List<SQLObject> children = new ArrayList<SQLObject>();
for (PersistedSPObject pso : indicesForPopulate) {
try {
SQLObject spo = (SQLObject) PersisterHelperFinder.findPersister(pso.getType()).commitObject(pso, persistedProperties,
persistedObjects, converter);
children.add(spo);
} catch (Exception ex) {
throw new SPPersistenceException("Could not find the persister helper for " + pso.getType(), ex);
}
}
SQLObjectUtils.populateChildrenWithList(parent, children);
childrenForPopulate.addAll(indicesForPopulate);
if (indicesForPopulate.isEmpty()) {
//Need to be set even if no columns exist so the relationships don't explode
table.setIndicesPopulated(indicesPopulated);
}
}
if (!table.isExportedKeysPopulated() && exportedKeysPopulated != null && exportedKeysPopulated) {
List<PersistedSPObject> exportedKeysForPopulate = new ArrayList<PersistedSPObject>();
for (PersistedSPObject spo : persistedObjects) {
if (!spo.isLoaded() && spo.getParentUUID().equals(parent.getUUID())
&& spo.getType().equals(SQLRelationship.class.getName())) {
exportedKeysForPopulate.add(spo);
}
}
List<SQLObject> children = new ArrayList<SQLObject>();
for (PersistedSPObject pso : exportedKeysForPopulate) {
try {
SQLObject spo = (SQLObject) PersisterHelperFinder.findPersister(pso.getType()).commitObject(pso, persistedProperties,
persistedObjects, converter);
children.add(spo);
} catch (Exception ex) {
throw new SPPersistenceException("Could not find the persister helper for " + pso.getType(), ex);
}
}
SQLObjectUtils.populateChildrenWithList(parent, children);
childrenForPopulate.addAll(exportedKeysForPopulate);
}
// SQLImportedKeys do not need to be added to parent in any special
// way, and in fact must wait until all relationships have been
// added.
} else {
//For all SQLObjects that are not SQLTable
Boolean populated = PersisterUtils.findPersistedBooleanProperty(persistedProperties, parent.getUUID(), "populated");
if (!parent.isPopulated() && populated != null && populated) {
for (PersistedSPObject spo : persistedObjects) {
if (!spo.isLoaded() && spo.getParentUUID().equals(parent.getUUID())) {
childrenForPopulate.add(spo);
}
}
}
List<SQLObject> children = new ArrayList<SQLObject>();
for (PersistedSPObject pso : childrenForPopulate) {
try {
SQLObject spo = (SQLObject) PersisterHelperFinder.findPersister(pso.getType()).commitObject(pso, persistedProperties,
persistedObjects, converter);
children.add(spo);
} catch (Exception ex) {
throw new SPPersistenceException("Could not find the persister helper for " + pso.getType(), ex);
}
}
SQLObjectUtils.populateChildrenWithList(parent, children);
}
if (!childrenForPopulate.isEmpty()) {
//TODO Assert children are in their correct location.
for (PersistedSPObject spo : childrenForPopulate) {
persistedObjectsRollbackList.add(
new PersistedObjectEntry(
parent.getUUID(),
spo.getUUID()));
}
return true;
}
return false;
}
/**
* Called when we get a persist object of the root node. This will reset the
* object tree and update the root node and any final children within it to
* values in the persisted objects and properties list.
*
* @param pso
* The persist object call that would create the root object.
*/
protected abstract void refreshRootNode(PersistedSPObject pso);
/**
* Commits the persisted {@link SPObject} property values
*
* @throws SPPersistenceException
* Thrown if an invalid SPObject type has been persisted into
* storage. This theoretically should not occur.
*/
private void commitProperties() throws SPPersistenceException {
SPObject spo;
String propertyName;
Object newValue;
for (String uuid : persistedProperties.keySet()) {
spo = findByUuid(root, uuid, SPObject.class);
if (spo == null) {
throw new IllegalStateException("Couldn't locate object "
+ uuid + " in session");
}
for (PersistedSPOProperty persistedProperty : persistedProperties.get(uuid)) {
propertyName = persistedProperty.getPropertyName();
newValue = persistedProperty.getNewValue();
if (logger.isDebugEnabled()) {
logger.debug("Applying property " + propertyName + " to " +
spo.getClass().getSimpleName() + " at " + spo.getUUID());
}
Object oldValue;
try {
SPPersisterHelper<? extends SPObject> persisterHelper = PersisterHelperFinder.findPersister(spo.getClass());
oldValue = persisterHelper.findProperty(spo, propertyName, converter);
persisterHelper.commitProperty(
spo, propertyName, newValue, persistedProperty.getDataType(), converter);
} catch (Exception e) {
throw new SPPersistenceException("Could not find the persister helper for " + spo.getClass(), e);
}
if (!persistedProperty.isUnconditional()) {
oldValue = persistedProperty.getOldValue();
}
persistedPropertiesRollbackList.add(
new PersistedPropertiesEntry(
spo.getUUID(), //The uuid can be changed so using the currently set one.
persistedProperty.getPropertyName(),
persistedProperty.getDataType(),
oldValue));
}
}
persistedProperties.clear();
}
/**
* Rolls back the removal of persisted {@link SPObject}s
*/
private void rollbackRemovals() {
// We must rollback in the inverse order the operations were performed.
List<RemovedObjectEntry> removedObjects =
new ArrayList<RemovedObjectEntry>(objectsToRemoveRollbackList.values());
Collections.reverse(removedObjects);
for (RemovedObjectEntry entry : removedObjects) {
final String parentUuid = entry.getParentUUID();
final SPObject objectToRestore = entry.getRemovedChild();
final int index = entry.getIndex();
final SPObject parent = findByUuid(root, parentUuid, SPObject.class);
try {
if (!parent.getChildren().contains(objectToRestore)) {
parent.addChild(objectToRestore, index);
}
} catch (Throwable t) {
// Keep going. We need to rollback as much as we can.
logger.error("Cannot rollback " + entry.getRemovedChild() + " child removal", t);
}
}
}
/**
* Rolls back the changed properties of persisted {@link SPObject}s.
*/
private void rollbackProperties() {
Collections.reverse(persistedPropertiesRollbackList);
Set<String> objectCreationRollbackUUIDs = new HashSet<String>();
for (PersistedObjectEntry entry : persistedObjectsRollbackList) {
objectCreationRollbackUUIDs.add(entry.getChildId());
}
for (PersistedPropertiesEntry entry : persistedPropertiesRollbackList) {
try {
final String parentUUID = entry.getUUID();
//These objects will be removed and we cannot roll back final properties so we
//will skip them.
if (objectCreationRollbackUUIDs.contains(parentUUID)) continue;
final String propertyName = entry.getPropertyName();
final Object rollbackValue = entry.getRollbackValue();
final SPObject parent = findByUuid(root, parentUUID, SPObject.class);
if (parent != null) {
PersisterHelperFinder.findPersister(parent.getClass()).commitProperty(
parent, propertyName, rollbackValue, entry.getPropertyType(), converter);
}
} catch (Throwable t) {
// Keep going. We need to rollback as much as we can.
logger.error("Cannot rollback change to " + entry.getPropertyName() +
" to value " + entry.getRollbackValue(), t);
}
}
}
/**
* Rolls back the created persisted {@link SPObject}s by removing them from
* their parents.
*/
private void rollbackCreations() {
Collections.reverse(persistedObjectsRollbackList);
for (PersistedObjectEntry entry : persistedObjectsRollbackList) {
try {
// We need to verify if the entry specifies a parent.
// Root objects don't have parents so we can't remove them really...
if (entry.getParentId() != null) {
final SPObject parent = findByUuid(root,
entry.getParentId(), SPObject.class);
final SPObject child = findByUuid(root,
entry.getChildId(), SPObject.class);
parent.removeChild(child);
}
} catch (Throwable t) {
// Keep going. We need to rollback as much as we can.
logger.error("Cannot rollback " + entry.getChildId() + " child creation", t);
}
}
}
/**
* Checks to see if a {@link SPObject} with a certain UUID exists
*
* @param uuid
* The UUID to search for
* @return Whether or not the {@link SPObject} exists
*/
private boolean exists(String uuid) {
if (persistedObjectsMap.get(uuid) != null) return true;
SPObject spo = findByUuid(root, uuid, SPObject.class);
if (spo != null) {
List<SPObject> ancestors = SQLPowerUtils.getAncestorList(spo);
ancestors.add(spo);
for (SPObject ancestor : ancestors) {
if (objectsToRemove.containsKey(ancestor.getUUID())) {
return false;
}
}
return true;
}
return false;
}
public boolean isHeadingToWisconsin() {
return headingToWisconsin;
}
/**
* This is part of the 'echo-cancellation' system to notify any
* {@link WorkspacePersisterListener} listening to the same session to
* ignore modifications to that session.
*/
public boolean isUpdatingWorkspace() {
if (transactionCount > 0) {
return true;
} else if (transactionCount == 0) {
return false;
} else {
rollback();
throw new IllegalStateException("This persister is in an illegal state. " +
"transactionCount was :" + transactionCount);
}
}
/**
* Enforces thread safety.
*/
public void enforceThreadSafety() {
if (currentThread == null) {
currentThread = Thread.currentThread();
} else {
if (currentThread != Thread.currentThread()) {
rollback(true);
throw new RuntimeException("A call from two different threads was detected. " +
"Callers of a sessionPersister should synchronize prior to " +
"opening transactions. The thread " + Thread.currentThread().getName() +
" tried to access the persister while the thread " + currentThread.getName() +
" was already using it.");
}
}
}
/**
* Turns this persister as a preacher of the truth and always the truth. All
* calls are turned into unconditionals.
*
* @param godMode
* True or False
*/
public void setGodMode(boolean godMode) {
this.godMode = godMode;
}
private void setObjectsToRemoveRollbackList(
LinkedHashMap<String, RemovedObjectEntry> objectsToRemoveRollbackList) {
this.objectsToRemoveRollbackList = objectsToRemoveRollbackList;
}
private void setPersistedObjectsRollbackList(
List<PersistedObjectEntry> persistedObjectsRollbackList) {
this.persistedObjectsRollbackList = persistedObjectsRollbackList;
}
private void setPersistedPropertiesRollbackList(
List<PersistedPropertiesEntry> persistedPropertiesRollbackList) {
this.persistedPropertiesRollbackList = persistedPropertiesRollbackList;
}
public void setWorkspaceContainer(WorkspaceContainer workspaceContainer) {
this.workspaceContainer = workspaceContainer;
}
public WorkspaceContainer getWorkspaceContainer() {
return workspaceContainer;
}
/**
* Undoes the persist calls on the root object that are passed into this
* method. This allows the persister listener to use the session persister's
* roll back method. Nothing will be done on the persist of a root node when
* rolling back. If we are rolling back a set of persist calls with the root
* node then a load or refresh failed and there is no simple way to go back.
*
* @param root
* The root of the object tree. The object tree will be searched
* for objects with corresponding UUIDs and will be updated based
* on the persist calls.
* @param creations
* A set of persist object calls that created objects that needs
* to be reversed.
* @param properties
* A set of persist property calls that updated objects that need
* to be reversed.
* @param removals
* An ordered set of remove object calls that need objects to be added
* back in. The key of each entry is the UUID of the object that was removed.
* @param converter
* An object converter that can convert the simple property types
* in the persist property calls to full objects to update the
* object tree.
* @throws SPPersistenceException
*/
public static void undoForSession(
SPObject root,
List<PersistedObjectEntry> creations,
List<PersistedPropertiesEntry> properties,
LinkedHashMap<String, RemovedObjectEntry> removals,
SessionPersisterSuperConverter converter) throws SPPersistenceException
{
SPSessionPersister persister = new SPSessionPersister("undoer", root, converter) {
@Override
protected void refreshRootNode(PersistedSPObject pso) {
//do nothing for refresh.
}
};
persister.setWorkspaceContainer(root.getWorkspaceContainer());
persister.setGodMode(true);
persister.setObjectsToRemoveRollbackList(removals);
persister.setPersistedObjectsRollbackList(creations);
persister.setPersistedPropertiesRollbackList(properties);
persister.rollback(true);
}
public static void undoForSession(SPObject root,
List<PersistedSPObject> creations,
Multimap<String, PersistedSPOProperty> properties,
List<RemovedObjectEntry> removals,
SessionPersisterSuperConverter converter) throws SPPersistenceException {
List<PersistedObjectEntry> c = new LinkedList<PersistedObjectEntry>();
List<PersistedPropertiesEntry> p = new LinkedList<PersistedPropertiesEntry>();
LinkedHashMap<String, RemovedObjectEntry> r = new LinkedHashMap<String, RemovedObjectEntry>();
for (PersistedSPObject pso : creations) {
c.add(new PersistedObjectEntry(pso.getParentUUID(), pso.getUUID()));
}
for (PersistedSPOProperty property : properties.values()) {
p.add(new PersistedPropertiesEntry(property.getUUID(), property.getPropertyName(), property.getDataType(), property.getOldValue()));
}
for (RemovedObjectEntry removal : removals) {
r.put(removal.getRemovedChild().getUUID(), removal);
}
undoForSession(root, c, p, r, converter);
}
public static void redoForSession(
SPObject root,
List<PersistedSPObject> creations,
Multimap<String, PersistedSPOProperty> properties,
List<RemovedObjectEntry> removals,
SessionPersisterSuperConverter converter) throws SPPersistenceException
{
SPSessionPersister persister = new SPSessionPersister("redoer", root, converter) {
@Override
protected void refreshRootNode(PersistedSPObject pso) {
//do nothing for refresh.
}
};
Map<String, String> objToRmv = new TreeMap<String, String>(persister.removedObjectComparator);
for (RemovedObjectEntry roe : removals) {
objToRmv.put(roe.getRemovedChild().getUUID(), roe.getParentUUID());
}
persister.setWorkspaceContainer(root.getWorkspaceContainer());
persister.setGodMode(true);
persister.setPersistedObjects(creations);
persister.setPersistedProperties(properties);
persister.setObjectsToRemove(objToRmv);
persister.begin();
persister.commit();
}
protected void clearUUIDCache() {
lookupCache.clear();
}
protected <T extends SPObject> T findByUuid(SPObject root, String uuid, Class<T> expectedType) {
if (lookupCache.get(uuid) != null) {
SPObject foundObject = lookupCache.get(uuid);
if (!expectedType.isAssignableFrom(foundObject.getClass())) {
throw new IllegalStateException("The object " + foundObject + " is not of type " +
expectedType + " from the cache.");
}
return expectedType.cast(foundObject);
}
if (uuid == null || uuid.trim().isEmpty() || !lookupCache.isEmpty()) return null;
lookupCache.putAll(SQLPowerUtils.buildIdMap(this.root));
return expectedType.cast(lookupCache.get(uuid));
}
public void setDisableMagic(boolean disableMagic) {
this.disableMagic = disableMagic;
}
public void setRollbackOnCommitError(boolean rollbackOnCommitError) {
this.rollbackOnCommitError = rollbackOnCommitError;
}
}