workcraft/workcraft

View on GitHub
workcraft/WorkcraftCore/src/org/workcraft/Framework.java

Summary

Maintainability
A
1 hr
Test Coverage
package org.workcraft;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.mozilla.javascript.*;
import org.workcraft.dom.ModelDescriptor;
import org.workcraft.dom.VisualModelDescriptor;
import org.workcraft.dom.math.MathModel;
import org.workcraft.dom.visual.VisualModel;
import org.workcraft.exceptions.*;
import org.workcraft.gui.MainWindow;
import org.workcraft.gui.editor.GraphEditorPanel;
import org.workcraft.gui.properties.Settings;
import org.workcraft.gui.tools.GraphEditor;
import org.workcraft.gui.workspace.Path;
import org.workcraft.interop.Exporter;
import org.workcraft.interop.Format;
import org.workcraft.interop.Importer;
import org.workcraft.plugins.CompatibilityManager;
import org.workcraft.plugins.PluginManager;
import org.workcraft.plugins.builtin.settings.EditorCommonSettings;
import org.workcraft.tasks.ExtendedTaskManager;
import org.workcraft.tasks.TaskManager;
import org.workcraft.utils.*;
import org.workcraft.workspace.*;

import javax.swing.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class Framework {

    private static final String SETTINGS_DIRECTORY_NAME = "workcraft";
    private static final String CONFIG_FILE_NAME = "config.xml";
    private static final String UILAYOUT_FILE_NAME = "uilayout.xml";
    private static final String SETTINGS_DIRECTORY_PATH = DesktopApi.getConfigPath() + File.separator + SETTINGS_DIRECTORY_NAME;
    private static final String CONFIG_FILE_PATH = SETTINGS_DIRECTORY_PATH + File.separator + CONFIG_FILE_NAME;
    public static final String UILAYOUT_FILE_PATH = SETTINGS_DIRECTORY_PATH + File.separator + UILAYOUT_FILE_NAME;

    private static final String FRAMEWORK_VARIABLE = "framework";
    private static final String MAIN_WINDOW_VARIABLE = "mainWindow";
    private static final String WORKSPACE_ENTRY_VARIABLE = "workspaceEntry";
    private static final String MODEL_ENTRY_VARIABLE = "modelEntry";
    private static final String MATH_MODEL_VARIABLE = "mathModel";
    private static final String VISUAL_MODEL_VARIABLE = "visualModel";
    private static final String ARGS_VARIABLE = "args";

    private static final String CONFIG_RECENT_LAST_DIRECTORY = "recent.lastDirectory";
    private static final String CONFIG_RECENT_FILE = "recent.file";

    private static final Pattern JAVASCRIPT_FUNCTION_PATTERN =
            Pattern.compile("\\s*function\\s+(\\w+)\\s*\\((.*)\\).*");
    private static final int JAVASCRIPT_FUNCTION_NAME_GROUP = 1;
    private static final int JAVASCRIPT_FUNCTION_PARAMS_GROUP = 2;


    private static Framework instance = null;
    private final PluginManager pluginManager;
    private final TaskManager taskManager;
    private final CompatibilityManager compatibilityManager;
    private final Workspace workspace;
    private MainWindow mainWindow;

    private Config config;
    private ScriptableObject systemScope;
    private ScriptableObject globalScope;

    private boolean shutdownRequested = false;
    private final ContextFactory contextFactory = new ContextFactory();
    public Resource clipboard;
    private final HashMap<String, JavascriptItem> javascriptHelp = new HashMap<>();

    private File workingDirectory = null;
    private File importContextDirectory = null;
    private File lastDirectory = null;
    private final LinkedHashSet<String> recentFilePaths = new LinkedHashSet<>();

    static class ExecuteScriptAction implements ContextAction<Object> {
        private final String script;
        private final Scriptable scope;

        ExecuteScriptAction(String script, Scriptable scope) {
            this.script = script;
            this.scope = scope;
        }

        @Override
        public Object run(Context cx) {
            cx.setLanguageVersion(Context.VERSION_ES6);
            return cx.evaluateString(scope, script, "<string>", 1, null);
        }
    }

    static class ExecuteCompiledScriptAction implements ContextAction<Object> {
        private final Script script;
        private final Scriptable scope;

        ExecuteCompiledScriptAction(Script script, Scriptable scope) {
            this.script = script;
            this.scope = scope;
        }

        @Override
        public Object run(Context cx) {
            return script.exec(cx, scope);
        }
    }

    static class CompileScriptFromReaderAction implements ContextAction<Object> {
        private final String sourceName;
        private final BufferedReader reader;

        CompileScriptFromReaderAction(BufferedReader reader, String sourceName) {
            this.sourceName = sourceName;
            this.reader = reader;
        }

        @Override
        public Script run(Context cx) {
            try {
                return cx.compileReader(reader, sourceName, 1, null);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    class SetArgs implements ContextAction<Void> {
        private Object[] args;

        public void setArgs(Object[] args) {
            this.args = args;
        }

        @Override
        public Void run(Context cx) {
            Object scriptable = Context.javaToJS(args, systemScope);
            ScriptableObject.putProperty(systemScope, ARGS_VARIABLE, scriptable);
            systemScope.setAttributes(ARGS_VARIABLE, ScriptableObject.READONLY);
            return null;
        }
    }

    static class JavascriptPassThroughException extends RuntimeException {
        private static final long serialVersionUID = 8906492547355596206L;
        private final String scriptTrace;

        JavascriptPassThroughException(Throwable wrapped, String scriptTrace) {
            super(wrapped);
            this.scriptTrace = scriptTrace;
        }

        @Override
        public String getMessage() {
            return String.format("Java %s was unhandled in javascript. \nJavascript stack trace: %s",
                    getCause().getClass().getSimpleName(), getScriptTrace());
        }

        public String getScriptTrace() {
            return scriptTrace;
        }
    }

    private static class JavascriptItem {
        public final String name;
        public final String params;
        public final String description;

        JavascriptItem(String name, String params, String description) {
            this.name = name;
            this.params = params;
            this.description = description;
        }

        @Override
        public String toString() {
            return name + (params == null ? "" : "(" + params + ")") + " - " + description;
        }
    }

    private Framework() {
        pluginManager = new PluginManager();
        taskManager = new ExtendedTaskManager();
        compatibilityManager = new CompatibilityManager();
        config = new Config();
        workspace = new Workspace();
    }

    public static Framework getInstance() {
        if (instance == null) {
            instance = new Framework();
        }
        return instance;
    }

    private void loadPluginsSettings() {
        for (Settings settings : pluginManager.getSortedSettings()) {
            settings.load(config);
        }
    }

    private void savePluginsSettings() {
        for (Settings settings : pluginManager.getSortedSettings()) {
            settings.save(config);
        }
    }

    public void resetConfig() {
        config = new Config();
        loadPluginsSettings();
        savePluginsSettings();
    }

    public void loadConfig(File file) {
        if (file == null) {
            file = new File(CONFIG_FILE_PATH);
        }
        LogUtils.logMessage("Loading global preferences from " + file.getAbsolutePath());
        config.load(file);
        loadPluginsSettings();
        loadRecentFilesFromConfig();
    }

    public void saveConfig(File file) {
        saveRecentFilesToConfig();
        savePluginsSettings();
        if (file == null) {
            file = new File(CONFIG_FILE_PATH);
        }
        LogUtils.logMessage("Saving global preferences to " + file.getAbsolutePath());
        config.save(file);
    }

    /**
     * Set a config variable. If requested, reload plugin settings.
     */
    public void setConfigVar(String key, String value, boolean reloadPluginSettings) {
        config.set(key, value);
        if (reloadPluginSettings) {
            loadPluginsSettings();
        }
    }

    /**
     * Get a config variable. If requested, flush plugin settings before that.
     */
    public String getConfigVar(String key, boolean flushPluginSettings) {
        if (flushPluginSettings) {
            savePluginsSettings();
        }
        return config.get(key);
    }

    public void init() {
        // Configure log4j output and set INFO verbosity.
        // This is necessary for some plugins (e.g. PdfExporter) that use log4j.
        BasicConfigurator.configure();
        Logger.getRootLogger().setLevel(Level.INFO);

        initJavaScript();
        initPlugins();
    }

    private void initPlugins() {
        try {
            pluginManager.initPlugins();
        } catch (PluginInstantiationException e) {
            e.printStackTrace();
        }
    }

    private void initJavaScript() {
        LogUtils.logMessage("Initialising JavaScript...");
        contextFactory.call(cx -> {
            ImporterTopLevel importer = new ImporterTopLevel();
            importer.initStandardObjects(cx, false);
            systemScope = importer;

            Object frameworkScriptable = Context.javaToJS(this, systemScope);
            ScriptableObject.putProperty(systemScope, FRAMEWORK_VARIABLE, frameworkScriptable);
            systemScope.setAttributes(FRAMEWORK_VARIABLE, ScriptableObject.READONLY);

            globalScope = (ScriptableObject) cx.newObject(systemScope);
            globalScope.setPrototype(systemScope);
            globalScope.setParentScope(null);

            return null;
        });
    }

    public void updateJavaScript(WorkspaceEntry we) {
        if (we != null) {
            ScriptableObject jsGlobalScope = getJavaScriptGlobalScope();
            setJavaScriptProperty(WORKSPACE_ENTRY_VARIABLE, we, jsGlobalScope, true);

            ModelEntry me = we.getModelEntry();
            setJavaScriptProperty(MODEL_ENTRY_VARIABLE, me, jsGlobalScope, true);

            VisualModel visualModel = me.getVisualModel();
            setJavaScriptProperty(VISUAL_MODEL_VARIABLE, visualModel, jsGlobalScope, true);

            MathModel mathModel = me.getMathModel();
            setJavaScriptProperty(MATH_MODEL_VARIABLE, mathModel, jsGlobalScope, true);
        }
    }

    public ScriptableObject getJavaScriptGlobalScope() {
        return globalScope;
    }

    public void registerJavaScriptFunction(String function, String description) {
        Matcher matcher = JAVASCRIPT_FUNCTION_PATTERN.matcher(function);
        if (matcher.find()) {
            String name = matcher.group(JAVASCRIPT_FUNCTION_NAME_GROUP);
            String params = matcher.group(JAVASCRIPT_FUNCTION_PARAMS_GROUP);
            addJavaScriptHelp(name, params, description);
            execJavaScript(function, globalScope);
        } else {
            LogUtils.logWarning("Cannot determine the function name in the following JavaScript code:\n" + function);
        }
    }

    public void addJavaScriptHelp(String name, String params, String description) {
        JavascriptItem item = new JavascriptItem(name, params, description);
        if (javascriptHelp.containsKey(name)) {
            LogUtils.logWarning("Overwriting JavaScrip function '" + name + "':"
                    + "\n  Old: " + javascriptHelp.get(name)
                    + "\n  New: " + item);
        }
        javascriptHelp.put(name, item);
    }

    /**
     * Used in core-help.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public String getJavaScriptHelp(String regex, boolean searchDescription) {
        ArrayList<String> result = new ArrayList<>();
        Pattern pattern = Pattern.compile(regex);
        for (Entry<String, JavascriptItem> entry : javascriptHelp.entrySet()) {
            String name = entry.getKey();
            JavascriptItem item = entry.getValue();
            Matcher nameMatcher = pattern.matcher(name);
            Matcher descriptionMatcher = pattern.matcher(item.description);
            if (nameMatcher.find() || (searchDescription && descriptionMatcher.find())) {
                result.add(item.toString() + '\n');
            }
        }
        Collections.sort(result);
        return String.join("",  result);
    }

    public void setJavaScriptProperty(final String name, final Object object,
            final ScriptableObject scope, final boolean readOnly) {

        deleteJavaScriptProperty(name, scope);

        contextFactory.call(arg0 -> {
            Object scriptable = Context.javaToJS(object, scope);
            ScriptableObject.putProperty(scope, name, scriptable);
            if (readOnly) {
                scope.setAttributes(name, ScriptableObject.READONLY);
            }
            return scriptable;
        });
    }

    public void deleteJavaScriptProperty(final String name, final ScriptableObject scope) {
        contextFactory.call(arg0 -> ScriptableObject.deleteProperty(scope, name));
    }

    /**
     * Used in core-exec.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public Object execJavaScript(File file) throws FileNotFoundException {
        BufferedReader reader = new BufferedReader(new FileReader(file));
        return execJavaScript(compileJavaScript(reader, file.getPath()));
    }

    public Object execJavaScript(Script script) {
        return execJavaScript(script, globalScope);
    }

    public Object execJavaScript(String script) {
        return execJavaScript(script, globalScope);
    }

    private Object execJavaScript(Script script, Scriptable scope) {
        return doContextAction(new ExecuteCompiledScriptAction(script, scope));
    }

    private Object execJavaScript(String script, Scriptable scope) {
        return doContextAction(new ExecuteScriptAction(script, scope));
    }

    private Object doContextAction(ContextAction<Object> action) {
        try {
            return contextFactory.call(action);
        } catch (JavaScriptException ex) {
            System.out.println("Script stack trace: " + ex.getScriptStackTrace());
            Object value = ex.getValue();
            if (value instanceof NativeJavaObject) {
                Object wrapped = ((NativeJavaObject) value).unwrap();
                if (wrapped instanceof Throwable) {
                    throw new JavascriptPassThroughException((Throwable) wrapped, ex.getScriptStackTrace());
                }
            }
            throw ex;
        }
    }

    public void execJavaScriptResource(String resourceName) throws IOException {
        String script = FileUtils.readAllTextFromSystemResource(resourceName);
        execJavaScript(script);
    }

    /**
     * Used in core-exec.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public void execJavaScriptFile(String path) throws IOException {
        File file = getFileByAbsoluteOrRelativePath(path);
        execJavaScriptFile(file);
    }

    public void execJavaScriptFile(File file) throws IOException {
        String script = FileUtils.readAllText(file);
        execJavaScript(script, globalScope);
    }

    public Script compileJavaScript(BufferedReader source, String sourceName) {
        return (Script) doContextAction(new CompileScriptFromReaderAction(source, sourceName));
    }

    public void startGUI() {
        if (isInGuiMode()) {
            System.out.println("Already in GUI mode");
            return;
        }
        System.out.println("Switching to GUI mode...");

        if (SwingUtilities.isEventDispatchThread()) {
            mainWindow = new MainWindow();
            mainWindow.startup();
        } else {
            try {
                SwingUtilities.invokeAndWait(() -> {
                    mainWindow = new MainWindow();
                    mainWindow.startup();
                });
            } catch (InterruptedException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }

        contextFactory.call(cx -> {
            Object guiScriptable = Context.javaToJS(mainWindow, systemScope);
            ScriptableObject.putProperty(systemScope, MAIN_WINDOW_VARIABLE, guiScriptable);
            systemScope.setAttributes(MAIN_WINDOW_VARIABLE, ScriptableObject.READONLY);
            return null;
        });
    }

    public void shutdownGUI() throws OperationCancelledException {
        if (isInGuiMode()) {
            mainWindow.shutdown();
            mainWindow.dispose();
            mainWindow = null;

            contextFactory.call(cx -> {
                ScriptableObject.deleteProperty(systemScope, MAIN_WINDOW_VARIABLE);
                return null;
            });
        }
    }

    public void shutdown() {
        shutdownRequested = true;
    }

    public boolean isShutdownRequested() {
        return shutdownRequested;
    }

    public void abortShutdown() {
        shutdownRequested = false;
    }

    public MainWindow getMainWindow() {
        return mainWindow;
    }

    public PluginManager getPluginManager() {
        return pluginManager;
    }

    public TaskManager getTaskManager() {
        return taskManager;
    }

    public CompatibilityManager getCompatibilityManager() {
        return compatibilityManager;
    }

    public Workspace getWorkspace() {
        return workspace;
    }

    public boolean isInGuiMode() {
        return mainWindow != null;
    }

    public void requestFocus(WorkspaceEntry we) {
        if (isInGuiMode() && (we != null)) {
            mainWindow.requestFocus(we);
        }
    }

    public void setArgs(Collection<String> args) {
        SetArgs setargs = new SetArgs();
        setargs.setArgs(args.toArray());
        contextFactory.call(setargs);
    }

    /**
     * Used in core-exec.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public void runCommand(WorkspaceEntry we, String className) {
        CommandUtils.run(we, className);
    }

    /**
     * Used in core-exec.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public <R> R executeCommand(WorkspaceEntry we, String className) {
        return CommandUtils.execute(we, className);
    }

    /**
     * Used in core-exec.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public <R, D> R executeCommand(WorkspaceEntry we, String className, String serialisedData) {
        return CommandUtils.execute(we, className, serialisedData);
    }

    public WorkspaceEntry createWork(ModelEntry me, String desiredName) {
        return createWork(me, Path.empty(), desiredName);
    }

    public WorkspaceEntry createWork(ModelEntry me, Path<String> parent, String name) {
        final Path<String> path = getWorkspace().createWorkPath(parent, name);
        boolean open = me.isVisual() || EditorCommonSettings.getOpenNonvisual();
        return createWork(me, path, open, true);
    }

    public WorkspaceEntry createWork(ModelEntry me, Path<String> path, boolean open, boolean changed) {
        WorkspaceEntry we = new WorkspaceEntry();
        we.setModelEntry(createVisualIfAbsent(me));
        getWorkspace().addWork(path, we);
        if (open && isInGuiMode()) {
            // Attempt automatic layout only if the model entry changed (because of creating visual layer)
            if ((me == we.getModelEntry()) || LayoutUtils.attemptLayout(we)) {
                getMainWindow().getOrCreateEditor(we);
            }
        }
        we.setChanged(changed);
        return we;
    }

    private ModelEntry createVisualIfAbsent(ModelEntry me) {
        ModelEntry result = me;
        VisualModel visualModel = me.getVisualModel();
        if (visualModel != null) {
            visualModel.selectNone();
        } else {
            ModelDescriptor md = me.getDescriptor();
            if (md == null) {
                DialogUtils.showError("Model descriptor is not defined.");
                return result;
            }
            VisualModelDescriptor vmd = md.getVisualModelDescriptor();
            if (vmd == null) {
                DialogUtils.showError("Visual model is not defined for '" + md.getDisplayName() + "'.");
                return result;
            }

            try {
                visualModel = vmd.create(me.getMathModel());
                result = new ModelEntry(md, visualModel);
            } catch (VisualModelInstantiationException e) {
                DialogUtils.showError(e.getMessage());
            }
        }
        return result;
    }

    /**
     * Used in core-file.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public WorkspaceEntry loadWork(String path) throws DeserialisationException {
        File file = getFileByAbsoluteOrRelativePath(path);
        return FileUtils.checkFileReadability(file, false) ? loadWork(file) : null;
    }

    public WorkspaceEntry loadWork(File file) throws DeserialisationException {
        return loadWork(file, true);
    }

    public WorkspaceEntry loadWork(File file, boolean open) throws DeserialisationException {
        if (!FileFilters.isWorkFile(file)) {
            throw new DeserialisationException("File '" + file.getAbsolutePath() + "; is not a work file");
        }
        // Check if work is already loaded
        WorkspaceEntry we = getWorkspace().getWork(file);
        if (we != null) {
            if (open && isInGuiMode()) {
                getMainWindow().getOrCreateEditor(we);
            }
            return we;
        }
        ModelEntry me = WorkUtils.loadModel(file);
        if (me != null) {
            Path<String> path = getWorkspace().getPath(file);
            if (path == null) {
                path = getWorkspace().tempMountExternalFile(file);
            }
            we = createWork(me, path, open, false);
            Collection<Resource> resources = WorkUtils.loadResources(file);
            resources.forEach(we::addResource);
        }
        updateJavaScript(we);
        return we;
    }

    public void mergeWork(WorkspaceEntry we, File file) throws DeserialisationException {
        if ((we != null) && FileFilters.isWorkFile(file)) {
            ModelEntry me = WorkUtils.loadModel(file);
            if (me != null) {
                we.insert(me);
            }
        }
    }

    /**
     * Used in core-workspace.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public void closeWork(WorkspaceEntry we) {
        if ((we != null) && (mainWindow != null)) {
            we.setChanged(false);
            mainWindow.closeEditor(we);
        }
        if (we != null) {
            workspace.removeWork(we);
        }
    }

    /**
     * Used in core-workspace.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public void closeAllWorks() {
        for (WorkspaceEntry we : workspace.getWorks()) {
            closeWork(we);
        }
    }

    /**
     * Used in core-file.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public void saveWork(WorkspaceEntry we, String path) throws SerialisationException {
        if (we == null) return;
        File file = getFileByAbsoluteOrRelativePath(path);
        saveWork(we, file);
    }

    public void saveWork(WorkspaceEntry we, File file) throws SerialisationException {
        if (we == null) return;
        Path<String> wsFrom = we.getWorkspacePath();
        Path<String> wsTo = workspace.getPath(file);
        if (wsTo == null) {
            wsTo = workspace.tempMountExternalFile(file);
        }
        if (wsFrom != wsTo) {
            try {
                workspace.moveEntry(wsFrom, wsTo);
            } catch (IOException e) {
                LogUtils.logError(e.getMessage());
            }
        }
        WorkUtils.saveModel(we.getModelEntry(), we.getResources(), file);

        we.setChanged(false);
        if (mainWindow != null) {
            mainWindow.refreshWorkspaceEntryTitle(we);
        }
    }

    /**
     * Used in core-file.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public WorkspaceEntry importWork(String path) throws DeserialisationException {
        return importWork(path, null);
    }

    public WorkspaceEntry importWork(String path, String serialisedUserData) throws DeserialisationException {
        File file = getFileByAbsoluteOrRelativePath(path);
        return FileUtils.checkFileReadability(file, false) ? importWork(file, serialisedUserData) : null;
    }

    public WorkspaceEntry importWork(File file) throws DeserialisationException {
        return importWork(file, null);
    }

    public WorkspaceEntry importWork(File file, String serialisedUserData) throws DeserialisationException {
        try {
            Importer importer = ExportUtils.chooseBestImporter(file);
            if (importer == null) {
                throw new DeserialisationException("Cannot identify appropriate importer for file '" + file.getAbsolutePath() + "'");
            }
            ModelEntry me = importer.importFromFile(file, serialisedUserData);
            Path<String> path = getWorkspace().createWorkPath(Path.empty(), me.getDesiredName());
            WorkspaceEntry we = createWork(me, path, true, false);
            updateJavaScript(we);
            return we;
        } catch (OperationCancelledException e) {
            // Operation cancelled by the user
        }
        return null;
    }

    /**
     * Used in core-file.js JavaScript wrapper.
     */
    @SuppressWarnings("unused")
    public void exportWork(WorkspaceEntry we, String path, String formatName) throws SerialisationException {
        File file = getFileByAbsoluteOrRelativePath(path);
        exportModel(we.getModelEntry(), file, formatName, null);
    }

    public void exportWork(WorkspaceEntry we, File file, Format format) throws SerialisationException {
        exportModel(we.getModelEntry(), file, format.getName(), format.getUuid());
    }

    public void exportModel(ModelEntry me, File file, Format format) throws SerialisationException {
        exportModel(me, file, format.getName(), format.getUuid());
    }

    private void exportModel(ModelEntry me, File file, String formatName, UUID formatUuid) throws SerialisationException {
        if (me == null) return;
        // Try to find exporter for visual model first.
        Exporter exporter = ExportUtils.chooseBestExporter(me.getVisualModel(), formatName, formatUuid);
        if (exporter == null) {
            // If no exporter found for visual model, then try to find exporter for math model.
            exporter = ExportUtils.chooseBestExporter(me.getMathModel(), formatName, formatUuid);
        }
        if (exporter == null) {
            String modelName = me.getMathModel().getDisplayName();
            LogUtils.logError("Cannot find exporter to " + formatName + " for " + modelName + ".");
        } else {
            try {
                ExportUtils.exportToFile(exporter, me.getModel(), file);
            } catch (IOException e) {
                throw new SerialisationException(e);
            }
        }
    }

    public void loadWorkspace(File file) throws DeserialisationException {
        workspace.load(file);
    }

    public File getFileByAbsoluteOrRelativePath(String path) {
        return FileUtils.getFileByAbsoluteOrRelativePath(path, getWorkingDirectory());
    }

    private static boolean checkDirectory(File value, String prefix) {
        if (value != null) {
            String path = value.getPath();
            if (!value.exists()) {
                LogUtils.logWarning(prefix + " is set to non-existent path: " + path);
                return false;
            }
            if (value.isFile()) {
                LogUtils.logWarning(prefix + " is set to a file path: " + path);
                return false;
            }
        }
        return true;
    }

    public void setWorkingDirectory(File value) {
        File directory = FileUtils.getFileDirectory(value);
        checkDirectory(directory, "Working directory");
        workingDirectory = directory;
    }

    public File getWorkingDirectory() {
        if (workingDirectory == null) {
            String path = System.getProperty("user.dir");
            setWorkingDirectory(path == null ? null : new File(path));
        }
        return workingDirectory;
    }

    public void setImportContextDirectory(File value) {
        File directory = FileUtils.getFileDirectory(value);
        checkDirectory(directory, "Context directory for import");
        importContextDirectory = directory;
    }

    public File getImportContextDirectory() {
        return importContextDirectory;
    }

    public void setLastDirectory(File value) {
        File directory = FileUtils.getFileDirectory(value);
        checkDirectory(directory, "Last directory");
        lastDirectory = directory;
    }

    public File getLastDirectory() {
        return lastDirectory;
    }

    public Config getConfig() {
        return config;
    }

    private void loadRecentFilesFromConfig() {
        String lastDirectoryPath = getConfigVar(CONFIG_RECENT_LAST_DIRECTORY, false);
        File lastDirectory = (lastDirectoryPath == null) ? null : new File(lastDirectoryPath);
        setLastDirectory(lastDirectory);
        for (int i = 0; i < EditorCommonSettings.getRecentCount(); i++) {
            String entry = getConfigVar(CONFIG_RECENT_FILE + i, false);
            pushRecentFilePath(entry);
        }
    }

    private void saveRecentFilesToConfig() {
        String lastDirectoryPath = FileUtils.getFullPath(getLastDirectory());
        if (lastDirectoryPath != null) {
            setConfigVar(CONFIG_RECENT_LAST_DIRECTORY, lastDirectoryPath, false);
        }
        int recentCount = EditorCommonSettings.getRecentCount();
        String[] tmp = recentFilePaths.toArray(new String[recentCount]);
        for (int i = 0; i < recentCount; i++) {
            setConfigVar(CONFIG_RECENT_FILE + i, tmp[i], false);
        }
    }

    public void pushRecentFilePath(File file) {
        pushRecentFilePath(FileUtils.getFullPath(file));
    }

    public void pushRecentFilePath(String filePath) {
        if ((filePath != null) && (new File(filePath).exists())) {
            // Remove previous entry of the fileName
            recentFilePaths.remove(filePath);
            // Make sure there is not too many entries
            int recentCount = EditorCommonSettings.getRecentCount();
            for (String entry: new ArrayList<>(recentFilePaths)) {
                if (recentFilePaths.size() < recentCount) {
                    break;
                }
                recentFilePaths.remove(entry);
            }
            // Add the fileName if possible
            if (recentFilePaths.size() < recentCount) {
                recentFilePaths.add(filePath);
            }
        }
    }

    public void clearRecentFilePaths() {
        recentFilePaths.clear();
    }

    public ArrayList<String> getRecentFilePaths() {
        ArrayList<String> result = new ArrayList<>(recentFilePaths);
        Collections.reverse(result);
        return result;
    }

    public WorkspaceEntry getWorkspaceEntry(ModelEntry me) {
        for (WorkspaceEntry we : getWorkspace().getWorks()) {
            if (we.getModelEntry() == me) {
                return we;
            }
        }
        return null;
    }

    public void updatePropertyView() {
        if (isInGuiMode()) {
            GraphEditor editor = getMainWindow().getCurrentEditor();
            if (editor instanceof GraphEditorPanel) {
                SwingUtilities.invokeLater(((GraphEditorPanel) editor)::updatePropertyView);
            }
        }
    }

}