/*
* Freeplane - mind map editor
* Copyright (C) 2008 Joerg Mueller, Daniel Polansky, Christian Foltin, Dimitry Polivaev
*
* This file author is Christian Foltin
* It is modified by Dimitry Polivaev in 2008.
*
* 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.awt.Dimension;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.PrintStream;
import java.net.URL;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Properties;
import javax.swing.ComboBoxEditor;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import org.apache.commons.lang.StringUtils;
import org.freeplane.core.resources.ResourceController;
import org.freeplane.core.resources.components.IValidator;
import org.freeplane.core.ui.IMenuContributor;
import org.freeplane.core.ui.IUserInputListenerFactory;
import org.freeplane.core.ui.MenuBuilder;
import org.freeplane.core.util.FileUtils;
import org.freeplane.core.util.LogUtils;
import org.freeplane.core.util.TextUtils;
import org.freeplane.features.filter.FilterController;
import org.freeplane.features.map.NodeModel;
import org.freeplane.features.mode.Controller;
import org.freeplane.features.mode.ModeController;
import org.freeplane.features.mode.mindmapmode.MModeController;
import org.freeplane.features.script.IScriptEditorStarter;
import org.freeplane.features.script.IScriptStarter;
import org.freeplane.main.addons.AddOnInstaller;
import org.freeplane.main.addons.AddOnsController;
import org.freeplane.n3.nanoxml.IXMLParser;
import org.freeplane.n3.nanoxml.IXMLReader;
import org.freeplane.n3.nanoxml.StdXMLReader;
import org.freeplane.n3.nanoxml.XMLElement;
import org.freeplane.n3.nanoxml.XMLParserFactory;
import org.freeplane.plugin.script.ExecuteScriptAction.ExecutionMode;
import org.freeplane.plugin.script.ScriptEditorPanel.IScriptModel;
import org.freeplane.plugin.script.ScriptEditorPanel.ScriptHolder;
import org.freeplane.plugin.script.ScriptingConfiguration.ScriptMetaData;
import org.freeplane.plugin.script.ScriptingEngine.IErrorHandler;
import org.freeplane.plugin.script.addons.ManageAddOnsAction;
import org.freeplane.plugin.script.addons.ManageAddOnsDialog;
import org.freeplane.plugin.script.addons.ScriptAddOnProperties;
import org.freeplane.plugin.script.filter.ScriptConditionController;
class ScriptingRegistration {
final private class ScriptModel implements IScriptModel {
final private String mOriginalScript;
private String mScript;
public ScriptModel(final String pScript) {
mScript = pScript;
mOriginalScript = pScript;
}
public int addNewScript() {
return 0;
}
public ScriptEditorWindowConfigurationStorage decorateDialog(final ScriptEditorPanel pPanel,
final String pWindow_preference_storage_property) {
final String marshalled = ResourceController.getResourceController().getProperty(
pWindow_preference_storage_property);
return ScriptEditorWindowConfigurationStorage.decorateDialog(marshalled, pPanel);
}
public void endDialog(final boolean pIsCanceled) {
if (pIsCanceled) {
mScript = mOriginalScript;
}
}
public Object executeScript(final int pIndex, final PrintStream pOutStream, final IErrorHandler pErrorHandler) {
final ModeController modeController = Controller.getCurrentModeController();
// the script is completely in the hand of the user -> no security issues.
final ScriptingPermissions restrictedPermissions = ScriptingPermissions.getPermissiveScriptingPermissions();
return ScriptingEngine.executeScript(modeController.getMapController().getSelectedNode(), mScript,
pErrorHandler, pOutStream, null, restrictedPermissions);
}
public int getAmountOfScripts() {
return 1;
}
public String getScript() {
return mScript;
}
public ScriptHolder getScript(final int pIndex) {
return new ScriptHolder("Script", mScript);
}
public boolean isDirty() {
return !StringUtils.equals(mScript, mOriginalScript);
}
public void setScript(final int pIndex, final ScriptHolder pScript) {
mScript = pScript.getScript();
}
public void storeDialogPositions(final ScriptEditorPanel pPanel,
final ScriptEditorWindowConfigurationStorage pStorage,
final String pWindow_preference_storage_property) {
pStorage.storeDialogPositions(pPanel, pWindow_preference_storage_property);
}
}
final private HashMap<String, Object> mScriptCookies = new HashMap<String, Object>();
public ScriptingRegistration(ModeController modeController) {
register(modeController);
}
private void addPropertiesToOptionPanel() {
final URL preferences = this.getClass().getResource("preferences.xml");
if (preferences == null)
throw new RuntimeException("cannot open preferences");
Controller.getCurrentController().addOptionValidator(new IValidator() {
public ValidationResult validate(Properties properties) {
final ValidationResult result = new ValidationResult();
final String readAccessString = properties
.getProperty(ScriptingPermissions.RESOURCES_EXECUTE_SCRIPTS_WITHOUT_READ_RESTRICTION);
final String writeAccessString = properties
.getProperty(ScriptingPermissions.RESOURCES_EXECUTE_SCRIPTS_WITHOUT_WRITE_RESTRICTION);
final String classpath = properties.getProperty(ScriptingEngine.RESOURCES_SCRIPT_CLASSPATH);
final boolean readAccess = readAccessString != null && Boolean.parseBoolean(readAccessString);
final boolean writeAccess = writeAccessString != null && Boolean.parseBoolean(writeAccessString);
final boolean classpathIsSet = classpath != null && classpath.length() > 0;
if (classpathIsSet && !readAccess) {
result.addError(TextUtils.getText("OptionPanel.validate_classpath_needs_readaccess"));
}
if (writeAccess && !readAccess) {
result.addWarning(TextUtils.getText("OptionPanel.validate_write_without_read"));
}
return result;
}
});
final MModeController modeController = (MModeController) Controller.getCurrentModeController();
modeController.getOptionPanelBuilder().load(preferences);
}
public HashMap<String, Object> getScriptCookies() {
return mScriptCookies;
}
private void register(ModeController modeController) {
modeController.addExtension(IScriptEditorStarter.class, new IScriptEditorStarter() {
public String startEditor(final String pScriptInput) {
final ScriptModel scriptModel = new ScriptModel(pScriptInput);
final ScriptEditorPanel scriptEditorPanel = new ScriptEditorPanel(scriptModel, false);
scriptEditorPanel.setVisible(true);
return scriptModel.getScript();
}
public ComboBoxEditor createComboBoxEditor(Dimension minimumSize) {
final ScriptComboBoxEditor scriptComboBoxEditor = new ScriptComboBoxEditor();
if(minimumSize != null)
scriptComboBoxEditor.setMinimumSize(minimumSize);
return scriptComboBoxEditor;
}
});
modeController.addExtension(IScriptStarter.class, new IScriptStarter() {
public void executeScript(NodeModel node, String script) {
ScriptingEngine.executeScript(node, script);
}
});
registerScriptAddOns();
if(! modeController.getController().getViewController().isHeadless()){
final IUserInputListenerFactory userInputListenerFactory = modeController.getUserInputListenerFactory();
addPropertiesToOptionPanel();
final MenuBuilder menuBuilder = userInputListenerFactory.getMenuBuilder();
modeController.addAction(new ScriptEditor());
modeController.addAction(new ExecuteScriptForAllNodes());
modeController.addAction(new ExecuteScriptForSelectionAction());
final ManageAddOnsAction manageAddOnsAction = new ManageAddOnsAction();
modeController.addAction(manageAddOnsAction);
modeController.addExtension(AddOnInstaller.class, new AddOnInstaller() {
public void install(final URL url) {
final ManageAddOnsDialog dialog = manageAddOnsAction.getDialog();
//FIXME: method does not exist --> boercher
//dialog.install(url);
}
});
final ScriptingConfiguration configuration = new ScriptingConfiguration();
ScriptingEngine.setClasspath(configuration.getClasspath());
ScriptCompiler.compileScriptsOnPath(configuration.getClasspath());
modeController.addMenuContributor(new IMenuContributor() {
public void updateMenus(ModeController modeController, MenuBuilder builder) {
registerScripts(menuBuilder, configuration);
}
});
createUserScriptsDirectory();
}
FilterController.getCurrentFilterController().getConditionFactory().addConditionController(10,
new ScriptConditionController());
}
private void registerScriptAddOns() {
File[] addonXmlFiles = AddOnsController.getController().getAddOnsDir().listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".script.xml");
}
});
final IXMLParser parser = XMLParserFactory.createDefaultXMLParser();
for (File file : addonXmlFiles) {
BufferedInputStream inputStream = null;
try {
inputStream = new BufferedInputStream(new FileInputStream(file));
final IXMLReader reader = new StdXMLReader(inputStream);
parser.setReader(reader);
final ScriptAddOnProperties addOn = new ScriptAddOnProperties((XMLElement) parser.parse());
addOn.setAddOnPropertiesFile(file);
AddOnsController.getController().registerInstalledAddOn(addOn);
}
catch (final Exception e) {
LogUtils.warn("error parsing " + file, e);
}
finally {
FileUtils.silentlyClose(inputStream);
}
}
}
private void createUserScriptsDirectory() {
final File scriptDir = ScriptingEngine.getUserScriptDir();
if (!scriptDir.exists()) {
LogUtils.info("creating user scripts directory " + scriptDir);
scriptDir.mkdirs();
}
}
private void registerScripts(final MenuBuilder menuBuilder, ScriptingConfiguration configuration) {
final HashSet<String> registeredLocations = new HashSet<String>();
for (final String scriptsParentLocation : ScriptingConfiguration.getScriptsParentLocations()) {
final String scriptsLocation = ScriptingConfiguration.getScriptsLocation(scriptsParentLocation);
addSubMenu(menuBuilder, scriptsParentLocation, scriptsLocation, TextUtils.getText("ExecuteScripts.text"));
registeredLocations.add(scriptsLocation);
if (configuration.getNameScriptMap().isEmpty()) {
final String message = "<html><body><em>" + TextUtils.getText("ExecuteScripts.noScriptsAvailable")
+ "</em></body></html>";
menuBuilder.addElement(scriptsLocation, new JMenuItem(message), 0);
}
for (final Entry<String, String> entry : configuration.getNameScriptMap().entrySet()) {
final String scriptName = entry.getKey();
final ScriptMetaData metaData = configuration.getNameScriptMetaDataMap().get(scriptName);
// in the worst case three actions will cache a script - should not matter that much since it's unlikely
// that one script is used in multiple modes by the same user
for (final ExecutionMode executionMode : metaData.getExecutionModes()) {
final String titleKey;
final String scriptLocation;
String location = metaData.getMenuLocation(executionMode);
// FIXME: reduce code duplication (VB)
if (location == null) {
location = scriptsLocation + "/" + scriptName;
if (!registeredLocations.contains(location)) {
final String parentMenuTitle = pimpMenuTitle(metaData.getScriptName());
addSubMenu(menuBuilder, parentLocation(location), location, parentMenuTitle);
registeredLocations.add(location);
}
titleKey = metaData.getTitleKey(executionMode);
scriptLocation = location + "/" + titleKey;
}
else {
if (!registeredLocations.contains(location)) {
addSubMenu(menuBuilder, parentLocation(location), location, getMenuTitle(location));
registeredLocations.add(location);
}
titleKey = metaData.getTitleKey(executionMode);
scriptLocation = location + "/" + titleKey;
}
if (!registeredLocations.contains(scriptLocation)) {
addMenuItem(menuBuilder, location, entry, executionMode, titleKey, metaData);
registeredLocations.add(scriptLocation);
}
}
}
}
}
// location might be something like /menu_bar/edit/editGoodies
private String getMenuTitle(final String location) {
int index = location.lastIndexOf('/');
final String lastKey = location.substring(index + 1);
return TextUtils.getText(lastKey, TextUtils.getText("addons." + lastKey, lastKey));
}
private String parentLocation(String location) {
return location.replaceFirst("/[^/]*$", "");
}
private void addSubMenu(final MenuBuilder menuBuilder, final String parentLocation, final String location,
String menuTitle) {
if (menuBuilder.get(location) == null) {
final JMenu menuItem = new JMenu();
MenuBuilder.setLabelAndMnemonic(menuItem, menuTitle);
menuBuilder.addMenuItem(parentLocation, menuItem, location, MenuBuilder.AS_CHILD);
}
}
private void addMenuItem(final MenuBuilder menuBuilder, final String location, final Entry<String, String> entry,
final ExecutionMode executionMode, final String titleKey, ScriptMetaData metaData) {
final String scriptName = entry.getKey();
final String translation = TextUtils.getText(titleKey, titleKey.replace('_', ' '));
final String menuName = translation.contains("{0}") ? MessageFormat.format(translation,
pimpMenuTitle(scriptName)) : translation;
menuBuilder.addAction(location, new ExecuteScriptAction(scriptName, menuName, entry.getValue(), executionMode,
metaData.cacheContent(), metaData.getPermissions()), MenuBuilder.AS_CHILD);
}
/** menuTitle may either be a scriptName or a translation key. */
private String pimpMenuTitle(final String menuTitle) {
final String translation = TextUtils.getText(menuTitle, null);
// convert CamelCase to Camel Case
return translation != null ? translation : menuTitle.replaceAll("([a-z])([A-Z])", "$1 $2");
}
}