/*
* 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.object;
import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import ca.sqlpower.object.SPChildEvent.EventType;
import ca.sqlpower.util.SQLPowerUtils;
import ca.sqlpower.util.TransactionEvent;
import ca.sqlpower.util.TransactionEvent.TransactionState;
/**
* Extend this class to add the behaviour to not respond to events when in a
* transaction. Instead of responding to events while in a transaction the
* listener will collect the events and then at the end of the transaction act
* on each event in the order it was received. If a rollback event was received
* the events will be discarded instead.
*/
public abstract class AbstractPoolingSPListener implements SPListener {
private static final Logger logger = Logger
.getLogger(AbstractPoolingSPListener.class);
/**
* Tracks the objects this listener is attached to that are in the middle of
* a transaction. If the map stores a number greater than 0 for a given
* object it is in a transaction state. If the set does not contain the
* object it is not in a transaction. This is a map as one instance of this
* object could be attached to several objects as a hierarchy listener.
*/
private final Map<SPObject, Integer> inTransactionMap =
new IdentityHashMap<SPObject, Integer>();
/**
* This creation counter is used for the creation time of the
* {@link EventObject}s so we can provide a specific order. There shouldn't
* be the case where two threads are firing events to the listener at the
* same time. If this case should arise it shouldn't matter who is first for
* the counter as it would end in a race condition in other locations.
*/
private long creationCounter = 0;
/**
* This object stores the events (child added, removed, or property change)
* and the index of when the change occurred. We need to store when the
* change occurred in the unusual case where we have a parent and child both
* in a transaction and operations are occuring to both objects. In this
* case we want to preserve the order as we can't say all parent events must
* come before the child events (cases where the child has a descendant
* added then the parent refers to the descendant) nor can we say the child
* events come before the parents (a sibling of the child is added to the
* parent and the child refers to its new sibling).
*/
private class EventObject {
private final Object event;
private final long creationTime;
public EventObject(Object event) {
this.event = event;
creationTime = getCurrentCreationTime();
}
public Object getEvent() {
return event;
}
public long getCreationTime() {
return creationTime;
}
}
/**
* Tracks the e that occur while an object is in a transaction state. These
* events will be acted on when the transaction ends or are removed when the
* transaction rolls back. The events can be {@link PropertyChangeEvent}s or
* {@link SPChildEvent}s.
*/
private final Map<SPObject, List<EventObject>> eventMap =
new IdentityHashMap<SPObject, List<EventObject>>();
private final Multimap<SPObject, SPObject> ancestorTransactionMap =
ArrayListMultimap.create();
private long getCurrentCreationTime() {
creationCounter++;
return creationCounter;
}
public final void transactionEnded(TransactionEvent e) {
Integer lastTransactionCount;
SPObject source = (SPObject) e.getSource();
if (errorOnDanglingCommit && inTransactionMap.get(source) == null) {
throw new IllegalStateException("An end transaction for object " + source
+ " of type " + source.getClass() + " was called while it was " +
"not in a transaction.");
} else if (!errorOnDanglingCommit && inTransactionMap.get(source) == null) {
return;
} else {
lastTransactionCount = inTransactionMap.get(source);
logger.debug("Transaction count on " + this + " for:" + source + ": " + inTransactionMap.get(source));
}
Integer nestedTransactionCount = lastTransactionCount - 1;
if (nestedTransactionCount < 0) {
throw new IllegalStateException("The transaction count was not removed properly.");
} else if (nestedTransactionCount > 0) {
inTransactionMap.put(source, nestedTransactionCount);
transactionEndedImpl(e);
} else {
inTransactionMap.remove(source);
if (!ancestorInTransaction(source)) {
handlePooledEvents(source);
transactionEndedImpl(e);
} else {
addEventToMap(e, source);
}
finalCommitImpl(e);
}
}
private void handlePooledEvents(SPObject source) {
List<EventObject> eventsForSource = collectPooledEvents(source);
Collections.sort(eventsForSource, new Comparator<EventObject>() {
@Override
public int compare(EventObject e1, EventObject e2) {
return Long.valueOf(e1.getCreationTime()).compareTo(Long.valueOf(e2.getCreationTime()));
}
});
for (EventObject event : eventsForSource) {
Object evt = event.getEvent();
if (evt instanceof PropertyChangeEvent) {
propertyChangeImpl((PropertyChangeEvent) evt);
} else if (evt instanceof SPChildEvent) {
SPChildEvent childEvent = (SPChildEvent) evt;
if (childEvent.getType().equals(EventType.ADDED)) {
childAddedImpl(childEvent);
} else if (childEvent.getType().equals(EventType.REMOVED)) {
childRemovedImpl(childEvent);
} else {
throw new IllegalStateException("Unknown child event of type " + childEvent.getType());
}
} else if (evt instanceof TransactionEvent) {
TransactionEvent transEvt = (TransactionEvent) evt;
if (transEvt.getState().equals(TransactionState.END)) {
transactionEndedImpl(transEvt);
} else {
throw new IllegalStateException("Unknown transaction event of type " + transEvt.getState());
}
} else {
throw new IllegalStateException("Unknown event type " + evt.getClass());
}
}
}
private List<EventObject> collectPooledEvents(SPObject source) {
List<EventObject> eventsForSource = new ArrayList<EventObject>();
if (eventMap.get(source) != null) {
//Copy of event list in case listener receiving events causes other events.
eventsForSource.addAll(eventMap.get(source));
eventMap.remove(source);
}
if (ancestorTransactionMap.get(source) != null) {
for (SPObject childSource : ancestorTransactionMap.get(source)) {
if (!isInTransaction(childSource)) {
eventsForSource.addAll(collectPooledEvents(childSource));
}
}
ancestorTransactionMap.removeAll(source);
}
return eventsForSource;
}
/**
* For almost all pooling listeners we want an error to occur if there
* is a commit outside of a transaction. However, in some specific listeners
* it is possible to add them to a parent object inside of a transaction
* which always results in a dangling commit at the end. For these specific
* cases we will disable the error.
*/
private final boolean errorOnDanglingCommit;
public AbstractPoolingSPListener() {
errorOnDanglingCommit = true;
}
public AbstractPoolingSPListener(boolean errorOnDanglingCommit) {
this.errorOnDanglingCommit = errorOnDanglingCommit;
}
/**
* Override this method if an action is required when a transaction ends.
* This will be called when any transactionEnded event is fired, even if
* it is the end of a transaction that is contained in another transaction.
*/
protected void transactionEndedImpl(TransactionEvent e) {
//meant to be overridden by classes extending this listener
}
/**
* Override this method if an action is required when the final transaction
* ends. This will only be called when the outermost transactionEnded event
* is fired. Note that transactionEndedImpl will be called before this
* method.
*/
protected void finalCommitImpl(TransactionEvent e) {
//meant to be overridden by classes extending this listener
}
public final void transactionRollback(TransactionEvent e) {
removePooledEvents((SPObject)e.getSource());
transactionRollbackImpl(e);
}
/**
* Removing all pooled events when doing rollback
* @param source
*/
private void removePooledEvents(SPObject source) {
inTransactionMap.remove(source);
if (eventMap.get(source) != null) {
eventMap.remove(source);
}
if (ancestorTransactionMap.get(source) != null) {
for (SPObject childSource : ancestorTransactionMap.get(source)) {
if (!isInTransaction(childSource)) {
removePooledEvents(childSource);
}
}
ancestorTransactionMap.removeAll(source);
}
}
/**
* Returns true if at least one ancestor is in a transaction.
*/
private boolean ancestorInTransaction(SPObject obj) {
List<SPObject> ancestors = SQLPowerUtils.getAncestorList(obj);
for (SPObject ancestor : ancestors) {
if (isInTransaction(ancestor)) return true;
}
return false;
}
private boolean isInTransaction(SPObject ancestor) {
return inTransactionMap.get(ancestor) != null
&& inTransactionMap.get(ancestor) > 0;
}
/**
* Connects the given object to it's closest's ancestor's transaction.
*/
private void addAncestorTransaction(SPObject obj) {
List<SPObject> ancestors = SQLPowerUtils.getAncestorList(obj);
Collections.reverse(ancestors);
for (SPObject ancestor : ancestors) {
if (isInTransaction(ancestor)) {
ancestorTransactionMap.put(ancestor, obj);
return;
}
}
}
/**
* Override this method if an action is required when a transaction rolls back.
*/
protected void transactionRollbackImpl(TransactionEvent e) {
//meant to be overridden by classes extending this listener
}
public final void transactionStarted(TransactionEvent e) {
Integer transactionCount = inTransactionMap.get(e.getSource());
SPObject source = (SPObject) e.getSource();
if (transactionCount == null) {
inTransactionMap.put(source, 1);
} else {
inTransactionMap.put(source, transactionCount + 1);
}
if (ancestorInTransaction(source)) {
addAncestorTransaction(source);
}
logger.debug("Transaction count on " + this + " for:" + e.getSource() + ": " + inTransactionMap.get(source));
transactionStartedImpl(e);
}
/**
* Override this method if an action is required when a transaction starts.
* This will be called when any transactionStarted event is fired, even if
* it is the start of a transaction that is contained in another transaction.
*/
protected void transactionStartedImpl(TransactionEvent e) {
//meant to be overridden by classes extending this listener
}
public final void childAdded(SPChildEvent e) {
SPObject source = e.getSource();
boolean ancestorTrans = ancestorInTransaction(source);
if (ancestorTrans || isInTransaction(source)) {
if (ancestorTrans) {
addAncestorTransaction(source);
}
addEventToMap(e, source);
} else {
childAddedImpl(e);
}
}
/**
* Override this method if an action is required when a child added event is
* acted upon.
*/
protected void childAddedImpl(SPChildEvent e) {
//meant to be overridden by classes extending this listener
}
public final void childRemoved(SPChildEvent e) {
SPObject source = e.getSource();
boolean ancestorTrans = ancestorInTransaction(source);
if (ancestorTrans || isInTransaction(source)) {
if (ancestorTrans) {
addAncestorTransaction(source);
}
addEventToMap(e, source);
} else {
childRemovedImpl(e);
}
}
private void addEventToMap(Object e, SPObject source) {
List<EventObject> events = eventMap.get(source);
if (events == null) {
events = new ArrayList<EventObject>();
eventMap.put(source, events);
}
events.add(new EventObject(e));
}
/**
* Override this method if an action is required when a child removed event is
* acted upon.
*/
protected void childRemovedImpl(SPChildEvent e) {
//meant to be overridden by classes extending this listener
}
public final void propertyChanged(PropertyChangeEvent evt) {
SPObject source = (SPObject) evt.getSource();
boolean ancestorTrans = ancestorInTransaction(source);
if (ancestorTrans || isInTransaction(source)) {
if (ancestorTrans) {
addAncestorTransaction(source);
}
addEventToMap(evt, source);
} else {
propertyChangeImpl(evt);
}
}
/**
* Override this method if an action is required when a property change event is
* acted upon.
*/
protected void propertyChangeImpl(PropertyChangeEvent evt) {
//meant to be overridden by classes extending this listener
}
}