org.freeplane.plugin.script.ScriptingConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for org.freeplane.plugin.script.ScriptingConfiguration.java

Source

/*
 *  Freeplane - mind map editor
 *  Copyright (C) 2009 Volker Boerchers
 *
 *  This file author is Volker Boerchers
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.freeplane.plugin.script;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.script.ScriptEngineFactory;

import org.apache.commons.lang.StringUtils;
import org.freeplane.core.resources.ResourceController;
import org.freeplane.core.ui.components.UITools;
import org.freeplane.core.util.ConfigurationUtils;
import org.freeplane.core.util.FileUtils;
import org.freeplane.core.util.HtmlUtils;
import org.freeplane.core.util.LogUtils;
import org.freeplane.core.util.MenuUtils;
import org.freeplane.core.util.TextUtils;
import org.freeplane.features.mode.Controller;
import org.freeplane.main.addons.AddOnProperties;
import org.freeplane.main.addons.AddOnProperties.AddOnType;
import org.freeplane.main.addons.AddOnsController;
import org.freeplane.plugin.script.ExecuteScriptAction.ExecutionMode;
import org.freeplane.plugin.script.addons.ScriptAddOnProperties;
import org.freeplane.plugin.script.addons.ScriptAddOnProperties.Script;

/**
 * scans for scripts to be registered via {@link ScriptingRegistration}.
 * 
 * @author Volker Boerchers
 */
class ScriptingConfiguration {
    static class ScriptMetaData {
        private final TreeMap<ExecutionMode, String> executionModeLocationMap = new TreeMap<ExecutionMode, String>();
        private final TreeMap<ExecutionMode, String> executionModeTitleKeyMap = new TreeMap<ExecutionMode, String>();
        private boolean cacheContent = false;
        private final String scriptName;
        private ScriptingPermissions permissions;

        ScriptMetaData(final String scriptName) {
            this.scriptName = scriptName;
            executionModeLocationMap.put(ExecutionMode.ON_SINGLE_NODE, null);
            executionModeLocationMap.put(ExecutionMode.ON_SELECTED_NODE, null);
            executionModeLocationMap.put(ExecutionMode.ON_SELECTED_NODE_RECURSIVELY, null);
        }

        public Set<ExecutionMode> getExecutionModes() {
            return executionModeLocationMap.keySet();
        }

        public void addExecutionMode(final ExecutionMode executionMode, final String location,
                final String titleKey) {
            executionModeLocationMap.put(executionMode, location);
            if (titleKey != null)
                executionModeTitleKeyMap.put(executionMode, titleKey);
        }

        public void removeExecutionMode(final ExecutionMode executionMode) {
            executionModeLocationMap.remove(executionMode);
        }

        public void removeAllExecutionModes() {
            executionModeLocationMap.clear();
        }

        protected String getMenuLocation(final ExecutionMode executionMode) {
            return executionModeLocationMap.get(executionMode);
        }

        public String getTitleKey(final ExecutionMode executionMode) {
            final String key = executionModeTitleKeyMap.get(executionMode);
            return key == null ? getExecutionModeKey(executionMode) : key;
        }

        public boolean cacheContent() {
            return cacheContent;
        }

        public void setCacheContent(final boolean cacheContent) {
            this.cacheContent = cacheContent;
        }

        public String getScriptName() {
            return scriptName;
        }

        public void setPermissions(ScriptingPermissions permissions) {
            this.permissions = permissions;
        }

        public ScriptingPermissions getPermissions() {
            return permissions;
        }
    }

    static final String MENU_SCRIPTS_LOCATION = "main_menu_scripting";
    static final String CONTEXT_MENU_SCRIPTS_LOCATIONS = "node_popup_scripting";
    private static final String JAR_REGEX = ".+\\.jar$";
    private final TreeMap<String, String> menuTitleToPathMap = new TreeMap<String, String>();
    private final TreeMap<String, ScriptMetaData> menuTitleToMetaDataMap = new TreeMap<String, ScriptMetaData>();
    private static Map<String, Object> staticProperties = createStaticProperties();

    ScriptingConfiguration() {
        ScriptResources.setClasspath(buildClasspath());
        addPluginDefaults();
        initMenuTitleToPathMap();
    }

    private void addPluginDefaults() {
        final URL defaults = this.getClass().getResource(ResourceController.PLUGIN_DEFAULTS_RESOURCE);
        if (defaults == null)
            throw new RuntimeException("cannot open " + ResourceController.PLUGIN_DEFAULTS_RESOURCE);
        Controller.getCurrentController().getResourceController().addDefaults(defaults);
    }

