blob: 46db7861f8bf46bb1cda86c48bfc36238894d84a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.xml.xam;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.EventListenerList;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;
import javax.swing.undo.UndoableEditSupport;
import org.netbeans.api.annotations.common.CheckReturnValue;
import org.netbeans.modules.xml.xam.Model.State;
import org.openide.util.RequestProcessor;
/**
* @author Chris Webster
* @author Rico
* @author Nam Nguyen
*/
public abstract class AbstractModel<T extends Component<T>>
implements Model<T>, UndoableEditListener {
private static Logger logger = Logger.getLogger(AbstractModel.class.getName());
private static final RequestProcessor RP = new RequestProcessor(
AbstractModel.class.getName(), 3, true);
private PropertyChangeSupport pcs;
protected ModelUndoableEditSupport ues;
private State status;
private boolean inSync;
private boolean inUndoRedo;
private EventListenerList componentListeners;
private Transaction transaction;
private ModelSource source;
private UndoableEditListener[] savedUndoableEditListeners;
public AbstractModel(ModelSource source) {
this.source = source;
pcs = new PropertyChangeSupport(this);
ues = new ModelUndoableEditSupport();
componentListeners = new EventListenerList();
status = State.VALID;
}
public abstract ModelAccess getAccess();
@Override
public void removePropertyChangeListener(java.beans.PropertyChangeListener pcl) {
pcs.removePropertyChangeListener(pcl);
}
/**
* Add property change listener which will receive events for any element
* in the underlying schema model.
*/
@Override
public void addPropertyChangeListener(java.beans.PropertyChangeListener pcl) {
pcs.addPropertyChangeListener(pcl);
}
public void firePropertyChangeEvent(PropertyChangeEvent event) {
assert transaction != null;
transaction.addPropertyChangeEvent(event);
}
@Override
public void removeUndoableEditListener(UndoableEditListener uel) {
ues.removeUndoableEditListener(uel);
}
@Override
public void addUndoableEditListener(UndoableEditListener uel) {
ues.addUndoableEditListener(uel);
}
@Override
public synchronized void addUndoableRefactorListener(UndoableEditListener uel) {
//
savedUndoableEditListeners = ues.getUndoableEditListeners();
if (savedUndoableEditListeners != null) {
for (UndoableEditListener saved : savedUndoableEditListeners) {
if (saved instanceof UndoManager) {
((UndoManager)saved).discardAllEdits();
}
}
}
ues = new ModelUndoableEditSupport();
ues.addUndoableEditListener(uel);
}
@Override
public synchronized void removeUndoableRefactorListener(UndoableEditListener uel) {
//
ues.removeUndoableEditListener(uel);
if (savedUndoableEditListeners != null) {
ues = new ModelUndoableEditSupport();
for (UndoableEditListener saved : savedUndoableEditListeners) {
ues.addUndoableEditListener(saved);
}
savedUndoableEditListeners = null;
}
}
protected CompoundEdit createModelUndoableEdit() {
return new ModelUndoableEdit();
}
protected class ModelUndoableEditSupport extends UndoableEditSupport {
@Override
protected CompoundEdit createCompoundEdit() {
return createModelUndoableEdit();
}
protected void abortUpdate() {
ModelUndoableEdit mue = (ModelUndoableEdit) compoundEdit;
mue.justUndo();
super.compoundEdit = createCompoundEdit();
super.updateLevel = 0;
}
}
@Override
public boolean inSync() {
return inSync;
}
protected void setInSync(boolean v) {
inSync = v;
}
/**
* Indicates if the model in Undo/Redo stage.
* @return
*/
public boolean inUndoRedo() {
return inUndoRedo;
}
protected void setInUndoRedo(boolean v) {
inUndoRedo = v;
}
@Override
public State getState() {
return status;
}
protected void setState(State s) {
if (s == status) {
return;
}
State old = status;
status = s;
PropertyChangeEvent event =
new PropertyChangeEvent(this, STATE_PROPERTY, old, status);
if (isIntransaction()) {
firePropertyChangeEvent(event);
} else {
pcs.firePropertyChange(event);
}
}
/**
* This method is overridden by subclasses to determine if sync needs to be
* performed. The default implementation simply returns true.
*/
protected boolean needsSync() {
return true;
}
/**
* This template method is invoked when a transaction is started. The
* default implementation does nothing.
*/
protected void transactionStarted() {
}
/**
* This method is invoked when a transaction has completed. The default
* implementation does nothing.
*/
protected void transactionCompleted() {
}
/**
* This method is invoked when sync has started. The default implementation
* does nothing.
*/
protected void syncStarted() {
}
/**
* This method is invoked when sync has completed. The default implementation
* does nothing.
*/
protected void syncCompleted() {
}
/**
* Prepare for sync. This allow splitting calculation intensive work from
* event firing tasks that are mostly running on UI threads. This should be
* optional step, meaning the actual call sync() should take care of the
* preparation if it is not done.
*/
private void prepareSync() {
if (needsSync()) {
getAccess().prepareSync();
}
}
@Override
public synchronized void sync() throws java.io.IOException {
if (needsSync()) {
syncStarted();
boolean syncStartedTransaction = false;
boolean success = false;
try {
startTransaction(true, false); //start pseudo transaction for event firing
syncStartedTransaction = true;
setState(getAccess().sync());
endTransaction();
success = true;
} catch (IOException e) {
setState(State.NOT_WELL_FORMED);
endTransaction(false); // do want to fire just the state transition event
throw e;
} finally {
if (syncStartedTransaction && isIntransaction()) { //CR: consider separate try/catch
try {
endTransaction(true); // do not fire events
} catch(Exception ex) {
Logger.getLogger(getClass().getName()).log(Level.INFO,
"Sync cleanup error.", ex); //NOI18N
}
}
if (!success && getState() != State.NOT_WELL_FORMED) {
setState(State.NOT_SYNCED);
refresh();
}
setInSync(false);
syncCompleted();
}
}
}
/**
* Refresh the domain model component trees. Refresh actually means recreation
* of root component from XDM root. The old model's content is totally lost after
* the operation.
*
* Because the fresh model is created, the model's state should be VALID
* as the result of this call.
*
* Note: subclasses need to override to provide the actual refresh service.
* Note: direct links to model's components become invalid after this operation.
*/
protected void refresh() {
setState(State.VALID);
}
@Override
public void removeComponentListener(ComponentListener cl) {
componentListeners.remove(ComponentListener.class, cl);
}
@Override
public void addComponentListener(ComponentListener cl) {
componentListeners.add(ComponentListener.class, cl);
}
public void fireComponentChangedEvent(ComponentEvent evt) {
assert transaction != null;
transaction.addComponentEvent(evt);
}
@Override
public boolean isIntransaction() {
return transaction != null;
}
/**
* Ends the transaction and commits changes to the document.
* The operation may throw {@link IllegalStateException} if it is not possible to
* flush changes, because e.g. file is read-only, deleted or the document is changed
* in an incompatible way during the transaction.
*
* @throws IllegalStateException when the backing file/document is read-only or the document
* changed in a way that prevent application of changes.
*/
@Override
public synchronized void endTransaction() throws IllegalStateException {
endTransaction(false);
}
protected synchronized void endTransaction(boolean quiet) {
if (transaction == null) return; // just no-op when not in transaction
if (!transaction.currentThreadIsTransactionThread()) return; // the thread isn't the owner of the transaciton
//
try {
if (! quiet) {
transaction.fireEvents();
}
// no-need to flush or undo/redo support while in sync
if (! inSync() && transaction.hasEvents() ||
transaction.hasEventsAfterFiring()) {
getAccess().flush();
}
if (! inUndoRedo()) {
ues.endUpdate();
}
} finally {
transaction = null;
setInSync(false);
setInUndoRedo(false);
notifyAll();
transactionCompleted();
}
}
@CheckReturnValue
@Override
public boolean startTransaction() {
return startTransaction(false, false);
}
/**
* Starts a transaction.
*
* @param inSync indicates that the model is in synchronization stage
* @param inUndoRedo indicates that the model is in undo/redo stage
* @return a flag which indicates if the transaction was started.
*/
private synchronized boolean startTransaction(boolean inSync, boolean inUndoRedo) {
if (transaction != null && transaction.currentThreadIsTransactionThread()) {
throw new IllegalStateException(
"Current thread has already started a transaction"); // NOI18N
}
// If model is being synchronized, then the changes are taken from the source document.
// Otherwise, the changes can be going to be pushed to the source document and
// it is impossible if the document is not editable.
if (! inSync && ! getModelSource().isEditable()) {
throw new IllegalArgumentException("Model source is read-only."); // NOI18N
}
while (transaction != null) {
try {
wait();
} catch (InterruptedException ignorredex) {}
}
if (! inSync && getState() == State.NOT_WELL_FORMED) {
notifyAll();
// It's allowed ot modify underlaing document if it's not well formed
return false;
}
transaction = new Transaction();
transactionStarted();
setInSync(inSync);
setInUndoRedo(inUndoRedo);
if (! inUndoRedo) {
ues.beginUpdate();
}
return true;
}
/**
* The method does nothing if the transaction hasn't been started or
* started by another thread.
*/
public synchronized void rollbackTransaction() {
if (transaction == null) return; // just no-op when not in transaction
if (!transaction.currentThreadIsTransactionThread()) return; // the thread isn't the owner of the transaciton
//
try {
if (inSync() || inUndoRedo()) {
throw new IllegalArgumentException(
"Should never call rollback during sync or undo/redo."); // NOI18N
}
ues.abortUpdate();
} finally {
transaction = null;
setInSync(false);
setInUndoRedo(false);
notifyAll();
transactionCompleted();
}
}
// # 121042
protected synchronized void finishTransaction() {
if (transaction == null) return; // just no-op when not in transaction
if (!transaction.currentThreadIsTransactionThread()) return; // the thread isn't the owner of the transaciton
//
try {
if (inSync() || inUndoRedo()) {
throw new IllegalArgumentException(
"Should never call rollback during sync or undo/redo."); // NOI18N
}
} finally {
transaction = null;
setInSync(false);
setInUndoRedo(false);
notifyAll();
transactionCompleted();
}
}
/**
* This method ensures that a transaction is currently in progress and
* that the current thread is able to write.
*/
public synchronized void validateWrite() {
if (transaction == null) {
throw new IllegalStateException("attempted model write without " +
"invoking startTransaction");
}
if (!transaction.currentThreadIsTransactionThread()) {
throw new IllegalStateException("attempted model write " +
"while a transaction is started by another thread");
}
}
private class Transaction {
private final List<PropertyChangeEvent> propertyChangeEvents;
private final List<ComponentEvent> componentListenerEvents;
private final Thread transactionThread;
private boolean eventAdded;
private Boolean eventsAddedAfterFiring;
private boolean hasEvents;
public Transaction() {
propertyChangeEvents = new ArrayList<PropertyChangeEvent>();
componentListenerEvents = new ArrayList<ComponentEvent>();
transactionThread = Thread.currentThread();
eventAdded = false;
eventsAddedAfterFiring = null;
hasEvents = false;
}
public void addPropertyChangeEvent(PropertyChangeEvent pce) {
propertyChangeEvents.add(pce);
// do not chain events during undo/redo
if (eventsAddedAfterFiring == null || ! inUndoRedo) {
eventAdded = true;
}
if (eventsAddedAfterFiring != null) {
eventsAddedAfterFiring = Boolean.TRUE;
}
hasEvents = true;
}
public void addComponentEvent(ComponentEvent cle) {
componentListenerEvents.add(cle);
// do not chain events during undo/redo
if (eventsAddedAfterFiring == null || ! inUndoRedo) {
eventAdded = true;
}
if (eventsAddedAfterFiring != null) {
eventsAddedAfterFiring = Boolean.TRUE;
}
hasEvents = true;
}
public boolean currentThreadIsTransactionThread() {
return Thread.currentThread().equals(transactionThread);
}
public void fireEvents() {
if (eventsAddedAfterFiring == null) {
eventsAddedAfterFiring = Boolean.FALSE;
}
while (eventAdded) {
eventAdded = false;
fireCompleteEventSet();
}
}
/**
* This method is added to allow mutations to occur inside events. The
* list is cloned so that additional events can be added.
*/
private void fireCompleteEventSet() {
final List<PropertyChangeEvent> clonedEvents =
new ArrayList<PropertyChangeEvent>(propertyChangeEvents);
//should clear event list
propertyChangeEvents.clear();
for (PropertyChangeEvent pce:clonedEvents) {
pcs.firePropertyChange(pce);
}
final List<ComponentEvent> cEvents =
new ArrayList<ComponentEvent>(componentListenerEvents);
//should clear event list
componentListenerEvents.clear();
Map<Object, Set<ComponentEvent.EventType>> fired = new HashMap<Object, Set<ComponentEvent.EventType>>();
for (ComponentEvent cle:cEvents) {
// make sure we only fire one event per component per event type.
Object source = cle.getSource();
if (fired.keySet().contains(source)) {
Set<ComponentEvent.EventType> types = fired.get(source);
if (types.contains(cle.getEventType())) {
continue;
} else {
types.add(cle.getEventType());
}
} else {
Set<ComponentEvent.EventType> types = new HashSet<ComponentEvent.EventType>();
types.add(cle.getEventType());
fired.put(cle.getSource(), types);
}
final ComponentListener[] listeners =
componentListeners.getListeners(ComponentListener.class);
for (ComponentListener cl : listeners) {
cle.getEventType().fireEvent(cle,cl);
}
}
}
public boolean hasEvents() {
return hasEvents;
}
public boolean hasEventsAfterFiring() {
return eventsAddedAfterFiring != null && eventsAddedAfterFiring.booleanValue();
}
}
/**
* Whether the model has started firing events. This is the indication of
* beginning of endTransaction call and any subsequent mutations are from
* handlers of main transaction events or some of their own events.
*/
public boolean startedFiringEvents() {
return transaction != null && transaction.eventsAddedAfterFiring != null;
}
protected class ModelUndoableEdit extends CompoundEdit {
static final long serialVersionUID = 1L;
@Override
public boolean addEdit(UndoableEdit anEdit) {
if (! isInProgress()) return false;
UndoableEdit last = lastEdit();
if (last == null) {
return super.addEdit(anEdit);
} else {
if (! last.addEdit(anEdit)) {
return super.addEdit(anEdit);
} else {
return true;
}
}
}
@Override
public void redo() throws CannotRedoException {
boolean redoStartedTransaction = false;
boolean needsRefresh = true;
try {
startTransaction(true, true); //start pseudo transaction for event firing
redoStartedTransaction = true;
AbstractModel.this.getAccess().prepareForUndoRedo();
super.redo();
AbstractModel.this.getAccess().finishUndoRedo();
endTransaction();
needsRefresh = false;
} catch(CannotRedoException ex) {
needsRefresh = false;
throw ex;
} finally {
if (isIntransaction() && redoStartedTransaction) {
try {
endTransaction(true); // do not fire events
} catch(Exception e) {
Logger.getLogger(getClass().getName()).log(Level.INFO, "Redo error", e); //NOI18N
}
}
if (needsRefresh) {
setState(State.NOT_SYNCED);
refresh();
}
}
}
@Override
public void undo() throws CannotUndoException {
boolean undoStartedTransaction = false;
boolean needsRefresh = true;
try {
startTransaction(true, true); //start pseudo transaction for event firing
undoStartedTransaction = true;
AbstractModel.this.getAccess().prepareForUndoRedo();
super.undo();
AbstractModel.this.getAccess().finishUndoRedo();
endTransaction();
needsRefresh = false;
} catch(CannotUndoException ex) {
needsRefresh = false;
throw ex;
} finally {
if (undoStartedTransaction && isIntransaction()) {
try {
endTransaction(true); // do not fire events
} catch(Exception e) {
Logger.getLogger(getClass().getName()).log(Level.INFO, "Undo error", e); //NOI18N
}
}
if (needsRefresh) {
setState(State.NOT_SYNCED);
refresh();
}
}
}
public void justUndo() {
super.end();
boolean oldValue = AbstractModel.this.inUndoRedo;
AbstractModel.this.inUndoRedo = true;
AbstractModel.this.getAccess().prepareForUndoRedo();
super.undo();
AbstractModel.this.getAccess().finishUndoRedo();
AbstractModel.this.inUndoRedo = oldValue;
}
}
@Override
public void undoableEditHappened(UndoableEditEvent e) {
ues.postEdit(e.getEdit());
}
@Override
public ModelSource getModelSource() {
return source;
}
EventListenerList getComponentListenerList() {
return componentListeners;
}
public boolean isAutoSyncActive() {
return getAccess().isAutoSync();
}
public void setAutoSyncActive(boolean v) {
getAccess().setAutoSync(v);
}
void runAutoSync() {
if (logger.getLevel() == Level.FINEST) {
logger.finest("Initiate auto sync for XAM model: " + toString()); // NOI18N
}
//
prepareSync();
RP.post(new Runnable() {
@Override
public void run() {
try {
sync();
//
if (logger.getLevel() == Level.FINEST) {
logger.finest("Auto sync is finished for XAM model: " +
AbstractModel.this.toString()); // NOI18N
}
} catch(Exception ioe) {
// just have to be quiet during background autosync
// sync() should have handled all faults
}
}
});
}
}