/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2003 Helma Software. All Rights Reserved.
*
* $RCSfile$
* $Author: hannes $
* $Revision: 9983 $
* $Date: 2009-11-04 12:31:10 +0100 (Mit, 04. Nov 2009) $
*/
package helma.framework.core;
import helma.framework.*;
import helma.framework.repository.Resource;
import helma.objectmodel.ConcurrencyException;
import helma.util.*;
import helma.scripting.ScriptingEngine;
import java.util.*;
import java.io.UnsupportedEncodingException;
import java.io.Reader;
import java.io.InputStreamReader;
import java.io.IOException;
/**
* This represents a Helma skin, i.e. a template created from containing Macro tags
* that will be dynamically evaluated.. It uses the request path array
* from the RequestEvaluator object to resolve Macro handlers by type name.
*/
public final class Skin {
private Macro[] macros;
private Application app;
private char[] source;
private int offset, length; // start and end index of skin content
private HashSet sandbox;
private HashMap subskins;
private Skin parentSkin = this;
private String extendz = null;
private boolean hasContent = false;
static private final int PARSE_MACRONAME = 0;
static private final int PARSE_PARAM = 1;
static private final int PARSE_DONE = 2;
static private final int ENCODE_NONE = 0;
static private final int ENCODE_HTML = 1;
static private final int ENCODE_XML = 2;
static private final int ENCODE_FORM = 3;
static private final int ENCODE_URL = 4;
static private final int ENCODE_ALL = 5;
static private final int HANDLER_RESPONSE = 0;
static private final int HANDLER_REQUEST = 1;
static private final int HANDLER_SESSION = 2;
static private final int HANDLER_PARAM = 3;
static private final int HANDLER_GLOBAL = 4;
static private final int HANDLER_THIS = 5;
static private final int HANDLER_OTHER = 6;
static private final int FAIL_DEFAULT = 0;
static private final int FAIL_SILENT = 1;
static private final int FAIL_VERBOSE = 2;
/**
* Create a skin without any restrictions on which macros are allowed to be called from it
*/
public Skin(String content, Application app) {
this.app = app;
this.sandbox = null;
this.source = content.toCharArray();
this.offset = 0;
this.length = source.length;
parse();
}
/**
* Create a skin with a sandbox which contains the names of macros allowed to be called
*/
public Skin(String content, Application app, HashSet sandbox) {
this.app = app;
this.sandbox = sandbox;
this.source = content.toCharArray();
this.offset = 0;
length = source.length;
parse();
}
/**
* Create a skin without any restrictions on the macros from a char array.
*/
public Skin(char[] content, int length, Application app) {
this.app = app;
this.sandbox = null;
this.source = content;
this.offset = 0;
this.length = length;
parse();
}
/**
* Subskin constructor.
*/
private Skin(Skin parentSkin, Macro anchorMacro) {
this.parentSkin = parentSkin;
this.app = parentSkin.app;
this.sandbox = parentSkin.sandbox;
this.source = parentSkin.source;
this.offset = anchorMacro.end;
this.length = parentSkin.length;
parentSkin.addSubskin(anchorMacro.name, this);
parse();
}
public static Skin getSkin(Resource res, Application app) throws IOException {
String encoding = app.getProperty("skinCharset");
Reader reader;
if (encoding == null) {
reader = new InputStreamReader(res.getInputStream());
} else {
reader = new InputStreamReader(res.getInputStream(), encoding);
}
int length = (int) res.getLength();
char[] characterBuffer = new char[length];
int read = 0;
try {
while (read < length) {
int r = reader.read(characterBuffer, read, length - read);
if (r == -1)
break;
read += r;
}
} finally {
reader.close();
}
return new Skin(characterBuffer, read, app);
}
/**
* Parse a skin object from source text
*/
private void parse() {
ArrayList partBuffer = new ArrayList();
boolean escape = false;
for (int i = offset; i < (length - 1); i++) {
if (source[i] == '<' && source[i + 1] == '%' && !escape) {
// found macro start tag
Macro macro = new Macro(i, 2);
if (macro.isSubskinMacro) {
new Skin(parentSkin, macro);
length = i;
break;
} else {
if (!macro.isCommentMacro) {
hasContent = true;
}
partBuffer.add(macro);
}
i = macro.end - 1;
} else {
if (!hasContent && !Character.isWhitespace(source[i])){
hasContent = true;
}
escape = source[i] == '\\' && !escape;
}
}
macros = new Macro[partBuffer.size()];
partBuffer.toArray(macros);
}
private void addSubskin(String name, Skin subskin) {
if (subskins == null) {
subskins = new HashMap();
}
subskins.put(name, subskin);
}
/**
* Return the list of macros found by the parser
* @return the list of macros
*/
public Macro[] getMacros() {
return macros;
}
/**
* Check if this skin has a main skin, as opposed to consisting just of subskins
* @return true if this skin contains a main skin
*/
public boolean hasMainskin() {
return hasContent;
}
/**
* Check if this skin contains a subskin with the given name
* @param name a subskin name
* @return true if the given subskin exists
*/
public boolean hasSubskin(String name) {
return subskins != null && subskins.containsKey(name);
}
/**
* Get a subskin by name
* @param name the subskin name
* @return the subskin
*/
public Skin getSubskin(String name) {
return subskins == null ? null : (Skin) subskins.get(name);
}
/**
* Return an array of subskin names defined in this skin
* @return a string array containing this skin's substrings
*/
public String[] getSubskinNames() {
return subskins == null ?
new String[0] :
(String[]) subskins.keySet().toArray(new String[0]);
}
public String getExtends() {
return extendz;
}
/**
* Get the raw source text this skin was parsed from
*/
public String getSource() {
return new String(source, offset, length - offset);
}
/**
* Render this skin and return it as string
*/
public String renderAsString(RequestEvaluator reval, Object thisObject, Object paramObject)
throws RedirectException, UnsupportedEncodingException {
String result = "";
ResponseTrans res = reval.getResponse();
res.pushBuffer(null);
try {
render(reval, thisObject, paramObject);
} finally {
result = res.popString();
}
return result;
}
/**
* Render this skin
*/
public void render(RequestEvaluator reval, Object thisObject, Object paramObject)
throws RedirectException, UnsupportedEncodingException {
// check for endless skin recursion
if (++reval.skinDepth > 50) {
throw new RuntimeException("Recursive skin invocation suspected");
}
ResponseTrans res = reval.getResponse();
if (macros == null) {
res.write(source, offset, length - offset);
reval.skinDepth--;
return;
}
// register param object, remember previous one to reset afterwards
Map handlers = res.getMacroHandlers();
Object previousParam = handlers.put("param", paramObject);
Skin previousSkin = res.switchActiveSkin(parentSkin);
try {
int written = offset;
Map handlerCache = null;
if (macros.length > 3) {
handlerCache = new HashMap();
}
RenderContext cx = new RenderContext(reval, thisObject, handlerCache);
for (int i = 0; i < macros.length; i++) {
if (macros[i].start > written) {
res.write(source, written, macros[i].start - written);
}
macros[i].render(cx);
written = macros[i].end;
}
if (written < length) {
res.write(source, written, length - written);
}
} finally {
reval.skinDepth--;
res.switchActiveSkin(previousSkin);
if (previousParam == null) {
handlers.remove("param");
} else {
handlers.put("param", previousParam);
}
}
}
/**
* Check if a certain macro is present in this skin. The macro name is in handler.name notation
*/
public boolean containsMacro(String macroname) {
for (int i = 0; i < macros.length; i++) {
if (macros[i] instanceof Macro) {
Macro m = macros[i];
if (macroname.equals(m.name)) {
return true;
}
}
}
return false;
}
/**
* Adds a macro to the list of allowed macros. The macro is in handler.name notation.
*/
public void allowMacro(String macroname) {
if (sandbox == null) {
sandbox = new HashSet();
}
sandbox.add(macroname);
}
private Object processParameter(Object value, RenderContext cx)
throws Exception {
if (value instanceof Macro) {
return ((Macro) value).invokeAsParameter(cx);
} else {
return value;
}
}
public class Macro {
public final int start, end;
String name;
String[] path;
int handlerType = HANDLER_OTHER;
int encoding = ENCODE_NONE;
boolean hasNestedMacros = false;
// default render parameters - may be overridden if macro changes
// param.prefix/suffix/default
StandardParams standardParams = new StandardParams();
Map namedParams = null;
List positionalParams = null;
// filters defined via <% foo | bar %>
Macro filterChain;
// comment macros are silently dropped during rendering
boolean isCommentMacro = false;
// subskin macros delimits the beginning of a new subskin
boolean isSubskinMacro = false;
/**
* Create and parse a new macro.
* @param start the start of the macro within the skin source
* @param macroOffset offset of the macro content from the start index
*/
Macro(int start, int macroOffset) {
this.start = start;
int i = parse(macroOffset, false);
if (isSubskinMacro) {
if (i + 1 < length && source[i] == '\r' && source[i + 1] == '\n')
end = Math.min(length, i + 2);
else if (i < length && (source[i] == '\r' || source[i] == '\n'))
end = Math.min(length, i + 1);
else
end = Math.min(length, i);
} else {
end = Math.min(length, i);
}
path = StringUtils.split(name, ".");
if (path.length <= 1) {
handlerType = HANDLER_GLOBAL;
} else {
String handlerName = path[0];
if ("this".equalsIgnoreCase(handlerName)) {
handlerType = HANDLER_THIS;
} else if ("response".equalsIgnoreCase(handlerName)) {
handlerType = HANDLER_RESPONSE;
} else if ("request".equalsIgnoreCase(handlerName)) {
handlerType = HANDLER_REQUEST;
} else if ("session".equalsIgnoreCase(handlerName)) {
handlerType = HANDLER_SESSION;
} else if ("param".equalsIgnoreCase(handlerName)) {
handlerType = HANDLER_PARAM;
}
}
if (".extends".equals(name)) {
if (parentSkin != Skin.this) {
throw new RuntimeException("Found .extends in subskin");
}
if (positionalParams == null || positionalParams.size() < 1
|| !(positionalParams.get(0) instanceof String)) {
throw new RuntimeException(".extends requires an unnamed string parameter");
}
extendz = (String) positionalParams.get(0);
isCommentMacro = true; // don't render
}
}
private int parse(int macroOffset, boolean lenient) {
int state = PARSE_MACRONAME;
boolean escape = false;
char quotechar = '\u0000';
String lastParamName = null;
StringBuffer b = new StringBuffer();
int i;
loop:
for (i = start + macroOffset; i < length - 1; i++) {
switch (source[i]) {
case '<':
if (state == PARSE_PARAM && quotechar == '\u0000'
&& b.length() == 0 && source[i + 1] == '%') {
Macro macro = new Macro(i, 2);
addParameter(lastParamName, macro);
lastParamName = null;
b.setLength(0);
i = macro.end - 1;
} else {
b.append(source[i]);
escape = false;
}
break;
case '%':
if ((state != PARSE_PARAM || quotechar == '\u0000' || lenient)
&& source[i + 1] == '>') {
state = PARSE_DONE;
break loop;
}
b.append(source[i]);
escape = false;
break;
case '/':
b.append(source[i]);
escape = false;
if (state == PARSE_MACRONAME && "//".equals(b.toString())) {
isCommentMacro = true;
// just continue parsing the macro as this is the only way
// to correctly catch embedded macros - see bug 588
}
break;
case '#':
if (state == PARSE_MACRONAME && b.length() == 0) {
// this is a subskin/skinlet
isSubskinMacro = true;
break;
}
b.append(source[i]);
escape = false;
break;
case '|':
if (!escape && quotechar == '\u0000') {
filterChain = new Macro(i, 1);
i = filterChain.end - 2;
lastParamName = null;
b.setLength(0);
state = PARSE_DONE;
break loop;
}
b.append(source[i]);
escape = false;
break;
case '\\':
if (escape) {
b.append(source[i]);
}
escape = !escape;
break;
case '"':
case '\'':
if (!escape && state == PARSE_PARAM) {
if (quotechar == source[i]) {
if (source[i + 1] != '%' && !Character.isWhitespace(source[i + 1]) && !lenient) {
// closing quotes and next character is not space or end tag -
// switch to lenient mode
reset();
return parse(macroOffset, true);
}
// add parameter
addParameter(lastParamName, b.toString());
lastParamName = null;
b.setLength(0);
quotechar = '\u0000';
} else if (quotechar == '\u0000') {
quotechar = source[i];
b.setLength(0);
} else {
b.append(source[i]);
}
} else {
b.append(source[i]);
}
escape = false;
break;
case ' ':
case '\t':
case '\n':
case '\r':
case '\f':
if (state == PARSE_MACRONAME && b.length() > 0) {
name = b.toString().trim();
b.setLength(0);
state = PARSE_PARAM;
} else if (state == PARSE_PARAM) {
if (quotechar == '\u0000') {
if (b.length() > 0) {
// add parameter
addParameter(lastParamName, b.toString());
lastParamName = null;
b.setLength(0);
}
} else {
b.append(source[i]);
escape = false;
}
}
break;
case '=':
if (!escape && quotechar == '\u0000' && state == PARSE_PARAM && lastParamName == null) {
lastParamName = b.toString().trim();
b.setLength(0);
} else {
b.append(source[i]);
escape = false;
}
break;
default:
b.append(source[i]);
escape = false;
}
if (i == length - 2 && !lenient &&
(state != PARSE_DONE ||quotechar != '\u0000')) {
// macro tag is not properly terminated, switch to lenient mode
reset();
return parse(macroOffset, true);
}
}
if (b.length() > 0) {
if (name == null) {
name = b.toString().trim();
} else {
addParameter(lastParamName, b.toString());
}
}
if (state != PARSE_DONE) {
app.logError("Unterminated Macro Tag: " +this);
}
return i + 2;
}
private void reset() {
filterChain = null;
name = null;
standardParams = new StandardParams();
namedParams = null;
positionalParams = null;
}
private void addParameter(String name, Object value) {
if (!(value instanceof String)) {
hasNestedMacros = true;
}
if (name == null) {
// take shortcut for positional parameters
if (positionalParams == null) {
positionalParams = new ArrayList();
}
positionalParams.add(value);
return;
}
// check if this is parameter is relevant to us
if ("prefix".equals(name)) {
standardParams.prefix = value;
} else if ("suffix".equals(name)) {
standardParams.suffix = value;
} else if ("encoding".equals(name)) {
if ("html".equals(value)) {
encoding = ENCODE_HTML;
} else if ("xml".equals(value)) {
encoding = ENCODE_XML;
} else if ("form".equals(value)) {
encoding = ENCODE_FORM;
} else if ("url".equals(value)) {
encoding = ENCODE_URL;
} else if ("all".equals(value)) {
encoding = ENCODE_ALL;
} else {
app.logEvent("Unrecognized encoding in skin macro: " + value);
}
} else if ("default".equals(name)) {
standardParams.defaultValue = value;
} else if ("failmode".equals(name)) {
standardParams.setFailMode(value);
}
// Add parameter to parameter map
if (namedParams == null) {
namedParams = new HashMap();
}
namedParams.put(name, value);
}
private Object invokeAsMacro(RenderContext cx, StandardParams stdParams, boolean asObject)
throws Exception {
// immediately return for comment macros
if (isCommentMacro || name == null) {
return null;
}
if ((sandbox != null) && !sandbox.contains(name)) {
throw new MacroException("Macro not allowed in sandbox: " + name);
}
Object handler = null;
Object value = null;
ScriptingEngine engine = cx.reval.scriptingEngine;
if (handlerType != HANDLER_GLOBAL) {
handler = cx.resolveHandler(path[0], handlerType);
handler = resolvePath(handler, cx.reval);
}
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 = resolveFunctionName(handler, propName + "_macro", engine);
// 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(handler,
funcName,
arguments);
// update StandardParams to override defaults in case the macro changed anything
if (stdParams != null) stdParams.readFrom(params);
// if macro has a filter chain and didn't return anything, use output
// as filter argument.
if (asObject && value == null && buffer.length() > bufLength) {
value = buffer.substring(bufLength);
buffer.setLength(bufLength);
}
return filter(value, cx);
} else {
if (handlerType == HANDLER_RESPONSE) {
// some special handling for response handler
if ("message".equals(propName))
value = cx.reval.getResponse().getMessage();
else if ("error".equals(propName))
value = cx.reval.getResponse().getErrorMessage();
if (value != null)
return filter(value, cx);
}
// display error message unless onUnhandledMacro is defined or silent failmode is on
if (!engine.hasProperty(handler, propName)) {
if (engine.hasFunction(handler, "onUnhandledMacro", false)) {
Object[] arguments = prepareArguments(1, cx);
arguments[0] = propName;
value = cx.reval.invokeDirectFunction(handler, "onUnhandledMacro", arguments);
// if macro has a filter chain and didn't return anything, use output
// as filter argument.
if (asObject && value == null && buffer.length() > bufLength) {
value = buffer.substring(bufLength);
buffer.setLength(bufLength);
}
} else if (standardParams.verboseFailmode(handler, engine)) {
throw new MacroException("Unhandled macro: " + name);
}
} else {
value = engine.getProperty(handler, propName);
}
return filter(value, cx);
}
} else if (standardParams.verboseFailmode(handler, engine)) {
throw new MacroException("Unhandled macro: " + name);
}
return filter(null, cx);
}
/**
* Render this macro as nested macro, only converting to string
* if necessary.
*/
Object invokeAsParameter(RenderContext cx) throws Exception {
StandardParams stdParams = standardParams.render(cx);
Object value = invokeAsMacro(cx, stdParams, true);
if (stdParams.prefix != null || stdParams.suffix != null) {
ResponseTrans res = cx.reval.getResponse();
res.pushBuffer(null);
writeResponse(value, cx.reval, stdParams, true);
return res.popString();
} else if (stdParams.defaultValue != null &&
(value == null || "".equals(value))) {
return stdParams.defaultValue;
} else {
return value;
}
}
/**
* Render the macro given a handler object.
*/
void render(RenderContext cx)
throws RedirectException, UnsupportedEncodingException {
StringBuffer buffer = cx.reval.getResponse().getBuffer();
// remember length of response buffer before calling macro
int bufLength = buffer.length();
try {
StandardParams stdParams = standardParams.render(cx);
boolean asObject = filterChain != null;
Object value = invokeAsMacro(cx, stdParams, asObject);
// check if macro wrote out to response buffer
if (buffer.length() == bufLength) {
// If the macro function didn't write anything to the response itself,
// we interpret its return value as macro output.
writeResponse(value, cx.reval, stdParams, true);
} else {
// if an encoding is specified, re-encode the macro's output
if (encoding != ENCODE_NONE) {
String output = buffer.substring(bufLength);
buffer.setLength(bufLength);
writeResponse(output, cx.reval, stdParams, false);
} else {
// insert prefix,
if (stdParams.prefix != null) {
buffer.insert(bufLength, stdParams.prefix);
}
// append suffix
if (stdParams.suffix != null) {
buffer.append(stdParams.suffix);
}
}
// Append macro return value even if it wrote something to the response,
// but don't render default value in case it returned nothing.
// We do this for the sake of consistency.
writeResponse(value, cx.reval, stdParams, false);
}
} catch (RedirectException redir) {
throw redir;
} catch (ConcurrencyException concur) {
throw concur;
} catch (TimeoutException timeout) {
throw timeout;
} catch (MacroException mx) {
String msg = mx.getMessage();
cx.reval.getResponse().write(" [" + msg + "] ");
app.logError(msg);
} catch (Exception x) {
String msg = x.getMessage();
if ((msg == null) || (msg.length() < 10)) {
msg = x.toString();
}
msg = new StringBuffer("Macro error in ").append(name)
.append(": ").append(msg).toString();
cx.reval.getResponse().write(" [" + msg + "] ");
app.logError(msg, x);
}
}
private Object filter(Object returnValue, RenderContext cx)
throws Exception {
// invoke filter chain if defined
if (filterChain != null) {
return filterChain.invokeAsFilter(returnValue, cx);
} else {
return returnValue;
}
}
private Object invokeAsFilter(Object returnValue, RenderContext cx)
throws Exception {
if (name == null) {
throw new MacroException("Empty macro filter");
} else if (sandbox != null && !sandbox.contains(name)) {
throw new MacroException("Macro not allowed in sandbox: " + name);
}
Object handlerObject = null;
if (handlerType != HANDLER_GLOBAL) {
handlerObject = cx.resolveHandler(path[0], handlerType);
handlerObject = resolvePath(handlerObject, cx.reval);
}
String propName = path[path.length - 1] + "_filter";
String funcName = resolveFunctionName(handlerObject, propName,
cx.reval.scriptingEngine);
if (funcName != null) {
Object[] arguments = prepareArguments(1, cx);
arguments[0] = returnValue;
Object retval = cx.reval.invokeDirectFunction(handlerObject,
funcName,
arguments);
return filter(retval, cx);
} else {
throw new MacroException("Undefined macro filter: " + name);
}
}
private Object[] prepareArguments(int offset, RenderContext cx)
throws Exception {
int nPosArgs = (positionalParams == null) ? 0 : positionalParams.size();
Object[] arguments = new Object[offset + 1 + nPosArgs];
if (namedParams == null) {
arguments[offset] = new SystemMap(4);
} else if (hasNestedMacros) {
SystemMap map = new SystemMap((int) (namedParams.size() * 1.5));
for (Iterator it = namedParams.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = (Map.Entry) it.next();
Object value = entry.getValue();
if (!(value instanceof String))
value = processParameter(value, cx);
map.put(entry.getKey(), value);
}
arguments[offset] = map;
} else {
// pass a clone/copy of the parameter map so if the script changes it,
arguments[offset] = new CopyOnWriteMap(namedParams);
}
if (positionalParams != null) {
for (int i = 0; i < nPosArgs; i++) {
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, RequestEvaluator reval) throws Exception {
for (int i = 1; i < path.length - 1; i++) {
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) {
return null;
}
} else {
return null;
}
}
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.
*/
void writeResponse(Object value, RequestEvaluator reval,
StandardParams stdParams, boolean useDefault)
throws Exception {
String text;
StringBuffer buffer = reval.getResponse().getBuffer();
if (value == null || "".equals(value)) {
if (useDefault) {
text = (String) stdParams.defaultValue;
} else {
return;
}
} else {
text = reval.scriptingEngine.toString(value);
}
if ((text != null) && (text.length() > 0)) {
// only write prefix/suffix if value is not null, if we write the default
// value provided by the macro tag, we assume it's already complete
if (stdParams.prefix != null && value != null) {
buffer.append(stdParams.prefix);
}
switch (encoding) {
case ENCODE_NONE:
buffer.append(text);
break;
case ENCODE_HTML:
HtmlEncoder.encode(text, buffer);
break;
case ENCODE_XML:
HtmlEncoder.encodeXml(text, buffer);
break;
case ENCODE_FORM:
HtmlEncoder.encodeFormValue(text, buffer);
break;
case ENCODE_URL:
buffer.append(UrlEncoded.encode(text, app.charset));
break;
case ENCODE_ALL:
HtmlEncoder.encodeAll(text, buffer);
break;
}
if (stdParams.suffix != null && value != null) {
buffer.append(stdParams.suffix);
}
}
}
public String toString() {
return "[Macro: " + name + "]";
}
/**
* Return the full name of the macro in handler.name notation
* @return the macro name
*/
public String getName() {
return name;
}
/**
* Return the numeric type of the macro handler
* @return the handler type
*/
public int getHandlerType() {
return handlerType;
}
/**
* Return the list of named parameters
* @return the list of named parameters
*/
public Map getNamedParams() {
return namedParams;
}
/**
* Return the list of positional parameters
* @return the list of positional parameters
*/
public List getPositionalParams() {
return positionalParams;
}
public boolean hasNestedMacros() {
return hasNestedMacros;
}
}
class StandardParams {
Object prefix = null;
Object suffix = null;
Object defaultValue = null;
int failmode = FAIL_DEFAULT;
StandardParams() {}
StandardParams(Map map) {
readFrom(map);
}
void readFrom(Map map) {
prefix = map.get("prefix");
suffix = map.get("suffix");
defaultValue = map.get("default");
}
boolean containsMacros() {
return !(prefix instanceof String)
|| !(suffix instanceof String)
|| !(defaultValue instanceof String);
}
void setFailMode(Object value) {
if ("silent".equals(value))
failmode = FAIL_SILENT;
else if ("verbose".equals(value))
failmode = FAIL_VERBOSE;
else if (value != null)
app.logEvent("unrecognized failmode value: " + value);
}
boolean verboseFailmode(Object handler, ScriptingEngine engine) {
return (failmode == FAIL_VERBOSE) ||
(failmode == FAIL_DEFAULT &&
(handler == null ||
engine.isTypedObject(handler)));
}
StandardParams render(RenderContext cx)
throws Exception {
if (!containsMacros())
return this;
StandardParams stdParams = new StandardParams();
stdParams.prefix = renderToString(prefix, cx);
stdParams.suffix = renderToString(suffix, cx);
stdParams.defaultValue = renderToString(defaultValue, cx);
return stdParams;
}
String renderToString(Object obj, RenderContext cx) throws Exception {
Object value = processParameter(obj, cx);
if (value == null)
return null;
else if (value instanceof String)
return (String) value;
else
return cx.reval.scriptingEngine.toString(value);
}
}
class RenderContext {
final RequestEvaluator reval;
final Object thisObject;
final Map handlerCache;
RenderContext(RequestEvaluator reval, Object thisObject, Map handlerCache) {
this.reval = reval;
this.thisObject = thisObject;
this.handlerCache = handlerCache;
}
private Object resolveHandler(String handlerName, int handlerType) {
switch (handlerType) {
case HANDLER_THIS:
return thisObject;
case HANDLER_RESPONSE:
return reval.getResponse().getResponseData();
case HANDLER_REQUEST:
return reval.getRequest().getRequestData();
case HANDLER_SESSION:
return reval.getSession().getCacheNode();
}
// try to get handler from handlerCache first
if (handlerCache != null && handlerCache.containsKey(handlerName)) {
return handlerCache.get(handlerName);
}
// if handler object wasn't found in cache first check this-object
if (thisObject != null) {
// not a global macro - need to find handler object
// was called with this object - check this-object for matching prototype
Prototype proto = app.getPrototype(thisObject);
if (proto != null && proto.isInstanceOf(handlerName)) {
return cacheHandler(handlerName, thisObject);
}
}
// next look in res.handlers
Map macroHandlers = reval.getResponse().getMacroHandlers();
Object obj = macroHandlers.get(handlerName);
if (obj != null) {
return cacheHandler(handlerName, obj);
}
// finally walk down the this-object's parent chain
if (thisObject != null) {
obj = app.getParentElement(thisObject);
// walk down parent chain to find handler object,
// limiting to 50 passes to avoid infinite loops
int maxloop = 50;
while (obj != null && maxloop-- > 0) {
Prototype proto = app.getPrototype(obj);
if (proto != null && proto.isInstanceOf(handlerName)) {
return cacheHandler(handlerName, obj);
}
obj = app.getParentElement(obj);
}
}
return cacheHandler(handlerName, null);
}
private Object cacheHandler(String name, Object handler) {
if (handlerCache != null) {
handlerCache.put(name, handler);
}
return handler;
}
}
/**
* Exception type for unhandled, forbidden or failed macros
*/
class MacroException extends Exception {
MacroException(String message) {
super(message);
}
}
}