From 4531ef6e4b15573c1039c0c1d9a32cc714362ac1 Mon Sep 17 00:00:00 2001 From: hns Date: Thu, 22 Mar 2007 15:34:10 +0000 Subject: [PATCH] * Implement subskins * Fix skin failmode levels * Add Resource.getOverloadedResource() * Implement ScriptingEngine.isTypedObject(Object) * Fix skin length bug with non-ASCII characters --- src/helma/framework/core/Prototype.java | 43 +++- src/helma/framework/core/Skin.java | 192 ++++++++++++------ src/helma/framework/core/SkinManager.java | 31 ++- .../repository/AbstractResource.java | 43 ++++ .../framework/repository/FileResource.java | 2 +- src/helma/framework/repository/Resource.java | 19 +- .../framework/repository/ZipResource.java | 2 +- src/helma/scripting/ScriptingEngine.java | 11 +- src/helma/scripting/rhino/RhinoEngine.java | 59 ++++-- 9 files changed, 315 insertions(+), 87 deletions(-) create mode 100644 src/helma/framework/repository/AbstractResource.java diff --git a/src/helma/framework/core/Prototype.java b/src/helma/framework/core/Prototype.java index 0c5a3a09..6af44c9a 100644 --- a/src/helma/framework/core/Prototype.java +++ b/src/helma/framework/core/Prototype.java @@ -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); } } diff --git a/src/helma/framework/core/Skin.java b/src/helma/framework/core/Skin.java index 83ba221b..a0f01cf2 100644 --- a/src/helma/framework/core/Skin.java +++ b/src/helma/framework/core/Skin.java @@ -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()) diff --git a/src/helma/framework/core/SkinManager.java b/src/helma/framework/core/SkinManager.java index 28b817d5..df07b621 100644 --- a/src/helma/framework/core/SkinManager.java +++ b/src/helma/framework/core/SkinManager.java @@ -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; } diff --git a/src/helma/framework/repository/AbstractResource.java b/src/helma/framework/repository/AbstractResource.java new file mode 100644 index 00000000..04da1771 --- /dev/null +++ b/src/helma/framework/repository/AbstractResource.java @@ -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; + } +} diff --git a/src/helma/framework/repository/FileResource.java b/src/helma/framework/repository/FileResource.java index 52ca9bca..c37de81e 100644 --- a/src/helma/framework/repository/FileResource.java +++ b/src/helma/framework/repository/FileResource.java @@ -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; diff --git a/src/helma/framework/repository/Resource.java b/src/helma/framework/repository/Resource.java index a8973c0a..3eb431b8 100644 --- a/src/helma/framework/repository/Resource.java +++ b/src/helma/framework/repository/Resource.java @@ -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 diff --git a/src/helma/framework/repository/ZipResource.java b/src/helma/framework/repository/ZipResource.java index aee40afd..cb607530 100644 --- a/src/helma/framework/repository/ZipResource.java +++ b/src/helma/framework/repository/ZipResource.java @@ -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; diff --git a/src/helma/scripting/ScriptingEngine.java b/src/helma/scripting/ScriptingEngine.java index 156e35d5..46cd5e98 100644 --- a/src/helma/scripting/ScriptingEngine.java +++ b/src/helma/scripting/ScriptingEngine.java @@ -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 diff --git a/src/helma/scripting/rhino/RhinoEngine.java b/src/helma/scripting/rhino/RhinoEngine.java index 62466d07..615188cf 100644 --- a/src/helma/scripting/rhino/RhinoEngine.java +++ b/src/helma/scripting/rhino/RhinoEngine.java @@ -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(); }