    private void initMenuTitleToPathMap() {
        final Map<File, Script> addOnScriptMap = createAddOnScriptMap();
        addAddOnScripts(addOnScriptMap);
        addNonAddOnScripts(addOnScriptMap);
    }

    private void addAddOnScripts(Map<File, Script> addOnScriptMap) {
        for (File file : addOnScriptMap.keySet()) {
            addScript(file, addOnScriptMap);
        }
    }

    private void addNonAddOnScripts(final Map<File, Script> addOnScriptMap) {
        final FilenameFilter scriptFilenameFilter = createFilenameFilter(createScriptRegExp());
        for (File dir : getScriptDirs()) {
            addNonAddOnScripts(dir, addOnScriptMap, scriptFilenameFilter);
        }
    }

    private Map<File, Script> createAddOnScriptMap() {
        Map<File, Script> result = new LinkedHashMap<File, Script>();
        for (ScriptAddOnProperties scriptAddOnProperties : getInstalledScriptAddOns()) {
            final List<Script> scripts = scriptAddOnProperties.getScripts();
            for (Script script : scripts) {
                script.active = scriptAddOnProperties.isActive();
                result.put(findScriptFile(scriptAddOnProperties, script), script);
            }
        }
        return result;
    }

    private List<ScriptAddOnProperties> getInstalledScriptAddOns() {
        final List<ScriptAddOnProperties> installedAddOns = new ArrayList<ScriptAddOnProperties>();
        for (AddOnProperties addOnProperties : AddOnsController.getController().getInstalledAddOns()) {
            if (addOnProperties.getAddOnType() == AddOnType.SCRIPT) {
                installedAddOns.add((ScriptAddOnProperties) addOnProperties);
            }
        }
        return installedAddOns;
    }

    private File findScriptFile(AddOnProperties addOnProperties, Script script) {
        final File dir = new File(getPrivateAddOnDirectory(addOnProperties), "scripts");
        final File result = new File(dir, script.name);
        return result.exists() ? result : findScriptFile_pre_1_3_x_final(script);
    }

    private File getPrivateAddOnDirectory(AddOnProperties addOnProperties) {
        return new File(AddOnsController.getController().getAddOnsDir(), addOnProperties.getName());
    }

    // add-on scripts are installed in a add-on-private directory since 1.3.x_beta
    @Deprecated
    private File findScriptFile_pre_1_3_x_final(Script script) {
        return new File(ScriptResources.getUserScriptsDir(), script.name);
    }

    private TreeSet<File> getScriptDirs() {
        final ResourceController resourceController = ResourceController.getResourceController();
        final String dirsString = resourceController.getProperty(ScriptResources.RESOURCES_SCRIPT_DIRECTORIES);
        final TreeSet<File> dirs = new TreeSet<File>(); // remove duplicates -> Set
        if (dirsString != null) {
            for (String dir : ConfigurationUtils.decodeListValue(dirsString, false)) {
                dirs.add(createFile(dir));
            }
        }
        dirs.add(ScriptResources.getBuiltinScriptsDir());
        dirs.add(ScriptResources.getUserScriptsDir());
        return dirs;
    }

    /**
     * if <code>path</code> is not an absolute path, prepends the freeplane user
     * directory to it.
     */
    private File createFile(final String path) {
        File file = new File(path);
        if (!file.isAbsolute()) {
            file = new File(ResourceController.getResourceController().getFreeplaneUserDirectory(), path);
        }
        return file;
    }

    /** scans <code>dir</code> for script files matching a given rexgex. */
    private void addNonAddOnScripts(final File dir, final Map<File, Script> addOnScriptMap,
            FilenameFilter filenameFilter) {
        // add all addOn scripts
        // find further scripts in configured directories
        if (dir.isDirectory()) {
            final File[] files = dir.listFiles(filenameFilter);
            if (files != null) {
                for (final File file : files) {
                    if (addOnScriptMap.get(file) == null)
                        addScript(file, addOnScriptMap);
                }
            }
        } else {
            LogUtils.warn("not a (script) directory: " + dir);
        }
    }

    private String createScriptRegExp() {
        final ArrayList<String> extensions = new ArrayList<String>();
        //        extensions.add("clj");
        for (ScriptEngineFactory scriptEngineFactory : GenericScript.getScriptEngineManager()
                .getEngineFactories()) {
            extensions.addAll(scriptEngineFactory.getExtensions());
        }
        LogUtils.info("looking for scripts with the following endings: " + extensions);
        return ".+\\.(" + StringUtils.join(extensions, "|") + ")$";
    }

    private FilenameFilter createFilenameFilter(final String regexp) {
        final FilenameFilter filter = new FilenameFilter() {
            public boolean accept(final File dir, final String name) {
                return name.matches(regexp);
            }
        };
        return filter;
    }

