Java tutorial
/* * 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"; } }