From 0559d2d53e6240ea80409af593729388e1427545 Mon Sep 17 00:00:00 2001 From: hns Date: Wed, 4 Apr 2007 12:46:14 +0000 Subject: [PATCH] Implement new skin features: * Set namespace for global macros using app.globalMacroPath * Implement macro parameter processing using app.processMacroParameter() callback and $(...) parameter syntax * Implement unhandled macro handling using unhandledMacro() callback * Implement deep macro lookup using getMacroHandler() callback, and drop allowDeepMacros app property * Allow access to HopObject properties that aren't defined in type.properties --- src/helma/framework/core/Application.java | 23 +-- src/helma/framework/core/ApplicationBean.java | 36 ++++ .../framework/core/RequestEvaluator.java | 19 +- src/helma/framework/core/Skin.java | 190 +++++++++++++----- src/helma/scripting/ScriptingEngine.java | 3 +- src/helma/scripting/rhino/RhinoEngine.java | 40 +++- 6 files changed, 221 insertions(+), 90 deletions(-) diff --git a/src/helma/framework/core/Application.java b/src/helma/framework/core/Application.java index fe25e21f..c4d1e57a 100644 --- a/src/helma/framework/core/Application.java +++ b/src/helma/framework/core/Application.java @@ -173,7 +173,16 @@ public final class Application implements Runnable { // Field to cache unmapped java classes private final static String CLASS_NOT_MAPPED = "(unmapped)"; - protected boolean allowDeepMacros = false; + + /** + * Function object for macro processing callback + */ + Object processMacroParameter = null; + + /** + * Namespace search path for global macros + */ + String[] globalMacroPath = null; /** * Simple constructor for dead application instances. @@ -1641,15 +1650,7 @@ public final class Application implements Runnable { * for it in the class.properties file. */ public boolean isJavaPrototype(String typename) { - for (Enumeration en = classMapping.elements(); en.hasMoreElements();) { - String value = (String) en.nextElement(); - - if (typename.equals(value)) { - return true; - } - } - - return false; + return classMapping.contains(typename); } /** @@ -1873,8 +1874,6 @@ public final class Application implements Runnable { ((Logger) eventLog).setLogLevel(debug ? Logger.DEBUG : Logger.INFO); } - allowDeepMacros = "true".equalsIgnoreCase(props.getProperty("allowDeepMacros")); - // set prop read timestamp lastPropertyRead = props.lastModified(); } diff --git a/src/helma/framework/core/ApplicationBean.java b/src/helma/framework/core/ApplicationBean.java index 3cbcc8bc..5807af44 100644 --- a/src/helma/framework/core/ApplicationBean.java +++ b/src/helma/framework/core/ApplicationBean.java @@ -623,6 +623,42 @@ public class ApplicationBean implements Serializable { return app.getCharset(); } + /** + * Set the path for global macro resolution + * @param path an array of global namespaces, or null + */ + public void setGlobalMacroPath(String[] path) { + app.globalMacroPath = path; + } + + /** + * Get the path for global macro resolution + * @return an array of global namespaces, or null + */ + public String[] getGlobalMacroPath() { + return app.globalMacroPath; + } + + /** + * Set a function for processing macro parameters formatted as $(value). + * The function is expected to take the parameter value as string argument, + * and return the processed parameter + * @param obj a callback function + */ + public void setProcessMacroParameter(Object obj) { + app.processMacroParameter = obj; + } + + /** + * Get the function for processing macro parameters formatted as $(value). + * The function is expected to take the parameter value as string argument, + * and return the processed parameter + * @return the current macro processor callback function or null + */ + public Object getProcessMacroParameter() { + return app.processMacroParameter; + } + /** * Trigger a synchronous Helma invocation with a default timeout of 30 seconds. * diff --git a/src/helma/framework/core/RequestEvaluator.java b/src/helma/framework/core/RequestEvaluator.java index 1859c8e4..68a203b8 100644 --- a/src/helma/framework/core/RequestEvaluator.java +++ b/src/helma/framework/core/RequestEvaluator.java @@ -179,8 +179,7 @@ public final class RequestEvaluator implements Runnable { } } // If function doesn't exist, return immediately - if (functionName != null && functionName.indexOf('.') < 0 && - !scriptingEngine.hasFunction(thisObject, functionName)) { + if (functionName != null && !scriptingEngine.hasFunction(thisObject, functionName, true)) { app.logEvent(missingFunctionMessage(thisObject, functionName)); done = true; reqtype = NONE; @@ -448,7 +447,7 @@ public final class RequestEvaluator implements Runnable { // reset skin recursion detection counter skinDepth = 0; - if (!scriptingEngine.hasFunction(currentElement, functionName)) { + if (!scriptingEngine.hasFunction(currentElement, functionName, false)) { throw new FrameworkException(missingFunctionMessage(currentElement, functionName)); } result = scriptingEngine.invoke(currentElement, @@ -486,10 +485,6 @@ public final class RequestEvaluator implements Runnable { // reset skin recursion detection counter skinDepth = 0; - if (functionName != null && !scriptingEngine.hasFunction(thisObject, functionName)) { - throw new FrameworkException(missingFunctionMessage(thisObject, functionName)); - } - result = scriptingEngine.invoke(thisObject, function, args, @@ -868,7 +863,7 @@ public final class RequestEvaluator implements Runnable { public Object invokeDirectFunction(Object obj, Object function, Object[] args) throws Exception { return scriptingEngine.invoke(obj, function, args, - ScriptingEngine.ARGS_WRAP_DEFAULT, false); + ScriptingEngine.ARGS_WRAP_DEFAULT, true); } /** @@ -993,7 +988,7 @@ public final class RequestEvaluator implements Runnable { * @throws ScriptingException */ private Object getChildElement(Object obj, String name) throws ScriptingException { - if (scriptingEngine.hasFunction(obj, "getChildElement")) { + if (scriptingEngine.hasFunction(obj, "getChildElement", false)) { return scriptingEngine.invoke(obj, "getChildElement", new Object[] {name}, ScriptingEngine.ARGS_WRAP_DEFAULT, false); } @@ -1037,7 +1032,7 @@ public final class RequestEvaluator implements Runnable { if (req.checkXmlRpc()) { // append _methodname buffer.append("_xmlrpc"); - if (scriptingEngine.hasFunction(obj, buffer.toString())) { + if (scriptingEngine.hasFunction(obj, buffer.toString(), false)) { // handle as XML-RPC request req.setMethod(RequestTrans.XMLRPC); return buffer.toString(); @@ -1051,7 +1046,7 @@ public final class RequestEvaluator implements Runnable { if (method != null) { // append _methodname buffer.append('_').append(method.toLowerCase()); - if (scriptingEngine.hasFunction(obj, buffer.toString())) + if (scriptingEngine.hasFunction(obj, buffer.toString(), false)) return buffer.toString(); // cut off method in case it has been appended @@ -1062,7 +1057,7 @@ public final class RequestEvaluator implements Runnable { if (method == null || "GET".equalsIgnoreCase(method) || "POST".equalsIgnoreCase(method) || "HEAD".equalsIgnoreCase(method)) { - if (scriptingEngine.hasFunction(obj, buffer.toString())) + if (scriptingEngine.hasFunction(obj, buffer.toString(), false)) return buffer.toString(); } diff --git a/src/helma/framework/core/Skin.java b/src/helma/framework/core/Skin.java index 00c2d73c..4f935842 100644 --- a/src/helma/framework/core/Skin.java +++ b/src/helma/framework/core/Skin.java @@ -319,10 +319,12 @@ public final class Skin { sandbox.add(macroname); } - private Object invokeMacro(Object value, RenderContext cx) + private Object processParameter(Object value, RenderContext cx) throws Exception { if (value instanceof Macro) { return ((Macro) value).invokeAsMacro(cx, null); + } else if (value instanceof ProcessedParameter) { + return ((ProcessedParameter) value).process(cx.reval); } else { return value; } @@ -376,7 +378,6 @@ public final class Skin { if (state == PARSE_PARAM && quotechar == '\u0000' && b.length() == 0 && source[i + 1] == '%') { Macro macro = new Macro(i, 2); - hasNestedMacros = true; addParameter(lastParamName, macro); lastParamName = null; b.setLength(0); @@ -455,7 +456,7 @@ public final class Skin { if (!escape && state == PARSE_PARAM) { if (quotechar == source[i]) { // add parameter - addParameter(lastParamName, b.toString()); + addParameter(lastParamName, parseParameter(b.toString())); lastParamName = null; b.setLength(0); quotechar = '\u0000'; @@ -487,7 +488,7 @@ public final class Skin { if (quotechar == '\u0000') { if (b.length() > 0) { // add parameter - addParameter(lastParamName, b.toString()); + addParameter(lastParamName, parseParameter(b.toString())); lastParamName = null; b.setLength(0); } @@ -533,7 +534,7 @@ public final class Skin { if (name == null) { name = b.toString().trim(); } else { - addParameter(lastParamName, b.toString()); + addParameter(lastParamName, parseParameter(b.toString())); } } @@ -560,7 +561,21 @@ public final class Skin { } } + private Object parseParameter(String str) { + int length = str.length(); + if (length > 3 && str.charAt(0) == '$') { + if (str.charAt(1) == '[' && str.charAt(length - 1) == ']') + return new ProcessedParameter(str.substring(2, str.length()-1), 0); + else if (str.charAt(1) == '(' && str.charAt(length - 1) == ')') + return new ProcessedParameter(str.substring(2, str.length()-1), 1); + } + return str; + } + private void addParameter(String name, Object value) { + if (!(value instanceof String)) { + hasNestedMacros = true; + } if (name == null) { // take shortcut for positional parameters if (positionalParams == null) { @@ -613,31 +628,32 @@ public final class Skin { throw new RuntimeException("Macro " + name + " not allowed in sandbox"); } - Object handlerObject = null; + Object handler = null; Object value = null; ScriptingEngine engine = cx.reval.scriptingEngine; if (handlerType != HANDLER_GLOBAL) { - handlerObject = cx.resolveHandler(path[0], handlerType); - handlerObject = resolvePath(handlerObject, engine); + handler = cx.resolveHandler(path[0], handlerType); + handler = resolvePath(handler, cx.reval); } - if (handlerType == HANDLER_GLOBAL || handlerObject != null) { + if (handlerType == HANDLER_GLOBAL || handler != null) { // check if a function called name_macro is defined. // if so, the macro evaluates to the function. Otherwise, // a property/field with the name is used, if defined. String propName = path[path.length - 1]; - String funcName = propName + "_macro"; + String funcName = resolveFunctionName(handler, propName + "_macro", engine); - if (engine.hasFunction(handlerObject, funcName)) { - StringBuffer buffer = cx.reval.getResponse().getBuffer(); - // remember length of response buffer before calling macro - int bufLength = buffer.length(); + // remember length of response buffer before calling macro + StringBuffer buffer = cx.reval.getResponse().getBuffer(); + int bufLength = buffer.length(); + + if (funcName != null) { Object[] arguments = prepareArguments(0, cx); // get reference to rendered named params for after invocation Map params = (Map) arguments[0]; - value = cx.reval.invokeDirectFunction(handlerObject, + value = cx.reval.invokeDirectFunction(handler, funcName, arguments); @@ -662,16 +678,28 @@ public final class Skin { if (value != null) return filter(value, cx); } - // display error message unless silent failmode is on - if (!engine.hasProperty(handlerObject, propName) - && standardParams.verboseFailmode(handlerObject, engine)) { - throw new MacroUnhandledException(name); + // display error message unless unhandledMacro is defined or silent failmode is on + if (!engine.hasProperty(handler, propName)) { + if (engine.hasFunction(handler, "unhandledMacro", false)) { + Object[] arguments = prepareArguments(1, cx); + arguments[0] = propName; + value = cx.reval.invokeDirectFunction(handler, "unhandledMacro", arguments); + // if macro has a filter chain and didn't return anything, use output + // as filter argument. + if (filterChain != null && value == null && buffer.length() > bufLength) { + value = buffer.substring(bufLength); + buffer.setLength(bufLength); + } + } else if (standardParams.verboseFailmode(handler, engine)) { + throw new UnhandledMacroException(name); + } + } else { + value = engine.getProperty(handler, propName); } - value = engine.getProperty(handlerObject, propName); return filter(value, cx); } - } else if (standardParams.verboseFailmode(handlerObject, engine)) { - throw new MacroUnhandledException(name); + } else if (standardParams.verboseFailmode(handler, engine)) { + throw new UnhandledMacroException(name); } return filter(null, cx); } @@ -723,8 +751,8 @@ public final class Skin { throw concur; } catch (TimeoutException timeout) { throw timeout; - } catch (MacroUnhandledException unhandled) { - String msg = "Macro unhandled: " + unhandled.getMessage(); + } catch (UnhandledMacroException unhandled) { + String msg = "Unhandled Macro: " + unhandled.getMessage(); cx.reval.getResponse().write(" [" + msg + "] "); app.logError(msg); } catch (Exception x) { @@ -761,12 +789,14 @@ public final class Skin { if (handlerType != HANDLER_GLOBAL) { handlerObject = cx.resolveHandler(path[0], handlerType); - handlerObject = resolvePath(handlerObject, cx.reval.scriptingEngine); + handlerObject = resolvePath(handlerObject, cx.reval); } - String funcName = path[path.length - 1] + "_filter"; + String propName = path[path.length - 1] + "_filter"; + String funcName = resolveFunctionName(handlerObject, propName, + cx.reval.scriptingEngine); - if (cx.reval.scriptingEngine.hasFunction(handlerObject, funcName)) { + if (funcName != null) { Object[] arguments = prepareArguments(1, cx); arguments[0] = returnValue; Object retval = cx.reval.invokeDirectFunction(handlerObject, @@ -791,7 +821,9 @@ public final class Skin { for (Iterator it = namedParams.entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = (Map.Entry) it.next(); Object value = entry.getValue(); - map.put(entry.getKey(), invokeMacro(value, cx)); + if (!(value instanceof String)) + value = processParameter(value, cx); + map.put(entry.getKey(), value); } arguments[offset] = map; } else { @@ -800,30 +832,54 @@ public final class Skin { } if (positionalParams != null) { for (int i = 0; i < nPosArgs; i++) { - Object param = positionalParams.get(i); - if (param instanceof Macro) - arguments[offset + 1 + i] = invokeMacro(param, cx); - else - arguments[offset + 1 + i] = param; + Object value = positionalParams.get(i); + if (!(value instanceof String)) + value = processParameter(value, cx); + arguments[offset + 1 + i] = value; } } return arguments; } - private Object resolvePath(Object handler, ScriptingEngine engine) { - if (path.length > 2 && !app.allowDeepMacros) { - throw new RuntimeException("allowDeepMacros property must be true " + - "in order to enable deep macro paths."); - } + private Object resolvePath(Object handler, RequestEvaluator reval) throws Exception { for (int i = 1; i < path.length - 1; i++) { - handler = engine.getProperty(handler, path[i]); - if (handler == null) { - break; + Object[] arguments = {path[i]}; + Object next = reval.invokeDirectFunction(handler, "getMacroHandler", arguments); + if (next != null) { + handler = next; + } else if (!reval.scriptingEngine.isTypedObject(handler)) { + handler = reval.scriptingEngine.getProperty(handler, path[i]); + if (handler == null) { + break; + } } } return handler; } + private String resolveFunctionName(Object handler, String functionName, + ScriptingEngine engine) { + if (handlerType == HANDLER_GLOBAL) { + String[] macroPath = app.globalMacroPath; + if (macroPath == null || macroPath.length == 0) { + if (engine.hasFunction(null, functionName, false)) + return functionName; + } else { + for (int i = 0; i < macroPath.length; i++) { + String path = macroPath[i]; + String funcName = path == null || path.length() == 0 ? + functionName : path + "." + functionName; + if (engine.hasFunction(null, funcName, true)) + return funcName; + } + } + } else { + if (engine.hasFunction(handler, functionName, false)) + return functionName; + } + return null; + } + /** * Utility method for writing text out to the response object. */ @@ -920,9 +976,9 @@ public final class Skin { } boolean containsMacros() { - return prefix instanceof Macro - || suffix instanceof Macro - || defaultValue instanceof Macro; + return !(prefix instanceof String) + || !(suffix instanceof String) + || !(defaultValue instanceof String); } void setFailMode(Object value) { @@ -946,9 +1002,9 @@ public final class Skin { if (!containsMacros()) return this; StandardParams stdParams = new StandardParams(); - stdParams.prefix = invokeMacro(prefix, cx); - stdParams.suffix = invokeMacro(suffix, cx); - stdParams.defaultValue = invokeMacro(defaultValue, cx); + stdParams.prefix = processParameter(prefix, cx); + stdParams.suffix = processParameter(suffix, cx); + stdParams.defaultValue = processParameter(defaultValue, cx); return stdParams; } @@ -1019,13 +1075,39 @@ public final class Skin { return obj; } } -} -/** - * Exception type for unhandled macros - */ -class MacroUnhandledException extends Exception { - MacroUnhandledException(String name) { - super(name); + /** + * Processed macro parameter + */ + class ProcessedParameter { + String value; + int type; + + ProcessedParameter(String value, int type) { + this.value = value; + this.type = type; + } + + Object process(RequestEvaluator reval) throws Exception { + switch (type) { + case 1: + Object function = app.processMacroParameter; + Object[] args = {value}; + return reval.invokeDirectFunction(null, function, args); + case 0: + default: + return reval.getResponse().getMacroHandlers().get(value); + } + } + } + + /** + * Exception type for unhandled macros + */ + class UnhandledMacroException extends Exception { + UnhandledMacroException(String name) { + super(name); + } } } + diff --git a/src/helma/scripting/ScriptingEngine.java b/src/helma/scripting/ScriptingEngine.java index 6eb56e88..6ea884fb 100644 --- a/src/helma/scripting/ScriptingEngine.java +++ b/src/helma/scripting/ScriptingEngine.java @@ -119,9 +119,10 @@ public interface ScriptingEngine { * Return true if a function by that name is defined for that object. * @param thisObject the object * @param functionName the function name + * @param resolve if member path in function name should be resolved * @return true if the function is defined on the object */ - public boolean hasFunction(Object thisObject, String functionName); + public boolean hasFunction(Object thisObject, String functionName, boolean resolve); /** * Return true if a property by that name is defined for that object. diff --git a/src/helma/scripting/rhino/RhinoEngine.java b/src/helma/scripting/rhino/RhinoEngine.java index cc6be143..6d864713 100644 --- a/src/helma/scripting/rhino/RhinoEngine.java +++ b/src/helma/scripting/rhino/RhinoEngine.java @@ -28,6 +28,7 @@ import helma.objectmodel.db.DbMapping; import helma.objectmodel.db.Relation; import helma.scripting.*; import helma.scripting.rhino.debug.Tracer; +import helma.util.StringUtils; import org.mozilla.javascript.*; import org.mozilla.javascript.serialize.ScriptableOutputStream; import org.mozilla.javascript.serialize.ScriptableInputStream; @@ -246,10 +247,9 @@ public class RhinoEngine implements ScriptingEngine { // otherwise replace dots with underscores. if (resolve) { if (funcName.indexOf('.') > 0) { - StringTokenizer st = new StringTokenizer(funcName, "."); - for (int i=0; i 0) { + Scriptable op = obj == null ? global : Context.toObject(obj, global); + String[] path = StringUtils.split(fname, "."); + for (int i = 0; i < path.length; i++) { + Object value = ScriptableObject.getProperty(op, path[i]); + if (value instanceof Scriptable) { + op = (Scriptable) value; + } else { + return false; + } + } + return (op instanceof Function); + } + } else { + // Convert '.' to '_' in function name + fname = fname.replace('.', '_'); + } + // Treat HopObjects separately - otherwise we risk to fetch database // references/child objects just to check for function properties. if (obj instanceof INode) { @@ -395,7 +412,8 @@ public class RhinoEngine implements ScriptingEngine { DbMapping dbm = app.getDbMapping(prototypeName); if (dbm != null) { Relation rel = dbm.propertyToRelation(propname); - return rel != null && (rel.isPrimitive() || rel.isCollection()); + if (rel != null && (rel.isPrimitive() || rel.isCollection())) + return true; } } Scriptable wrapped = Context.toObject(obj, global); @@ -448,8 +466,8 @@ public class RhinoEngine implements ScriptingEngine { public boolean isTypedObject(Object obj) { if (obj == null || obj instanceof Map || obj instanceof NativeObject) return false; - if (obj instanceof INode) { - String protoName = app.getPrototypeName(obj); + if (obj instanceof IPathElement) { + String protoName = ((IPathElement) obj).getPrototype(); return protoName != null && !"hopobject".equalsIgnoreCase(protoName); } // assume java object is typed