    private void addScript(final File file, final Map<File, Script> addOnScriptMap) {
        final Script scriptConfig = addOnScriptMap.get(file);
        if (scriptConfig != null && !scriptConfig.active) {
            LogUtils.info("skipping deactivated " + scriptConfig);
            return;
        }
        final String menuTitle = disambiguateMenuTitle(getOrCreateMenuTitle(file, scriptConfig));
        try {
            menuTitleToPathMap.put(menuTitle, file.getAbsolutePath());
            final ScriptMetaData metaData = createMetaData(file, menuTitle, scriptConfig);
            menuTitleToMetaDataMap.put(menuTitle, metaData);
            final File parentFile = file.getParentFile();
            if (parentFile.equals(ScriptResources.getBuiltinScriptsDir())) {
                metaData.setPermissions(ScriptingPermissions.getPermissiveScriptingPermissions());
            }
        } catch (final IOException e) {
            LogUtils.warn("problems with script " + file.getAbsolutePath(), e);
            menuTitleToPathMap.remove(menuTitle);
            menuTitleToMetaDataMap.remove(menuTitle);
        }
    }

    private String disambiguateMenuTitle(final String menuTitleOrig) {
        String menuTitle = menuTitleOrig;
        // add suffix if the same script exists in multiple dirs
        for (int i = 2; menuTitleToPathMap.containsKey(menuTitle); ++i) {
            menuTitle = menuTitleOrig + i;
        }
        return menuTitle;
    }

    private ScriptMetaData createMetaData(final File file, final String scriptName, final Script scriptConfig)
            throws IOException {
        return scriptConfig == null ? analyseScriptContent(FileUtils.slurpFile(file), scriptName) //
                : createMetaData(scriptName, scriptConfig);
    }

    // not private to enable tests
    ScriptMetaData analyseScriptContent(final String content, final String scriptName) {
        final ScriptMetaData metaData = new ScriptMetaData(scriptName);
        if (ScriptingConfiguration.firstCharIsEquals(content)) {
            // would make no sense
            metaData.removeExecutionMode(ExecutionMode.ON_SINGLE_NODE);
        }
        setExecutionModes(content, metaData);
        setCacheMode(content, metaData);
        return metaData;
    }

    private ScriptMetaData createMetaData(final String scriptName, final Script scriptConfig) {
        final ScriptMetaData metaData = new ScriptMetaData(scriptName);
        metaData.removeAllExecutionModes();
        metaData.addExecutionMode(scriptConfig.executionMode, scriptConfig.menuLocation, scriptConfig.menuTitleKey);
        //      metaData.setCacheContent(true);
        metaData.setPermissions(scriptConfig.permissions);
        return metaData;
    }

    private void setCacheMode(final String content, final ScriptMetaData metaData) {
        final Pattern cacheScriptPattern = ScriptingConfiguration
                .makeCaseInsensitivePattern("@CacheScriptContent\\s*\\(\\s*(true|false)\\s*\\)");
        final Matcher matcher = cacheScriptPattern.matcher(content);
        if (matcher.find()) {
            metaData.setCacheContent(new Boolean(matcher.group(1)));
        }
    }

    public static void setExecutionModes(final String content, final ScriptMetaData metaData) {
        final String modeName = StringUtils.join(ExecutionMode.values(), "|");
        final String modeDef = "(?:ExecutionMode\\.)?(" + modeName + ")(?:=\"([^]\"]+)(?:\\[([^]\"]+)\\])?\")?";
        final String modeDefs = "(?:" + modeDef + ",?)+";
        final Pattern pOuter = makeCaseInsensitivePattern("@ExecutionModes\\(\\{(" + modeDefs + ")\\}\\)");
        final Matcher mOuter = pOuter.matcher(content.replaceAll("\\s+", ""));
        if (!mOuter.find()) {
            //         System.err.println(metaData.getScriptName() + ": '" + pOuter + "' did not match "
            //                 + content.replaceAll("\\s+", ""));
            return;
        }
        metaData.removeAllExecutionModes();
        final Pattern pattern = makeCaseInsensitivePattern(modeDef);
        final String[] locations = mOuter.group(1).split(",");
        for (String match : locations) {
            final Matcher m = pattern.matcher(match);
            if (m.matches()) {
                //            System.err.println(metaData.getScriptName() + ":" + m.group(1) + "->" + m.group(2) + "->" + m.group(3));
                metaData.addExecutionMode(ExecutionMode.valueOf(m.group(1).toUpperCase(Locale.ENGLISH)), m.group(2),
                        m.group(3));
            } else {
                LogUtils.severe("script " + metaData.getScriptName() + ": not a menu location: '" + match + "'");
                continue;
            }
        }
    }

