org.netbeans.modules.xml.xam.AbstractModel Maven / Gradle / Ivy
The newest version!
/*
* 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>
implements Model, 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 propertyChangeEvents;
private final List componentListenerEvents;
private final Thread transactionThread;
private boolean eventAdded;
private Boolean eventsAddedAfterFiring;
private boolean hasEvents;
public Transaction() {
propertyChangeEvents = new ArrayList();
componentListenerEvents = new ArrayList();
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 clonedEvents =
new ArrayList(propertyChangeEvents);
//should clear event list
propertyChangeEvents.clear();
for (PropertyChangeEvent pce:clonedEvents) {
pcs.firePropertyChange(pce);
}
final List cEvents =
new ArrayList(componentListenerEvents);
//should clear event list
componentListenerEvents.clear();
Map
© 2015 - 2025 Weber Informatics LLC | Privacy Policy