package org.freeplane.plugin.script; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.freeplane.core.util.HtmlUtils; import org.freeplane.core.util.TextUtils; import org.freeplane.features.map.MapModel; import org.freeplane.features.map.NodeModel; import org.freeplane.features.mode.Controller; import org.freeplane.plugin.script.proxy.FormulaCache; public class FormulaUtils { // don't let caching use too much memory - but currently there are little means to cope with unavailable // dependency data. It has to be tested but it should "only" lead to some missing updates. private static final boolean ENABLE_CACHING = !Controller.getCurrentController().getResourceController() .getBooleanProperty("formula_disable_caching"); private static final boolean DEBUG_FORMULA_EVALUATION = false; /** evaluate text as a script if it starts with '='. * @return the evaluation result for script and the original text otherwise * @throws ExecuteScriptException */ public static Object evalIfScript(final NodeModel nodeModel, ScriptContext scriptContext, final String text){ if (containsFormula(text)) { scriptContext = (scriptContext == null) ? new ScriptContext() : scriptContext; return eval(nodeModel, scriptContext, text.substring(1)); } else { return text; } } public static boolean containsFormula(final String text) { return text != null && text.length() > 1 && text.charAt(0) == '='; } public static boolean containsFormulaCheckHTML(String text) { if(HtmlUtils.isHtmlNode(text)) return htmlContainsFormula(text); else return containsFormula(text); } private static Pattern FIRST_CHARACTER_IN_HTML = Pattern.compile("(?m)>\\s*[^<\\s]"); private static boolean htmlContainsFormula(String text) { final Matcher matcher = FIRST_CHARACTER_IN_HTML.matcher(text); return matcher.find() && text.charAt(matcher.end()-1) == '='; } /** evaluate text as a script. * @return the evaluation result. * @throws ExecuteScriptException */ public static Object eval(final NodeModel nodeModel, final ScriptContext scriptContext, final String text) { if (DEBUG_FORMULA_EVALUATION) System.err.println("eval " + nodeModel.getID() + ": " + text); if (!scriptContext.push(nodeModel, text)) { throw new StackOverflowError(TextUtils.format("formula.error.circularReference", HtmlUtils.htmlToPlain(scriptContext.getStackFront().getText()))); } final ScriptingPermissions restrictedPermissions = ScriptingPermissions.getFormulaPermissions(); try { if (ENABLE_CACHING) { final FormulaCache formulaCache = getFormulaCache(nodeModel.getMap()); Object value = formulaCache.get(nodeModel, text); if (value == null) { try { value = ScriptingEngine.executeScript(nodeModel, text, scriptContext, restrictedPermissions); formulaCache.put(nodeModel, text, value); if (DEBUG_FORMULA_EVALUATION) System.err.println("eval: cache miss: recalculated: " + text); } catch (ExecuteScriptException e) { formulaCache.put(nodeModel, text, e); if (DEBUG_FORMULA_EVALUATION) System.err.println("eval: cache miss: exception for: " + text); throw e; } } else { if (DEBUG_FORMULA_EVALUATION) System.err.println("eval: cache hit for: " + text); scriptContext.accessNode(nodeModel); } return value; } else { return ScriptingEngine.executeScript(nodeModel, text, scriptContext, restrictedPermissions); } } finally { scriptContext.pop(); } } public static List<NodeModel> manageChangeAndReturnDependencies(boolean includeChanged, final NodeModel... nodes) { final ArrayList<NodeModel> dependencies = new ArrayList<NodeModel>(); for (int i = 0; i < nodes.length; i++) { final LinkedHashSet<NodeModel> nodeDependencies = new LinkedHashSet<NodeModel>(0); getEvaluationDependencies(nodes[i].getMap()).getDependencies(nodeDependencies, nodes[i]); if (nodeDependencies != null) dependencies.addAll(nodeDependencies); if (includeChanged) dependencies.add(nodes[i]); } if (ENABLE_CACHING) { for (NodeModel nodeModel : dependencies) { getFormulaCache(nodeModel.getMap()).markAsDirtyIfFormulaNode(nodeModel); } } return dependencies; } private static FormulaCache getFormulaCache(MapModel map) { FormulaCache formulaCache = (FormulaCache) map.getExtension(FormulaCache.class); if (formulaCache == null) { formulaCache = new FormulaCache(); map.addExtension(formulaCache); } return formulaCache; } private static EvaluationDependencies getEvaluationDependencies(MapModel map) { EvaluationDependencies dependencies = (EvaluationDependencies) map.getExtension(EvaluationDependencies.class); if (dependencies == null) { dependencies = new EvaluationDependencies(); map.addExtension(dependencies); } return dependencies; } public static void accessNode(NodeModel accessingNode, NodeModel accessedNode) { getEvaluationDependencies(accessingNode.getMap()).accessNode(accessingNode, accessedNode); } public static void accessBranch(NodeModel accessingNode, NodeModel accessedNode) { getEvaluationDependencies(accessingNode.getMap()).accessBranch(accessingNode, accessingNode); } public static void accessAll(NodeModel accessingNode) { getEvaluationDependencies(accessingNode.getMap()).accessAll(accessingNode); } public static void clearCache(MapModel map) { if (DEBUG_FORMULA_EVALUATION) System.out.println("clearing formula cache for " + map.getTitle()); map.removeExtension(FormulaCache.class); map.removeExtension(EvaluationDependencies.class); } }