    private static boolean firstCharIsEquals(final String content) {
        return content.length() == 0 ? false : content.charAt(0) == '=';
    }

    /** some beautification: remove directory and suffix + make first letter uppercase. */
    private String getOrCreateMenuTitle(final File file, Script scriptConfig) {
        if (scriptConfig != null)
            return scriptConfig.menuTitleKey;
        // TODO: we could add mnemonics handling here! (e.g. by reading '_' as '&')
        String string = file.getName().replaceFirst("\\.[^.]+", "");
        // fixup characters that might cause problems in menus
        string = string.replaceAll("\\s+", "_");
        return string.length() < 2 ? string : string.substring(0, 1).toUpperCase() + string.substring(1);
    }

    private static Pattern makeCaseInsensitivePattern(final String regexp) {
        return Pattern.compile(regexp, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
    }

    SortedMap<String, String> getMenuTitleToPathMap() {
        return Collections.unmodifiableSortedMap(menuTitleToPathMap);
    }

    SortedMap<String, ScriptMetaData> getMenuTitleToMetaDataMap() {
        return Collections.unmodifiableSortedMap(menuTitleToMetaDataMap);
    }

    private ArrayList<String> buildClasspath() {
        final ArrayList<String> classpath = new ArrayList<String>();
        addClasspathForAddOns(classpath);
        addClasspathForConfiguredEntries(classpath);
        return classpath;
    }

    private void addClasspathForAddOns(final ArrayList<String> classpath) {
        final List<ScriptAddOnProperties> installedScriptAddOns = getInstalledScriptAddOns();
        for (ScriptAddOnProperties scriptAddOnProperties : installedScriptAddOns) {
            final List<String> lib = scriptAddOnProperties.getLib();
            if (lib != null) {
                for (String libEntry : lib) {
                    final File dir = new File(getPrivateAddOnDirectory(scriptAddOnProperties), "lib");
                    classpath.add(new File(dir, libEntry).getAbsolutePath());
                }
            }
        }
    }

    private void addClasspathForConfiguredEntries(final ArrayList<String> classpath) {
        for (File classpathElement : uniqueClassPathElements(ResourceController.getResourceController())) {
            addClasspathElement(classpath, classpathElement);
        }
    }

    private Set<File> uniqueClassPathElements(final ResourceController resourceController) {
        final String classpathString = resourceController.getProperty(ScriptResources.RESOURCES_SCRIPT_CLASSPATH);
        final TreeSet<File> classpathElements = new TreeSet<File>();
        if (classpathString != null) {
            for (String string : ConfigurationUtils.decodeListValue(classpathString, false)) {
                classpathElements.add(createFile(string));
            }
        }
        classpathElements.add(ScriptResources.getUserLibDir());
        return classpathElements;
    }

    private void addClasspathElement(final ArrayList<String> classpath, File classpathElement) {
        final File file = classpathElement;
        if (!file.exists()) {
            LogUtils.warn("classpath entry '" + classpathElement + "' doesn't exist. (Use " + File.pathSeparator
                    + " to separate entries.)");
        } else if (file.isDirectory()) {
            classpath.add(file.getAbsolutePath());
            for (final File jar : file.listFiles(createFilenameFilter(JAR_REGEX))) {
                classpath.add(jar.getAbsolutePath());
            }
        } else {
            classpath.add(file.getAbsolutePath());
        }
    }

    List<String> getClasspath() {
        return ScriptResources.getClasspath();
    }

    public static Map<String, Object> getStaticProperties() {
        return staticProperties;
    }

    private static Map<String, Object> createStaticProperties() {
        Map<String, Object> properties = new LinkedHashMap<String, Object>();
        properties.put("logger", new LogUtils());
        properties.put("ui", new UITools());
        properties.put("htmlUtils", HtmlUtils.getInstance());
        properties.put("textUtils", new TextUtils());
        properties.put("menuUtils", new MenuUtils());
        properties.put("config", new FreeplaneScriptBaseClass.ConfigProperties());
        return properties;
    }

    static String getExecutionModeKey(final ExecuteScriptAction.ExecutionMode executionMode) {
        switch (executionMode) {
        case ON_SINGLE_NODE:
            return "ExecuteScriptOnSingleNode.text";
        case ON_SELECTED_NODE:
            return "ExecuteScriptOnSelectedNode.text";
        case ON_SELECTED_NODE_RECURSIVELY:
            return "ExecuteScriptOnSelectedNodeRecursively.text";
        default:
            throw new AssertionError("unknown ExecutionMode " + executionMode);
        }
    }

    public static String getScriptsLocation(String parentKey) {
        return parentKey + "/scripts";
    }
}