/* * 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.List; import java.util.UUID; import org.apache.log4j.Logger; import ca.sqlpower.dao.PersisterUtils; import ca.sqlpower.dao.VetoableSPListener; import ca.sqlpower.object.SPChildEvent.EventType; import ca.sqlpower.object.annotation.Accessor; import ca.sqlpower.object.annotation.Constructor; import ca.sqlpower.object.annotation.Mutator; import ca.sqlpower.object.annotation.NonBound; import ca.sqlpower.object.annotation.NonProperty; import ca.sqlpower.object.annotation.Persistable; import ca.sqlpower.object.annotation.Transient; import ca.sqlpower.util.RunnableDispatcher; import ca.sqlpower.util.SessionNotFoundException; import ca.sqlpower.util.TransactionEvent; import ca.sqlpower.util.WorkspaceContainer; @Persistable public abstract class AbstractSPObject implements SPObject { private static final Logger logger = Logger.getLogger(AbstractSPObject.class); protected final List<SPListener> listeners = Collections.synchronizedList(new ArrayList<SPListener>()); private SPObject parent; private String name; /** * If magic is disabled, secondary side effects of methods or events should * not be performed. Magic is disabled if the disabled value is greater than * 0. For example in Wabit, if a guide is moved in a report the content * boxes attached to it will normally be moved with it. If magic is disabled * the content boxes attached to a guide should not be moved as the guide * moves. The guide that is moved while magic is disabled should still fire * an event that it was moved but the secondary event of moving the content * boxes and any other side effects should not take place. */ private int magicDisableCount = 0; @Constructor public AbstractSPObject() { this(null); } /** * The uuid string passed in must be the toString representation of the UUID * for this object. If the uuid string given is null then a new UUID will be * automatically generated. */ public AbstractSPObject(String uuid) { if (uuid == null) { generateNewUUID(); } else { this.uuid = uuid; } } /** * This UUID is for saving and loading to allow saved files to be diff friendly. */ protected String uuid; public boolean allowsChildType(Class<? extends SPObject> type) { for (Class<? extends SPObject> child : getAllowedChildTypes()) { if (child.isAssignableFrom(type)) { return true; } } return false; } public final void addChild(SPObject child, int index) throws IllegalArgumentException { if (!allowsChildType(child.getClass())) { throw new IllegalArgumentException(child.getClass() + " is not a valid child type of " + this.getClass()); } child.setParent(this); addChildImpl(child, index); } public void addChild(SPObject child) throws IllegalArgumentException { if (!allowsChildType(child.getClass())) { throw new IllegalArgumentException(child.getClass() + " is not a valid child type of " + this.getClass()); } child.setParent(this); Class<? extends SPObject> childClass; try { childClass = PersisterUtils.getParentAllowedChildType(child.getClass(), this.getClass()); } catch (IllegalAccessException e) { throw new RuntimeException("The allowedChildTypes field must be accessible", e); } catch (NoSuchFieldException e) { throw new RuntimeException("The allowedChildTypes field must exist", e); } addChild(child,getChildren(childClass).size()); } /** * This is the object specific implementation of * {@link #addChild(SPObject, int)}. There are checks in the * {@link #addChild(SPObject, int))} method to ensure that the object given * here is a valid child type of this object. * <p> * This method should be overwritten if children are allowed. * * @param child * The child to add to this object. * @param index * The index to add the child at. */ protected void addChildImpl(SPObject child, int index) { throw new UnsupportedOperationException("This SPObject item cannot have children. " + "This class is " + getClass() + " and trying to add " + child.getName() + " of type " + child.getClass()); } public void addSPListener(SPListener l) { if (l == null) { throw new NullPointerException("Cannot add child listeners that are null."); } synchronized (listeners) { if (listeners.contains(l)) { logger.debug("Listener " + l + " was added twice! Ignoring second add"); } else { listeners.add(l); } } } /** * Default cleanup method that does nothing. Override and implement this * method if cleanup is necessary. */ public CleanupExceptions cleanup() { return new CleanupExceptions(); } public void generateNewUUID() { uuid = UUID.randomUUID().toString(); } @NonProperty public <T extends SPObject> List<T> getChildren(Class<T> type) { return getChildren(type, getChildren()); } protected <T extends SPObject> List<T> getChildren(Class<T> type, List<? extends SPObject> childList) { List<T> children = new ArrayList<T>(); for (SPObject child : childList) { if (type.isAssignableFrom(child.getClass())) { children.add(type.cast(child)); } } return Collections.unmodifiableList(children); } public boolean allowsChildren() { return !getAllowedChildTypes().isEmpty(); } public int childPositionOffset(Class<? extends SPObject> childType) { int offset = 0; for (Class<? extends SPObject> type : getAllowedChildTypes()) { if (type.isAssignableFrom(childType)) { return offset; } else { offset += getChildren(type).size(); } } throw new IllegalArgumentException(childType.getName() + " is not a valid child type of " + getClass().getName()); } @Accessor(isInteresting=true) public String getName() { return name; } @Accessor public SPObject getParent() { return parent; } @Accessor public String getUUID() { return uuid; } public boolean removeChild(SPObject child) throws ObjectDependentException, IllegalArgumentException { if (!getChildren().contains(child)) { throw new IllegalArgumentException("Child object " + child.getName() + " of type " + child.getClass() + " is not a child of " + getName() + " of type " + getClass()); } if (removeChildImpl(child)) { child.setParent(null); return true; } return false; } /** * This is the object specific implementation of removeChild. There are * checks in the removeChild method to ensure the child being removed has no * dependencies and is a child of this object. * * @see #removeChild(SPObject) */ protected abstract boolean removeChildImpl(SPObject child); public void removeSPListener(SPListener l) { synchronized (listeners) { listeners.remove(l); } } public void rollback(String message) { fireTransactionRollback(message); } @Mutator public void setName(String name) { String oldName = this.name; this.name = name; firePropertyChange("name", oldName, name); } @Mutator public void setParent(SPObject parent) { SPObject oldParent = this.parent; this.parent = parent; firePropertyChange("parent", oldParent, parent); } @Mutator public void setUUID(String uuid) { String oldUUID = this.uuid; if (uuid == null) { generateNewUUID(); } else { this.uuid = uuid; } firePropertyChange("UUID", oldUUID, this.uuid); } /** * Gets the current workspace container by passing the request up the tree. */ @Transient @Accessor public WorkspaceContainer getWorkspaceContainer() throws SessionNotFoundException { // The root object of the tree model should have a reference back to the // session (like WabitWorkspace), and should therefore override this // method. If it does not, a SessionNotFoundException will be thrown. if (getParent() != null) { return getParent().getWorkspaceContainer(); } else { throw new SessionNotFoundException("Root object does not have a workspace container reference"); } } /** * Gets the current runnable dispatcher by passing the request up the tree. */ @Transient @Accessor public RunnableDispatcher getRunnableDispatcher() throws SessionNotFoundException { // The root object of the tree model should have a reference back to the // session (like WabitWorkspace), and should therefore override this // method. If it does not, a SessionNotFoundException will be thrown. if (getParent() != null) { return getParent().getRunnableDispatcher(); } else { throw new SessionNotFoundException("Root object does not have a runnable dispatcher reference"); } } /** * Fires a child added event to all child listeners. The child should have * been added by the calling code already. * * @param type * The canonical type of the child being added * @param child * The child object that was added * @param index * The index of the added child within its own child list (this * will be converted to the overall child position before the * event object is constructed). * @return The child event that was fired or null if no event was fired, for * testing purposes. */ protected SPChildEvent fireChildAdded(Class<? extends SPObject> type, SPObject child, int index) { if (logger.isDebugEnabled()) logger.debug("Child Added: " + type + " notifying " + listeners.size() + " listeners"); synchronized(listeners) { if (listeners.isEmpty()) return null; } if (!isForegroundThread()) { throw new IllegalStateException("Event for adding the child " + child.getName() + " must fired on the foreground thread."); } final SPChildEvent e = new SPChildEvent(this, type, child, index, EventType.ADDED); synchronized(listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { final SPListener listener = staticListeners.get(i); listener.childAdded(e); } } return e; } /** * Fires a child removed event to all child listeners. The child should have * been removed by the calling code. * * @param type * The canonical type of the child being removed * @param child * The child object that was removed * @param index * The index that the removed child was at within its own child * list (this will be converted to the overall child position * before the event object is constructed). * @return The child event that was fired or null if no event was fired, for * testing purposes. */ protected SPChildEvent fireChildRemoved(Class<? extends SPObject> type, SPObject child, int index) { logger.debug("Child Removed: " + type + " notifying " + listeners.size() + " listeners: " + listeners); synchronized(listeners) { if (listeners.isEmpty()) return null; } if (!isForegroundThread()) { throw new IllegalStateException("Event for removing the child " + child.getName() + " must fired on the foreground thread."); } final SPChildEvent e = new SPChildEvent(this, type, child, index, EventType.REMOVED); synchronized(listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { final SPListener listener = staticListeners.get(i); listener.childRemoved(e); } } return e; } /** * Fires a property change on the foreground thread as defined by the * current session being used. * * @return The property change event that was fired or null if no event was * fired, for testing purposes. */ protected PropertyChangeEvent firePropertyChange(final String propertyName, final boolean oldValue, final boolean newValue) { if (oldValue == newValue) return null; synchronized(listeners) { if (listeners.size() == 0) return null; } if (!isForegroundThread()) { throw new IllegalStateException("Event for property change " + propertyName + " must fired on the foreground thread."); } final PropertyChangeEvent evt = new PropertyChangeEvent(this, propertyName, oldValue, newValue); synchronized(listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { SPListener listener = staticListeners.get(i); listener.propertyChanged(evt); } } return evt; } /** * Fires a property change on the foreground thread as defined by the * current session being used. * * @return The property change event that was fired or null if no event was * fired, for testing purposes. */ protected PropertyChangeEvent firePropertyChange(final String propertyName, final int oldValue, final int newValue) { if (oldValue == newValue) return null; synchronized(listeners) { if (listeners.size() == 0) return null; } if (!isForegroundThread()) { throw new IllegalStateException("Event for property change " + propertyName + " must fired on the foreground thread."); } final PropertyChangeEvent evt = new PropertyChangeEvent(this, propertyName, oldValue, newValue); synchronized(listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { SPListener listener = staticListeners.get(i); listener.propertyChanged(evt); } } return evt; } /** * Fires a property change on the foreground thread as defined by the * current session being used. * * @return The property change event that was fired or null if no event was * fired, for testing purposes. */ protected PropertyChangeEvent firePropertyChange(final String propertyName, final char oldValue, final char newValue) { if (oldValue == newValue) return null; synchronized(listeners) { if (listeners.size() == 0) return null; } if (!isForegroundThread()) { throw new IllegalStateException("Event for property change " + propertyName + " must fired on the foreground thread."); } final PropertyChangeEvent evt = new PropertyChangeEvent(this, propertyName, oldValue, newValue); synchronized(listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { SPListener listener = staticListeners.get(i); listener.propertyChanged(evt); } } return evt; } /** * Fires a property change on the foreground thread as defined by the * current session being used. * * @return The property change event that was fired or null if no event was * fired, for testing purposes. */ protected PropertyChangeEvent firePropertyChange(final String propertyName, final Object oldValue, final Object newValue) { if ((oldValue == null && newValue == null) || (oldValue != null && oldValue.equals(newValue))) return null; synchronized(listeners) { if (listeners.size() == 0) return null; if (logger.isDebugEnabled()) { logger.debug("Firing property change \"" + propertyName + "\" to " + listeners.size() + " listeners: " + listeners); } } if (!isForegroundThread()) { throw new IllegalStateException("Event for property change " + propertyName + " must fired on the foreground thread."); } final PropertyChangeEvent evt = new PropertyChangeEvent(this, propertyName, oldValue, newValue); synchronized(listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { SPListener listener = staticListeners.get(i); listener.propertyChanged(evt); } } return evt; } /** * Fires a transaction started event with a message indicating the * reason/type of the transaction. * * @return The event that was fired or null if no event was fired, for * testing purposes. */ protected TransactionEvent fireTransactionStarted(final String message) { synchronized (listeners) { if (listeners.size() == 0) return null; } if (!isForegroundThread()) { throw new IllegalStateException("Event for a transaction start" + " must fired on the foreground thread."); } logger.debug(getName() + "[" + getUUID() + "]: Firing transaction started to " + listeners.size() + " listeners"); final TransactionEvent evt = TransactionEvent.createStartTransactionEvent(this, message); synchronized (listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { final SPListener listener = staticListeners.get(i); listener.transactionStarted(evt); } } return evt; } /** * Fires a transaction ended event. * * @return The event that was fired or null if no event was fired, for * testing purposes. */ protected TransactionEvent fireTransactionEnded() { return fireTransactionEnded("Transaction Ended; Source: " + this); } /** * Fires a transaction ended event. * * @return The event that was fired or null if no event was fired, for * testing purposes. */ protected TransactionEvent fireTransactionEnded(final String message) { synchronized (listeners) { if (listeners.size() == 0) return null; } if (!isForegroundThread()) { throw new IllegalStateException("Event for a transaction end" + " must fired on the foreground thread."); } logger.debug(getName() + "[" + getUUID() + "]: Firing transaction ended to " + listeners.size() + " listeners"); final TransactionEvent evt = TransactionEvent.createEndTransactionEvent(this, message); synchronized (listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { if (staticListeners.get(i) instanceof VetoableSPListener) { final VetoableSPListener vetoableListener = (VetoableSPListener) staticListeners.get(i); try { vetoableListener.vetoableChange(); } catch (Exception e) { rollback(e.getMessage()); throw new RuntimeException(e); } } } } synchronized (listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { final SPListener listener = staticListeners.get(i); listener.transactionEnded(evt); } } return evt; } /** * Fires a transaction rollback event with a message indicating the * reason/type of the rollback. * * @return The event that was fired or null if no event was fired, for * testing purposes. */ protected TransactionEvent fireTransactionRollback(final String message) { synchronized (listeners) { if (listeners.size() == 0) return null; } if (!isForegroundThread()) { throw new IllegalStateException("Event for a transaction rollback" + " must fired on the foreground thread."); } logger.debug(getName() + "[" + getUUID() + "]: Firing transaction rollback to " + listeners.size() + " listeners"); final TransactionEvent evt = TransactionEvent.createRollbackTransactionEvent(this, message); synchronized (listeners) { List<SPListener> staticListeners = new ArrayList<SPListener>(listeners); for (int i = staticListeners.size() - 1; i >= 0; i--) { final SPListener listener = staticListeners.get(i); listener.transactionRollback(evt); } } return evt; } public void begin(String message) { fireTransactionStarted(message); } public void commit() { fireTransactionEnded(); } public void commit(String message) { fireTransactionEnded(message); } protected boolean isForegroundThread() { try { return getRunnableDispatcher().isForegroundThread(); } catch (SessionNotFoundException e) { return true; } } /** * Calls the runInBackground method on the session this object is attached * to if it exists. If this object is not attached to a session, which can * occur when loading, copying, or creating a new object, the runner will be * run on the current thread due to not being able to run elsewhere. Any * SPObject that wants to run a runnable in the background should call to * this method instead of to the session. * * @see WabitSession#runInBackground(Runnable) */ protected void runInBackground(Runnable runner) { try { getRunnableDispatcher().runInBackground(runner); } catch (SessionNotFoundException e) { runner.run(); } } /** * Calls the runInForeground method on the session this object is attached * to if it exists. If this object is not attached to a session, which can * occur when loading, copying, or creating a new object, the runner will be * run on the current thread due to not being able to run elsewhere. Any * SPObject that wants to run a runnable in the foreground should call to * this method instead of to the session. * * @see WabitSession#runInBackground(Runnable) */ protected void runInForeground(Runnable runner) { try { getRunnableDispatcher().runInForeground(runner); } catch (SessionNotFoundException e) { runner.run(); } } @NonBound public boolean isMagicEnabled() { return (magicDisableCount == 0 && (getParent() == null || getParent().isMagicEnabled())); } @NonBound public synchronized void setMagicEnabled(boolean enable) { if (enable) { if (magicDisableCount == 0) { throw new IllegalArgumentException("Cannot enable magic because it is already enabled."); } magicDisableCount--; } else { magicDisableCount++; } } @Override public boolean equals(Object obj) { return (obj instanceof SPObject && getUUID().equals(((SPObject) obj).getUUID())); } @Override public int hashCode() { final int prime = 31; int result = 17; result = prime * result + uuid.hashCode(); return result; } @Override public String toString() { return super.toString() + ", " + getName() + ":" + getUUID(); } }