/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Blacksmith, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.web; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.PushbackInputStream; import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import net.wotonomy.control.EOKeyValueCodingSupport; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSDictionary; import net.wotonomy.foundation.NSMutableDictionary; import net.wotonomy.foundation.NSSelector; /** * Pure java implementation of WOComponent. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 905 $ */ public class WOComponent extends WOElement implements WOActionResults, net.wotonomy.control.EOKeyValueCodingAdditions, net.wotonomy.control.EOKeyValueCoding { WOElement rootElement; private static final String DIRECTORY_SUFFIX = ".wo"; private static final String TEMPLATE_SUFFIX = ".html"; private static final String DECLARATION_SUFFIX = ".wod"; private static final String OPEN_TAG = "webobject"; private static final String CLOSE_TAG = "/webobject"; private static final String NAME_KEY = "name"; protected transient WOContext context; // don't persist protected boolean cachingEnabled; protected WOElement template; protected WOComponent parent; /** * Default constructor. Deprecated in latest spec. */ public WOComponent() { parent = null; cachingEnabled = true; template = null; } /** * Constructor specifying a context. */ public WOComponent(WOContext aContext) { this(); context = aContext; } /** * Returns the name of the component, which is usually just the class name. */ public String name() { return justTheClassName(); } /** * Returns the system-dependent file path to the current component directory, * including the ".wo" extension. */ public String path() { throw new RuntimeException("Not implemented yet."); } /** * Returns the URL for this component, relative to the server's document root on * the server's file system. This is not an http url. */ public String baseURL() { throw new RuntimeException("Not implemented yet."); } /** * Returns the name of the framework that contains this component, or null if * the component does not belong to a framework. This currently returns the * package path of the class, or null if it does not belong to a package. */ public String frameworkName() { return justTheResourcePath(); } /** * Sets whether templates are cached. If true, templates will only be read once * per application lifetime. Otherwise, templates will be read each time this * class is instantiated. Defaults to false. */ public void setCachingEnabled(boolean enabled) { cachingEnabled = enabled; } /** * Returns whether templates are cached. If true, templates are read once per * application lifetime. Otherwise, templates are read each time this class is * instantiated. */ public boolean isCachingEnabled() { return cachingEnabled && WOApplication.application().isCachingEnabled(); } /** * Returns the root of the tree of elements produced by parsing the templates in * the component directory for this component. */ public WOElement template() { return template; } /** * Returns the root of the tree of elements produced by parsing the templates in * the component directory for the named component. * * @deprecated Use template() instead. */ public WOElement templateWithName(String aComponentName) { return templateWithName(aComponentName, null); } /** * Returns the root of the tree of elements produced by parsing the templates in * the component directory for the named component. */ WOElement templateWithName(String aComponentName, String aFramework) { NSArray languages = null; WOContext context = context(); if (context != null) { languages = context.request().browserLanguages(); } WOElement result = templateWithHTMLString( readTemplateResource(aComponentName, aFramework, TEMPLATE_SUFFIX, languages), readTemplateResource(aComponentName, aFramework, DECLARATION_SUFFIX, languages), languages); if (result == null) { System.out.println("WOComponent.templateWithName: failed for " + aComponentName); } return result; } /** * Returns the root of the tree of elements produced by parsing the specfified * HTML string and bindings declaration string. Note: language list is currently * ignored. */ public static WOElement templateWithHTMLString(String anHTMLString, String aDeclaration, List aLanguageList) { if (anHTMLString == null) return null; WOElement result = null; try { NSDictionary bindings = processDeclaration(aDeclaration); List elements = new LinkedList(); int index = processTemplate(elements, anHTMLString, 0, bindings, aLanguageList); if (index == -1) { if (elements.size() == 1) { result = (WOElement) elements.get(0); } else { result = new WOParentElement(elements); } } else // entire template did not process { throw new RuntimeException("No closing tag: " + anHTMLString.substring(index)); } } catch (Exception exc) { exc.printStackTrace(); } return result; } /** * Called at the beginning of a request-response cycle. Override to perform any * necessary initialization. This implementation does nothing. */ public void awake() { } /** * Package access only. Called to initialize the component with the proper * context before the start of the request-response cycle. If the context has a * current component, that component becomes this component's parent. */ void ensureAwakeInContext(WOContext aContext) { context = aContext; parent = aContext.parent(); if (template == null) { template = templateWithName(name(), frameworkName()); } if (template != null) { template.ensureAwakeInContext(aContext); } awake(); } public void takeValuesFromRequest(WORequest aRequest, WOContext aContext) { if (synchronizesVariablesWithBindings()) { pullValuesFromParent(); if (template != null) { template.takeValuesFromRequest(aRequest, aContext); } pushValuesToParent(); } else if (template != null) { template.takeValuesFromRequest(aRequest, aContext); } } public WOActionResults invokeAction(WORequest aRequest, WOContext aContext) { WOActionResults result = null; if (synchronizesVariablesWithBindings()) { pullValuesFromParent(); if (template != null) { result = template.invokeAction(aRequest, aContext); } pushValuesToParent(); } else if (template != null) { result = template.invokeAction(aRequest, aContext); } return result; } public void appendToResponse(WOResponse aResponse, WOContext aContext) { if (synchronizesVariablesWithBindings()) { pullValuesFromParent(); if (template != null) { template.appendToResponse(aResponse, aContext); } pushValuesToParent(); } else if (template != null) { template.appendToResponse(aResponse, aContext); } context = null; } /** * Called at the end of a request-response cycle. Override to perform any * necessary clean-up. This implementation does nothing. */ public void sleep() { } /** * Generates a WOResponse and calls appendToResponse() on it. */ public WOResponse generateResponse() { WOResponse response = new WOResponse(); WOContext context = context(); appendToResponse(response, context); // nulls out context context.session().savePage(this); // ?is this the right place for this? return response; } /** * Returns this component's parent component, or null if none. */ public WOComponent parent() { return parent; } /** * Invokes the specified action on this component's parent. Variables will be * synchronized when this method returns. */ public WOActionResults performParentAction(String anAction) { WOActionResults result = parent().performAction(anAction); if (synchronizesVariablesWithBindings()) { pullValuesFromParent(); } return result; } /** * Invokes the specified action on this component. */ WOActionResults performAction(String anAction) { try { return (WOActionResults) NSSelector.invoke(anAction, this); } catch (NoSuchMethodException exc) { // returns below } catch (InvocationTargetException exc) { Throwable t = exc.getTargetException(); exc.printStackTrace(); throw new RuntimeException(t.toString()); } catch (Exception exc) { exc.printStackTrace(); throw new RuntimeException(exc.toString()); } return null; } /** * Called before each phase of the request-response cycle, if * synchronizesVariablesWithBindings is true and the component is not stateless. */ public void pullValuesFromParent() { if (associations == null) return; String key; Enumeration e = associations.keyEnumerator(); while (e.hasMoreElements()) { key = e.nextElement().toString(); takeValueForKey(valueForBinding(key), key); } } /** * Called after each phase of the request-response cycle, if * synchronizesVariablesWithBindings is true and the component is not stateless. */ public void pushValuesToParent() { if (associations == null) return; String key; Enumeration e = associations.keyEnumerator(); while (e.hasMoreElements()) { key = e.nextElement().toString(); setValueForBinding(valueForKey(key), key); } } /** * Returns whether this component should be considered stateless. Stateless * components are shared between sessions to conserve memory. This * implementation returns false; override to return true. */ public boolean isStateless() { return false; } /** * Called only on stateless components to tell themselves to reset themselves * for another invocation using a different context. This implementation does * nothing. */ public void reset() { // does nothing } /** * Returns the application containing this instance of the class. */ public WOApplication application() { return context.application(); } /** * Returns whether a session has been created for this user. */ public boolean hasSession() { return context.hasSession(); } /** * Returns the current session object, creating it if it doesn't exist. */ public WOSession session() { return context.session(); } /** * Returns the current context for this component. */ public WOContext context() { return context; } /** * Returns a new WOComponent with the specified name. If null, returns the * component named "Main". If the named component doesn't exist, returns null. */ public WOComponent pageWithName(String aName) { return application().pageWithName(aName, context()); } /** * Called when exceptions are raised by assigning values to this object. This * implementation does nothing, but subclasses may override to do something * useful. */ public void validationFailedWithException(Throwable anException, Object aValue, String aPath) { // does nothing } /** * Called on the component that represents the requested page. Override to * return logging information specific to your component. This implementation * returns the component's name. */ public String descriptionForResponse(WOResponse aResponse, WOContext aContext) { return name(); } /** * Returns true if this component should get and set values in its parent. This * implementation returns true. Override to create a component that does not * automatically synchronize bindings with its parent, useful if you wish to * handle synchronization manually. */ public boolean synchronizesVariablesWithBindings() { return true; } /** * Returns whether this component has a readable value that maps to the * specified binding. This implementation calls hasBinding(aBinding). */ public boolean canGetValueForBinding(String aBinding) { return hasBinding(aBinding); } /** * Returns whether this component has a writable value that maps to the * specified binding. */ public boolean canSetValueForBinding(String aBinding) { WOAssociation assoc = (WOAssociation) associations.objectForKey(aBinding); if (assoc != null) { if (assoc.isValueSettable()) return true; } return false; } /** * Returns whether this component has the specified binding. */ public boolean hasBinding(String aBinding) { if (associations == null) return false; return associations.containsKey(aBinding); } /** * Returns the value for the specified binding for this component. The parent * component is expected to have set the binding for this component. If no such * binding exists, the binding is treated as a property is and obtained using * valueForKey. If the property is not found, this method returns null. */ public Object valueForBinding(String aBinding) { WOComponent parent = parent(); if (associations != null) { WOAssociation assoc = (WOAssociation) associations.objectForKey(aBinding); if (assoc != null && parent != null) { return assoc.valueInComponent(parent); } } if (parent != null) { return parent.valueForKey(aBinding); } return null; } /** * Sets the value for the specified binding for this component. The parent * component is expected to have set the binding for this component. If no such * binding exists, the binding is treated as a property and is set using * takeValueForKey. If the property is not found, this method fails silently. */ public void setValueForBinding(Object aValue, String aBinding) { if (associations == null) return; WOComponent parent = parent(); if (associations != null) { WOAssociation assoc = (WOAssociation) associations.objectForKey(aBinding); if (assoc != null && parent != null) { if (assoc.isValueSettable()) { assoc.setValue(aValue, parent); return; } } } if (parent != null) { parent.takeValueForKey(aValue, aBinding); } } public static void logString(String aString) { System.out.println(aString); } public static void debugString(String aString) { System.err.println(aString); } public Object valueForKeyPath(String aPath) { // currently key value coding support also handles keypaths return valueForKey(aPath); } public void takeValueForKeyPath(Object aValue, String aPath) { // currently key value coding support also handles keypaths takeValueForKey(aValue, aPath); } public NSDictionary valuesForKeys(List aKeyList) { throw new RuntimeException("Not implemented yet."); } public void takeValuesFromDictionary(Map aValueMap) { throw new RuntimeException("Not implemented yet."); } public Object valueForKey(String aKey) { // System.out.println( "valueForKey: " + aKey + "->" + this ); // handle "^" property keys if (aKey.startsWith("^")) { return valueForBinding(aKey.substring(1)); } return EOKeyValueCodingSupport.valueForKey(this, aKey); } public void takeValueForKey(Object aValue, String aKey) { // System.out.println( "takeValueForKey: " + aKey + " : " // + aValue + "->" + this ); // handle "^" property keys if (aKey.startsWith("^")) { setValueForBinding(aValue, aKey.substring(1)); return; } EOKeyValueCodingSupport.takeValueForKey(this, aValue, aKey); } public Object storedValueForKey(String aKey) { return EOKeyValueCodingSupport.storedValueForKey(this, aKey); } public void takeStoredValueForKey(Object aValue, String aKey) { EOKeyValueCodingSupport.takeStoredValueForKey(this, aValue, aKey); } public Object handleQueryWithUnboundKey(String aKey) { return EOKeyValueCodingSupport.handleQueryWithUnboundKey(this, aKey); } public void handleTakeValueForUnboundKey(Object aValue, String aKey) { EOKeyValueCodingSupport.handleTakeValueForUnboundKey(this, aValue, aKey); } public void unableToSetNullForKey(String aKey) { EOKeyValueCodingSupport.unableToSetNullForKey(this, aKey); } public Object validateTakeValueForKeyPath(Object aValue, String aKey) { throw new RuntimeException("Not implemented yet."); } // Template Processing /** * Takes a template string and a location to begin parsing, looking only for * interesting tags, and calling itself recursively as necessary. Returns the * index to resume parsing, or -1 if done. */ static private int processTemplate(List elements, String template, int index, Map bindings, List aLanguageList) throws java.io.IOException { // System.out.println( "processTemplate: " + index ); if (template == null) return -1; int start = index; while (true) { // search for start of next tag start = template.indexOf('<', start); if (start == -1) { // if no tags, send output and return elements.add(new WOStaticElement(template.substring(index))); return -1; } // search for end of opening tag int end = template.indexOf(">", start + 1); if (end == -1) { // if no end to tag throw new RuntimeException("No end to tag: " + template.substring(start)); } boolean hasBody = true; if (template.charAt(end - 1) == '/') { // tag is standalone - no body end = end - 1; hasBody = false; } // search for name of tag int endName = start + 1; while (endName < end) { if (Character.isWhitespace(template.charAt(endName))) break; endName++; } String name = template.substring(start + 1, endName); if (name.toLowerCase().startsWith(OPEN_TAG)) { // add the contents before the tag // System.out.println( index + " : " + start + " : " + hasBody ); elements.add(new WOStaticElement(template.substring(index, start))); // interesting tag; parse parameters Map params = new HashMap(5); // arbitrary init length if (endName < end) { // delimit by whitespace StringTokenizer tokens = new StringTokenizer(template.substring(endName + 1, end)); int equals; String token; String value; while (tokens.hasMoreTokens()) { token = tokens.nextToken(); equals = token.indexOf('='); if (equals != -1) { value = token.substring(equals + 1); if (value.startsWith("\"")) { // handle spaces within parameter names while (!value.endsWith("\"")) { value = value + " " + tokens.nextToken(); } // strip quotation marks if (value.endsWith("\"")) { value = value.substring(1, value.length() - 1); } } // register key with specified value params.put(token.substring(0, equals).toLowerCase(), value); } else { // no value found, register the key name params.put(token.toLowerCase(), ""); } } } index = end + (hasBody ? 1 : 2); WOElement body = null; if (hasBody) { List childElements = new LinkedList(); index = processTemplate(childElements, template, index, bindings, aLanguageList); start = index; if (index == -1) { throw new RuntimeException("No closing tag found: " + template.substring(end)); } if (childElements.size() == 1) { body = (WOElement) childElements.get(0); } else { body = new WOParentElement(childElements); } } WOElement element = null; String nameProperty = (String) params.get(NAME_KEY); NSDictionary original = (NSDictionary) bindings.get(nameProperty); // System.out.println( nameProperty + " : " + associations ); if (original == null) { original = NSDictionary.EmptyDictionary; System.err.println("No associations for: " + nameProperty); System.err.println(bindings); } NSDictionary associations = new NSMutableDictionary(original); String elementClass = (String) associations.remove(WOApplication.ELEMENT_CLASS); WOApplication application = WOApplication.application(); element = application.dynamicElementWithName(elementClass, associations, body, aLanguageList); if (element == null) { // unable to create element: show assocs in static element element = new WOStaticElement(associations.toString()); } // System.out.println( element ); elements.add(element); if (!hasBody) { start = end + 2; } } else if (name.toLowerCase().startsWith(CLOSE_TAG)) { // add any contents before the tag elements.add(new WOStaticElement(template.substring(index, start))); // return end + name.length() + 1; // "<" + ">" - 1 = 1 return end + (hasBody ? 1 : 2); // "<" + ">" - 1 = 1 } else { // tag not interesting: continue start = end + (hasBody ? 1 : 2); } } } // Utility Methods static private void rewriteTag(String tagName, Map properties, String body, StringBuffer context) throws java.io.IOException { context.append("<" + tagName); Iterator it = properties.keySet().iterator(); String key; while (it.hasNext()) { key = (String) it.next(); context.append(" " + key + "=\"" + properties.get(key) + "\""); } if (body == null) { context.append("/>"); return; } context.append(">" + body + ""); } private String justTheClassName() { String className = getClass().getName(); int index = className.lastIndexOf("."); if (index == -1) return className; return className.substring(index + 1); } private String justTheResourcePath() { int last = -1; char[] src = getClass().getName().toCharArray(); char[] dst = new char[src.length]; for (int i = 0; i < src.length; i++) { if (src[i] == '.') { dst[i] = '/'; last = i; } else { dst[i] = src[i]; } } if (last == -1) return null; return new String(dst, 0, last); } private String readTemplateResource(String name, String framework, String suffix, NSArray languages) { if (name == null) return null; name = name + DIRECTORY_SUFFIX + '/' + name + suffix; InputStream is = null; if (isCachingEnabled()) { byte[] data = WOApplication.application().resourceManager().bytesForResourceNamed(name, framework, languages); if (data != null) { is = new ByteArrayInputStream(data); } } else { is = WOApplication.application().resourceManager().inputStreamForResourceNamed(name, framework, languages); } if (is == null) { System.err.println("No resources found for: " + name); return null; } // try to autodetect encoding String encoding = "ISO8859_1"; try { byte[] header = new byte[4]; is = new PushbackInputStream(is, 4); is.read(header); if (header[0] < 33 || header[0] > 126) { // if any funny characters, presume UTF-16 encoding = "UTF-16"; if (!(header[1] < 33 || header[1] > 126)) { // if second character is valid, presume UTF-8 encoding = "UTF-8"; } } // check byte-order-mark if (header[0] == 0xef && header[1] == 0xbb && header[2] == 0xbf) // utf-8 { encoding = "UTF-8"; } else if (header[0] == 0xfe && header[1] == 0xff) // utf-16 { encoding = "UTF-16"; } else if (header[0] == 0 && header[1] == 0 && header[2] == 0xfe && header[3] == 0xff) // ucs-4 { encoding = "UCS-4"; // ?? } else if (header[0] == 0xff && header[1] == 0xfe) // ucs-2le, ucs-4le, and { encoding = "UCS-16le"; // ?? } // put back the header ((PushbackInputStream) is).unread(header); } catch (Throwable t) { t.printStackTrace(); System.err.println("Error while autodetecting encoding: should never happen"); } try { String line; StringBuffer buf = new StringBuffer(); BufferedReader r = new BufferedReader(new InputStreamReader(is, encoding)); while ((line = r.readLine()) != null) { buf.append(line); buf.append('\n'); } is.close(); // release the resource return buf.toString(); } catch (IOException exc) { System.err.println("Error while reading: " + name); exc.printStackTrace(); return null; } } // Declaration Parsing /** * Parses the declarations in the specified content and returns a map of element * names to maps of attribute names to WOAssociations. */ private static NSDictionary processDeclaration(String content) { int index; NSMutableDictionary result = new NSMutableDictionary(); // strip out comments StringBuffer stripped = new StringBuffer(); try { LineNumberReader reader = new LineNumberReader(new StringReader(content)); String line; while ((line = reader.readLine()) != null) { index = line.indexOf("//"); while (index > -1) { // (chochos) This used to truncate lines with quoted URLs // in them. We have to check that the "//" is not inside quotes. boolean quoted = false; if (index > 0) { for (int _position = 0; _position < index; _position++) if (line.charAt(_position) == '"') quoted = !quoted; } if (!quoted) { line = line.substring(0, index); index = -1; } else { // if we didn't truncate the line it's because the // // were quoted. let's look for more, and check if they're not quoted... index = line.indexOf("\"", index); if (index > 0) { index = line.indexOf("//", index); } } } stripped.append(line); } } catch (IOException exc) { throw new RuntimeException("Error while stripping comments from declaration: " + stripped); } while ((index = stripped.toString().indexOf("/*")) != -1) { int j = stripped.toString().indexOf("*/", index + 1); if (j == -1) break; stripped.delete(index, j + 2); } String token; StringTokenizer tokens = new StringTokenizer(stripped.toString(), "{}", true); while (tokens.hasMoreTokens()) { token = tokens.nextToken(); // next token is the name and class String name, cl; index = token.indexOf(":"); if (index > -1) { name = token.substring(0, index).trim(); cl = token.substring(index + 1).trim(); } else { System.err.println("Could not parse declaration:"); System.err.println(content); throw new RuntimeException("Could not parse declaration: " + token); } // next token is the declaration for the name and class if (!tokens.hasMoreTokens()) { System.err.println("Could not find associations for declaration:"); System.err.println(content); throw new RuntimeException("Could not find associations for declaration: " + name); } token = tokens.nextToken(); if (token.equals("{")) { if (!tokens.hasMoreTokens()) throw new RuntimeException("Error parsing declaration: expected { but found: '" + token + "'"); token = tokens.nextToken(); } NSMutableDictionary associations = new NSMutableDictionary(); if (!token.equals("}")) { String line, key, value; StringTokenizer lines = new StringTokenizer(token, ";"); while (lines.hasMoreElements()) { line = lines.nextToken(); index = line.indexOf("="); if (index > -1) { if (line.length() == index + 1) line += " "; key = line.substring(0, index).trim(); value = line.substring(index + 1).trim(); } else { // not a valid key: skip key = null; value = null; } if (key != null) { // if in quotation marks if ((value.startsWith("\"")) && (value.endsWith("\""))) { // it's a constant value association value = value.substring(1, value.length() - 1); associations.put(key, WOAssociation.associationWithValue(value)); } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { // HACK: needed to be compatible with woextensions // apparently true and false are allowed without quotes associations.put(key, WOAssociation.associationWithValue(value)); } else { // HACK: needed to be compatible with woextensions: // apparently a standalone integer is allowed without quotes. try { Integer.parseInt(value); // does it parse? associations.put(key, WOAssociation.associationWithValue(value)); } catch (NumberFormatException nfe) { // did not parse: // it's a key path association associations.put(key, WOAssociation.associationWithKeyPath(value)); } } } } if (tokens.hasMoreTokens()) { token = tokens.nextToken(); if (!token.equals("}")) throw new RuntimeException("Error parsing declaration: expected } but found: '" + token + "'"); } } associations.put(WOApplication.ELEMENT_CLASS, cl); // store classname result.put(name, associations); } // System.out.println( "processDeclaration: " + result ); return result; } } /* * $Log$ Revision 1.2 2006/02/19 01:44:02 cgruber Add xmlrpc files Remove jclark * and replace with dom4j and javax.xml.sax stuff Re-work dependencies and * imports so it all compiles. * * Revision 1.1 2006/02/16 13:22:22 cgruber Check in all sources in * eclipse-friendly maven-enabled packages. * * Revision 1.32 2003/08/07 00:15:14 chochos general cleanup (mostly removing * unused imports) * * Revision 1.31 2003/07/24 00:23:21 chochos fixed problem with parsing wod * files that have //-type comments. Quotes URL's would be truncated. * * Revision 1.30 2003/03/28 15:33:11 mpowers Now using a PushBackInputStream for * auto detection of content encoding. No longer relying on markSupported() * since jar input streams don't have it. * * Revision 1.29 2003/03/03 16:41:52 mpowers Bad characters in cvs log. * * Revision 1.28 2003/03/03 16:37:35 mpowers Better handling for string * encodings. Now trying to autodetect unicode-formatted templates and * declarations. Now handlings block-style comments in declarations. * * Revision 1.27 2003/01/28 19:33:51 mpowers Implemented the rest of * WOResourceManager. Implemented support for java-style i18n. Components now * use the resource manager to load templates. * * Revision 1.26 2003/01/24 20:13:22 mpowers Now accepting immutable * NSDictionary in constructor, not Map. * * Revision 1.25 2003/01/21 17:53:45 mpowers Now correctly reporting error for * missing bindings. * * Revision 1.24 2003/01/20 17:50:11 mpowers Caught a loop condition when same * declaration was used twice. * * Revision 1.23 2003/01/19 22:33:25 mpowers Fixed problems with classpath and * dynamic class loading. Dynamic elements now pass on ensureAwakeInContext. * Parser how handles tags. * * Revision 1.22 2003/01/17 22:55:09 mpowers Straighted out the parent binding * issue (I think). Fixes for woextensions compatibility. * * Revision 1.21 2003/01/17 20:34:57 mpowers Better handling for components and * parents in the context's element stack. * * Revision 1.19 2003/01/17 15:32:22 mpowers Changes to better support generic * elements and containers. Now preserving newlines in templates. * * Revision 1.17 2003/01/16 22:47:30 mpowers Compatibility changes to support * compiling woextensions source. (34 out of 56 classes compile!) * * Revision 1.15 2003/01/16 15:50:43 mpowers More robust declaration parsing. * Subcomponents are now supported. dynamicElementWithName can now return * subcomponents. * * Revision 1.14 2003/01/15 19:50:49 mpowers Fixed issues with WOSession and * Serializable. Can now persist sessions between classloaders (hot swap of * class impls). * * Revision 1.13 2003/01/15 14:33:48 mpowers Refactoring: element id handling is * now confined to WOParentElement. Other elements/components should not have to * do element id incrementing. * * Revision 1.12 2003/01/14 16:05:12 mpowers Removed extraneous printlns. * * Revision 1.11 2003/01/13 22:24:25 mpowers Request-response cycle is working * with session and page persistence. * * Revision 1.10 2003/01/10 19:33:28 mpowers Added contextID for the component * url generation. * * Revision 1.9 2003/01/10 19:16:40 mpowers Implemented support for page * caching. * * Revision 1.8 2003/01/09 21:16:48 mpowers Bringing request-response cycle more * into conformance. * * Revision 1.7 2003/01/09 16:13:55 mpowers Implemented * WOComponentRequestHandler: Bringing the request-response cycle more into * conformance. * * Revision 1.6 2002/12/20 22:56:33 mpowers Reimplemented the template parsing * again. Nested components are now correctly parsed. ElementID numbering is now * working. * * Revision 1.3 2002/12/18 14:12:38 mpowers Support for differentiated request * handlers. Support url generation for WOContext and WORequest. * * Revision 1.2 2002/11/07 18:52:33 mpowers New components courtesy of * ezamudio@nasoft.com. Many thanks! * * Revision 1.1.1.1 2000/12/21 15:53:01 mpowers Contributing wotonomy. * * Revision 1.2 2000/12/20 16:25:49 michael Added log to all files. * * */