blob: aadb3f15ea88aaa14c72007879d3895a8678fcea [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2007-2010 The University of Manchester
*
* Modifications to the initial code base are copyright of their
* respective authors, or their employers as appropriate.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This program 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
******************************************************************************/
package net.sf.taverna.t2.workbench.file.impl;
import static java.awt.GraphicsEnvironment.isHeadless;
import static java.util.Collections.singleton;
import static javax.swing.SwingUtilities.invokeAndWait;
import static javax.swing.SwingUtilities.isEventDispatchThread;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import javax.swing.filechooser.FileFilter;
import net.sf.taverna.t2.lang.observer.MultiCaster;
import net.sf.taverna.t2.lang.observer.Observable;
import net.sf.taverna.t2.lang.observer.Observer;
import net.sf.taverna.t2.workbench.edits.EditManager;
import net.sf.taverna.t2.workbench.edits.EditManager.AbstractDataflowEditEvent;
import net.sf.taverna.t2.workbench.edits.EditManager.EditManagerEvent;
import net.sf.taverna.t2.workbench.file.DataflowInfo;
import net.sf.taverna.t2.workbench.file.DataflowPersistenceHandler;
import net.sf.taverna.t2.workbench.file.FileManager;
import net.sf.taverna.t2.workbench.file.FileType;
import net.sf.taverna.t2.workbench.file.events.ClosedDataflowEvent;
import net.sf.taverna.t2.workbench.file.events.ClosingDataflowEvent;
import net.sf.taverna.t2.workbench.file.events.FileManagerEvent;
import net.sf.taverna.t2.workbench.file.events.OpenedDataflowEvent;
import net.sf.taverna.t2.workbench.file.events.SavedDataflowEvent;
import net.sf.taverna.t2.workbench.file.events.SetCurrentDataflowEvent;
import net.sf.taverna.t2.workbench.file.exceptions.OpenException;
import net.sf.taverna.t2.workbench.file.exceptions.OverwriteException;
import net.sf.taverna.t2.workbench.file.exceptions.SaveException;
import net.sf.taverna.t2.workbench.file.exceptions.UnsavedException;
import org.apache.log4j.Logger;
import uk.org.taverna.scufl2.api.common.Scufl2Tools;
import uk.org.taverna.scufl2.api.container.WorkflowBundle;
import uk.org.taverna.scufl2.api.core.Workflow;
import uk.org.taverna.scufl2.api.profiles.Profile;
/**
* Implementation of {@link FileManager}
*
* @author Stian Soiland-Reyes
*/
public class FileManagerImpl implements FileManager {
private static Logger logger = Logger.getLogger(FileManagerImpl.class);
private static int nameIndex = 1;
/**
* The last blank workflowBundle created using #newDataflow() until it has
* been changed - when this variable will be set to null again. Used to
* automatically close unmodified blank workflowBundles on open.
*/
private WorkflowBundle blankWorkflowBundle = null;
@SuppressWarnings("unused")
private EditManager editManager;
private EditManagerObserver editManagerObserver = new EditManagerObserver();
protected MultiCaster<FileManagerEvent> observers = new MultiCaster<>(this);
/**
* Ordered list of open WorkflowBundle
*/
private LinkedHashMap<WorkflowBundle, OpenDataflowInfo> openDataflowInfos = new LinkedHashMap<>();
private DataflowPersistenceHandlerRegistry dataflowPersistenceHandlerRegistry;
private Scufl2Tools scufl2Tools = new Scufl2Tools();
private WorkflowBundle currentWorkflowBundle;
public DataflowPersistenceHandlerRegistry getPersistanceHandlerRegistry() {
return dataflowPersistenceHandlerRegistry;
}
public FileManagerImpl(EditManager editManager) {
this.editManager = editManager;
editManager.addObserver(editManagerObserver);
}
/**
* Add an observer to be notified of {@link FileManagerEvent}s, such as
* {@link OpenedDataflowEvent} and {@link SavedDataflowEvent}.
*
* {@inheritDoc}
*/
@Override
public void addObserver(Observer<FileManagerEvent> observer) {
observers.addObserver(observer);
}
@Override
public boolean canSaveWithoutDestination(WorkflowBundle workflowBundle) {
OpenDataflowInfo dataflowInfo = getOpenDataflowInfo(workflowBundle);
if (dataflowInfo.getSource() == null)
return false;
Set<?> handlers = getPersistanceHandlerRegistry()
.getSaveHandlersForType(
dataflowInfo.getFileType(),
dataflowInfo.getDataflowInfo().getCanonicalSource()
.getClass());
return !handlers.isEmpty();
}
@Override
public boolean closeDataflow(WorkflowBundle workflowBundle,
boolean failOnUnsaved) throws UnsavedException {
if (workflowBundle == null)
throw new NullPointerException("Dataflow can't be null");
ClosingDataflowEvent message = new ClosingDataflowEvent(workflowBundle);
observers.notify(message);
if (message.isAbortClose())
return false;
if ((failOnUnsaved && getOpenDataflowInfo(workflowBundle).isChanged()))
throw new UnsavedException(workflowBundle);
if (workflowBundle.equals(getCurrentDataflow())) {
// We'll need to change current workflowBundle
// Find best candidate to the left or right
List<WorkflowBundle> workflowBundles = getOpenDataflows();
int openIndex = workflowBundles.indexOf(workflowBundle);
if (openIndex == -1)
throw new IllegalArgumentException("Workflow was not opened "
+ workflowBundle);
if (openIndex > 0)
setCurrentDataflow(workflowBundles.get(openIndex - 1));
else if (openIndex == 0 && workflowBundles.size() > 1)
setCurrentDataflow(workflowBundles.get(1));
else
// If it was the last one, start a new, empty workflowBundle
newDataflow();
}
if (workflowBundle == blankWorkflowBundle)
blankWorkflowBundle = null;
openDataflowInfos.remove(workflowBundle);
observers.notify(new ClosedDataflowEvent(workflowBundle));
return true;
}
@Override
public WorkflowBundle getCurrentDataflow() {
return currentWorkflowBundle;
}
@Override
public WorkflowBundle getDataflowBySource(Object source) {
for (Entry<WorkflowBundle, OpenDataflowInfo> infoEntry : openDataflowInfos
.entrySet()) {
OpenDataflowInfo info = infoEntry.getValue();
if (source.equals(info.getSource()))
return infoEntry.getKey();
}
// Not found
return null;
}
@Override
public String getDataflowName(WorkflowBundle workflowBundle) {
Object source = null;
if (isDataflowOpen(workflowBundle))
source = getDataflowSource(workflowBundle);
// Fallback
String name;
Workflow workflow = workflowBundle.getMainWorkflow();
if (workflow != null)
name = workflow.getName();
else
name = workflowBundle.getName();
if (source == null)
return name;
if (source instanceof File)
return ((File) source).getAbsolutePath();
else if (source instanceof URL)
return source.toString();
// Check if it has implemented a toString() method
Method toStringMethod = null;
Method toStringMethodFromObject = null;
try {
toStringMethod = source.getClass().getMethod("toString");
toStringMethodFromObject = Object.class.getMethod("toString");
} catch (Exception e) {
throw new IllegalStateException(
"Source did not implement Object.toString() " + source);
}
if (!toStringMethod.equals(toStringMethodFromObject))
return source.toString();
return name;
}
@Override
public String getDefaultWorkflowName() {
return "Workflow" + (nameIndex++);
}
@Override
public Object getDataflowSource(WorkflowBundle workflowBundle) {
return getOpenDataflowInfo(workflowBundle).getSource();
}
@Override
public FileType getDataflowType(WorkflowBundle workflowBundle) {
return getOpenDataflowInfo(workflowBundle).getFileType();
}
@Override
public List<Observer<FileManagerEvent>> getObservers() {
return observers.getObservers();
}
/**
* Get the {@link OpenDataflowInfo} for the given WorkflowBundle
*
* @throws NullPointerException
* if the WorkflowBundle was <code>null</code>
* @throws IllegalArgumentException
* if the WorkflowBundle was not open.
* @param workflowBundle
* WorkflowBundle which information is to be found
* @return The {@link OpenDataflowInfo} describing the WorkflowBundle
*/
protected synchronized OpenDataflowInfo getOpenDataflowInfo(
WorkflowBundle workflowBundle) {
if (workflowBundle == null)
throw new NullPointerException("Dataflow can't be null");
OpenDataflowInfo info = openDataflowInfos.get(workflowBundle);
if (info == null)
throw new IllegalArgumentException("Workflow was not opened "
+ workflowBundle);
return info;
}
@Override
public List<WorkflowBundle> getOpenDataflows() {
return new ArrayList<>(openDataflowInfos.keySet());
}
@Override
public List<FileFilter> getOpenFileFilters() {
List<FileFilter> fileFilters = new ArrayList<>();
Set<FileType> fileTypes = getPersistanceHandlerRegistry()
.getOpenFileTypes();
if (!fileTypes.isEmpty())
fileFilters.add(new MultipleFileTypes(fileTypes,
"All supported workflows"));
for (FileType fileType : fileTypes)
fileFilters.add(new FileTypeFileFilter(fileType));
return fileFilters;
}
@Override
public List<FileFilter> getOpenFileFilters(Class<?> sourceClass) {
List<FileFilter> fileFilters = new ArrayList<>();
for (FileType fileType : getPersistanceHandlerRegistry()
.getOpenFileTypesFor(sourceClass))
fileFilters.add(new FileTypeFileFilter(fileType));
return fileFilters;
}
@Override
public List<FileFilter> getSaveFileFilters() {
List<FileFilter> fileFilters = new ArrayList<>();
for (FileType fileType : getPersistanceHandlerRegistry()
.getSaveFileTypes())
fileFilters.add(new FileTypeFileFilter(fileType));
return fileFilters;
}
@Override
public List<FileFilter> getSaveFileFilters(Class<?> destinationClass) {
List<FileFilter> fileFilters = new ArrayList<>();
for (FileType fileType : getPersistanceHandlerRegistry()
.getSaveFileTypesFor(destinationClass))
fileFilters.add(new FileTypeFileFilter(fileType));
return fileFilters;
}
@Override
public boolean isDataflowChanged(WorkflowBundle workflowBundle) {
return getOpenDataflowInfo(workflowBundle).isChanged();
}
@Override
public boolean isDataflowOpen(WorkflowBundle workflowBundle) {
return openDataflowInfos.containsKey(workflowBundle);
}
@Override
public WorkflowBundle newDataflow() {
WorkflowBundle workflowBundle = new WorkflowBundle();
workflowBundle.setMainWorkflow(new Workflow());
workflowBundle.getMainWorkflow().setName(getDefaultWorkflowName());
workflowBundle.setMainProfile(new Profile());
scufl2Tools.setParents(workflowBundle);
blankWorkflowBundle = null;
openDataflowInternal(workflowBundle);
blankWorkflowBundle = workflowBundle;
observers.notify(new OpenedDataflowEvent(workflowBundle));
return workflowBundle;
}
@Override
public void openDataflow(WorkflowBundle workflowBundle) {
openDataflowInternal(workflowBundle);
observers.notify(new OpenedDataflowEvent(workflowBundle));
}
/**
* {@inheritDoc}
*/
@Override
public WorkflowBundle openDataflow(FileType fileType, Object source)
throws OpenException {
if (isHeadless())
return performOpenDataflow(fileType, source);
OpenDataflowRunnable r = new OpenDataflowRunnable(this, fileType,
source);
if (isEventDispatchThread()) {
r.run();
} else
try {
invokeAndWait(r);
} catch (InterruptedException | InvocationTargetException e) {
throw new OpenException("Opening was interrupted", e);
}
OpenException thrownException = r.getException();
if (thrownException != null)
throw thrownException;
return r.getDataflow();
}
public WorkflowBundle performOpenDataflow(FileType fileType, Object source)
throws OpenException {
DataflowInfo dataflowInfo;
WorkflowBundle workflowBundle;
dataflowInfo = openDataflowSilently(fileType, source);
workflowBundle = dataflowInfo.getDataflow();
openDataflowInternal(workflowBundle);
getOpenDataflowInfo(workflowBundle).setOpenedFrom(dataflowInfo);
observers.notify(new OpenedDataflowEvent(workflowBundle));
return workflowBundle;
}
@Override
public DataflowInfo openDataflowSilently(FileType fileType, Object source)
throws OpenException {
Set<DataflowPersistenceHandler> handlers;
Class<? extends Object> sourceClass = source.getClass();
boolean unknownFileType = (fileType == null);
if (unknownFileType)
handlers = getPersistanceHandlerRegistry().getOpenHandlersFor(
sourceClass);
else
handlers = getPersistanceHandlerRegistry().getOpenHandlersFor(
fileType, sourceClass);
if (handlers.isEmpty())
throw new OpenException("Unsupported file type or class "
+ fileType + " " + sourceClass);
Throwable lastException = null;
for (DataflowPersistenceHandler handler : handlers) {
Collection<FileType> fileTypes;
if (unknownFileType)
fileTypes = handler.getOpenFileTypes();
else
fileTypes = singleton(fileType);
for (FileType candidateFileType : fileTypes) {
if (unknownFileType && (source instanceof File))
/*
* If source is file but fileType was not explicitly set
* from the open workflow dialog - check the file extension
* and decide which handler to use based on that (so that we
* do not loop though all handlers)
*/
if (!((File) source).getPath().endsWith(
candidateFileType.getExtension()))
continue;
try {
DataflowInfo openDataflow = handler.openDataflow(
candidateFileType, source);
WorkflowBundle workflowBundle = openDataflow.getDataflow();
logger.info("Loaded workflow: " + workflowBundle.getName()
+ " " + workflowBundle.getGlobalBaseURI()
+ " from " + source + " using " + handler);
return openDataflow;
} catch (OpenException ex) {
logger.warn("Could not open workflow " + source + " using "
+ handler + " of type " + candidateFileType);
lastException = ex;
}
}
}
throw new OpenException("Could not open workflow " + source + "\n",
lastException);
}
/**
* Mark the WorkflowBundle as opened, and close the blank WorkflowBundle if
* needed.
*
* @param workflowBundle
* WorkflowBundle that has been opened
*/
protected void openDataflowInternal(WorkflowBundle workflowBundle) {
if (workflowBundle == null)
throw new NullPointerException("Dataflow can't be null");
if (isDataflowOpen(workflowBundle))
throw new IllegalArgumentException("Workflow is already open: "
+ workflowBundle);
openDataflowInfos.put(workflowBundle, new OpenDataflowInfo());
setCurrentDataflow(workflowBundle);
if (openDataflowInfos.size() == 2 && blankWorkflowBundle != null)
/*
* Behave like a word processor and close the blank WorkflowBundle
* when another workflow has been opened
*/
try {
closeDataflow(blankWorkflowBundle, true);
} catch (UnsavedException e) {
logger.error("Blank workflow was modified "
+ "and could not be closed");
}
}
@Override
public void removeObserver(Observer<FileManagerEvent> observer) {
observers.removeObserver(observer);
}
@Override
public void saveDataflow(WorkflowBundle workflowBundle,
boolean failOnOverwrite) throws SaveException {
if (workflowBundle == null)
throw new NullPointerException("Dataflow can't be null");
OpenDataflowInfo lastSave = getOpenDataflowInfo(workflowBundle);
if (lastSave.getSource() == null)
throw new SaveException("Can't save without source "
+ workflowBundle);
saveDataflow(workflowBundle, lastSave.getFileType(),
lastSave.getSource(), failOnOverwrite);
}
@Override
public void saveDataflow(WorkflowBundle workflowBundle, FileType fileType,
Object destination, boolean failOnOverwrite) throws SaveException {
DataflowInfo savedDataflow = saveDataflowSilently(workflowBundle,
fileType, destination, failOnOverwrite);
getOpenDataflowInfo(workflowBundle).setSavedTo(savedDataflow);
observers.notify(new SavedDataflowEvent(workflowBundle));
}
@Override
public DataflowInfo saveDataflowSilently(WorkflowBundle workflowBundle,
FileType fileType, Object destination, boolean failOnOverwrite)
throws SaveException, OverwriteException {
Set<DataflowPersistenceHandler> handlers;
Class<? extends Object> destinationClass = destination.getClass();
if (fileType != null)
handlers = getPersistanceHandlerRegistry().getSaveHandlersForType(
fileType, destinationClass);
else
handlers = getPersistanceHandlerRegistry().getSaveHandlersFor(
destinationClass);
SaveException lastException = null;
for (DataflowPersistenceHandler handler : handlers) {
if (failOnOverwrite) {
OpenDataflowInfo openDataflowInfo = getOpenDataflowInfo(workflowBundle);
if (handler.wouldOverwriteDataflow(workflowBundle, fileType,
destination, openDataflowInfo.getDataflowInfo()))
throw new OverwriteException(destination);
}
try {
DataflowInfo savedDataflow = handler.saveDataflow(
workflowBundle, fileType, destination);
savedDataflow.getDataflow();
logger.info("Saved workflow: " + workflowBundle.getName() + " "
+ workflowBundle.getGlobalBaseURI() + " to "
+ savedDataflow.getCanonicalSource() + " using "
+ handler);
return savedDataflow;
} catch (SaveException ex) {
logger.warn("Could not save to " + destination + " using "
+ handler);
lastException = ex;
}
}
if (lastException == null)
throw new SaveException("Unsupported file type or class "
+ fileType + " " + destinationClass);
throw new SaveException("Could not save to " + destination + ":\n"
+ lastException.getLocalizedMessage(), lastException);
}
@Override
public void setCurrentDataflow(WorkflowBundle workflowBundle) {
setCurrentDataflow(workflowBundle, false);
}
@Override
public void setCurrentDataflow(WorkflowBundle workflowBundle,
boolean openIfNeeded) {
currentWorkflowBundle = workflowBundle;
if (!isDataflowOpen(workflowBundle)) {
if (!openIfNeeded)
throw new IllegalArgumentException("Workflow is not open: "
+ workflowBundle);
openDataflow(workflowBundle);
return;
}
observers.notify(new SetCurrentDataflowEvent(workflowBundle));
}
@Override
public void setDataflowChanged(WorkflowBundle workflowBundle,
boolean isChanged) {
getOpenDataflowInfo(workflowBundle).setIsChanged(isChanged);
if (blankWorkflowBundle == workflowBundle)
blankWorkflowBundle = null;
}
@Override
public Object getCanonical(Object source) throws IllegalArgumentException,
URISyntaxException, IOException {
Object canonicalSource = source;
if (source instanceof URL) {
URL url = ((URL) source);
if (url.getProtocol().equalsIgnoreCase("file"))
canonicalSource = new File(url.toURI());
}
if (canonicalSource instanceof File)
canonicalSource = ((File) canonicalSource).getCanonicalFile();
return canonicalSource;
}
public void setDataflowPersistenceHandlerRegistry(
DataflowPersistenceHandlerRegistry dataflowPersistenceHandlerRegistry) {
this.dataflowPersistenceHandlerRegistry = dataflowPersistenceHandlerRegistry;
}
/**
* Observe the {@link EditManager} for changes to open workflowBundles. A
* change of an open workflow would set it as changed using
* {@link FileManagerImpl#setDataflowChanged(Dataflow, boolean)}.
*
* @author Stian Soiland-Reyes
*
*/
private final class EditManagerObserver implements
Observer<EditManagerEvent> {
@Override
public void notify(Observable<EditManagerEvent> sender,
EditManagerEvent message) throws Exception {
if (message instanceof AbstractDataflowEditEvent) {
AbstractDataflowEditEvent dataflowEdit = (AbstractDataflowEditEvent) message;
WorkflowBundle workflowBundle = dataflowEdit.getDataFlow();
/**
* TODO: on undo/redo - keep last event or similar to determine
* if workflow was saved before. See
* FileManagerTest#isChangedWithUndo().
*/
setDataflowChanged(workflowBundle, true);
}
}
}
}