/*
* 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.Arrays;
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 org.apache.commons.lang.StringUtils;
import org.freeplane.core.resources.ResourceController;
import org.freeplane.core.util.ConfigurationUtils;
import org.freeplane.core.util.FileUtils;
import org.freeplane.core.util.LogUtils;
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;
}
}
private static final String[] MENU_BAR_SCRIPTS_PARENT_LOCATIONS = {"main_menu_scripting", "node_popup_scripting"};
private static final String SCRIPT_REGEX = ".*\\.groovy$";
private static final String JAR_REGEX = ".*\\.jar$";
// or use property script_directories?
static final String USER_SCRIPTS_DIR = "scripts";
private final TreeMap<String, String> nameScriptMap = new TreeMap<String, String>();
private final TreeMap<String, ScriptMetaData> nameScriptMetaDataMap = new TreeMap<String, ScriptMetaData>();
private ArrayList<String> classpath;
private File builtinScriptsDir;
ScriptingConfiguration() {
addPluginDefaults();
initNameScriptMap();
initClasspath();
}
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 initNameScriptMap() {
final Map<File, Script> addOnScriptMap = getAddOnScriptMap();
for (String dir : getScriptDirs()) {
addScripts(createFile(dir), addOnScriptMap);
}
addScripts(getBuiltinScriptsDir(), addOnScriptMap);
}
public Map<File, ScriptAddOnProperties.Script> getAddOnScriptMap() {
List<AddOnProperties> installedAddOns = AddOnsController.getController().getInstalledAddOns();
Map<File, ScriptAddOnProperties.Script> result = new LinkedHashMap<File, ScriptAddOnProperties.Script>();
for (AddOnProperties addOnProperties : installedAddOns) {
if (addOnProperties.getAddOnType() == AddOnType.SCRIPT) {
final ScriptAddOnProperties scriptAddOnProperties = (ScriptAddOnProperties) addOnProperties;
final List<Script> scripts = scriptAddOnProperties.getScripts();
for (Script script : scripts) {
script.active = addOnProperties.isActive();
result.put(script.file, script);
}
}
}
return result;
}
private TreeSet<String> getScriptDirs() {
final ResourceController resourceController = ResourceController.getResourceController();
final String dirsString = resourceController.getProperty(ScriptingEngine.RESOURCES_SCRIPT_DIRECTORIES);
final TreeSet<String> dirs = new TreeSet<String>(); // remove duplicates -> Set
if (dirsString != null) {
dirs.addAll(ConfigurationUtils.decodeListValue(dirsString, false));
}
return dirs;
}
private File getBuiltinScriptsDir() {
if (builtinScriptsDir == null) {
final String installationBase = ResourceController.getResourceController().getInstallationBaseDir();
builtinScriptsDir = new File(installationBase, "scripts");
}
return builtinScriptsDir;
}
/**
* 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 addScripts(final File dir, final Map<File, Script> addOnScriptMap) {
if (dir.isDirectory()) {
for (final File file : Arrays.asList(dir.listFiles(createFilenameFilter(SCRIPT_REGEX)))) {
addScript(file, addOnScriptMap);
}
}
else {
LogUtils.warn("not a (script) directory: " + dir);
}
}
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 scriptName = getScriptName(file, scriptConfig);
String name = scriptName;
// add suffix if the same script exists in multiple dirs
for (int i = 2; nameScriptMap.containsKey(name); ++i) {
name = scriptName + i;
}
try {
nameScriptMap.put(name, file.getAbsolutePath());
final ScriptMetaData metaData = createMetaData(file, name, scriptConfig);
nameScriptMetaDataMap.put(name, metaData);
final File parentFile = file.getParentFile();
if (parentFile.equals(getBuiltinScriptsDir())) {
metaData.setPermissions(ScriptingPermissions.getPermissiveScriptingPermissions());
// metaData.setCacheContent(true);
}
}
catch (final IOException e) {
LogUtils.warn("problems with script " + file.getAbsolutePath(), e);
nameScriptMap.remove(name);
nameScriptMetaDataMap.remove(name);
}
}
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 getScriptName(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> getNameScriptMap() {
return Collections.unmodifiableSortedMap(nameScriptMap);
}
SortedMap<String, ScriptMetaData> getNameScriptMetaDataMap() {
return Collections.unmodifiableSortedMap(nameScriptMetaDataMap);
}
private void initClasspath() {
final ResourceController resourceController = ResourceController.getResourceController();
final String entries = resourceController.getProperty(ScriptingEngine.RESOURCES_SCRIPT_CLASSPATH);
classpath = new ArrayList<String>();
if (entries != null) {
for (String entry : ConfigurationUtils.decodeListValue(entries, false)) {
final File file = createFile(entry);
if (!file.exists()) {
LogUtils.warn("classpath entry '" + entry + "' 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());
}
}
}
}
ArrayList<String> getClasspath() {
return classpath;
}
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[] getScriptsParentLocations() {
return MENU_BAR_SCRIPTS_PARENT_LOCATIONS;
}
public static String getScriptsLocation(String parentKey) {
return parentKey + "/scripts";
}
}