/*
* *****************************************************************************
* * Copyright (c) 2006-2012 XMind Ltd. and others. This file is a part of XMind
* 3. XMind releases 3 and above are dual-licensed under the Eclipse Public
* License (EPL), which is available at
* http://www.eclipse.org/legal/epl-v10.html and the GNU Lesser General Public
* License (LGPL), which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details. Contributors: XMind Ltd. -
* initial API and implementation
*******************************************************************************/
package org.xmind.gef.ui.editor;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.xmind.gef.GEF;
import org.xmind.gef.command.CommandStack;
import org.xmind.gef.command.CommandStackEvent;
import org.xmind.gef.command.ICommandStack;
import org.xmind.gef.command.ICommandStackListener;
/**
* <p>
* This class implements basic behaviors of {@link IEditable}. A document-based
* application should create its own subclass of <code>Editable</code> and
* provide abilities to access actual content.
* </p>
* <h2>Subclassing Notes</h2>
* <ul>
* <li>Each subclass <b>MUST</b> override {@link #doOpen(IProgressMonitor)
* doOpen()} and <b>MUST NOT</b> call <code>super.doOpen()</code>, unless it
* overrides {@link #open(IProgressMonitor) open()} to change the default
* behavior.</li>
* <li>Each subclass <b>MUST</b> override {@link #doSave(IProgressMonitor)
* doSave()} and <b>MUST NOT</b> call <code>super.doSave()</code> if it may
* return <code>true</code> by overriding {@link #canSave()}, unless it
* overrides {@link #save(IProgressMonitor)} to change the default behavior.
* </li>
* <li>Each subclass may override {@link #doClose(IProgressMonitor) doClose()}
* to perform additional actions while closing the document.</li>
* </ul>
*
* @author Frank Shaka
* @since 3.6.50
*/
public abstract class Editable implements IEditable {
private URI uri;
private int state;
private long modificationTime = 0;
private int progress = 0;
private ICommandStack commandStack = null;
private IEditingContext activeContext = null;
private final List<IEditableCleaner> cleaners = new ArrayList<IEditableCleaner>();
private final ListenerList<IPropertyChangeListener> listenerManager = new ListenerList<IPropertyChangeListener>();
private int contentRefCount = 0;
private ICommandStackListener commandStackHook = new ICommandStackListener() {
@Override
public void handleCommandStackEvent(CommandStackEvent event) {
if ((event.getStatus() & GEF.CS_UPDATED) != 0) {
boolean isDirty = isDirty();
firePropertyChanged(PROP_DIRTY, false, isDirty);
}
doHandleCommandStackChange(event);
}
};
private final List<IInteractiveMessage> messages = new ArrayList<IInteractiveMessage>();
protected Editable(URI uri) {
this.uri = uri;
this.state = CLOSED;
}
@Override
public <T> T getAdapter(Class<T> adapter) {
if (URI.class.equals(adapter))
return adapter.cast(getURI());
if (ICommandStack.class.equals(adapter))
return adapter.cast(getCommandStack());
return Platform.getAdapterManager().getAdapter(this, adapter);
}
@Override
public URI getURI() {
return this.uri;
}
@Override
public String getName() {
if (this.uri != null) {
String path = this.uri.getPath();
if (path != null && path.length() > 0) {
if (path.charAt(path.length() - 1) == '/') {
path = path.substring(0, path.length() - 1);
}
int sep = path.lastIndexOf('/');
if (sep >= 0) {
return path.substring(sep + 1);
}
}
}
return null;
}
@Override
public String getDescription() {
return getName();
}
@Override
public long getModificationTime() {
return this.modificationTime;
}
protected void setModificationTime(long modificationTime) {
long oldModificationTime = this.modificationTime;
if (modificationTime == oldModificationTime)
return;
this.modificationTime = modificationTime;
firePropertyChanged(PROP_MODIFICATION_TIME, oldModificationTime,
modificationTime);
}
@Override
public synchronized int getState() {
return this.state;
}
@Override
public boolean isInState(int state) {
return (getState() & state) != 0;
}
protected void setState(int newState) {
int oldState;
synchronized (this) {
oldState = this.state;
if (newState == oldState)
return;
this.state = newState;
}
firePropertyChanged(PROP_STATE, oldState, newState);
}
protected void modifyState(int stateToAdd, int stateToRemove) {
int newState = getState();
if (stateToAdd != 0) {
newState |= stateToAdd;
}
if (stateToRemove != 0) {
newState &= ~stateToRemove;
}
setState(newState);
}
protected void addState(int state) {
setState(getState() | state);
}
protected void removeState(int state) {
setState(getState() & (~state));
}
@Override
public synchronized int getProgress() {
return this.progress;
}
protected void setProgress(int newProgress) {
int oldProgress;
synchronized (this) {
oldProgress = this.progress;
if (newProgress == oldProgress)
return;
this.progress = newProgress;
}
firePropertyChanged(PROP_PROGRESS, oldProgress, newProgress);
}
protected void addProgress(int delta) {
setProgress(getProgress() + delta);
}
protected void removeProgress(int delta) {
setProgress(getProgress() - delta);
}
@Override
public ICommandStack getCommandStack() {
if (this.commandStack == null) {
setCommandStack(createDefaultCommandStack());
}
return this.commandStack;
}
protected ICommandStack createDefaultCommandStack() {
return new CommandStack();
}
protected void setCommandStack(ICommandStack commandStack) {
ICommandStack oldCommandStack = this.commandStack;
if (oldCommandStack == commandStack)
return;
if (oldCommandStack != null) {
oldCommandStack.removeCSListener(commandStackHook);
}
this.commandStack = commandStack;
if (commandStack != null) {
commandStack.addCSListener(commandStackHook);
}
firePropertyChanged(PROP_COMMAND_STACK, oldCommandStack, commandStack);
}
@Override
public IEditingContext getActiveContext() {
IEditingContext context = this.activeContext;
return context == null ? IEditingContext.NULL : context;
}
protected <T> T getService(Class<T> serviceType) {
return getActiveContext().getAdapter(serviceType);
}
@Override
public void setActiveContext(IEditingContext context) {
IEditingContext oldContext = this.activeContext;
if (oldContext == context)
return;
this.activeContext = context;
firePropertyChanged(PROP_ACTIVE_CONTEXT, oldContext, context);
}
@Override
public boolean exists() {
return false;
}
@Override
public boolean isDirty() {
return (commandStack != null && commandStack.isDirty())
|| !cleaners.isEmpty();
}
@Override
public void markDirtyWith(IEditableCleaner cleaner) {
boolean wasDirty = isDirty();
synchronized (this.cleaners) {
this.cleaners.add(cleaner);
}
boolean isDirty = isDirty();
if (wasDirty != isDirty) {
firePropertyChanged(PROP_DIRTY, wasDirty, isDirty);
}
}
@Override
public void unmarkDirtyWith(IEditableCleaner cleaner) {
boolean wasDirty = isDirty();
synchronized (this.cleaners) {
this.cleaners.remove(cleaner);
}
boolean isDirty = isDirty();
if (wasDirty != isDirty) {
firePropertyChanged(PROP_DIRTY, wasDirty, isDirty);
}
}
/*
* (non-Javadoc)
* @see org.xmind.gef.ui.editor.IEditable#discardChanges()
*/
@Override
public void discardChanges() {
boolean wasDirty = isDirty();
doDiscardChanges();
boolean isDirty = isDirty();
if (wasDirty != isDirty) {
firePropertyChanged(PROP_DIRTY, wasDirty, isDirty);
}
}
protected void doDiscardChanges() {
if (commandStack != null) {
while (commandStack.isDirty() && commandStack.canUndo()) {
commandStack.markSaved();
}
}
synchronized (this.cleaners) {
this.cleaners.clear();
}
}
protected void clean(IProgressMonitor monitor)
throws InterruptedException, InvocationTargetException {
IEditableCleaner[] theCleaners = new IEditableCleaner[0];
synchronized (this.cleaners) {
theCleaners = this.cleaners.toArray(theCleaners);
this.cleaners.clear();
}
SubMonitor subMonitor = SubMonitor.convert(monitor, theCleaners.length);
for (int i = 0; i < theCleaners.length; i++) {
IEditableCleaner cleaner = theCleaners[i];
doClean(subMonitor.newChild(1), cleaner);
}
}
protected void doClean(IProgressMonitor monitor, IEditableCleaner cleaner)
throws InterruptedException, InvocationTargetException {
cleaner.cleanEditable(monitor, this);
}
@Override
public void addPropertyChangeListener(IPropertyChangeListener listener) {
listenerManager.add(listener);
}
@Override
public void removePropertyChangeListener(IPropertyChangeListener listener) {
listenerManager.remove(listener);
}
protected void firePropertyChanged(String property, Object oldValue,
Object newValue) {
final PropertyChangeEvent event = new PropertyChangeEvent(this,
property, oldValue, newValue);
for (final Object o : listenerManager.getListeners()) {
if (o instanceof IPropertyChangeListener) {
try {
((IPropertyChangeListener) o).propertyChange(event);
} catch (Exception e) {
handlePropertyChangeNotificationError(e);
}
}
}
}
private void handlePropertyChangeNotificationError(Exception e) {
// TODO handle errors during editable state change notification
}
@Override
public void open(IProgressMonitor monitor)
throws InterruptedException, InvocationTargetException {
if (!isInState(CLOSED)) {
// already opened
contentRefCount += 1;
return;
}
if (isInState(OPENING | CLOSING | SAVING))
// already being opened
throw new IllegalStateException(
"Concurrent open/close/save operations are not allowed"); //$NON-NLS-1$
try {
SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
subMonitor.newChild(10);
addState(OPENING);
try {
if (subMonitor.isCanceled())
throw new InterruptedException();
doOpen(subMonitor.newChild(80));
subMonitor.newChild(5);
contentRefCount += 1;
if (isInState(CLOSED)) {
removeState(CLOSED);
}
} finally {
subMonitor.setWorkRemaining(5);
subMonitor.newChild(5);
removeState(OPENING);
}
} catch (OperationCanceledException e) {
// interpret cancellation
throw new InterruptedException();
}
}
/**
* Perform actual <em>open</em> operations.
* <p>
* This method is, by default, called by {@link #open(IProgressMonitor)} and
* its default implementation from {@link Editable} does nothing but throws
* {@link UnsupportedOperationException}, so subclasses <b>MUST</b> override
* this method and <b>MUST NOT</b> call <code>super.doOpen()</code>, unless
* they override {@link #open(IProgressMonitor)} to change the default
* behavior.
* </p>
*
* @see #open(IProgressMonitor)
* @param monitor
* the progress monitor to use for reporting progress to the
* user. It is the caller's responsibility to call done() on the
* given monitor. Accepts null, indicating that no progress
* should be reported and that the operation cannot be cancelled.
* @exception InvocationTargetException
* if this method must propagate a checked exception, it
* should wrap it inside an
* <code>InvocationTargetException</code>; runtime exceptions
* are automatically wrapped in an
* <code>InvocationTargetException</code> by the calling
* context
* @exception InterruptedException
* if the operation detects a request to cancel, using
* <code>IProgressMonitor.isCanceled()</code>, it should exit
* by throwing <code>InterruptedException</code>
*/
protected void doOpen(IProgressMonitor monitor)
throws InterruptedException, InvocationTargetException {
throw new UnsupportedOperationException();
}
@Override
public boolean canSave() {
return false;
}
@Override
public void save(IProgressMonitor monitor)
throws InterruptedException, InvocationTargetException {
if (!canSave())
throw new IllegalStateException("Save operation is not allowed"); //$NON-NLS-1$
if (isInState(CLOSED))
// already closed
throw new IllegalStateException(
"Can't perform save operation while editable is closed"); //$NON-NLS-1$
if (isInState(OPENING | CLOSING | SAVING))
// already being opened/closing/saving
throw new IllegalStateException(
"Concurrent open/close/save operations are not allowed in SynchronizedEditable"); //$NON-NLS-1$
try {
SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
subMonitor.newChild(5);
addState(SAVING);
try {
boolean wasDirty = isDirty();
if (subMonitor.isCanceled())
throw new InterruptedException();
clean(subMonitor.newChild(5));
if (subMonitor.isCanceled())
throw new OperationCanceledException();
doSave(subMonitor.newChild(80));
markSaved(subMonitor.newChild(5));
subMonitor.newChild(4);
boolean isDirty = isDirty();
if (wasDirty != isDirty) {
firePropertyChanged(PROP_DIRTY, wasDirty, isDirty);
}
} finally {
subMonitor.setWorkRemaining(1);
subMonitor.newChild(1);
removeState(SAVING);
}
} catch (OperationCanceledException e) {
// interpret cancellation
throw new InterruptedException();
}
}
/**
* Perform actual <em>save</em> operations.
* <p>
* This method is, by default, called by {@link #save(IProgressMonitor)} and
* its default implementation from {@link Editable} does nothing but throws
* {@link UnsupportedOperationException}, so subclasses <b>MUST</b> override
* this method and <b>MUST NOT</b> call <code>super.doSave()</code> if they
* return <code>true</code> by overriding {@link #canSave()}, unless they
* override {@link #save(IProgressMonitor)} to change the default behavior.
* </p>
*
* @see #save(IProgressMonitor)
* @param monitor
* the progress monitor to use for reporting progress to the
* user. It is the caller's responsibility to call done() on the
* given monitor. Accepts null, indicating that no progress
* should be reported and that the operation cannot be cancelled.
* @exception InvocationTargetException
* if this method must propagate a checked exception, it
* should wrap it inside an
* <code>InvocationTargetException</code>; runtime exceptions
* are automatically wrapped in an
* <code>InvocationTargetException</code> by the calling
* context
* @exception InterruptedException
* if the operation detects a request to cancel, using
* <code>IProgressMonitor.isCanceled()</code>, it should exit
* by throwing <code>InterruptedException</code>
*/
protected void doSave(IProgressMonitor monitor)
throws InterruptedException, InvocationTargetException {
throw new UnsupportedOperationException();
}
/**
* Called after the save operation has successfully completed. Typically
* used to send notifications.
* <p>
* Subclasses may override and call <code>super.markSaved()</code>.
* </p>
*
* @param monitor
* @throws InterruptedException
* @throws InvocationTargetException
*/
protected void markSaved(IProgressMonitor monitor)
throws InterruptedException, InvocationTargetException {
ICommandStack commandStack = getCommandStack();
if (commandStack != null) {
commandStack.markSaved();
}
}
@Override
public void close(IProgressMonitor monitor)
throws InterruptedException, InvocationTargetException {
if (isInState(CLOSED))
// already closed
return;
if (isInState(OPENING | CLOSING | SAVING))
// already being opened
throw new IllegalStateException(
"Concurrent open/close/save operations are not allowed in SynchronizedEditable"); //$NON-NLS-1$
if (contentRefCount > 1) {
// some other clients requiring content
// should do nothing
contentRefCount -= 1;
return;
}
try {
// no other clients requiring content
SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
if (isDirty() && canSave()) {
// should save first
doSave(subMonitor.newChild(70));
}
if (subMonitor.isCanceled())
throw new InterruptedException();
subMonitor.setWorkRemaining(30);
if (commandStack != null) {
commandStack.clear();
}
subMonitor.newChild(5);
contentRefCount -= 1;
addState(CLOSED);
try {
if (subMonitor.isCanceled())
throw new InterruptedException();
doClose(subMonitor.newChild(20));
if (!isInState(CLOSED)) {
addState(CLOSED);
}
} finally {
subMonitor.setWorkRemaining(5);
subMonitor.newChild(5);
removeState(CLOSING);
}
} catch (OperationCanceledException e) {
// interpret cancellation
throw new InterruptedException();
}
}
/**
* Perform actual <em>close</em> operations.
* <p>
* This method is, by default, called from {@link #close(IProgressMonitor)}
* and its default implementation from {@link Editable} does nothing, so
* subclasses <em>may or may not</em> override this method and need not to
* call <code>super.doClose()</code>.
* </p>
*
* @see #close(IProgressMonitor)
* @param monitor
* the progress monitor to use for reporting progress to the
* user. It is the caller's responsibility to call done() on the
* given monitor. Accepts null, indicating that no progress
* should be reported and that the operation cannot be cancelled.
* @exception InvocationTargetException
* if this method must propagate a checked exception, it
* should wrap it inside an
* <code>InvocationTargetException</code>; runtime exceptions
* are automatically wrapped in an
* <code>InvocationTargetException</code> by the calling
* context
* @exception InterruptedException
* if the operation detects a request to cancel, using
* <code>IProgressMonitor.isCanceled()</code>, it should exit
* by throwing <code>InterruptedException</code>
*/
protected void doClose(IProgressMonitor monitor)
throws InterruptedException, InvocationTargetException {
// do nothing, subclasses may override
}
/*
* (non-Javadoc)
* @see org.xmind.gef.ui.editor.IEditable#getMessages()
*/
@Override
public List<IInteractiveMessage> getMessages() {
return Collections.unmodifiableList(messages);
}
/*
* (non-Javadoc)
* @see
* org.xmind.gef.ui.editor.IEditable#addMessage(org.eclipse.jface.dialogs.
* IMessageProvider)
*/
@Override
public void addMessage(IInteractiveMessage message) {
List<IInteractiveMessage> oldMessages = new ArrayList<IInteractiveMessage>(
messages);
if (messages.add(message)) {
firePropertyChanged(PROP_MESSAGES, oldMessages,
new ArrayList<IInteractiveMessage>(messages));
}
}
/*
* (non-Javadoc)
* @see
* org.xmind.gef.ui.editor.IEditable#removeMessage(org.eclipse.jface.dialogs
* .IMessageProvider)
*/
@Override
public void removeMessage(IInteractiveMessage message) {
List<IInteractiveMessage> oldMessages = new ArrayList<IInteractiveMessage>(
messages);
if (messages.remove(message)) {
firePropertyChanged(PROP_MESSAGES, oldMessages,
new ArrayList<IInteractiveMessage>(messages));
}
}
protected void doHandleCommandStackChange(CommandStackEvent event) {
/// do nothing, subclasses may override
}
}