* Implement subskins

* Fix skin failmode levels
* Add Resource.getOverloadedResource()
* Implement ScriptingEngine.isTypedObject(Object)
* Fix skin length bug with non-ASCII characters
This commit is contained in:
hns 2007-03-22 15:34:10 +00:00
parent 0b363a4b96
commit 4531ef6e4b
9 changed files with 315 additions and 87 deletions

View file

@ -324,10 +324,36 @@ public final class Prototype {
/**
* Get a skin for this prototype. This only works for skins
* residing in the prototype directory, not for skins files in
* other locations or database stored skins.
* other locations or database stored skins. If parentName and
* subName are defined, the skin may be a subskin of another skin.
*/
public Skin getSkin(String skinName) throws IOException {
return skinMap.getSkin(skinName);
public Skin getSkin(String skinName, String parentName, String subName)
throws IOException {
Skin skin = null;
Resource res = skinMap.getResource(skinName);
while (res != null) {
skin = Skin.getSkin(res, app);
if (skin.hasMainskin())
break;
res = res.getOverloadedResource();
}
if (parentName != null) {
Skin parentSkin = null;
Resource parent = skinMap.getResource(parentName);
while (parent != null) {
parentSkin = Skin.getSkin(parent, app);
if (parentSkin.hasSubskin(subName))
break;
parent = parent.getOverloadedResource();
}
if (parent != null) {
if (res != null && app.getResourceComparator().compare(res, parent) > 0)
return skin;
else
return parentSkin.getSubskin(subName);
}
}
return skin;
}
/**
@ -494,6 +520,10 @@ public final class Prototype {
}
}
public Resource getResource(Object key) {
return (Resource) get(key);
}
public Object get(Object key) {
checkForUpdates();
return super.get(key);
@ -559,7 +589,8 @@ public final class Prototype {
// load Skins
for (Iterator i = skins.iterator(); i.hasNext();) {
Resource res = (Resource) i.next();
super.put(res.getBaseName(), res);
Resource prev = (Resource) super.put(res.getBaseName(), res);
res.setOverloadedResource(prev);
}
// if skinpath is not null, overload/add skins from there
@ -594,7 +625,9 @@ public final class Prototype {
String name = skinNames[i].substring(0, skinNames[i].length() - 5);
File file = new File(dir, skinNames[i]);
super.put(name, (new FileResource(file)));
Resource res = new FileResource(file);
Resource prev = (Resource) super.put(name, res);
res.setOverloadedResource(prev);
}
}

View file

@ -33,6 +33,15 @@ import java.io.IOException;
* 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;
static private final int PARSE_MACRONAME = 0;
static private final int PARSE_PARAM = 1;
static private final int PARSE_DONE = 2;
@ -52,20 +61,19 @@ public final class Skin {
static private final int HANDLER_THIS = 5;
static private final int HANDLER_OTHER = 6;
private Macro[] macros;
private Application app;
private char[] source;
private int sourceLength;
private HashSet sandbox;
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;
sandbox = null;
source = content.toCharArray();
sourceLength = source.length;
this.sandbox = null;
this.source = content.toCharArray();
this.offset = 0;
this.length = source.length;
parse();
}
@ -75,8 +83,9 @@ public final class Skin {
public Skin(String content, Application app, HashSet sandbox) {
this.app = app;
this.sandbox = sandbox;
source = content.toCharArray();
sourceLength = source.length;
this.source = content.toCharArray();
this.offset = 0;
length = source.length;
parse();
}
@ -86,8 +95,23 @@ public final class Skin {
public Skin(char[] content, int length, Application app) {
this.app = app;
this.sandbox = null;
source = content;
sourceLength = length;
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();
}
@ -113,7 +137,7 @@ public final class Skin {
} finally {
reader.close();
}
return new Skin(characterBuffer, length, app);
return new Skin(characterBuffer, read, app);
}
/**
@ -123,11 +147,17 @@ public final class Skin {
ArrayList partBuffer = new ArrayList();
boolean escape = false;
for (int i = 0; i < (sourceLength - 1); i++) {
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);
partBuffer.add(macro);
if (macro.isSubskinMacro) {
new Skin(parentSkin, macro);
length = i;
break;
} else {
partBuffer.add(macro);
}
i = macro.end - 1;
} else {
escape = source[i] == '\\' && !escape;
@ -138,11 +168,54 @@ public final class Skin {
partBuffer.toArray(macros);
}
private void addSubskin(String name, Skin subskin) {
if (subskins == null) {
subskins = new HashMap();
}
subskins.put(name, subskin);
}
/**
* 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 length - offset > 0;
}
/**
* 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]);
}
/**
* Get the raw source text this skin was parsed from
*/
public String getSource() {
return new String(source, 0, sourceLength);
return new String(source, offset, length - offset);
}
/**
@ -175,9 +248,8 @@ public final class Skin {
ResponseTrans res = reval.getResponse();
if (macros == null) {
res.write(source, 0, sourceLength);
res.write(source, offset, length - offset);
reval.skinDepth--;
return;
}
@ -186,7 +258,7 @@ public final class Skin {
Object previousParam = handlers.put("param", paramObject);
try {
int written = 0;
int written = offset;
Map handlerCache = null;
if (macros.length > 3) {
@ -202,8 +274,8 @@ public final class Skin {
written = macros[i].end;
}
if (written < sourceLength) {
res.write(source, written, sourceLength - written);
if (written < length) {
res.write(source, written, length - written);
}
} finally {
reval.skinDepth--;
@ -276,6 +348,8 @@ public final class Skin {
// 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.
@ -293,7 +367,7 @@ public final class Skin {
int i;
loop:
for (i = start + offset; i < sourceLength - 1; i++) {
for (i = start + offset; i < length - 1; i++) {
switch (source[i]) {
@ -330,7 +404,7 @@ public final class Skin {
if (state == PARSE_MACRONAME && "//".equals(b.toString())) {
isCommentMacro = true;
// search macro end tag
while (i < sourceLength - 1 &&
while (i < length - 1 &&
(source[i] != '%' || source[i + 1] != '>')) {
i++;
}
@ -339,6 +413,16 @@ public final class Skin {
}
break;
case '#':
if (state == PARSE_MACRONAME && b.length() == 0) {
// this is a subskin/skinlet
isSubskinMacro = true;
break;
}
b.append(source[i]);
escape = false;
case '|':
if (!escape && quotechar == '\u0000') {
@ -431,7 +515,17 @@ public final class Skin {
}
}
this.end = Math.min(sourceLength, i + 2);
i += 2;
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);
}
if (b.length() > 0) {
if (name == null) {
@ -462,11 +556,6 @@ public final class Skin {
handler = HANDLER_PARAM;
}
}
// Set default failmode unless explicitly set:
// silent for default handlers, verbose
if (namedParams == null || !namedParams.containsKey("failmode")) {
standardParams.silentFailMode = (handler < HANDLER_GLOBAL);
}
}
private void addParameter(String name, Object value) {
@ -607,8 +696,8 @@ public final class Skin {
}
}
// display error message unless silent failmode is on
if (handlerObject == null || !hasProperty(handlerObject, propName, reval)) {
if (!standardParams.silentFailMode) {
if (handlerObject == null || !reval.scriptingEngine.hasProperty(handlerObject, propName)) {
if (standardParams.verboseFailmode(handlerObject, reval)) {
String msg = "[Macro unhandled: " + name + "]";
reval.getResponse().write(" " + msg + " ");
app.logEvent(msg);
@ -616,13 +705,13 @@ public final class Skin {
writeResponse(null, reval, thisObject, handlerCache, standardParams, true);
}
} else {
Object value = getProperty(handlerObject, propName, reval);
Object value = reval.scriptingEngine.getProperty(handlerObject, propName);
writeResponse(value, reval, thisObject, handlerCache, standardParams, true);
}
}
} else {
if (!standardParams.silentFailMode) {
if (standardParams.verboseFailmode(handlerObject, reval)) {
String msg = "[Macro unhandled: " + name + "]";
reval.getResponse().write(" " + msg + " ");
app.logEvent(msg);
@ -770,12 +859,12 @@ public final class Skin {
}
private Object resolvePath(Object handler, RequestEvaluator reval) {
if (!app.allowDeepMacros && path.length > 2) {
if (path.length > 2 && !app.allowDeepMacros) {
throw new RuntimeException("allowDeepMacros property must be true " +
"in order to enable deep macro paths.");
}
for (int i = 1; i < path.length - 1; i++) {
handler = getProperty(handler, path[i], reval);
handler = reval.scriptingEngine.getProperty(handler, path[i]);
if (handler == null) {
break;
}
@ -783,23 +872,6 @@ public final class Skin {
return handler;
}
private Object getProperty(Object obj, String name,
RequestEvaluator reval) {
if (obj instanceof Map) {
return ((Map) obj).get(name);
} else {
return reval.getScriptingEngine().getProperty(obj, name);
}
}
private boolean hasProperty(Object obj, String name, RequestEvaluator reval) {
if (obj instanceof Map) {
return ((Map) obj).containsKey(name);
} else {
return reval.getScriptingEngine().hasProperty(obj, name);
}
}
/**
* Utility method for writing text out to the response object.
*/
@ -822,7 +894,7 @@ public final class Skin {
return;
}
} else {
text = reval.getScriptingEngine().toString(value);
text = reval.scriptingEngine.toString(value);
}
if ((text != null) && (text.length() > 0)) {
@ -887,7 +959,7 @@ public final class Skin {
Object prefix = null;
Object suffix = null;
Object defaultValue = null;
boolean silentFailMode = false;
int failmode = FAIL_DEFAULT;
StandardParams() {}
@ -895,7 +967,6 @@ public final class Skin {
prefix = map.get("prefix");
suffix = map.get("suffix");
defaultValue = map.get("default");
silentFailMode = "silent".equals(map.get("failmode"));
}
boolean containsMacros() {
@ -906,13 +977,18 @@ public final class Skin {
void setFailMode(Object value) {
if ("silent".equals(value))
silentFailMode = true;
failmode = FAIL_SILENT;
else if ("verbose".equals(value))
silentFailMode = false;
else
failmode = FAIL_VERBOSE;
else if (value != null)
app.logEvent("unrecognized failmode value: " + value);
}
boolean verboseFailmode(Object handler, RequestEvaluator reval) {
return (failmode == FAIL_VERBOSE) ||
(failmode == FAIL_DEFAULT && reval.scriptingEngine.isTypedObject(handler));
}
StandardParams render(RequestEvaluator reval, Object thisObj, Map handlerCache)
throws UnsupportedEncodingException {
if (!containsMacros())

View file

@ -42,12 +42,22 @@ public final class SkinManager implements FilenameFilter {
skinExtension = ".skin";
}
protected Skin getSkin(Prototype proto, String skinname, Object[] skinpath) throws IOException {
if (proto == null) {
protected Skin getSkin(Prototype prototype, String skinname, Object[] skinpath)
throws IOException {
if (prototype == null) {
return null;
}
Skin skin = null;
Skin skin;
Prototype proto = prototype;
// if name contains dot, this might be a substring of some other string
String parentName = null, subskinName = null;
int hash = skinname.indexOf('#');
if (hash > -1) {
parentName = skinname.substring(0, hash);
subskinName = skinname.substring(hash + 1);
}
// First check if the skin has been already used within the execution of this request
// check for skinsets set via res.skinpath property
@ -55,16 +65,23 @@ public final class SkinManager implements FilenameFilter {
if (skinpath != null) {
for (int i = 0; i < skinpath.length; i++) {
skin = getSkinInPath(skinpath[i], proto.getName(), skinname);
if (skin != null) {
if (skin != null && skin.hasMainskin()) {
// check if skin skin contains main skin
return skin;
} else if (parentName != null) {
// get parent skin
skin = getSkinInPath(skinpath[i], proto.getName(), parentName);
// check if it contains subskin
if (skin != null && skin.hasSubskin(subskinName)) {
return skin.getSubskin(subskinName);
}
}
}
}
// skin for this prototype wasn't found in the skinsets.
// the next step is to look if it is defined as skin file in the application directory
skin = proto.getSkin(skinname);
skin = proto.getSkin(skinname, parentName, subskinName);
if (skin != null) {
return skin;
@ -78,7 +95,7 @@ public final class SkinManager implements FilenameFilter {
return null;
}
protected Skin getSkinInPath(Object skinset, String prototype, String skinname) throws IOException {
private Skin getSkinInPath(Object skinset, String prototype, String skinname) throws IOException {
if ((prototype == null) || (skinset == null)) {
return null;
}

View file

@ -0,0 +1,43 @@
/*
* 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$
* $Revision$
* $Date$
*/
package helma.framework.repository;
/**
* Abstract resource base class that implents get/setOverloadedResource.
*/
public abstract class AbstractResource implements Resource {
protected Resource overloaded = null;
/**
* Method for registering a Resource this Resource is overloading
*
* @param res the overloaded resource
*/
public void setOverloadedResource(Resource res) {
overloaded = res;
}
/**
* Get a Resource this Resource is overloading
*
* @return the overloaded resource
*/
public Resource getOverloadedResource() {
return overloaded;
}
}

View file

@ -19,7 +19,7 @@ package helma.framework.repository;
import java.net.*;
import java.io.*;
public class FileResource implements Resource {
public class FileResource extends AbstractResource {
File file;
Repository repository;

View file

@ -41,25 +41,29 @@ public interface Resource {
/**
* Returns the lengh of the resource's content
* @return content length
* @throws IOException I/O related problem
*/
public long getLength() throws IOException;
/**
* Returns an input stream to the content of the resource
* @return content input stream
* @throws IOException I/O related problem
*/
public InputStream getInputStream() throws IOException;
/**
* Returns the content of the resource in a given encoding
* @param encoding
* @param encoding the character encoding
* @return content
* @throws IOException I/O related problem
*/
public String getContent(String encoding) throws IOException;
/**
* Returns the content of the resource
* @return content
* @throws IOException I/O related problem
*/
public String getContent() throws IOException;
@ -88,9 +92,22 @@ public interface Resource {
* Returns an url to the resource if the repository of this resource is
* able to provide urls
* @return url to the resource
* @throws UnsupportedOperationException if resource does not support URL schema
*/
public URL getUrl() throws UnsupportedOperationException;
/**
* Get a Resource this Resource is overloading
* @return the overloaded resource
*/
public Resource getOverloadedResource();
/**
* Method for registering a Resource this Resource is overloading
* @param res the overloaded resource
*/
public void setOverloadedResource(Resource res);
/**
* Returns the repository the resource does belong to
* @return upper repository

View file

@ -21,7 +21,7 @@ import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class ZipResource implements Resource {
public final class ZipResource extends AbstractResource {
private String entryName;
private ZipRepository repository;

View file

@ -110,10 +110,10 @@ public interface ScriptingEngine {
/**
* Get a property on an object
* @param thisObject the object
* @param key the property name
* @param propertyName the property name
* @return true the property value, or null
*/
public Object getProperty(Object thisObject, String key);
public Object getProperty(Object thisObject, String propertyName);
/**
* Return true if a function by that name is defined for that object.
@ -131,6 +131,13 @@ public interface ScriptingEngine {
*/
public boolean hasProperty(Object thisObject, String propertyName);
/**
* Determine if the given object is mapped to a type of the scripting engine
* @param obj an object
* @return true if the object is mapped to a type
*/
public boolean isTypedObject(Object obj);
/**
* Return a string representation for the given object
* @param obj an object

View file

@ -346,28 +346,31 @@ public class RhinoEngine implements ScriptingEngine {
// references/child objects just to check for function properties.
if (obj instanceof INode) {
String protoname = ((INode) obj).getPrototype();
return core.hasFunction(protoname, fname);
if (protoname != null && core.hasFunction(protoname, fname))
return true;
}
Scriptable op = obj == null ? global : Context.toObject(obj, global);
Object func = ScriptableObject.getProperty(op, fname);
return func instanceof Callable;
return ScriptableObject.getProperty(op, fname) instanceof Callable;
}
/**
* Check if an object has a value property defined with that name.
*/
public boolean hasProperty(Object obj, String propname) {
if ((obj == null) || (propname == null)) {
if (obj == null || propname == null) {
return false;
} else if (obj instanceof Map) {
return ((Map) obj).containsKey(propname);
}
String prototypeName = app.getPrototypeName(obj);
if ("user".equalsIgnoreCase(prototypeName)
&& "password".equalsIgnoreCase(propname)) {
return false;
}
// if this is a HopObject, check if the property is defined
// in the type.properties db-mapping.
if (obj instanceof INode && ! "hopobject".equalsIgnoreCase(prototypeName)) {
@ -386,22 +389,36 @@ public class RhinoEngine implements ScriptingEngine {
* is a java object) with that name.
*/
public Object getProperty(Object obj, String propname) {
if ((obj == null) || (propname == null))
if (obj == null || propname == null) {
return null;
} else if (obj instanceof Map) {
Object prop = ((Map) obj).get(propname);
// Do not return functions as properties as this
// is a potential security problem
return (prop instanceof Function) ? null : prop;
} else if (obj instanceof INode) {
IProperty prop = ((INode) obj).get(propname);
if (prop == null) return null;
Object value = prop.getValue();
return (value instanceof Function) ? null : value;
}
// use Rhino wrappers and methods to get property
Scriptable so = Context.toObject(obj, global);
try {
Object prop = so.get(propname, so);
if ((prop == null)
|| (prop == Undefined.instance)
|| (prop == ScriptableObject.NOT_FOUND)) {
if (prop == null
|| prop == Undefined.instance
|| prop == ScriptableObject.NOT_FOUND) {
return null;
} else if (prop instanceof Wrapper) {
return ((Wrapper) prop).unwrap();
} else {
return prop;
// Do not return functions as properties as this
// is a potential security problem
return (prop instanceof Function) ? null : prop;
}
} catch (Exception esx) {
app.logError("Error getting property " + propname + ": " + esx);
@ -410,6 +427,22 @@ public class RhinoEngine implements ScriptingEngine {
}
/**
* Determine if the given object is mapped to a type of the scripting engine
* @param obj an object
* @return true if the object is mapped to a type
*/
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);
return protoName != null && !"hopobject".equalsIgnoreCase(protoName);
}
// assume java object is typed
return true;
}
/**
* Return a string representation for the given object
* @param obj an object
@ -544,7 +577,9 @@ public class RhinoEngine implements ScriptingEngine {
* Try to get a skin from the parameter object.
*/
public Skin toSkin(Object skinobj, String protoName) throws IOException {
if (skinobj instanceof Wrapper) {
if (skinobj == null) {
return null;
} else if (skinobj instanceof Wrapper) {
skinobj = ((Wrapper) skinobj).unwrap();
}