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
This commit is contained in:
hns 2007-04-04 12:46:14 +00:00
parent 7f58c102bf
commit 0559d2d53e
6 changed files with 221 additions and 90 deletions

View file

@ -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();
}

View file

@ -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.
*

View file

@ -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();
}

View file

@ -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);
}
}
}

View file

@ -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.

View file

@ -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<st.countTokens()-1; i++) {
String propName = st.nextToken();
Object propValue = ScriptableObject.getProperty(obj, propName);
String[] path = StringUtils.split(funcName, ".");
for (int i = 0; i < path.length - 1; i++) {
Object propValue = ScriptableObject.getProperty(obj, path[i]);
if (propValue instanceof Scriptable) {
obj = (Scriptable) propValue;
} else {
@ -257,7 +257,7 @@ public class RhinoEngine implements ScriptingEngine {
funcName + " in " + thisObject);
}
}
funcName = st.nextToken();
funcName = path[path.length - 1];
}
} else {
funcName = funcName.replace('.', '_');
@ -357,9 +357,26 @@ public class RhinoEngine implements ScriptingEngine {
* Check if an object has a function property (public method if it
* is a java object) with that name.
*/
public boolean hasFunction(Object obj, String fname) {
// Convert '.' to '_' in function name
fname = fname.replace('.', '_');
public boolean hasFunction(Object obj, String fname, boolean resolve) {
if (resolve) {
if (fname.indexOf('.') > 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