diff options
Diffstat (limited to 'projects/net.wotonomy.web/src/main/java/net/wotonomy')
61 files changed, 18516 insertions, 0 deletions
diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/ObservableArray.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/ObservableArray.java new file mode 100644 index 0000000..cef1372 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/ObservableArray.java @@ -0,0 +1,351 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +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.util.Collection; +import java.util.Iterator; +import java.util.List; + +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOObserving; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSRange; + +/** +* A package class that extends NSMutableArray but makes use +* of the fact that wotonomy's implementation extends ArrayList +* to intercept insertions and deletion and register and +* unregister objects for change notifications as appropriate. +* Since we can't be sure of ArrayList's implementation, we're +* forced to override each and every add and remove method, +* some of which probably call each other. However, +* EOObserverCenter will only register us once per object. +*/ +class ObservableArray extends NSMutableArray +{ + EOObserving observer; + + ObservableArray( EOObserving anObserver ) + { + observer = anObserver; + } + + /** + * Removes the last object from the array. + */ + public void removeLastObject () + { + remove( count() - 1 ); + } + + /** + * Removes the object at the specified index. + */ + public void removeObjectAtIndex (int index) + { + remove( index ); + } + + /** + * Adds all objects in the specified collection. + */ + public void addObjectsFromArray (Collection aCollection) + { + addAll( aCollection ); + } + + /** + * Removes all objects from the array. + */ + public void removeAllObjects () + { + clear(); + } + + /** + * Removes all objects equivalent to the specified object + * within the range of specified indices. + */ + public void removeObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject.equals( get( i ) ) ) + { + remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all instances of the specified object within the + * range of specified indices, comparing by reference. + */ + public void removeIdenticalObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject == get( i ) ) + { + remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all objects in the specified collection from the array. + */ + public void removeObjectsInArray (Collection aCollection) + { + removeAll( aCollection ); + } + + /** + * Removes all objects in the indices within the specified range + * from the array. + */ + public void removeObjectsInRange (NSRange aRange) + { + if ( aRange == null ) return; + + for ( int i = 0; i < aRange.length(); i++ ) + { + remove( aRange.location() ); + } + } + + /** + * Replaces objects in the current range with objects from + * the specified range of the specified array. If currentRange + * is larger than otherRange, the extra objects are removed. + * If otherRange is larger than currentRange, the extra objects + * are added. + */ + public void replaceObjectsInRange (NSRange currentRange, + List otherArray, NSRange otherRange) + { + if ( ( currentRange == null ) || ( otherArray == null ) || + ( otherRange == null ) ) return; + + // transform otherRange if out of bounds for array + if ( otherRange.maxRange() > otherArray.size() ) + { + // TODO: Test this logic. + int loc = Math.min( otherRange.location(), otherArray.size() - 1 ); + otherRange = new NSRange( loc, otherArray.size() - loc ); + } + + Object o; + List subList = subList( + currentRange.location(), currentRange.maxRange() ); + int otherIndex = otherRange.location(); + // TODO: Test this logic. + for ( int i = 0; i < subList.size(); i++ ) + { + if ( otherIndex < otherRange.maxRange() ) + { // set object + subList.set( i, otherArray.get( otherIndex ) ); + } + else + { // remove extra elements from currentRange + subList.remove( i ); + i--; + } + otherIndex++; + } + // TODO: Test this logic. + for ( int i = otherIndex; i < otherRange.maxRange(); i++ ) + { + add( otherArray.get( i ) ); + } + } + + /** + * Clears the current array and then populates it with the + * contents of the specified collection. + */ + public void setArray (Collection aCollection) + { + clear(); + addAll( aCollection ); + } + + /** + * Removes all objects equivalent to the specified object. + */ + public void removeObject (Object anObject) + { + remove( anObject ); + } + + /** + * Removes all occurences of the specified object, + * comparing by reference. + */ + public void removeIdenticalObject (Object anObject) + { + EOObserverCenter.removeObserver( observer, anObject ); + super.removeIdenticalObject( anObject ); + } + + /** + * Inserts the specified object into this array at the + * specified index. + */ + public void insertObjectAtIndex (Object anObject, int anIndex) + { + add( anIndex, anObject ); + } + + /** + * Replaces the object at the specified index with the + * specified object. + */ + public void replaceObjectAtIndex (int anIndex, Object anObject) + { + set( anIndex, anObject ); + } + + /** + * Adds the specified object to the end of this array. + */ + public void addObject (Object anObject) + { + add( anObject ); + } + + // interface List: mutators + + public void add(int index, Object element) + { + EOObserverCenter.addObserver( observer, element ); + super.add( index, element ); + } + + public boolean add(Object o) + { + EOObserverCenter.addObserver( observer, o ); + return super.add(o); + } + + public boolean addAll(Collection coll) + { + Iterator it = coll.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.addObserver( observer, it.next() ); + } + return super.addAll(coll); + } + + public boolean addAll(int index, Collection c) + { + Iterator it = c.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.addObserver( observer, it.next() ); + } + return super.addAll( index, c ); + } + + public void clear() + { + Iterator it = iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.removeObserver( observer, it.next() ); + } + super.clear(); + } + + public Object remove(int index) + { + EOObserverCenter.removeObserver( observer, get(index) ); + return super.remove( index ); + } + + public boolean remove(Object o) + { + EOObserverCenter.removeObserver( observer, o ); + return super.remove(o); + } + + public boolean removeAll(Collection coll) + { + Iterator it = coll.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.removeObserver( observer, it.next() ); + } + return super.removeAll(coll); + } + + public boolean retainAll(Collection coll) + { + throw new UnsupportedOperationException(); + } + + public Object set(int index, Object element) + { + EOObserverCenter.removeObserver( observer, get(index) ); + EOObserverCenter.addObserver( observer, element ); + return super.set( index, element ); + } +} + +/* + * $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.1 2003/01/18 23:31:32 mpowers + * Needed for WODisplayGroup. + * + * Revision 1.2 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.1 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.1 2001/01/24 14:37:24 mpowers + * Contributing a delegate useful for debugging. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/URI.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/URI.java new file mode 100644 index 0000000..41f77f5 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/URI.java @@ -0,0 +1,3464 @@ +/* + * $Header$ + * $Revision: 905 $ + * $Date: 2006-02-18 20:44:03 -0500 (Sat, 18 Feb 2006) $ + * + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2002 the Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "HttpClient", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +// excellent class borrowed from Apache Commons project: +//package org.apache.commons.httpclient; + +package net.wotonomy.web; + +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.security.AccessController; +import java.util.BitSet; +import java.util.Hashtable; +import java.util.Locale; + +import sun.security.action.GetPropertyAction; + +/** + * The interface for the URI(Uniform Resource Identifiers) version of RFC 2396. + * This class has the purpose of supportting of parsing a URI reference to + * extend any specific protocols, the character encoding of the protocol to + * be transported and the charset of the document. + * <p> + * A URI is always in an "escaped" form, since escaping or unescaping a + * completed URI might change its semantics. + * <p> + * Implementers should be careful not to escape or unescape the same string + * more than once, since unescaping an already unescaped string might lead to + * misinterpreting a percent data character as another escaped character, + * or vice versa in the case of escaping an already escaped string. + * <p> + * In order to avoid these problems, data types used as follows: + * <p><blockquote><pre> + * URI character sequence: char + * octet sequence: byte + * original character sequence: String + * </pre></blockquote><p> + * + * So, a URI is a sequence of characters as an array of a char type, which + * is not always represented as a sequence of octets as an array of byte. + * <p> + * + * URI Syntactic Components + * <p><blockquote><pre> + * - In general, written as follows: + * Absolute URI = <scheme>:<scheme-specific-part> + * Generic URI = <scheme>://<authority><path>?<query> + * + * - Syntax + * absoluteURI = scheme ":" ( hier_part | opaque_part ) + * hier_part = ( net_path | abs_path ) [ "?" query ] + * net_path = "//" authority [ abs_path ] + * abs_path = "/" path_segments + * </pre></blockquote><p> + * + * The following examples illustrate URI that are in common use. + * <pre> + * ftp://ftp.is.co.za/rfc/rfc1808.txt + * -- ftp scheme for File Transfer Protocol services + * gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles + * -- gopher scheme for Gopher and Gopher+ Protocol services + * http://www.math.uio.no/faq/compression-faq/part1.html + * -- http scheme for Hypertext Transfer Protocol services + * mailto:mduerst@ifi.unizh.ch + * -- mailto scheme for electronic mail addresses + * news:comp.infosystems.www.servers.unix + * -- news scheme for USENET news groups and articles + * telnet://melvyl.ucop.edu/ + * -- telnet scheme for interactive services via the TELNET Protocol + * </pre> + * Please, notice that there are many modifications from URL(RFC 1738) and + * relative URL(RFC 1808). + * <p> + * <b>The expressions for a URI</b> + * <p><pre> + * For escaped URI forms + * - URI(char[]) // constructor + * - char[] getRawXxx() // method + * - String getEscapedXxx() // method + * - String toString() // method + * <p> + * For unescaped URI forms + * - URI(String) // constructor + * - String getXXX() // method + * </pre><p> + * + * @author <a href="mailto:jericho@apache.org">Sung-Gu</a> + * @version $Revision: 905 $ $Date: 2002/03/14 15:14:01 + */ +class URI implements Cloneable, Comparable, Serializable { + + + // ----------------------------------------------------------- Constructors + + protected URI() { + } + + /** + * Construct a URI as an escaped form of a character array. + * An URI can be placed within double-quotes or angle brackets like + * "http://test.com/" and <http://test.com/> + * + * @param escaped the URI character sequence + * @exception IOException + * @throws NullPointerException if <code>escaped</code> is <code>null</code> + */ + public URI(char[] escaped) throws IOException { + parseUriReference(new String(escaped), true); + } + + + /** + * Construct a URI from the given string. + * <p><blockquote><pre> + * URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + * </pre></blockquote><p> + * An URI can be placed within double-quotes or angle brackets like + * "http://test.com/" and <http://test.com/> + * + * @param original the string to be represented to URI character sequence + * It is one of absoluteURI and relativeURI. + * @exception IOException + */ + public URI(String original) throws IOException { + parseUriReference(original, false); + } + + /** + * Construct a URI from a URL. + * + * @param url a valid URL. + * @throws IOException + * @since 2.0 + */ + public URI(URL url) throws IOException { + this(url.toString()); + } + + + /** + * Construct a general URI from the given components. + * <p><blockquote><pre> + * URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + * absoluteURI = scheme ":" ( hier_part | opaque_part ) + * opaque_part = uric_no_slash *uric + * </pre></blockquote><p> + * It's for absolute URI = <scheme>:<scheme-specific-part># + * <fragment>. + * + * @param scheme the scheme string + * @param scheme_specific_part scheme_specific_part + * @param fragment the fragment string + * @exception IOException + */ + public URI(String scheme, String scheme_specific_part, String fragment) + throws IOException { + + // validate and contruct the URI character sequence + if (scheme == null) { + throw new IOException(/*IOException.PARSING,*/ "URI: scheme required"); + } + char[] s = scheme.toLowerCase().toCharArray(); + if (validate(s, URI.scheme)) { + _scheme = s; // is_absoluteURI + } else { + throw new IOException(/*IOException.PARSING,*/ "URI: incorrect scheme"); + } + _opaque = encode(scheme_specific_part, allowed_opaque_part); + // Set flag + _is_opaque_part = true; + setUriReference(); + } + + + /** + * Construct a general URI from the given components. + * <p><blockquote><pre> + * URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + * absoluteURI = scheme ":" ( hier_part | opaque_part ) + * relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + * hier_part = ( net_path | abs_path ) [ "?" query ] + * </pre></blockquote><p> + * It's for absolute URI = <scheme>:<path>?<query>#< + * fragment> and relative URI = <path>?<query>#<fragment + * >. + * + * @param scheme the scheme string + * @param authority the authority string + * @param path the path string + * @param query the query string + * @param fragment the fragment string + * @exception IOException + */ + public URI(String scheme, String authority, String path, String query, + String fragment) throws IOException { + + // validate and contruct the URI character sequence + StringBuffer buff = new StringBuffer(); + if (scheme != null) { + buff.append(scheme); + buff.append(':'); + } + if (authority != null) { + buff.append("//"); + buff.append(authority); + } + if (path != null) { // accept empty path + if ((scheme != null || authority != null) + && !path.startsWith("/")) { + throw new IOException(/*IOException.PARSING*,*/ + "URI: abs_path requested"); + } + buff.append(path); + } + if (query != null) { + buff.append('?'); + buff.append(query); + } + if (fragment != null) { + buff.append('#'); + buff.append(fragment); + } + parseUriReference(buff.toString(), false); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param userinfo the userinfo string + * @param host the host string + * @param port the port number + * @exception IOException + */ + public URI(String scheme, String userinfo, String host, int port) + throws IOException { + + this(scheme, userinfo, host, port, null, null, null); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param userinfo the userinfo string + * @param host the host string + * @param port the port number + * @param path the path string + * @exception IOException + */ + public URI(String scheme, String userinfo, String host, int port, + String path) throws IOException { + + this(scheme, userinfo, host, port, path, null, null); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param userinfo the userinfo string + * @param host the host string + * @param port the port number + * @param path the path string + * @param query the query string + * @exception IOException + */ + public URI(String scheme, String userinfo, String host, int port, + String path, String query) throws IOException { + + this(scheme, userinfo, host, port, path, query, null); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param userinfo the userinfo string + * @param host the host string + * @param port the port number + * @param path the path string + * @param query the query string + * @param fragment the fragment string + * @exception IOException + */ + public URI(String scheme, String userinfo, String host, int port, + String path, String query, String fragment) throws IOException { + + this(scheme, (host == null) ? null : + ((userinfo != null) ? userinfo + '@' : "") + host + + ((port != -1) ? ":" + port : ""), path, query, fragment); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param host the host string + * @param path the path string + * @param fragment the fragment string + * @exception IOException + */ + public URI(String scheme, String host, String path, String fragment) + throws IOException { + + this(scheme, host, path, null, fragment); + } + + + /** + * Construct a general URI with the given relative URI string. + * + * @param base the base URI + * @param relative the relative URI string + * @exception IOException + */ + public URI(URI base, String relative) throws IOException { + this(base, new URI(relative)); + } + + + /** + * Construct a general URI with the given relative URI. + * <p><blockquote><pre> + * URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + * relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + * </pre></blockquote><p> + * Resolving Relative References to Absolute Form. + * + * <strong>Examples of Resolving Relative URI References</strong> + * + * Within an object with a well-defined base URI of + * <p><blockquote><pre> + * http://a/b/c/d;p?q + * </pre></blockquote><p> + * the relative URI would be resolved as follows: + * + * Normal Examples + * + * <p><blockquote><pre> + * g:h = g:h + * g = http://a/b/c/g + * ./g = http://a/b/c/g + * g/ = http://a/b/c/g/ + * /g = http://a/g + * //g = http://g + * ?y = http://a/b/c/?y + * g?y = http://a/b/c/g?y + * #s = (current document)#s + * g#s = http://a/b/c/g#s + * g?y#s = http://a/b/c/g?y#s + * ;x = http://a/b/c/;x + * g;x = http://a/b/c/g;x + * g;x?y#s = http://a/b/c/g;x?y#s + * . = http://a/b/c/ + * ./ = http://a/b/c/ + * .. = http://a/b/ + * ../ = http://a/b/ + * ../g = http://a/b/g + * ../.. = http://a/ + * ../../ = http://a/ + * ../../g = http://a/g + * </pre></blockquote><p> + * + * Some URI schemes do not allow a hierarchical syntax matching the + * <hier_part> syntax, and thus cannot use relative references. + * + * @param base the base URI + * @param relative the relative URI + * @exception IOException + */ + public URI(URI base, URI relative) throws IOException { + + if (base._scheme == null) { + throw new IOException(/* IOException.PARSING,*/ "URI: base URI required"); + } + if (base._scheme != null) { + this._scheme = base._scheme; + this._authority = base._authority; + } + if (base._is_opaque_part || relative._is_opaque_part) { + this._scheme = base._scheme; + this._is_opaque_part = relative._is_opaque_part; + this._opaque = relative._opaque; + this._fragment = relative._fragment; + this.setUriReference(); + return; + } + if (relative._scheme != null) { + this._scheme = relative._scheme; + this._is_net_path = relative._is_net_path; + this._authority = relative._authority; + if (relative._is_server) { + this._userinfo = relative._userinfo; + this._host = relative._host; + this._port = relative._port; + } else if (relative._is_reg_name) { + this._is_reg_name = relative._is_reg_name; + } + this._is_abs_path = relative._is_abs_path; + this._is_rel_path = relative._is_rel_path; + this._path = relative._path; + } else if (base._authority != null && relative._scheme == null) { + this._is_net_path = base._is_net_path; + this._authority = base._authority; + if (base._is_server) { + this._userinfo = base._userinfo; + this._host = base._host; + this._port = base._port; + } else if (base._is_reg_name) { + this._is_reg_name = base._is_reg_name; + } + } + if (relative._authority != null) { + this._is_net_path = relative._is_net_path; + this._authority = relative._authority; + if (relative._is_server) { + this._is_server = relative._is_server; + this._userinfo = relative._userinfo; + this._host = relative._host; + this._port = relative._port; + } else if (relative._is_reg_name) { + this._is_reg_name = relative._is_reg_name; + } + this._is_abs_path = relative._is_abs_path; + this._is_rel_path = relative._is_rel_path; + this._path = relative._path; + } + // resolve the path + if (relative._scheme == null && relative._authority == null || + equals(base._scheme, relative._scheme)) { + this._path = resolvePath(base._path, relative._path); + } + // base._query removed + if (relative._query != null) { + this._query = relative._query; + } + // base._fragment removed + if (relative._fragment != null) { + this._fragment = relative._fragment; + } + this.setUriReference(); + } + + // --------------------------------------------------- Instance Variables + + static final long serialVersionUID = 604752400577948726L; + + + /** + * This Uniform Resource Identifier (URI). + * The URI is always in an "escaped" form, since escaping or unescaping + * a completed URI might change its semantics. + */ + protected char[] _uri = null; + + + /** + * The default charset of the protocol. RFC 2277, 2396 + */ + protected static String _protocolCharset = "UTF-8"; + + + /** + * The default charset of the document. RFC 2277, 2396 + * The platform's charset is used for the document by default. + */ + protected static String _documentCharset = null; + // Static initializer for _documentCharset + static { + Locale locale = Locale.getDefault(); + if (locale != null) { + // in order to support backward compatiblity + _documentCharset = LocaleToCharsetMap.getCharset(locale); + } else { + _documentCharset = (String)AccessController.doPrivileged( + new GetPropertyAction("file.encoding")); + } + } + + /** + * The scheme. + */ + protected char[] _scheme = null; + + + /** + * The opaque. + */ + protected char[] _opaque = null; + + + /** + * The authority. + */ + protected char[] _authority = null; + + + /** + * The userinfo. + */ + protected char[] _userinfo = null; + + + /** + * The host. + */ + protected char[] _host = null; + + + /** + * The port. + */ + protected int _port = -1; + + + /** + * The path. + */ + protected char[] _path = null; + + + /** + * The query. + */ + protected char[] _query = null; + + + /** + * The fragment. + */ + protected char[] _fragment = null; + + + /** + * The root path. + */ + protected static char[] rootPath = { '/' }; + + // ---------------------- Generous characters for each component validation + + /** + * The percent "%" character always has the reserved purpose of being the + * escape indicator, it must be escaped as "%25" in order to be used as + * data within a URI. + */ + protected static final BitSet percent = new BitSet(256); + // Static initializer for percent + static { + percent.set('%'); + } + + + /** + * BitSet for digit. + * <p><blockquote><pre> + * digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | + * "8" | "9" + * </pre></blockquote><p> + */ + protected static final BitSet digit = new BitSet(256); + // Static initializer for digit + static { + for(int i = '0'; i <= '9'; i++) { + digit.set(i); + } + } + + + /** + * BitSet for alpha. + * <p><blockquote><pre> + * alpha = lowalpha | upalpha + * </pre></blockquote><p> + */ + protected static final BitSet alpha = new BitSet(256); + // Static initializer for alpha + static { + for (int i = 'a'; i <= 'z'; i++) { + alpha.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + alpha.set(i); + } + } + + + /** + * BitSet for alphanum (join of alpha & digit). + * <p><blockquote><pre> + * alphanum = alpha | digit + * </pre></blockquote><p> + */ + protected static final BitSet alphanum = new BitSet(256); + // Static initializer for alphanum + static { + alphanum.or(alpha); + alphanum.or(digit); + } + + + /** + * BitSet for hex. + * <p><blockquote><pre> + * hex = digit | "A" | "B" | "C" | "D" | "E" | "F" | + * "a" | "b" | "c" | "d" | "e" | "f" + * </pre></blockquote><p> + */ + protected static final BitSet hex = new BitSet(256); + // Static initializer for hex + static { + hex.or(digit); + for(int i = 'a'; i <= 'f'; i++) { + hex.set(i); + } + for(int i = 'A'; i <= 'F'; i++) { + hex.set(i); + } + } + + + /** + * BitSet for escaped. + * <p><blockquote><pre> + * escaped = "%" hex hex + * </pre></blockquote><p> + */ + protected static final BitSet escaped = new BitSet(256); + // Static initializer for escaped + static { + escaped.or(percent); + escaped.or(hex); + } + + + /** + * BitSet for mark. + * <p><blockquote><pre> + * mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | + * "(" | ")" + * </pre></blockquote><p> + */ + protected static final BitSet mark = new BitSet(256); + // Static initializer for mark + static { + mark.set('-'); + mark.set('_'); + mark.set('.'); + mark.set('!'); + mark.set('~'); + mark.set('*'); + mark.set('\''); + mark.set('('); + mark.set(')'); + } + + + /** + * Data characters that are allowed in a URI but do not have a reserved + * purpose are called unreserved. + * <p><blockquote><pre> + * unreserved = alphanum | mark + * </pre></blockquote><p> + */ + protected static final BitSet unreserved = new BitSet(256); + // Static initializer for unreserved + static { + unreserved.or(alphanum); + unreserved.or(mark); + } + + + /** + * BitSet for reserved. + * <p><blockquote><pre> + * reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + * "$" | "," + * </pre></blockquote><p> + */ + protected static final BitSet reserved = new BitSet(256); + // Static initializer for reserved + static { + reserved.set(';'); + reserved.set('/'); + reserved.set('?'); + reserved.set(':'); + reserved.set('@'); + reserved.set('&'); + reserved.set('='); + reserved.set('+'); + reserved.set('$'); + reserved.set(','); + } + + + /** + * BitSet for uric. + * <p><blockquote><pre> + * uric = reserved | unreserved | escaped + * </pre></blockquote><p> + */ + protected static final BitSet uric = new BitSet(256); + // Static initializer for uric + static { + uric.or(reserved); + uric.or(unreserved); + uric.or(escaped); + } + + + /** + * BitSet for fragment (alias for uric). + * <p><blockquote><pre> + * fragment = *uric + * </pre></blockquote><p> + */ + protected static final BitSet fragment = uric; + + + /** + * BitSet for query (alias for uric). + * <p><blockquote><pre> + * query = *uric + * </pre></blockquote><p> + */ + protected static final BitSet query = uric; + + + /** + * BitSet for pchar. + * <p><blockquote><pre> + * pchar = unreserved | escaped | + * ":" | "@" | "&" | "=" | "+" | "$" | "," + * </pre></blockquote><p> + */ + protected static final BitSet pchar = new BitSet(256); + // Static initializer for pchar + static { + pchar.or(unreserved); + pchar.or(escaped); + pchar.set(':'); + pchar.set('@'); + pchar.set('&'); + pchar.set('='); + pchar.set('+'); + pchar.set('$'); + pchar.set(','); + } + + + /** + * BitSet for param (alias for pchar). + * <p><blockquote><pre> + * param = *pchar + * </pre></blockquote><p> + */ + protected static final BitSet param = pchar; + + + /** + * BitSet for segment. + * <p><blockquote><pre> + * segment = *pchar *( ";" param ) + * </pre></blockquote><p> + */ + protected static final BitSet segment = new BitSet(256); + // Static initializer for segment + static { + segment.or(pchar); + segment.set(';'); + segment.or(param); + } + + + /** + * BitSet for path segments. + * <p><blockquote><pre> + * path_segments = segment *( "/" segment ) + * </pre></blockquote><p> + */ + protected static final BitSet path_segments = new BitSet(256); + // Static initializer for path_segments + static { + path_segments.set('/'); + path_segments.or(segment); + } + + + /** + * URI absolute path. + * <p><blockquote><pre> + * abs_path = "/" path_segments + * </pre><blockquote><p> + */ + protected static final BitSet abs_path = new BitSet(256); + // Static initializer for abs_path + static { + abs_path.set('/'); + abs_path.or(path_segments); + } + + + /** + * URI bitset for encoding typical non-slash characters. + * <p><blockquote><pre> + * uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | + * "&" | "=" | "+" | "$" | "," + * </pre></blockquote><p> + */ + protected static final BitSet uric_no_slash = new BitSet(256); + // Static initializer for uric_no_slash + static { + uric_no_slash.or(unreserved); + uric_no_slash.or(escaped); + uric_no_slash.set(';'); + uric_no_slash.set('?'); + uric_no_slash.set(';'); + uric_no_slash.set('@'); + uric_no_slash.set('&'); + uric_no_slash.set('='); + uric_no_slash.set('+'); + uric_no_slash.set('$'); + uric_no_slash.set(','); + } + + + /** + * URI bitset that combines uric_no_slash and uric. + * <p><blockquote><pre> + * opaque_part = uric_no_slash *uric + * </pre></blockquote><p> + */ + protected static final BitSet opaque_part = new BitSet(256); + // Static initializer for opaque_part + static { + opaque_part.or(uric_no_slash); + opaque_part.or(uric); + } + + + /** + * URI bitset that combines absolute path and opaque part. + * <p><blockquote><pre> + * path = [ abs_path | opaque_part ] + * </pre></blockquote><p> + */ + protected static final BitSet path = new BitSet(256); + // Static initializer for path + static { + path.or(abs_path); + path.or(opaque_part); + } + + + /** + * Port, a logical alias for digit. + */ + protected static final BitSet port = digit; + + + /** + * Bitset that combines digit and dot fo IPv$address. + * <p><blockquote><pre> + * IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit + * </pre></blockquote><p> + */ + protected static final BitSet IPv4address = new BitSet(256); + // Static initializer for IPv4address + static { + IPv4address.or(digit); + IPv4address.set('.'); + } + + + /** + * RFC 2373. + * <p><blockquote><pre> + * IPv6address = hexpart [ ":" IPv4address ] + * </pre></blockquote><p> + */ + protected static final BitSet IPv6address = new BitSet(256); + // Static initializer for IPv6address reference + static { + IPv6address.or(hex); // hexpart + IPv6address.set(':'); + IPv6address.or(IPv4address); + } + + + /** + * RFC 2732, 2373. + * <p><blockquote><pre> + * IPv6reference = "[" IPv6address "]" + * </pre></blockquote><p> + */ + protected static final BitSet IPv6reference = new BitSet(256); + // Static initializer for IPv6reference + static { + IPv6reference.set('['); + IPv6reference.or(IPv6address); + IPv6reference.set(']'); + } + + + /** + * BitSet for toplabel. + * <p><blockquote><pre> + * toplabel = alpha | alpha *( alphanum | "-" ) alphanum + * </pre></blockquote><p> + */ + protected static final BitSet toplabel = new BitSet(256); + // Static initializer for toplabel + static { + toplabel.or(alphanum); + toplabel.set('-'); + } + + + /** + * BitSet for domainlabel. + * <p><blockquote><pre> + * domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + * </pre></blockquote><p> + */ + protected static final BitSet domainlabel = toplabel; + + + /** + * BitSet for hostname. + * <p><blockquote><pre> + * hostname = *( domainlabel "." ) toplabel [ "." ] + * </pre></blockquote><p> + */ + protected static final BitSet hostname = new BitSet(256); + // Static initializer for hostname + static { + hostname.or(toplabel); + // hostname.or(domainlabel); + hostname.set('.'); + } + + + /** + * BitSet for host. + * <p><blockquote><pre> + * host = hostname | IPv4address | IPv6reference + * </pre></blockquote><p> + */ + protected static final BitSet host = new BitSet(256); + // Static initializer for host + static { + host.or(hostname); + // host.or(IPv4address); + host.or(IPv6reference); // IPv4address + } + + + /** + * BitSet for hostport. + * <p><blockquote><pre> + * hostport = host [ ":" port ] + * </pre></blockquote><p> + */ + protected static final BitSet hostport = new BitSet(256); + // Static initializer for hostport + static { + hostport.or(host); + hostport.set(':'); + hostport.or(port); + } + + + /** + * Bitset for userinfo. + * <p><blockquote><pre> + * userinfo = *( unreserved | escaped | + * ";" | ":" | "&" | "=" | "+" | "$" | "," ) + * </pre></blockquote><p> + */ + protected static final BitSet userinfo = new BitSet(256); + // Static initializer for userinfo + static { + userinfo.or(unreserved); + userinfo.or(escaped); + userinfo.set(';'); + userinfo.set(':'); + userinfo.set('&'); + userinfo.set('='); + userinfo.set('+'); + userinfo.set('$'); + userinfo.set(','); + } + + + /** + * BitSet for within the userinfo component like user and password. + */ + public static final BitSet within_userinfo = new BitSet(256); + // Static initializer for within_userinfo + static { + within_userinfo.or(userinfo); + within_userinfo.clear(';'); // reserved within authority + within_userinfo.clear(':'); + within_userinfo.clear('@'); + within_userinfo.clear('?'); + within_userinfo.clear('/'); + } + + + /** + * Bitset for server. + * <p><blockquote><pre> + * server = [ [ userinfo "@" ] hostport ] + * </pre></blockquote><p> + */ + protected static final BitSet server = new BitSet(256); + // Static initializer for server + static { + server.or(userinfo); + server.set('@'); + server.or(hostport); + } + + + /** + * BitSet for reg_name. + * <p><blockquote><pre> + * reg_name = 1*( unreserved | escaped | "$" | "," | + * ";" | ":" | "@" | "&" | "=" | "+" ) + * </pre></blockquote><p> + */ + protected static final BitSet reg_name = new BitSet(256); + // Static initializer for reg_name + static { + reg_name.or(unreserved); + reg_name.or(escaped); + reg_name.set('$'); + reg_name.set(','); + reg_name.set(';'); + reg_name.set(':'); + reg_name.set('@'); + reg_name.set('&'); + reg_name.set('='); + reg_name.set('+'); + } + + + /** + * BitSet for authority. + * <p><blockquote><pre> + * authority = server | reg_name + * </pre></blockquote><p> + */ + protected static final BitSet authority = new BitSet(256); + // Static initializer for authority + static { + authority.or(server); + authority.or(reg_name); + } + + + /** + * BitSet for scheme. + * <p><blockquote><pre> + * scheme = alpha *( alpha | digit | "+" | "-" | "." ) + * </pre></blockquote><p> + */ + protected static final BitSet scheme = new BitSet(256); + // Static initializer for scheme + static { + scheme.or(alpha); + scheme.or(digit); + scheme.set('+'); + scheme.set('-'); + scheme.set('.'); + } + + + /** + * BitSet for rel_segment. + * <p><blockquote><pre> + * rel_segment = 1*( unreserved | escaped | + * ";" | "@" | "&" | "=" | "+" | "$" | "," ) + * </pre></blockquote><p> + */ + protected static final BitSet rel_segment = new BitSet(256); + // Static initializer for rel_segment + static { + rel_segment.or(unreserved); + rel_segment.or(escaped); + rel_segment.set(';'); + rel_segment.set('@'); + rel_segment.set('&'); + rel_segment.set('='); + rel_segment.set('+'); + rel_segment.set('$'); + rel_segment.set(','); + } + + + /** + * BitSet for rel_path. + * <p><blockquote><pre> + * rel_path = rel_segment [ abs_path ] + * </pre></blockquote><p> + */ + protected static final BitSet rel_path = new BitSet(256); + // Static initializer for rel_path + static { + rel_path.or(rel_segment); + rel_path.or(abs_path); + } + + + /** + * BitSet for net_path. + * <p><blockquote><pre> + * net_path = "//" authority [ abs_path ] + * </pre></blockquote><p> + */ + protected static final BitSet net_path = new BitSet(256); + // Static initializer for net_path + static { + net_path.set('/'); + net_path.or(authority); + net_path.or(abs_path); + } + + + /** + * BitSet for hier_part. + * <p><blockquote><pre> + * hier_part = ( net_path | abs_path ) [ "?" query ] + * </pre></blockquote><p> + */ + protected static final BitSet hier_part = new BitSet(256); + // Static initializer for hier_part + static { + hier_part.or(net_path); + hier_part.or(abs_path); + // hier_part.set('?'); aleady included + hier_part.or(query); + } + + + /** + * BitSet for relativeURI. + * <p><blockquote><pre> + * relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + * </pre></blockquote><p> + */ + protected static final BitSet relativeURI = new BitSet(256); + // Static initializer for relativeURI + static { + relativeURI.or(net_path); + relativeURI.or(abs_path); + relativeURI.or(rel_path); + // relativeURI.set('?'); aleady included + relativeURI.or(query); + } + + + /** + * BitSet for absoluteURI. + * <p><blockquote><pre> + * absoluteURI = scheme ":" ( hier_part | opaque_part ) + * </pre></blockquote><p> + */ + protected static final BitSet absoluteURI = new BitSet(256); + // Static initializer for absoluteURI + static { + absoluteURI.or(scheme); + absoluteURI.set(':'); + absoluteURI.or(hier_part); + absoluteURI.or(opaque_part); + } + + + /** + * BitSet for URI-reference. + * <p><blockquote><pre> + * URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + * </pre></blockquote><p> + */ + protected static final BitSet URI_reference = new BitSet(256); + // Static initializer for URI_reference + static { + URI_reference.or(absoluteURI); + URI_reference.or(relativeURI); + URI_reference.set('#'); + URI_reference.or(fragment); + } + + // ---------------------------- Characters disallowed within the URI syntax + // Excluded US-ASCII Characters are like control, space, delims and unwise + + /** + * BitSet for control. + */ + public static final BitSet control = new BitSet(256); + // Static initializer for control + static { + for (int i = 0; i <= 0x1F; i++) { + control.set(i); + } + control.set(0x7F); + } + + /** + * BitSet for space. + */ + public static final BitSet space = new BitSet(256); + // Static initializer for space + static { + space.set(0x20); + } + + + /** + * BitSet for delims. + */ + public static final BitSet delims = new BitSet(256); + // Static initializer for delims + static { + delims.set('<'); + delims.set('>'); + delims.set('#'); + delims.set('%'); + delims.set('"'); + } + + + /** + * BitSet for unwise. + */ + public static final BitSet unwise = new BitSet(256); + // Static initializer for unwise + static { + unwise.set('{'); + unwise.set('}'); + unwise.set('|'); + unwise.set('\\'); + unwise.set('^'); + unwise.set('['); + unwise.set(']'); + unwise.set('`'); + } + + + /** + * Disallowed rel_path before escaping. + */ + public static final BitSet disallowed_rel_path = new BitSet(256); + // Static initializer for disallowed_rel_path + static { + disallowed_rel_path.or(uric); + disallowed_rel_path.andNot(rel_path); + } + + + /** + * Disallowed opaque_part before escaping. + */ + public static final BitSet disallowed_opaque_part = new BitSet(256); + // Static initializer for disallowed_opaque_part + static { + disallowed_opaque_part.or(uric); + disallowed_opaque_part.andNot(opaque_part); + } + + // ----------------------- Characters allowed within and for each component + + /** + * Those characters that are allowed for the authority component. + */ + public static final BitSet allowed_authority = new BitSet(256); + // Static initializer for allowed_authority + static { + allowed_authority.or(authority); + allowed_authority.clear('%'); + } + + + /** + * Those characters that are allowed for the opaque_part. + */ + public static final BitSet allowed_opaque_part = new BitSet(256); + // Static initializer for allowed_opaque_part + static { + allowed_opaque_part.or(opaque_part); + allowed_opaque_part.clear('%'); + } + + + /** + * Those characters that are allowed for the reg_name. + */ + public static final BitSet allowed_reg_name = new BitSet(256); + // Static initializer for allowed_reg_name + static { + allowed_reg_name.or(reg_name); + // allowed_reg_name.andNot(percent); + allowed_reg_name.clear('%'); + } + + + /** + * Those characters that are allowed for the userinfo component. + */ + public static final BitSet allowed_userinfo = new BitSet(256); + // Static initializer for allowed_userinfo + static { + allowed_userinfo.or(userinfo); + // allowed_userinfo.andNot(percent); + allowed_userinfo.clear('%'); + } + + + /** + * Those characters that are allowed for within the userinfo component. + */ + public static final BitSet allowed_within_userinfo = new BitSet(256); + // Static initializer for allowed_within_userinfo + static { + allowed_within_userinfo.or(within_userinfo); + allowed_within_userinfo.clear('%'); + } + + + /** + * Those characters that are allowed for the IPv6reference component. + * The characters '[', ']' in IPv6reference should be excluded. + */ + public static final BitSet allowed_IPv6reference = new BitSet(256); + // Static initializer for allowed_IPv6reference + static { + allowed_IPv6reference.or(IPv6reference); + // allowed_IPv6reference.andNot(unwise); + allowed_IPv6reference.clear('['); + allowed_IPv6reference.clear(']'); + } + + + /** + * Those characters that are allowed for the host component. + * The characters '[', ']' in IPv6reference should be excluded. + */ + public static final BitSet allowed_host = new BitSet(256); + // Static initializer for allowed_host + static { + allowed_host.or(hostname); + allowed_host.or(allowed_IPv6reference); + } + + + /** + * Those characters that are allowed for the authority component. + */ + public static final BitSet allowed_within_authority = new BitSet(256); + // Static initializer for allowed_within_authority + static { + allowed_within_authority.or(server); + allowed_within_authority.or(reg_name); + allowed_within_authority.clear(';'); + allowed_within_authority.clear(':'); + allowed_within_authority.clear('@'); + allowed_within_authority.clear('?'); + allowed_within_authority.clear('/'); + } + + + /** + * Those characters that are allowed for the abs_path. + */ + public static final BitSet allowed_abs_path = new BitSet(256); + // Static initializer for allowed_abs_path + static { + allowed_abs_path.or(abs_path); + // allowed_abs_path.set('/'); // aleady included + allowed_abs_path.andNot(percent); + } + + + /** + * Those characters that are allowed for the rel_path. + */ + public static final BitSet allowed_rel_path = new BitSet(256); + // Static initializer for allowed_rel_path + static { + allowed_rel_path.or(rel_path); + allowed_rel_path.clear('%'); + } + + + /** + * Those characters that are allowed within the path. + */ + public static final BitSet allowed_within_path = new BitSet(256); + // Static initializer for allowed_within_path + static { + allowed_within_path.or(abs_path); + allowed_within_path.clear('/'); + allowed_within_path.clear(';'); + allowed_within_path.clear('='); + allowed_within_path.clear('?'); + } + + + /** + * Those characters that are allowed for the query component. + */ + public static final BitSet allowed_query = new BitSet(256); + // Static initializer for allowed_query + static { + allowed_query.or(uric); + allowed_query.clear('%'); + } + + + /** + * Those characters that are allowed within the query component. + */ + public static final BitSet allowed_within_query = new BitSet(256); + // Static initializer for allowed_within_query + static { + allowed_within_query.or(allowed_query); + allowed_within_query.andNot(reserved); // excluded 'reserved' + allowed_within_query.clear('#'); // avoid confict with the fragment + } + + + /** + * Those characters that are allowed for the fragment component. + */ + public static final BitSet allowed_fragment = new BitSet(256); + // Static initializer for allowed_fragment + static { + allowed_fragment.or(uric); + allowed_fragment.clear('%'); + } + + // ------------------------------------------- Flags for this URI-reference + + // URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + // absoluteURI = scheme ":" ( hier_part | opaque_part ) + protected boolean _is_hier_part; + protected boolean _is_opaque_part; + // relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + // hier_part = ( net_path | abs_path ) [ "?" query ] + protected boolean _is_net_path; + protected boolean _is_abs_path; + protected boolean _is_rel_path; + // net_path = "//" authority [ abs_path ] + // authority = server | reg_name + protected boolean _is_reg_name; + protected boolean _is_server; // = _has_server + // server = [ [ userinfo "@" ] hostport ] + // host = hostname | IPv4address | IPv6reference + protected boolean _is_hostname; + protected boolean _is_IPv4address; + protected boolean _is_IPv6reference; + + // ------------------------------------------ Character and escape encoding + + /** + * Encode with the default protocol charset. + * + * @param original the original character sequence + * @param allowed those characters that are allowed within a component + * @return URI character sequence + * @exception IOException null component or unsupported character encoding + */ + protected static char[] encode(String original, BitSet allowed) + throws IOException { + + return encode(original, allowed, _protocolCharset); + } + + + /** + * Encodes URI string. + * + * This is a two mapping, one from original characters to octets, and + * subsequently a second from octets to URI characters: + * <p><blockquote><pre> + * original character sequence->octet sequence->URI character sequence + * </pre></blockquote><p> + * + * An escaped octet is encoded as a character triplet, consisting of the + * percent character "%" followed by the two hexadecimal digits + * representing the octet code. For example, "%20" is the escaped + * encoding for the US-ASCII space character. + * <p> + * Conversion from the local filesystem character set to UTF-8 will + * normally involve a two step process. First convert the local character + * set to the UCS; then convert the UCS to UTF-8. + * The first step in the process can be performed by maintaining a mapping + * table that includes the local character set code and the corresponding + * UCS code. + * The next step is to convert the UCS character code to the UTF-8 encoding. + * <p> + * Mapping between vendor codepages can be done in a very similar manner + * as described above. + * <p> + * The only time escape encodings can allowedly be made is when a URI is + * being created from its component parts. The escape and validate methods + * are internally performed within this method. + * + * @param original the original character sequence + * @param allowed those characters that are allowed within a component + * @param charset the protocol charset + * @return URI character sequence + * @exception IOException null component or unsupported character encoding + */ + protected static char[] encode(String original, BitSet allowed, + String charset) throws IOException { + + // encode original to uri characters. + if (original == null) { + throw new IOException(/*IOException.PARSING,*/ "URI: null"); + } + // escape octet to uri characters. + if (allowed == null) { + throw new IOException(/*IOException.PARSING,*/ + "URI: null allowed characters"); + } + byte[] octets; + try { + octets = original.getBytes(charset); + } catch (UnsupportedEncodingException error) { + throw new IOException(/*IOException.UNSUPPORTED_ENCODING,*/ "Unsupported Encoding: " + charset); + } + StringBuffer buf = new StringBuffer(octets.length); + for (int i = 0; i < octets.length; i++) { + char c = (char) octets[i]; + if (allowed.get(c)) { + buf.append(c); + } else { + buf.append('%'); + byte b = octets[i]; // use the original byte value + char hexadecimal = Character.forDigit((b >> 4) & 0xF, 16); + buf.append(Character.toUpperCase(hexadecimal)); // high + hexadecimal = Character.forDigit(b & 0xF, 16); + buf.append(Character.toUpperCase(hexadecimal)); // low + } + } + + return buf.toString().toCharArray(); + } + + + /** + * Decode with the default protocol charset. + * + * @param component the URI character sequence + * @return original character sequence + * @exception IOException incomplete trailing escape pattern + * or unsupported character encoding + */ + protected static String decode(char[] component) throws IOException { + return decode(component, _protocolCharset); + } + + + /** + * Decodes URI encoded string. + * + * This is a two mapping, one from URI characters to octets, and + * subsequently a second from octets to original characters: + * <p><blockquote><pre> + * URI character sequence->octet sequence->original character sequence + * </pre></blockquote><p> + * + * A URI must be separated into its components before the escaped + * characters within those components can be allowedly decoded. + * <p> + * Notice that there is a chance that URI characters that are non UTF-8 + * may be parsed as valid UTF-8. A recent non-scientific analysis found + * that EUC encoded Japanese words had a 2.7% false reading; SJIS had a + * 0.0005% false reading; other encoding such as ASCII or KOI-8 have a 0% + * false reading. + * <p> + * The percent "%" character always has the reserved purpose of being + * the escape indicator, it must be escaped as "%25" in order to be used + * as data within a URI. + * <p> + * The unescape method is internally performed within this method. + * + * @param component the URI character sequence + * @param charset the protocol charset + * @return original character sequence + * @exception IOException incomplete trailing escape pattern + * or unsupported character encoding + */ + protected static String decode(char[] component, String charset) + throws IOException { + + // unescape uri characters to octets + if (component == null) return null; + + byte[] octets; + try { + octets = new String(component).getBytes(charset); + } catch (UnsupportedEncodingException error) { + throw new IOException(/* IOException.UNSUPPORTED_ENCODING, */ + "URI: not supported " + charset + " encoding"); + } + int length = octets.length; + int oi = 0; // output index + for (int ii = 0; ii < length; oi++) { + byte aByte = (byte) octets[ii++]; + if (aByte == '%' && ii+2 <= length) { + byte high = (byte) Character.digit((char) octets[ii++], 16); + byte low = (byte) Character.digit((char) octets[ii++], 16); + if (high == -1 || low == -1) { + throw new IOException(/* IOException.ESCAPING, */ + "URI: incomplete trailing escape pattern"); + + } + aByte = (byte) ((high << 4) + low); + } + octets[oi] = (byte) aByte; + } + + String result; + try { + result = new String(octets, 0, oi, charset); + } catch (UnsupportedEncodingException error) { + throw new IOException(/* IOException.UNSUPPORTED_ENCODING, */ + "URI: not supported " + charset + " encoding"); + } + + return result; + } + + + /** + * Pre-validate the unescaped URI string within a specific component. + * + * @param component the component string within the component + * @param disallowed those characters disallowed within the component + * @return if true, it doesn't have the disallowed characters + * if false, the component is undefined or an incorrect one + */ + protected boolean prevalidate(String component, BitSet disallowed) { + // prevalidate the given component by disallowed characters + if (component == null) { + return false; // undefined + } + char[] target = component.toCharArray(); + for (int i = 0; i < target.length; i++) { + if (disallowed.get(target[i])) { + return false; + } + } + return true; + } + + + /** + * Validate the URI characters within a specific component. + * The component must be performed after escape encoding. Or it doesn't + * include escaped characters. + * + * @param component the characters sequence within the component + * @param generous those characters that are allowed within a component + * @return if true, it's the correct URI character sequence + */ + protected boolean validate(char[] component, BitSet generous) { + // validate each component by generous characters + return validate(component, 0, -1, generous); + } + + + /** + * Validate the URI characters within a specific component. + * The component must be performed after escape encoding. Or it doesn't + * include escaped characters. + * <p> + * It's not that much strict, generous. The strict validation might be + * performed before being called this method. + * + * @param component the characters sequence within the component + * @param soffset the starting offset of the given component + * @param eoffset the ending offset of the given component + * if -1, it means the length of the component + * @param generous those characters that are allowed within a component + * @return if true, it's the correct URI character sequence + * @throws NullPointerException null component + */ + protected boolean validate(char[] component, int soffset, int eoffset, + BitSet generous) { + // validate each component by generous characters + if (eoffset == -1) { + eoffset = component.length -1; + } + for (int i = soffset; i <= eoffset; i++) { + if (!generous.get(component[i])) return false; + } + return true; + } + + + /** + * In order to avoid any possilbity of conflict with non-ASCII characters, + * Parse a URI reference as a <code>String</code> with the character + * encoding of the local system or the document. + * <p> + * The following line is the regular expression for breaking-down a URI + * reference into its components. + * <p><blockquote><pre> + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * 12 3 4 5 6 7 8 9 + * </pre></blockquote><p> + * For example, matching the above expression to + * http://jakarta.apache.org/ietf/uri/#Related + * results in the following subexpression matches: + * <p><blockquote><pre> + * $1 = http: + * scheme = $2 = http + * $3 = //jakarta.apache.org + * authority = $4 = jakarta.apache.org + * path = $5 = /ietf/uri/ + * $6 = <undefined> + * query = $7 = <undefined> + * $8 = #Related + * fragment = $9 = Related + * </pre></blockquote><p> + * + * @param original the original character sequence + * @param escaped <code>true</code> if <code>original</code> is escaped + * @return the original character sequence + * @exception IOException + */ + protected void parseUriReference(String original, boolean escaped) + throws IOException { + + // validate and contruct the URI character sequence + if (original == null || original.length() == 0) { + throw new IOException("URI-Reference required"); + } + + /** @ + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + */ + String tmp = original.trim(); + + /** + * The length of the string sequence of characters. + * It may not be equal to the length of the byte array. + */ + int length = tmp.length(); + + /** + * Remove the delimiters like angle brackets around an URI. + */ + char[] firstDelimiter = { tmp.charAt(0) }; + if (validate(firstDelimiter, delims)) { + if (length >= 2) { + char[] lastDelimiter = { tmp.charAt(length - 1) }; + if (validate(lastDelimiter, delims)) { + tmp = tmp.substring(1, length - 1); + length = length - 2; + } + } + } + + /** + * The starting index + */ + int from = 0; + + /** + * The test flag whether the URI is started from the path component. + */ + boolean isStartedFromPath = false; + int atColon = tmp.indexOf(':'); + int atSlash = tmp.indexOf('/'); + if (atColon < 0 || (atSlash >= 0 && atSlash < atColon)) { + isStartedFromPath = true; + } + + /** + * <p><blockquote><pre> + * @@@@@@@@ + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * </pre></blockquote><p> + */ + int at = indexFirstOf(tmp, isStartedFromPath ? "/?#" : ":/?#", from); + if (at == -1) at = 0; + + /** + * Parse the scheme. + * <p><blockquote><pre> + * scheme = $2 = http + * @ + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * </pre></blockquote><p> + */ + if (at < length && tmp.charAt(at) == ':') { + char[] target = tmp.substring(0, at).toLowerCase().toCharArray(); + if (validate(target, scheme)) { + _scheme = target; + } else { + throw new IOException("incorrect scheme"); + } + from = ++at; + } + + /** + * Parse the authority component. + * <p><blockquote><pre> + * authority = $4 = jakarta.apache.org + * @@ + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * </pre></blockquote><p> + */ + // Reset flags + _is_net_path = _is_abs_path = _is_rel_path = _is_hier_part = false; + if (0 <= at && at < length && tmp.charAt(at) == '/') { + // Set flag + _is_hier_part = true; + if (at + 2 < length && tmp.charAt(at + 1) == '/') { + // the temporary index to start the search from + int next = indexFirstOf(tmp, "/?#", at + 2); + if (next == -1) { + next = (tmp.substring(at + 2).length() == 0) ? at + 2 : + tmp.length(); + } + parseAuthority(tmp.substring(at + 2, next), escaped); + from = at = next; + // Set flag + _is_net_path = true; + } + if (from == at) { + // Set flag + _is_abs_path = true; + } + } + + /** + * Parse the path component. + * <p><blockquote><pre> + * path = $5 = /ietf/uri/ + * @@@@@@ + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * </pre></blockquote><p> + */ + if (from < length) { + // rel_path = rel_segment [ abs_path ] + int next = indexFirstOf(tmp, "?#", from); + if (next == -1) { + next = tmp.length(); + } + if (!_is_abs_path) { + if (!escaped && prevalidate(tmp.substring(from, next), + disallowed_rel_path) || escaped && + validate(tmp.substring(from, next).toCharArray(), + rel_path)) { + // Set flag + _is_rel_path = true; + } else if (!escaped && prevalidate(tmp.substring(from, next), + disallowed_opaque_part) || escaped && + validate(tmp.substring(from, next).toCharArray(), + opaque_part)) { + // Set flag + _is_opaque_part = true; + } else { + // the path component may be empty + _path = null; + } + } + setPath(tmp.substring(from, next)); + at = next; + } + + /** + * Parse the query component. + * <p><blockquote><pre> + * query = $7 = <undefined> + * @@@@@@@@@ + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * </pre></blockquote><p> + */ + if (0 <= at && at+1 < length && tmp.charAt(at) == '?') { + int next = tmp.indexOf('#', at + 1); + if (next == -1) { + next = tmp.length(); + } + _query = (escaped) ? tmp.substring(at + 1, next).toCharArray() : + encode(tmp.substring(at + 1, next), allowed_query); + at = next; + } + + /** + * Parse the fragment component. + * <p><blockquote><pre> + * fragment = $9 = Related + * @@@@@@@@ + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * </pre></blockquote><p> + */ + if (0 <= at && at+1 < length && tmp.charAt(at) == '#') { + _fragment = (escaped) ? tmp.substring(at + 1).toCharArray() : + encode(tmp.substring(at + 1), allowed_fragment); + } + + // set this URI. + setUriReference(); + } + + + /** + * Get the earlier index that to be searched for the first occurrance in + * one of any of the given string. + * + * @param s the string to be indexed + * @param delims the delimiters used to index + * @return the earlier index if there are delimiters + */ + protected int indexFirstOf(String s, String delims) { + return indexFirstOf(s, delims, -1); + } + + + /** + * Get the earlier index that to be searched for the first occurrance in + * one of any of the given string. + * + * @param s the string to be indexed + * @param delims the delimiters used to index + * @param offset the from index + * @return the earlier index if there are delimiters + */ + protected int indexFirstOf(String s, String delims, int offset) { + if (s == null || s.length() == 0) { + return -1; + } + if (delims == null || delims.length() == 0) { + return -1; + } + // check boundaries + if (offset < 0) { + offset = 0; + } else if (offset > s.length()) { + return -1; + } + // s is never null + int min = s.length(); + char[] delim = delims.toCharArray(); + for (int i = 0; i < delim.length; i++) { + int at = s.indexOf(delim[i], offset); + if (at >= 0 && at < min) { + min = at; + } + } + return (min == s.length()) ? -1 : min; + } + + + /** + * Get the earlier index that to be searched for the first occurrance in + * one of any of the given array. + * + * @param s the character array to be indexed + * @param delim the delimiter used to index + * @return the ealier index if there are a delimiter + */ + protected int indexFirstOf(char[] s, char delim) { + return indexFirstOf(s, delim, 0); + } + + + /** + * Get the earlier index that to be searched for the first occurrance in + * one of any of the given array. + * + * @param s the character array to be indexed + * @param delim the delimiter used to index + * @return the ealier index if there is a delimiter + */ + protected int indexFirstOf(char[] s, char delim, int offset) { + if (s == null || s.length == 0) { + return -1; + } + // check boundaries + if (offset < 0) { + offset = 0; + } else if (offset > s.length) { + return -1; + } + for (int i = offset; i < s.length; i++) { + if (s[i] == delim) { + return i; + } + } + return -1; + } + + + /** + * Parse the authority component. + * + * @param original the original character sequence of authority component + * @param escaped <code>true</code> if <code>original</code> is escaped + * @exception IOException + */ + protected void parseAuthority(String original, boolean escaped) + throws IOException { + + // Reset flags + _is_reg_name = _is_server = + _is_hostname = _is_IPv4address = _is_IPv6reference = false; + + boolean has_port = true; + int from = 0; + int next = original.indexOf('@'); + if (next != -1) { // neither -1 and 0 + // each protocol extented from URI supports the specific userinfo + _userinfo = (escaped) ? original.substring(0, next).toCharArray() : + encode(original.substring(0, next), allowed_userinfo); + from = next + 1; + } + next = original.indexOf('[', from); + if (next >= from) { + next = original.indexOf(']', from); + if (next == -1) { + throw new IOException(/* IOException.PARSING,*/ "URI: IPv6reference"); + } else { + next++; + } + // In IPv6reference, '[', ']' should be excluded + _host = (escaped) ? original.substring(from, next).toCharArray() : + encode(original.substring(from, next), allowed_IPv6reference); + // Set flag + _is_IPv6reference = true; + } else { // only for !_is_IPv6reference + next = original.indexOf(':', from); + if (next == -1) { + next = original.length(); + has_port = false; + } + // REMINDME: it doesn't need the pre-validation + _host = original.substring(from, next).toCharArray(); + if (validate(_host, IPv4address)) { + // Set flag + _is_IPv4address = true; + } else if (validate(_host, hostname)) { + // Set flag + _is_hostname = true; + } else { + // Set flag + _is_reg_name = true; + } + } + if (_is_reg_name) { + // Reset flags for a server-based naming authority + _is_server = _is_hostname = _is_IPv4address = + _is_IPv6reference = false; + // set a registry-based naming authority + _authority = (escaped) ? original.toString().toCharArray() : + encode(original.toString(), allowed_reg_name); + } else { + if (original.length()-1 > next && has_port && + original.charAt(next) == ':') { // not empty + from = next + 1; + try { + _port = Integer.parseInt(original.substring(from)); + } catch (NumberFormatException error) { + throw new IOException(/*IOException.PARSING, */ + "URI: invalid port number"); + } + } + // set a server-based naming authority + StringBuffer buf = new StringBuffer(); + if (_userinfo != null) { // has_userinfo + buf.append(_userinfo); + buf.append('@'); + } + if (_host != null) { + buf.append(_host); + if (_port != -1) { + buf.append(':'); + buf.append(_port); + } + } + _authority = buf.toString().toCharArray(); + // Set flag + _is_server = true; + } + } + + + /** + * Once it's parsed successfully, set this URI. + * + * @see #getRawURI + */ + protected void setUriReference() { + // set _uri + StringBuffer buf = new StringBuffer(); + // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + if (_scheme != null) { + buf.append(_scheme); + buf.append(':'); + } + if (_is_net_path) { + buf.append("//"); + if (_authority != null) { // has_authority + if (_userinfo != null) { // by default, remove userinfo part + if (_host != null) { + buf.append(_host); + if (_port != -1) { + buf.append(':'); + buf.append(_port); + } + } + } else { + buf.append(_authority); + } + } + } + if (_opaque != null && _is_opaque_part) { + buf.append(_opaque); + } else if (_path != null) { + // _is_hier_part or _is_relativeURI + if (_path.length != 0) { + buf.append(_path); + } + } + if (_query != null) { // has_query + buf.append('?'); + buf.append(_query); + } + if (_fragment != null) { // has_fragment + buf.append('#'); + buf.append(_fragment); + } + + _uri = buf.toString().toCharArray(); + } + + // ----------------------------------------------------------- Test methods + + + /** + * Tell whether or not this URI is absolute. + * + * @return true iif this URI is absoluteURI + */ + public boolean isAbsoluteURI() { + return (_scheme != null); + } + + + /** + * Tell whether or not this URI is relative. + * + * @return true iif this URI is relativeURI + */ + public boolean isRelativeURI() { + return (_scheme == null); + } + + + /** + * Tell whether or not the absoluteURI of this URI is hier_part. + * + * @return true iif the absoluteURI is hier_part + */ + public boolean isHierPart() { + return _is_hier_part; + } + + + /** + * Tell whether or not the absoluteURI of this URI is opaque_part. + * + * @return true iif the absoluteURI is opaque_part + */ + public boolean isOpaquePart() { + return _is_opaque_part; + } + + + /** + * Tell whether or not the relativeURI or heir_part of this URI is net_path. + * It's the same function as the has_authority() method. + * + * @return true iif the relativeURI or heir_part is net_path + * @see #hasAuthority + */ + public boolean isNetPath() { + return _is_net_path || (_authority != null); + } + + + /** + * Tell whether or not the relativeURI or hier_part of this URI is abs_path. + * + * @return true iif the relativeURI or hier_part is abs_path + */ + public boolean isAbsPath() { + return _is_abs_path; + } + + + /** + * Tell whether or not the relativeURI of this URI is rel_path. + * + * @return true iif the relativeURI is rel_path + */ + public boolean isRelPath() { + return _is_rel_path; + } + + + /** + * Tell whether or not this URI has authority. + * It's the same function as the is_net_path() method. + * + * @return true iif this URI has authority + * @see #isNetPath + */ + public boolean hasAuthority() { + return (_authority != null) || _is_net_path; + } + + /** + * Tell whether or not the authority component of this URI is reg_name. + * + * @return true iif the authority component is reg_name + */ + public boolean isRegName() { + return _is_reg_name; + } + + + /** + * Tell whether or not the authority component of this URI is server. + * + * @return true iif the authority component is server + */ + public boolean isServer() { + return _is_server; + } + + + /** + * Tell whether or not this URI has userinfo. + * + * @return true iif this URI has userinfo + */ + public boolean hasUserinfo() { + return (_userinfo != null); + } + + + /** + * Tell whether or not the host part of this URI is hostname. + * + * @return true iif the host part is hostname + */ + public boolean isHostname() { + return _is_hostname; + } + + + /** + * Tell whether or not the host part of this URI is IPv4address. + * + * @return true iif the host part is IPv4address + */ + public boolean isIPv4address() { + return _is_IPv4address; + } + + + /** + * Tell whether or not the host part of this URI is IPv6reference. + * + * @return true iif the host part is IPv6reference + */ + public boolean isIPv6reference() { + return _is_IPv6reference; + } + + + /** + * Tell whether or not this URI has query. + * + * @return true iif this URI has query + */ + public boolean hasQuery() { + return (_query != null); + } + + + /** + * Tell whether or not this URI has fragment. + * + * @return true iif this URI has fragment + */ + public boolean hasFragment() { + return (_fragment != null); + } + + + // ---------------------------------------------------------------- Charset + + + /** + * Set the default charset of the protocol. + * <p> + * The character set used to store files SHALL remain a local decision and + * MAY depend on the capability of local operating systems. Prior to the + * exchange of URIs they SHOULD be converted into a ISO/IEC 10646 format + * and UTF-8 encoded. This approach, while allowing international exchange + * of URIs, will still allow backward compatibility with older systems + * because the code set positions for ASCII characters are identical to the + * one byte sequence in UTF-8. + * <p> + * An individual URI scheme may require a single charset, define a default + * charset, or provide a way to indicate the charset used. + * + * @param charset the default charset for each protocol + */ + public static void setProtocolCharset(String charset) { + _protocolCharset = charset; + } + + + /** + * Get the default charset of the protocol. + * <p> + * An individual URI scheme may require a single charset, define a default + * charset, or provide a way to indicate the charset used. + * <p> + * To work globally either requires support of a number of character sets + * and to be able to convert between them, or the use of a single preferred + * character set. + * For support of global compatibility it is STRONGLY RECOMMENDED that + * clients and servers use UTF-8 encoding when exchanging URIs. + * + * @return the charset string + */ + public static String getProtocolCharset() { + return _protocolCharset; + } + + + /** + * Set the default charset of the document. + * <p> + * Notice that it will be possible to contain mixed characters (e.g. + * ftp://host/KoreanNamespace/ChineseResource). To handle the Bi-directional + * display of these character sets, the protocol charset could be simply + * used again. Because it's not yet implemented that the insertion of BIDI + * control characters at different points during composition is extracted. + * + * @param charset the default charset for the document + */ + public static void setDocumentCharset(String charset) { + _documentCharset = charset; + } + + + /** + * Get the default charset of the document. + * + * @return the charset string + */ + public static String getDocumentCharset() { + return _documentCharset; + } + + // ------------------------------------------------------------- The scheme + + /** + * Get the scheme. + * + * @return the scheme + */ + public char[] getRawScheme() { + return _scheme; + } + + + /** + * Get the scheme. + * + * @return the scheme + * null if undefined scheme + */ + public String getScheme() { + return (_scheme == null) ? null : new String(_scheme); + } + + // ---------------------------------------------------------- The authority + + /** + * Set the authority. It can be one type of server, hostport, hostname, + * IPv4address, IPv6reference and reg_name. + * <p><blockquote><pre> + * authority = server | reg_name + * </pre></blockquote><p> + * + * @param escapedAuthority the raw escaped authority + * @exception IOException + * @throws NullPointerException null authority + */ + public void setRawAuthority(char[] escapedAuthority) throws IOException { + parseAuthority(new String(escapedAuthority), true); + setUriReference(); + } + + + /** + * Set the authority. It can be one type of server, hostport, hostname, + * IPv4address, IPv6reference and reg_name. + * Note that there is no setAuthority method by the escape encoding reason. + * + * @param escapedAuthority the escaped authority string + * @exception IOException + */ + public void setEscapedAuthority(String escapedAuthority) + throws IOException { + + parseAuthority(escapedAuthority, true); + setUriReference(); + } + + + /** + * Get the raw-escaped authority. + * + * @return the raw-escaped authority + */ + public char[] getRawAuthority() { + return _authority; + } + + + /** + * Get the escaped authority. + * + * @return the escaped authority + */ + public String getEscapedAuthority() { + return (_authority == null) ? null : new String(_authority); + } + + + /** + * Get the authority. + * + * @return the authority + * @exception IOException + * @see #decode + */ + public String getAuthority() throws IOException { + return (_authority == null) ? null : decode(_authority); + } + + // ----------------------------------------------------------- The userinfo + + /** + * Get the raw-escaped userinfo. + * + * @return the raw-escaped userinfo + * @see #getAuthority + */ + public char[] getRawUserinfo() { + return _userinfo; + } + + + /** + * Get the escaped userinfo. + * + * @return the escaped userinfo + * @see #getAuthority + */ + public String getEscapedUserinfo() { + return (_userinfo == null) ? null : new String(_userinfo); + } + + + /** + * Get the userinfo. + * + * @return the userinfo + * @exception IOException + * @see #decode + * @see #getAuthority + */ + public String getUserinfo() throws IOException { + return (_userinfo == null) ? null : decode(_userinfo); + } + + // --------------------------------------------------------------- The host + + /** + * Get the host. + * <p><blockquote><pre> + * host = hostname | IPv4address | IPv6reference + * </pre></blockquote><p> + * + * @return the host + * @see #getAuthority + */ + public char[] getRawHost() { + return _host; + } + + + /** + * Get the host. + * <p><blockquote><pre> + * host = hostname | IPv4address | IPv6reference + * </pre></blockquote><p> + * + * @return the host + * @exception IOException + * @see #decode + * @see #getAuthority + */ + public String getHost() throws IOException { + return decode(_host); + } + + // --------------------------------------------------------------- The port + + /** + * Get the port. In order to get the specfic default port, the specific + * protocol-supported class extended from the URI class should be used. + * It has the server-based naming authority. + * + * @return the port + * if -1, it has the default port for the scheme or the server-based + * naming authority is not supported in the specific URI. + */ + public int getPort() { + return _port; + } + + // --------------------------------------------------------------- The path + + /** + * Set the path. The method couldn't be used by API programmers. + * + * @param path the path string + * @exception IOException set incorrectly or fragment only + * @see #encode + */ + protected void setPath(String path) throws IOException { + + // set path + if (_is_net_path || _is_abs_path) { + _path = encode(path, allowed_abs_path); + } else if (_is_rel_path) { + StringBuffer buff = new StringBuffer(path.length()); + int at = path.indexOf('/'); + if (at > 0) { // never 0 + buff.append(encode(path.substring(0, at), allowed_rel_path)); + buff.append(encode(path.substring(at), allowed_abs_path)); + } else { + buff.append(encode(path, allowed_rel_path)); + } + _path = buff.toString().toCharArray(); + } else if (_is_opaque_part) { + _opaque = encode(path, allowed_opaque_part); + } else { + throw new IOException(/*IOException.PARSING, */"URI: incorrect path"); + } + } + + + /** + * Resolve the base and relative path. + * + * @param base_path a character array of the base_path + * @param rel_path a character array of the rel_path + * @return the resolved path + */ + protected char[] resolvePath(char[] base_path, char[] rel_path) { + + // REMINDME: paths are never null + String base = (base_path == null) ? "" : new String(base_path); + int at = base.lastIndexOf('/'); + if (at != -1) { + base_path = base.substring(0, at + 1).toCharArray(); + } + // _path could be empty + if (rel_path == null || rel_path.length == 0) { + return normalize(base_path); + } else if (rel_path[0] == '/') { + return rel_path; + } else { + StringBuffer buff = new StringBuffer(base.length() + + rel_path.length); + if (at != -1) { + buff.append(base.substring(0, at + 1)); + buff.append(rel_path); + } + return normalize(buff.toString().toCharArray()); + } + } + + + /** + * Get the raw-escaped current hierarchy level in the given path. + * If the last namespace is a collection, the slash mark ('/') should be + * ended with at the last character of the path string. + * + * @param path the path + * @return the current hierarchy level + * @exception IOException no hierarchy level + */ + protected char[] getRawCurrentHierPath(char[] path) throws IOException { + + if (_is_opaque_part) { + throw new IOException(/*IOException.PARSING,*/ "URI: no hierarchy level"); + } + if (path == null) { + throw new IOException(/*IOException.PARSING,*/ "URI: emtpy path"); + } + String buff = new String(path); + int first = buff.indexOf('/'); + int last = buff.lastIndexOf('/'); + if (last == 0) { + return rootPath; + } else if (first != last && last != -1) { + return buff.substring(0, last).toCharArray(); + } + // FIXME: it could be a document on the server side + return path; + } + + + /** + * Get the raw-escaped current hierarchy level. + * + * @return the raw-escaped current hierarchy level + * @exception IOException no hierarchy level + */ + public char[] getRawCurrentHierPath() throws IOException { + return (_path == null) ? null : getRawCurrentHierPath(_path); + } + + + /** + * Get the escaped current hierarchy level. + * + * @return the escaped current hierarchy level + * @exception IOException no hierarchy level + */ + public String getEscapedCurrentHierPath() throws IOException { + char[] path = getRawCurrentHierPath(); + return (path == null) ? null : new String(path); + } + + + /** + * Get the current hierarchy level. + * + * @return the current hierarchy level + * @exception IOException + * @see #decode + */ + public String getCurrentHierPath() throws IOException { + char[] path = getRawCurrentHierPath(); + return (path == null) ? null : decode(path); + } + + + /** + * Get the level above the this hierarchy level. + * + * @return the raw above hierarchy level + * @exception IOException + */ + public char[] getRawAboveHierPath() throws IOException { + char[] path = getRawCurrentHierPath(); + return (path == null) ? null : getRawCurrentHierPath(path); + } + + + /** + * Get the level above the this hierarchy level. + * + * @return the raw above hierarchy level + * @exception IOException + */ + public String getEscapedAboveHierPath() throws IOException { + char[] path = getRawAboveHierPath(); + return (path == null) ? null : new String(path); + } + + + /** + * Get the level above the this hierarchy level. + * + * @return the above hierarchy level + * @exception IOException + * @see #decode + */ + public String getAboveHierPath() throws IOException { + char[] path = getRawAboveHierPath(); + return (path == null) ? null : decode(path); + } + + + /** + * Get the raw-escaped path. + * <p><blockquote><pre> + * path = [ abs_path | opaque_part ] + * </pre></blockquote><p> + * + * @return the raw-escaped path + */ + public char[] getRawPath() { + return _is_opaque_part ? _opaque : _path; + } + + + /** + * Get the escaped path. + * <p><blockquote><pre> + * path = [ abs_path | opaque_part ] + * abs_path = "/" path_segments + * opaque_part = uric_no_slash *uric + * </pre></blockquote><p> + * + * @return the escaped path string + */ + public String getEscapedPath() { + char[] path = getRawPath(); + return (path == null) ? null : new String(path); + } + + + /** + * Get the path. + * <p><blockquote><pre> + * path = [ abs_path | opaque_part ] + * </pre></blockquote><p> + * @return the path string + * @exception IOException + * @see #decode + */ + public String getPath() throws IOException { + char[] path = getRawPath(); + return (path == null) ? null : decode(path); + } + + + /** + * Get the raw-escaped basename of the path. + * + * @return the raw-escaped basename + */ + public char[] getRawName() { + if (_path == null) return null; + + int at = 0; + for (int i = _path.length - 1; i >= 0; i--) { + if (_path[i] == '/') { + at = i + 1; + break; + } + } + int len = _path.length - at; + char[] basename = new char[len]; + System.arraycopy(_path, at, basename, 0, len); + return basename; + } + + + /** + * Get the escaped basename of the path. + * + * @return the escaped basename string + */ + public String getEscapedName() { + char[] basename = getRawName(); + return (basename == null) ? null : new String(basename); + } + + + /** + * Get the basename of the path. + * + * @return the basename string + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getName() throws IOException { + char[] basename = getRawName(); + return (basename == null) ? null : decode(getRawName()); + } + + // ----------------------------------------------------- The path and query + + /** + * Get the raw-escaped path and query. + * + * @return the raw-escaped path and query + */ + public char[] getRawPathQuery() { + + if (_path == null && _query == null) { + return null; + } + StringBuffer buff = new StringBuffer(); + if (_path != null) { + buff.append(_path); + } + if (_query != null) { + buff.append('?'); + buff.append(_query); + } + return buff.toString().toCharArray(); + } + + + /** + * Get the escaped query. + * + * @return the escaped path and query string + */ + public String getEscapedPathQuery() { + char[] rawPathQuery = getRawPathQuery(); + return (rawPathQuery == null) ? null : new String(rawPathQuery); + } + + + /** + * Get the path and query. + * + * @return the path and query string. + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getPathQuery() throws IOException { + char[] rawPathQuery = getRawPathQuery(); + return (rawPathQuery == null) ? null : decode(rawPathQuery); + } + + // -------------------------------------------------------------- The query + + /** + * Set the raw-escaped query. + * + * @param escapedQuery the raw-escaped query + * @exception IOException escaped query not valid + * @throws NullPointerException null query + */ + public void setRawQuery(char[] escapedQuery) throws IOException { + if (!validate(escapedQuery, query)) + throw new IOException(/*IOException.ESCAPING,*/ + "URI: escaped query not valid"); + _query = escapedQuery; + setUriReference(); + } + + + /** + * Set the escaped query string. + * + * @param escapedQuery the escaped query string + * @exception IOException escaped query not valid + * @throws NullPointerException null query + */ + public void setEscapedQuery(String escapedQuery) throws IOException { + setRawQuery(escapedQuery.toCharArray()); + } + + + /** + * Set the query. + * When a query string is not misunderstood the reserved special characters + * ("&", "=", "+", ",", and "$") within a query component, it is + * recommended to use in encoding the whole query with this method. + * + * @param query the query string. + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @throws NullPointerException null query + * @see #encode + */ + public void setQuery(String query) throws IOException { + setRawQuery(encode(query, allowed_query)); + } + + + /** + * Get the raw-escaped query. + * + * @return the raw-escaped query + */ + public char[] getRawQuery() { + return _query; + } + + + /** + * Get the escaped query. + * + * @return the escaped query string + */ + public String getEscapedQuery() { + return (_query == null) ? null : new String(_query); + } + + + /** + * Get the query. + * + * @return the query string. + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getQuery() throws IOException { + return (_query == null) ? null : decode(_query); + } + + // ----------------------------------------------------------- The fragment + + /** + * Set the raw-escaped fragment. + * + * @param escapedFragment the raw-escaped fragment + * @exception IOException escaped fragment not valid + * @throws NullPointerException null fragment + */ + public void setRawFragment(char[] escapedFragment) throws IOException { + if (!validate(escapedFragment, fragment)) + throw new IOException(/*IOException.ESCAPING,*/ + "URI: escaped fragment not valid"); + _fragment = escapedFragment; + setUriReference(); + } + + + /** + * Set the escaped fragment string. + * + * @param escapedFragment the escaped fragment string + * @exception IOException escaped fragment not valid + * @throws NullPointerException null fragment + */ + public void setEscapedFragment(String escapedFragment) throws IOException { + char[] fragmentSequence = escapedFragment.toCharArray(); + if (!validate(fragmentSequence, fragment)) + throw new IOException(/*IOException.ESCAPING,*/ + "URI: escaped fragment not valid"); + _fragment = fragmentSequence; + setUriReference(); + } + + + /** + * Set the fragment. + * + * @param the fragment string. + * @exception IOException + * Or unsupported character encoding + * @throws NullPointerException null fragment + */ + public void setFragment(String fragment) throws IOException { + _fragment = encode(fragment, allowed_fragment); + setUriReference(); + } + + + /** + * Get the raw-escaped fragment. + * <p> + * The optional fragment identifier is not part of a URI, but is often used + * in conjunction with a URI. + * <p> + * The format and interpretation of fragment identifiers is dependent on + * the media type [RFC2046] of the retrieval result. + * <p> + * A fragment identifier is only meaningful when a URI reference is + * intended for retrieval and the result of that retrieval is a document + * for which the identified fragment is consistently defined. + * + * @return the raw-escaped fragment + */ + public char[] getRawFragment() { + return _fragment; + } + + + /** + * Get the escaped fragment. + * + * @return the escaped fragment string + */ + public String getEscapedFragment() { + return (_fragment == null) ? null : new String(_fragment); + } + + + /** + * Get the fragment. + * + * @return the fragment string + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getFragment() throws IOException { + return (_fragment == null) ? null : decode(_fragment); + } + + // ------------------------------------------------------------- Utilities + + /** + * Normalize the given hier path part. + * + * @param path the path to normalize + * @return the normalized path + */ + protected char[] normalize(char[] path) { + + if (path == null) return null; + + String normalized = new String(path); + boolean endsWithSlash = true; + // precondition + if (!normalized.endsWith("/")) { + normalized += '/'; + endsWithSlash = false; + } + if (normalized.endsWith("/./") || normalized.endsWith("/../")) { + endsWithSlash = true; + } + // Resolve occurrences of "/./" in the normalized path + while (true) { + int at = normalized.indexOf("/./"); + if (at == -1) { + break; + } + normalized = normalized.substring(0, at) + + normalized.substring(at + 2); + } + // Resolve occurrences of "/../" in the normalized path + while (true) { + int at = normalized.indexOf("/../"); + if (at == -1) { + break; + } + if (at == 0) { + normalized = "/"; + break; + } + int backward = normalized.lastIndexOf('/', at - 1); + if (backward == -1) { + // consider the rel_path + normalized = normalized.substring(at + 4); + } else { + normalized = normalized.substring(0, backward) + + normalized.substring(at + 3); + } + } + // Resolve occurrences of "//" in the normalized path + while (true) { + int at = normalized.indexOf("//"); + if (at == -1) { + break; + } + normalized = normalized.substring(0, at) + + normalized.substring(at + 1); + } + if (!endsWithSlash && normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length()-1); + } else if (endsWithSlash && !normalized.endsWith("/")) { + normalized = normalized + "/"; + } + // Set the normalized path that we have completed + return normalized.toCharArray(); + } + + + /** + * Normalize the path part of this URI. + */ + public void normalize() { + _path = normalize(_path); + } + + + /** + * Test if the first array is equal to the second array. + * + * @param first the first character array + * @param second the second character array + * @return true if they're equal + */ + protected boolean equals(char[] first, char[] second) { + + if (first == null && second == null) { + return true; + } + if (first == null || second == null) { + return false; + } + if (first.length != second.length) { + return false; + } + for (int i = 0; i < first.length; i++) { + if (first[i] != second[i]) { + return false; + } + } + return true; + } + + + /** + * Test an object if this URI is equal to another. + * + * @param obj an object to compare + * @return true if two URI objects are equal + */ + public boolean equals(Object obj) { + + // normalize and test each components + if (obj == this) { + return true; + } + if (!(obj instanceof URI)) { + return false; + } + URI another = (URI) obj; + // scheme + if (!equals(_scheme, another._scheme)) { + return false; + } + // is_opaque_part or is_hier_part? and opaque + if (!equals(_opaque, another._opaque)) { + return false; + } + // is_hier_part + // has_authority + if (!equals(_authority, another._authority)) { + return false; + } + // path + if (!equals(_path, another._path)) { + return false; + } + // has_query + if (!equals(_query, another._query)) { + return false; + } + // has_fragment? should be careful of the only fragment case. + if (!equals(_fragment, another._fragment)) { + return false; + } + return true; + } + + // ---------------------------------------------------------- Serialization + + /** + * Write the content of this URI. + * + * @param oos the object-output stream + */ + protected void writeObject(java.io.ObjectOutputStream oos) + throws IOException { + + oos.defaultWriteObject(); + } + + + /** + * Read a URI. + * + * @param ois the object-input stream + */ + protected void readObject(java.io.ObjectInputStream ois) + throws ClassNotFoundException, IOException { + + ois.defaultReadObject(); + } + + // ------------------------------------------------------------- Comparison + + /** + * Compare this URI to another object. + * + * @param obj the object to be compared. + * @return 0, if it's same, + * -1, if failed, first being compared with in the authority component + * @exception ClassCastException not URI argument + * @throws NullPointerException null object + */ + public int compareTo(Object obj) { + + URI another = (URI) obj; + if (!equals(_authority, another.getRawAuthority())) return -1; + return toString().compareTo(another.toString()); + } + + // ------------------------------------------------------------------ Clone + + /** + * Create and return a copy of this object, the URI-reference containing + * the userinfo component. Notice that the whole URI-reference including + * the userinfo component counld not be gotten as a <code>String</code>. + * <p> + * To copy the identical <code>URI</code> object including the userinfo + * component, it should be used. + * + * @return a clone of this instance + */ + public synchronized Object clone() { + + URI instance = new URI(); + + instance._uri = _uri; + instance._scheme = _scheme; + instance._opaque = _opaque; + instance._authority = _authority; + instance._userinfo = _userinfo; + instance._host = _host; + instance._port = _port; + instance._path = _path; + instance._query = _query; + instance._fragment = _fragment; + // flags + instance._is_hier_part = _is_hier_part; + instance._is_opaque_part = _is_opaque_part; + instance._is_net_path = _is_net_path; + instance._is_abs_path = _is_abs_path; + instance._is_rel_path = _is_rel_path; + instance._is_reg_name = _is_reg_name; + instance._is_server = _is_server; + instance._is_hostname = _is_hostname; + instance._is_IPv4address = _is_IPv4address; + instance._is_IPv6reference = _is_IPv6reference; + + return instance; + } + + // ------------------------------------------------------------ Get the URI + + /** + * It can be gotten the URI character sequence. It's raw-escaped. + * For the purpose of the protocol to be transported, it will be useful. + * <p> + * It is clearly unwise to use a URL that contains a password which is + * intended to be secret. In particular, the use of a password within + * the 'userinfo' component of a URL is strongly disrecommended except + * in those rare cases where the 'password' parameter is intended to be + * public. + * <p> + * When you want to get each part of the userinfo, you need to use the + * specific methods in the specific URL. It depends on the specific URL. + * + * @return URI character sequence + */ + public char[] getRawURI() { + return _uri; + } + + + /** + * It can be gotten the URI character sequence. It's escaped. + * For the purpose of the protocol to be transported, it will be useful. + * + * @return the URI string + */ + public String getEscapedURI() { + return (_uri == null) ? null : new String(_uri); + } + + + /** + * It can be gotten the URI character sequence. + * + * @return the URI string + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getURI() throws IOException { + return (_uri == null) ? null : decode(_uri); + } + + + /** + * Get the escaped URI string. + * <p> + * On the document, the URI-reference form is only used without the userinfo + * component like http://jakarta.apache.org/ by the security reason. + * But the URI-reference form with the userinfo component could be parsed. + * <p> + * In other words, this URI and any its subclasses must not expose the + * URI-reference expression with the userinfo component like + * http://user:password@hostport/restricted_zone.<br> + * It means that the API client programmer should extract each user and + * password to access manually. Probably it will be supported in the each + * subclass, however, not a whole URI-reference expression. + * + * @return the URI string + * @see #clone() + */ + public String toString() { + return getEscapedURI(); + } + + + // ------------------------------------------------------------ Inner class + + /** + * A mapping to determine the (somewhat arbitrarily) preferred charset for + * a given locale. Supports all locales recognized in JDK 1.1. + * <p> + * The distribution of this class is Servlets.com. It was originally + * written by Jason Hunter [jhunter at acm.org] and used by with permission. + */ + public static class LocaleToCharsetMap { + + private static Hashtable map; + static { + map = new Hashtable(); + map.put("ar", "ISO-8859-6"); + map.put("be", "ISO-8859-5"); + map.put("bg", "ISO-8859-5"); + map.put("ca", "ISO-8859-1"); + map.put("cs", "ISO-8859-2"); + map.put("da", "ISO-8859-1"); + map.put("de", "ISO-8859-1"); + map.put("el", "ISO-8859-7"); + map.put("en", "ISO-8859-1"); + map.put("es", "ISO-8859-1"); + map.put("et", "ISO-8859-1"); + map.put("fi", "ISO-8859-1"); + map.put("fr", "ISO-8859-1"); + map.put("hr", "ISO-8859-2"); + map.put("hu", "ISO-8859-2"); + map.put("is", "ISO-8859-1"); + map.put("it", "ISO-8859-1"); + map.put("iw", "ISO-8859-8"); + map.put("ja", "Shift_JIS"); + map.put("ko", "EUC-KR"); + map.put("lt", "ISO-8859-2"); + map.put("lv", "ISO-8859-2"); + map.put("mk", "ISO-8859-5"); + map.put("nl", "ISO-8859-1"); + map.put("no", "ISO-8859-1"); + map.put("pl", "ISO-8859-2"); + map.put("pt", "ISO-8859-1"); + map.put("ro", "ISO-8859-2"); + map.put("ru", "ISO-8859-5"); + map.put("sh", "ISO-8859-5"); + map.put("sk", "ISO-8859-2"); + map.put("sl", "ISO-8859-2"); + map.put("sq", "ISO-8859-2"); + map.put("sr", "ISO-8859-5"); + map.put("sv", "ISO-8859-1"); + map.put("tr", "ISO-8859-9"); + map.put("uk", "ISO-8859-5"); + map.put("zh", "GB2312"); + map.put("zh_TW", "Big5"); + } + + /** + * Get the preferred charset for the given locale. + * + * @param locale the locale + * @return the preferred charset + * or null if the locale is not recognized + */ + public static String getCharset(Locale locale) { + // try for an full name match (may include country) + String charset = (String) map.get(locale.toString()); + if (charset != null) return charset; + + // if a full name didn't match, try just the language + charset = (String) map.get(locale.getLanguage()); + return charset; // may be null + } + + } + +} + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionResults.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionResults.java new file mode 100644 index 0000000..377345c --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionResults.java @@ -0,0 +1,51 @@ +/* +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; + +/** +* An interface defining the results of a DirectAction. +* Implemented by both WOResponse and WOComponent so both +* can be returned from a DirectAction. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface WOActionResults +{ +/** +* Returns a response object as appropriate for the target. +*/ + WOResponse generateResponse (); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:52:44 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionURL.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionURL.java new file mode 100644 index 0000000..78191b6 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionURL.java @@ -0,0 +1,57 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; + +/** + * This dynamic element renders only the URL of a hyperlink. + * Bindings are: + * <ul> + * <li>href: The URL that the hyperlink should point to.</li> + * <li>pageName: The name of the WOComponent that the hyperlink should point to.</li> + * <li>directActionName: The name of the direct action to call when the link is activated.</li> + * <li>actionClass: The name of the WODirectAction subclass where the direct action resides.</li> + * <li>action: A pointer to a method on the component that contains this element. If the link is activated, + * the method will be called. + * <li>ref: The name of the anchor to go to inside the resulting page.</li> + * </ul> + * + * The href, pageName and directActionName/actionClass and name properties are mutually exclusive and you should + * only use at most one of them simultaneously. + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOActionURL extends WOHyperlink { + + public WOActionURL() { + super(); + } + + public WOActionURL(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString(actionURL(c)); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActiveImage.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActiveImage.java new file mode 100644 index 0000000..cad6f64 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActiveImage.java @@ -0,0 +1,81 @@ + +package net.wotonomy.web; + +import java.util.Enumeration; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + +/** + * WOActiveImage renders a dynamically generated IMG tag, enclosed in a hyperlink. + * Internally, it uses a WOImage and a WOHyperlink to do the actual work. + * + * The bindings are those of WOImage and WOHyperlink combined. + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOActiveImage extends WODynamicElement { + + protected WOActiveImage() { + super(); + } + + public WOActiveImage(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public void appendToResponse(WOResponse r, WOContext c) { + NSMutableDictionary atribs = new NSMutableDictionary(5); + if (associations.objectForKey("mimeType") != null) + atribs.setObjectForKey(associations.objectForKey("mimeType"), "mimeType"); + if (associations.objectForKey("data") != null) + atribs.setObjectForKey(associations.objectForKey("data"), "data"); + if (associations.objectForKey("src") != null) + atribs.setObjectForKey(associations.objectForKey("src"), "src"); + if (associations.objectForKey("framework") != null) + atribs.setObjectForKey(associations.objectForKey("framework"), "framework"); + if (associations.objectForKey("filename") != null) + atribs.setObjectForKey(associations.objectForKey("filename"), "filename"); + if (associations.objectForKey("alt") != null) + atribs.setObjectForKey(associations.objectForKey("alt"), "alt"); + if (associations.objectForKey("border") != null) + atribs.setObjectForKey(associations.objectForKey("border"), "border"); + if (associations.objectForKey("width") != null) + atribs.setObjectForKey(associations.objectForKey("width"), "width"); + if (associations.objectForKey("height") != null) + atribs.setObjectForKey(associations.objectForKey("height"), "height"); + WODynamicElement img = new WOImage("WOImage_" + name, atribs, null); + NSMutableDictionary uf = new NSMutableDictionary(); + Enumeration enumeration = associations.keyEnumerator(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + if (key.startsWith("?")) + uf.setObjectForKey(associations.objectForKey(key), key); + } + createLink(img).appendToResponse(r, c); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + return createLink(null).invokeAction(r, c); + } + + protected WOHyperlink createLink(WOElement e) { + NSMutableDictionary atribs = new NSMutableDictionary(5); + if (associations.objectForKey("href") != null) + atribs.setObjectForKey(associations.objectForKey("href"), "href"); + if (associations.objectForKey("pageName") != null) + atribs.setObjectForKey(associations.objectForKey("pageName"), "pageName"); + if (associations.objectForKey("action") != null) + atribs.setObjectForKey(associations.objectForKey("action"), "action"); + if (associations.objectForKey("directActionName") != null) + atribs.setObjectForKey(associations.objectForKey("directActionName"), "directActionName"); + if (associations.objectForKey("actionClass") != null) + atribs.setObjectForKey(associations.objectForKey("actionClass"), "actionClass"); + if (associations.objectForKey("target") != null) + atribs.setObjectForKey(associations.objectForKey("target"), "target"); + return new WOHyperlink(name, atribs, e); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOApplication.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOApplication.java new file mode 100644 index 0000000..90e40c6 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOApplication.java @@ -0,0 +1,1193 @@ +/* +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.lang.reflect.Constructor; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.web.util.BrowserLauncher; + +import org.mortbay.http.HttpListener; +import org.mortbay.http.HttpServer; +import org.mortbay.jetty.servlet.ServletHandler; +import org.mortbay.util.InetAddrPort; + +/** +* A pure java implementation of WOApplication. <br><br> +* +* The application is responsible for creating and managing sessions +* and dispatching requests to the appropriate handlers. <br><br> +* +* This implementation extends HttpServlet, so the application itself +* is a servlet and can be configured and managed as such by the servlet +* container. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOApplication + extends HttpServlet +{ + /** + * A tricky way to allow multiple WOApplications + * in the same servlet container. + */ + static private ThreadLocal threadLocal; + //static private WOApplication application; + + /** + * Determines application-wide page caching. + * Pages may individually prevent caching. + */ + static private boolean cachingEnabled = false; + private static boolean autoOpenInBrowser = true; + + private String name; + private WORequestHandler defaultRequestHandler; + private NSMutableDictionary requestHandlers; + private WOSessionStore sessionStore; + private WOResourceManager resourceManager; + private boolean pageRefreshOnBacktrack; + private int pageCacheSize; + private int permanentPageCacheSize; + + public static final String WOApplicationWillFinishLaunchingNotification + = "WOApplicationWillFinishLaunchingNotification"; + public static final String WOApplicationDidFinishLaunchingNotification + = "WOApplicationDidFinishLaunchingNotification"; + public static final String WOGarbageCollectionPeriodKey + = "WOGarbageCollectionPeriodKey"; + + static String _DirectActionRequestHandlerKey = "_DirectActionRequestHandlerKey"; + static String _ComponentRequestHandlerKey = "_ComponentRequestHandlerKey"; + static String _ResourceRequestHandlerKey = "_ResourceRequestHandlerKey"; + static String WOPort = "WOPort"; + static String WOSMTPHost = "WOSMTPHost"; + static final String ELEMENT_CLASS = "elementClass"; + + public WOApplication () + { + if ( threadLocal == null ) + { + threadLocal = new ThreadLocal(); + } + threadLocal.set( this ); + + //application = this; + resourceManager = createResourceManager(); + requestHandlers = new NSMutableDictionary(); + defaultRequestHandler = new WODirectActionRequestHandler(); + registerRequestHandler( defaultRequestHandler, directActionRequestHandlerKey() ); + registerRequestHandler( new WOComponentRequestHandler(), componentRequestHandlerKey() ); + registerRequestHandler( new WOResourceRequestHandler(), resourceRequestHandlerKey() ); + sessionStore = WOSessionStore.serverSessionStore(); + + pageRefreshOnBacktrack = true; + pageCacheSize = 30; + permanentPageCacheSize = 30; + + threadLocal.set( null ); + } + + /** + * Dispatches the request and updates the specified response + * as appropriate. This implementation creates a new WORequest + * and WOContext from the request, sends the response to the + * appropriate WORequestHandler, and then updates the servlet + * response from the resulting WOResponse. + */ + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, java.io.IOException + { + threadLocal.set( this ); + + WORequest request = new WORequest( req, this ); + WOResponse response = dispatchRequest( request ); + response.generateServletResponse( resp ); + } + + /** + * Handles post requests by calling doGet(), since the framework + * handles both gets and posts similarly. Override to handle + * post requests in a different manner. + */ + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, java.io.IOException + { + doGet( req, resp ); + } + + // obtaining attributes + + /** + * Returns the singleton instance of this application. + */ + public static WOApplication application() + { + return (WOApplication) threadLocal.get(); + //return application; + } + + /** + * Returns the name of the application. This implementation returns + * the name the jar file or directory from which the class was loaded, + * with no extensions. + */ + public String name() + { + if ( name == null ) + { + name = path(); + int i; + if ( name.endsWith( "/" ) ) + { // path + name = name.substring( 0, name.length() - 1 ); + } + else + { // jar file + i = name.lastIndexOf( '.' ); + if ( i != -1 ) name = name.substring( 0, i ); + } + i = name.lastIndexOf( '/' ); + if ( i != -1 ) name = name.substring( i+1 ); + } + return name; + } + + /** + * Returns the absolute path to the application on the local file system. + * Note that the application might be embedded inside of a jar file. + */ + public String path () + { + return getClass().getProtectionDomain().getCodeSource().getLocation().toString(); + } + + /** + * Returns the path to the application on the local file system + * relative to the server's document root. + */ + public String baseURL () + { + String root = getServletContext().getRealPath( "/" ); + String result = path(); + if ( result.endsWith("/") ) + { // path + if ( result.startsWith( root ) ) + { // relative to root + result = result.substring( root.length() ); + } + // else leave as absolute reference + } + // jar or war file: leave as absolute reference + return result; + } + + // concurrent request handling + + /** + * Returns whether this application allows request + * to be handled concurrently. + * This implementation returns true. + * Subclasses may override to return false to force + * single-threaded request handling, although this + * is not implemented. + */ + public boolean allowsConcurrentRequestHandling () + { + return true; + } + + /** + * Returns whether this application allows request + * to be handled concurrently. + * This implementation returns true. + */ + public boolean adaptorsDispatchRequestsConcurrently () + { + return true; + } + + /** + * Returns whether this application allows request + * to be handled concurrently. + * This implementation returns true. + */ + public boolean isConcurrentRequestHandlingEnabled () + { + return true; + } + + // handling requests + + /** + * Invoked first in the request-response cycle. + * Override to perform any kind of initialization at the + * start of a request. This implementation does nothing. + */ + public void awake () + { + + } + + /** + * Invoked to start the first phase of the request-response cycle, + * after all calls to awake() have been completed. + */ + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + aContext.session().takeValuesFromRequest( aRequest, aContext ); + } + + /** + * Invoked to start the second phase of the request-response cycle, + * after all calls to takeValuesFromRequest have finished. + */ + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + return aContext.session().invokeAction( aRequest, aContext ); + } + + /** + * Invoked to start the third phase of the request-response cycle, + * after invokeAction() has completed and returned a WOResponse. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + aContext.session().appendToResponse( aResponse, aContext ); + } + + /** + * Invoked last in the request-response cycle. + * Override to perform any kind of clean-up at the + * end of a request. This implementation does nothing. + */ + public void sleep () + { + + } + + /** + * Dispatches the request to the appropriate handler. + */ + public WOResponse dispatchRequest (WORequest aRequest) + { + return handlerForRequest( aRequest ).handleRequest( aRequest ); + } + + // request handling + + /** + * Returns the default request handler used if the requested + * handler isn't specified or cannot be found. (This defaults + * to the WODirectActionRequestHandler.) + */ + public WORequestHandler defaultRequestHandler () + { + return defaultRequestHandler; + } + + /** + * Sets the default request handler used if the requested + * handler isn't specified or cannot be found. + */ + public void setDefaultRequestHandler (WORequestHandler aRequestHandler) + { + defaultRequestHandler = aRequestHandler; + } + + /** + * Registers the specified request handler for the specified key. + */ + public void registerRequestHandler (WORequestHandler aRequestHandler, String aKey) + { + requestHandlers.setObjectForKey( aRequestHandler, aKey ); + } + + /** + * Unregisters any existing request handler for the specified key + * returning the existing request handler, if any. + */ + public WORequestHandler removeRequestHandlerForKey (String aKey) + { + WORequestHandler result = requestHandlerForKey( aKey ); + requestHandlers.removeObjectForKey( aKey ); + return result; + } + + /** + * Returns the keys under which request handlers are registered. + */ + public NSArray registeredRequestHandlerKeys () + { + return requestHandlers.allKeys(); + } + + /** + * Returns the request handler registered for the specified key, + * or null if no request handler is registered for that key. + */ + public WORequestHandler requestHandlerForKey (String aKey) + { + return (WORequestHandler) requestHandlers.objectForKey( aKey ); + } + + /** + * Returns the request handler that would best service the specified request. + */ + public WORequestHandler handlerForRequest (WORequest aRequest) + { + WORequestHandler result = requestHandlerForKey( aRequest.requestHandlerKey() ); + if ( aRequest == null ) result = defaultRequestHandler(); + return result; + } + + // handling errors + + public WOResponse handleSessionCreationErrorInContext( WOContext aContext ) + { + WOResponse response = new WOResponse(); + response.setStatus( 500 ); // internal server error + //TODO: add more useful information to the response + System.err.println( "Failed to create session: " + aContext ); +new RuntimeException().printStackTrace(); // remove me + return response; + } + + public WOResponse handleSessionRestorationErrorInContext( WOContext aContext ) + { + WOResponse response = new WOResponse(); + response.setStatus( 500 ); // internal server error + //TODO: add more useful information to the response + System.err.println( "Failed to restore session: " + aContext ); +new RuntimeException().printStackTrace(); // remove me + return response; + } + + public WOResponse handlePageRestorationErrorInContext( WOContext aContext ) + { + WOResponse response = new WOResponse(); + response.setStatus( 500 ); // internal server error + //TODO: add more useful information to the response + System.err.println( "Failed to restore page: " + aContext ); +new RuntimeException().printStackTrace(); // remove me + return response; + } + + public WOResponse handleException( Throwable aThrowable, WOContext aContext ) + { + WOResponse response = new WOResponse(); + response.setStatus( 500 ); // internal server error + System.err.println( "Exception occurred: " + aContext ); + if ( aThrowable.getMessage() != null ) + { + response.appendContentString( aThrowable.getMessage() ); + aThrowable.printStackTrace(); + } + else + { + response.appendContentString( aThrowable.toString() ); + aThrowable.printStackTrace(); + } + aThrowable.printStackTrace(); + return response; + } + + // managing pages + + /** + * Sets the number of pages that will be retained + * in the user's session. Set to zero to disable page caching. + */ + public void setPageCacheSize (int aPositiveInt) + { + pageCacheSize = aPositiveInt; + } + + /** + * Returns the number of pages that will be retained + * in the user's session. The default page cache size is 30. + */ + public int pageCacheSize () + { + return pageCacheSize; + } + + /** + * Returns the number of pages that will be retained in the + * longer-term "permanent" page cache in the user's session, + * which is typically used for navigation bars in frames, etc. + * The default permanent page cache size is 30. + */ + public int permanentPageCacheSize () + { + return permanentPageCacheSize; + } + + /** + * Returns the number of pages that will be retained in the + * longer-term "permanent" page cache in the user's session, + * which is typically used for navigation bars in frames, etc. + * Set to zero to disable permanent page caching. + */ + public void setPermanentPageCacheSize (int aPositiveInt) + { + permanentPageCacheSize = aPositiveInt; + } + + /** + * Returns whether a "backtrack" for an existing page should + * simply call generateResponse() on the existing page instance. + * If false, a new page is created instead. The default is true. + */ + public void setPageRefreshOnBacktrackEnabled (boolean enabled) + { + pageRefreshOnBacktrack = enabled; + } + + /** + * Returns whether a "backtrack" for an existing page should + * simply call generateResponse() on the existing page instance. + * If false, a new page is created instead. The default is true. + */ + public boolean isPageRefreshOnBacktrackEnabled () + { + return pageRefreshOnBacktrack; + } + + // managing sessions + + /** + * Sets the session store used by this application to persist + * sessions between request-response transactions. + */ + public void setSessionStore(WOSessionStore aSessionStore) + { + sessionStore = aSessionStore; + } + + /** + * Returns the session store used by this application to persist + * sessions between request-response transactions. + */ + public WOSessionStore sessionStore() + { + return sessionStore; + } + + /** + * Called at the end of the request-response cycle + * to persist the current session until the user's next request. + */ + public void saveSessionForContext(WOContext aContext) + { + sessionStore.saveSessionForContext( aContext ); + } + + /** + * Called at the beginning of the request-response cycle + * to obtain the current session from the user's last request. + * Returns null if no such session has been created. + * This method sets the context of the session to the specified context. + */ + public WOSession restoreSessionWithID(String aSessionID, WOContext aContext) + { + WORequest request = aContext.request(); + WOSession session = sessionStore.restoreSessionWithID( aSessionID, request ); + if ( session != null ) + { + session.setContext( aContext ); + session.setServletSession( request.servletRequest().getSession() ); + } + return session; + } + + /** + * Called to create a session for a new request. This implementation + * looks for a class in the same package as the application class + * called "Session" and failing that returns a WOSession. + */ + public WOSession createSessionForRequest(WORequest aRequest) + { + WOSession result = null; + try + { + // using our class loader, which is hopefully dynamic. + result = (WOSession) getLocalClass( "Session" ).newInstance(); + } + catch ( Throwable t ) + { + // ignore: fall back to WOSession + //t.printStackTrace(); + } + + if ( result == null ) + { + result = new WOSession(); + } + + result.setServletSession( aRequest.servletRequest().getSession( true ) ); + return result; + } + + /** + * Returns the page component with the specified name. + * A context is created with the specified request, + * along with a session if necessary. + */ + public WOComponent pageWithName (String aName, WORequest aRequest) + { + return pageWithName( aName, WOContext.contextWithRequest( aRequest ) ); + } + + /** + * Called to retrieve a component for the specified context. + */ + public WOComponent pageWithName (String aName, WOContext aContext) + { + if ( aName == null ) + { + throw new IllegalArgumentException( + "WOApplication.pageWithName: name is null" ); + } + + WOComponent result = null; + try + { + // using our class loader, which is hopefully dynamic. + Class c = getLocalClass( aName ); + + if ( c != null ) + { + // get constructor + Constructor ctor; + try + { + ctor = c.getConstructor( new Class[] { WOContext.class } ); + } + catch ( NoSuchMethodException nsme ) + { + ctor = null; + } + + // create instance of class + if ( ctor != null ) + { + result = (WOComponent) ctor.newInstance( new Object[] { aContext } ); + } + else // call back on default constructor (deprecated) + { + result = (WOComponent) c.newInstance(); + } + } + } + catch ( Throwable t ) + { + // ignore for now + //TODO: Throw appropriate exception here + //System.err.println( "Not found: pageWithName: " + aName ); + t.printStackTrace(); + } + + if ( result != null && aContext != null ) + { + // this is where components get their context + result.ensureAwakeInContext( aContext ); + } + else + if ( result == null ) + { + System.err.println( "Not found: pageWithName: " + aName ); + } + + return result; + } + + /** + * Returns a class in the same package as the Application class, + * or, failing that, from the WOApplication package, or finally + * from the root of the class path. Returns null if not found. + */ + Class getLocalClass( String aName ) + { + Class result = null; + if ( getClass() != WOApplication.class ) + { + result = loadLocalClass( getClass(), aName ); + } + if ( result == null ) + { + result = loadLocalClass( WOApplication.class, aName ); + } + if ( result == null ) + { + result = loadLocalClass( null, aName ); + } + return result; + } + + private static final Class loadLocalClass( Class aClass, String aName ) + { + ClassLoader loader; + String packageName = ""; + if ( aClass != null ) + { + loader = aClass.getClassLoader(); + packageName = aClass.getName(); + int index = packageName.lastIndexOf( "." ); + if ( index > -1 ) + { + packageName = packageName.substring( 0, index+1 ); + } + else + { + packageName = ""; + } + } + else + { + loader = WOApplication.class.getClassLoader(); + } + + try + { + return loader.loadClass( packageName + aName ); + } + catch ( ClassNotFoundException e ) + { + return null; + } + } + + // creating elements + + /** + * Returns either a dynamic element or a component + * for the specified name. + */ + public WOElement dynamicElementWithName( + String anElementName, NSDictionary anAssociationMap, + WOElement aBodyElement, List aLanguageList) + { + WOElement element = null; + Class c = null; + try + { + c = getLocalClass( anElementName ); + if ( c == null ) + { + System.out.println( "Not found: dynamicElementWithName: " + + "could not find WODynamicElement subclass: " + anElementName ); + c = WODynamicElement.class; + } + + // get constructor + Class[] params = new Class[] + { String.class, NSDictionary.class, WOElement.class }; + Constructor ctor = c.getConstructor( params ); + + // create instance of class + if ( ctor != null ) + { + element = (WODynamicElement) ctor.newInstance( + new Object[] { anElementName, anAssociationMap, aBodyElement } ); + } + } + catch ( Throwable t ) + { + // ignore: not a dynamic element + //System.out.println( "Not a dynamic element: dynamicElementWithName: " + t ); + //exc.printStackTrace(); + } + + // no dynamic element found: look for a component + if ( element == null ) + { + WOComponent component = (WOComponent) pageWithName( anElementName, (WOContext) null ); + + // this seems hackish: + // I don't see another way of setting the bindings. + component.associations = anAssociationMap; + component.rootElement = aBodyElement; + + element = component; + } + + return element; + } + + // resource handling + + /** + * Called to create the application's resource manager. + * Override to create a custom resource manager. + */ + public WOResourceManager createResourceManager() + { + return new WOResourceManager(); + } + + /** + * Returns the application's current resource manager. + */ + public WOResourceManager resourceManager() + { + return resourceManager; + } + + /** + * Installs a custom resource manager into the current application. + * @deprecated Override createResourceManager() instead. + */ + public void setResourceManager(WOResourceManager aResourceManager) + { + resourceManager = aResourceManager; + } + +/* + // request handling undocumented + + public WOComponent pageWithName (String); + public void savePage (WOComponent); + public WOComponent restorePageForContextID (String); + public WOContext context (); + public WOSession session (); + public WOSession createSession (); + public WOSession restoreSession (); + public void saveSession (WOSession); + + public WOResponse handleRequest (WORequest aRequest) + { + } + + // error handling undocumented + + WOResponse handleSessionCreationError (); + WOResponse handleSessionRestorationError (); + WOResponse handlePageRestorationError (); + WOResponse handleException (Throwable); + + // running + + public NSRunLoop runLoop (); + public void run (); + public void setTimeOut (double); + public double timeOut (); + public void terminate (); + public boolean isTerminating (); + + // script debugging + + public void traceScriptedMessages (boolean); + public void traceAssignments (boolean); + public void traceStatements (boolean); + public void traceObjectiveCMessages (boolean); + public void trace (boolean); + public void logTakeValueForDeclarationNamed (String, String, String, String, Object); + public void logSetValueForDeclarationNamed (String, String, String, String, Object); + + // script handling + + public String scriptedClassNameWithPath (String); + public String scriptedClassNameWithPathEncoding (String, int); + + // statistics report + + public void setStatisticsStore (WOStatisticsStore); + public WOStatisticsStore statisticsStore (); + public NSDictionary statistics (); + + // managing adaptors + + public WOAdaptor adaptorWithName (String, NSDictionary); + public NSArray adaptors (); + + // monitor support + + public boolean monitoringEnabled (); + public int activeSessionsCount (); + public void refuseNewSessions (boolean); + public boolean isRefusingNewSessions (); + public void setMinimumActiveSessionsCount (int); + public int minimumActiveSessionsCount (); + public void terminateAfterTimeInterval (double); + + // garbage collection undocumented + + int garbageCollectionPeriod (); + void setGarbageCollectionPeriod (int); + + // backwards compatibility + + public boolean requiresWOF35RequestHandling (); + public boolean requiresWOF35TemplateParser (); + + public void setPrintsHTMLParserDiagnostics (boolean); + public boolean printsHTMLParserDiagnostics (); + + // configuration and defaults + + public static NSArray loadFrameworks (); + public static void setLoadFrameworks (NSArray); +*/ + static boolean debuggingEnabled = false; + /** + * Returns whether the application is in "debug mode". + */ + public static boolean isDebuggingEnabled() + { + return debuggingEnabled; + } + + /** + * Sets whether the application is in "debug mode". + */ + public static void setDebuggingEnabled( boolean enabled ) + { + debuggingEnabled = enabled; + } + + /** + * 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 static 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 static boolean isCachingEnabled () + { + return cachingEnabled; + } + + // configuration + + /** + * Returns the component request handler key, + * which is defined by the system property _ComponentRequestHandlerKey. + * The default is "wo". + */ + public static String componentRequestHandlerKey() + { + return System.getProperty( _ComponentRequestHandlerKey, "wo" ); + } + + /** + * Sets the component request handler key. + * @deprecated Set the system property _ComponentRequestHandlerKey. + */ + public static void setComponentRequestHandlerKey(String aKey) + { + System.setProperty( _ComponentRequestHandlerKey, aKey ); + } + + /** + * Returns the direct action request handler key, + * which is defined by the system property _DirectActionRequestHandlerKey. + * The default is "wa". + */ + public static String directActionRequestHandlerKey() + { + return System.getProperty( _DirectActionRequestHandlerKey, "wa" ); + } + + /** + * Sets the direct action request handler key. + * @deprecated Set the system property _DirectActionRequestHandlerKey. + */ + public static void setDirectActionRequestHandlerKey(String aKey) + { + System.setProperty( _DirectActionRequestHandlerKey, aKey ); + } + + /** + * Returns the resource request handler key, + * which is defined by the system property _ResourceRequestHandlerKey. + * The default is "wr". + */ + public static String resourceRequestHandlerKey() + { + return System.getProperty( _ResourceRequestHandlerKey, "wr" ); + } + + /** + * Sets the resource request handler key. + * @deprecated Set the system property _ResourceRequestHandlerKey. + */ + public static void setResourceRequestHandlerKey(String aKey) + { + System.setProperty( _ResourceRequestHandlerKey, aKey ); + } + + /** + * Returns whether this application should attempt to open + * a web browser on the host machine when launched standalone. + * The default is true. + */ + public static boolean autoOpenInBrowser() + { + return autoOpenInBrowser; + } + + /** + * Sets whether this application should attempt to open + * a web browser on the host machine when launched standalone. + */ + public static void setAutoOpenInBrowser(boolean autoOpen) + { + autoOpenInBrowser = autoOpen; + } + + /** + * Gets the port used when run as a standalone server. + * Returns the value of the system property WOPort. + * By default, this is zero, which causes the application + * to automatically select an available port. + */ + public static Number port () + { + return Integer.getInteger( WOPort, 0 ); + } + + /** + * Gets the smtp server that will be used to send email. + * Returns the system property WOSMTPHost. + */ + public static String SMTPHost() + { + return System.getProperty( WOSMTPHost ); + } + + /** + * Sets the smtp server that will be used to send email. + * @deprecated Set the system property WOSMTPHost. + */ + public static void setSMTPHost( String aHost ) + { + System.setProperty( WOSMTPHost, aHost ); + } +/* + public static boolean isDirectConnectEnabled (); + public static void setDirectConnectEnabled (boolean); + public static String cgiAdaptorURL (); + public static void setCGIAdaptorURL (String); + public static String applicationBaseURL (); + public static void setApplicationBaseURL (String); + public static String frameworksBaseURL (); + public static void setFrameworksBaseURL (String); + public static String recordingPath (); + public static void setRecordingPath (String); + public static NSArray projectSearchPath (); + public static void setProjectSearchPath (NSArray); + public static boolean isMonitorEnabled (); + public static void setMonitorEnabled (boolean); + public static String monitorHost (); + public static String adaptor (); + public String number (); // deprecated + public static Number listenQueueSize (); + public static void setListenQueueSize (Number); + public static NSArray additionalAdaptors (); + public static void setAdditionalAdaptors (NSArray); + public static boolean includeCommentsInResponses (); + public static void setIncludeCommentsInResponses (boolean); + public static void setSessionTimeOut (Number); + public static Number sessionTimeOut (); + public static void logString (String); + public static void debugString (String); + public static void logToMonitorString (String); +*/ + + /** + * Main entry point for applications that do not subclass WOApplication. + */ + public static void main( String[] argv ) + { + main( argv, WOApplication.class ); + } + + /** + * Subclasses may call this method to start a self-hosted + * web server to serve themselves directly (for testing). + */ + public static void main( String[] argv, Class subclass ) + { + try + { + int port = 0; + boolean open = false; + try + { + port = ((Number)subclass.getMethod( "port", + new Class[0]).invoke(subclass,new Object[0])).intValue(); + open = ((Boolean)subclass.getMethod( "autoOpenInBrowser", + new Class[0]).invoke(subclass,new Object[0])).booleanValue(); + } + catch ( Throwable t ) + { + System.err.print("Error reading configuration:" ); + t.printStackTrace(); + } + + HttpServer server = new HttpServer(); + HttpListener listener = server.addListener(new InetAddrPort(port)); + org.mortbay.http.HttpContext context = server.getContext("/"); + ServletHandler handler = new ServletHandler(); + handler.addServlet("/",subclass.getName()); + context.addHandler(handler); + server.start(); + port = listener.getPort(); + System.out.println("Waiting for requests: http://127.0.0.1:" + port); + if ( open ) + { + BrowserLauncher.openURL( "http://127.0.0.1:" + port ); + } + } + catch ( Throwable t ) + { + t.printStackTrace(); + } + } +} + +/* + * $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.30 2003/03/28 18:01:19 mpowers + * Now defaulting port to zero. + * + * Revision 1.29 2003/03/28 17:31:58 mpowers + * Implemented support for autoselection of free port. (thanks gmuth!) + * + * Revision 1.28 2003/03/28 17:26:17 mpowers + * Implemented package support: Applications can now live in packages. + * Better support for locating package local classes. + * + * Revision 1.27 2003/02/21 16:40:22 mpowers + * Now reading port and smtp host from system properties. + * Implemented WOApplication.main. + * + * Revision 1.26 2003/02/14 22:33:18 mpowers + * Better handling for standalone mode. + * + * Revision 1.25 2003/02/14 15:18:27 mpowers + * Now launching standalone app as a servlet, not a webapp. + * Disabled jetty's event logging. + * + * Revision 1.24 2003/02/13 22:41:04 mpowers + * WOApplications can now be self-serving. Added configuration params too. + * + * Revision 1.23 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.22 2003/01/27 15:08:00 mpowers + * Implemented WOResourceManager, using java resources for now. + * + * Revision 1.21 2003/01/24 20:13:22 mpowers + * Now accepting immutable NSDictionary in constructor, not Map. + * + * Revision 1.20 2003/01/20 17:50:11 mpowers + * Caught a loop condition when same declaration was used twice. + * + * Revision 1.19 2003/01/19 22:33:25 mpowers + * Fixed problems with classpath and dynamic class loading. + * Dynamic elements now pass on ensureAwakeInContext. + * Parser how handles <standalone/> tags. + * + * Revision 1.18 2003/01/18 23:54:50 mpowers + * Implemented debugging enabled. + * + * Revision 1.17 2003/01/17 20:58:18 mpowers + * Fixed up WOHyperlink. + * + * Revision 1.16 2003/01/17 20:34:17 mpowers + * Rudimentary support for resource requests. + * + * Revision 1.15 2003/01/17 15:31:56 mpowers + * Removed spurious error message. + * + * Revision 1.14 2003/01/17 14:39:00 mpowers + * Now calling preferred constructor: WOComponent(WOContext) + * + * Revision 1.13 2003/01/16 20:10:46 mpowers + * - components now synchronize bindings + * - support for WOComponentContent + * - implemented performParentAction + * + * Revision 1.12 2003/01/16 15:50:43 mpowers + * More robust declaration parsing. + * Subcomponents are now supported. + * dynamicElementWithName can now return subcomponents. + * + * Revision 1.11 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.10 2003/01/13 22:24:18 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.9 2003/01/10 20:17:41 mpowers + * Component action urls are now working. + * + * Revision 1.8 2003/01/10 19:16:40 mpowers + * Implemented support for page caching. + * + * Revision 1.4 2002/12/19 17:58:52 mpowers + * Rewrote the template parsing - no longer confused about "root element". + * + * 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/12/17 14:57:41 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:52:50 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOAssociation.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOAssociation.java new file mode 100644 index 0000000..608f9fa --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOAssociation.java @@ -0,0 +1,168 @@ +/* +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; + +/** +* A pure java implementation of WOAssociation. <br><br> +* +* A WOAssociation represents the mapping of a property on a +* WOComponent to a property on a WOAssociation. For example: <br><br> +* +* MyAssociation: WOString { value = currentCustomer.location.city }; <br><br> +* +* This example represents a WOAssociation between the value field +* on a WOString element and the city property of the location property +* of the currentCustomer property of a WOComponent. <br><br> +* +* To resolve values, a property accessor method will be used in +* preference to a public field, if both exist. Any null value +* in the path will produce null. <br><br> +* +* A mapping represented in quotation marks: { value = "This is a test." } +* is considered a constant value. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class WOAssociation implements java.io.Serializable +{ + protected Object value; + protected String path; + + /** + * The default constructor. The static factory methods should + * be used to create instances of WOAssociation. + */ + protected WOAssociation () + { + value = null; + path = null; + } + + /** + * Creates a WOAssociation that maps to a constant value. + */ + public static WOAssociation associationWithValue (Object anObject) + { + WOAssociation result = new WOAssociation(); + result.value = anObject; + return result; + } + + /** + * Creates a WOAssociation that maps to the specified key path. + * If the path is null, the association will map to null. + * Throws an exception if the property cannot be resolved. + */ + public static WOAssociation associationWithKeyPath (String aString) + { + WOAssociation result = new WOAssociation(); + result.path = aString; + return result; + } + + /** + * Returns the value for this association's key path in the + * specified component, or null if any value in the path is + * null or if the key path is null. + */ + public Object valueInComponent (WOComponent aComponent) + { + if ( aComponent == null ) return null; + if ( value != null ) return value; + if ( path != null ) return aComponent.valueForKey( path ); + throw new RuntimeException( + "WOAssociation: neither value nor path specified!" ); + } + + /** + * Sets the property in the specified component to the specified value. + * Throws an exception if the property cannot be resolved. + */ + public void setValue (Object aValue, WOComponent aComponent) + { + if ( path != null ) + { + aComponent.takeValueForKey( aValue, path ); + return; + } + throw new RuntimeException( + "WOAssociation: tried to set value but no path was specified!" ); + } + + /** + * Returns true if this association is writable; that is, + * returns true if this association is not constant. + */ + public boolean isValueSettable () + { + return ( path != null ); + } + + /** + * Returns true if this association is constant + * and therefore read-only. + */ + public boolean isValueConstant () + { + return ( path == null ); + } + + /** + * For debugging purposes. + */ + public String toString() + { + if ( path != null ) + { + return "[WOAssociation:" + path + "]"; + } + return "[WOAssociation:\"" + value + "\"]"; + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/01/24 20:13:22 mpowers + * Now accepting immutable NSDictionary in constructor, not Map. + * + * Revision 1.5 2003/01/17 22:55:08 mpowers + * Straighted out the parent binding issue (I think). + * Fixes for woextensions compatibility. + * + * Revision 1.3 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.2 2003/01/14 15:51:48 mpowers + * Removed value() method from WOAssociaton. + * + * Revision 1.1.1.1 2000/12/21 15:52:50 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOBody.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOBody.java new file mode 100644 index 0000000..9f0707d --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOBody.java @@ -0,0 +1,121 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2003 Intersect Software Corporation + +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 net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; + +/** +* WOBody represents a page's "body" tag and is used to dynamically +* set the background image for a page, and therefore works much like WOImage. +* +* Bindings are: +* <UL> +* <li>src: A static URL for the image source.</li> +* <li>data: A NSData object with the image content. Must be used with mimeType.</li> +* <li>mimeType: The MIME type for the image data. Can be used with filename or data bindings.</li> +* <li>filename: The path to a file containing an image.</li> +* <li>framework: The optional framework from whence the image should be retrieved (used in conjunction with filename).</li> +* <li>key: A key under which this resource will be cached for repeated access.</li> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOBody extends WOImage { + + protected String src; + protected String filename; + protected String framework; + protected NSData data; + protected String mimeType; + + protected WOBody() { + super(); + } + + public WOBody(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + void ensureAwakeInContext (WOContext aContext) + { + if ( rootElement != null ) + { + rootElement.ensureAwakeInContext( aContext ); + } + } + + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + if ( rootElement != null ) + { + rootElement.takeValuesFromRequest( aRequest, aContext ); + } + } + + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + if ( rootElement != null ) + { + return rootElement.invokeAction( aRequest, aContext ); + } + return null; + } + + public void appendToResponse(WOResponse r, WOContext c) + { + r.appendContentString("<body background=\""); + r.appendContentString(sourceURL(c)); + r.appendContentString("\""); + r.appendContentString(additionalHTMLProperties(c.component(), new NSArray(new Object[]{ + "src", "filename", "framework", "data", "mimeType" }))); + r.appendContentString(">"); + if ( rootElement != null ) + { + rootElement.appendToResponse( r, c ); + } + r.appendContentString("</body>"); + } + +} + +/* + * $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.4 2003/08/07 00:15:14 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.3 2003/01/27 15:57:28 mpowers + * Now participates in all parts of request/response cycle. + * + * Revision 1.2 2003/01/27 15:08:23 mpowers + * Now appending to response. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCheckBox.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCheckBox.java new file mode 100644 index 0000000..5d22d36 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCheckBox.java @@ -0,0 +1,81 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +public class WOCheckBox extends WOInput { + + protected boolean checked = false; + + public WOCheckBox() { + super(); + } + + public WOCheckBox(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "CHECKBOX"; + } + + protected Object value(WOContext c) { + Object val = null; + boolean checked = false; + if (associations.objectForKey("value") != null) { + val = valueForProperty("value", c.component()); + Object sel = valueForProperty("selection", c.component()); + if (sel != null && val != null && sel.equals(val)) + checked = true; + } + if (val == null) { + val = c.elementID(); + } + return val; + } + + protected void appendExtras(WOResponse r, WOContext c) { + checked |= booleanForProperty("checked", c.component()); + if (checked) + r.appendContentString(" CHECKED"); + } + + protected NSMutableArray additionalAttributes() { + NSMutableArray a = super.additionalAttributes(); + a.addObject("checked"); + a.addObject("selection"); + return a; + } + + public void appendToResponse(WOResponse r, WOContext c) { + checked = false; + super.appendToResponse(r, c); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + if (disabled(c)) + return; + NSArray values = r.formValuesForKey(inputName(c)); + Object val = valueForProperty("value", c.component()); + if (val == null) + val = c.elementID(); + java.util.Enumeration enumerator = values.objectEnumerator(); + checked = false; + while (enumerator.hasMoreElements()) { + Object nextval = enumerator.nextElement(); + if (nextval.equals(val)) + checked = true; + } + if (associations.objectForKey("value") != null && associations.objectForKey("selection") != null) { + if (checked) + setValueForProperty("selection", val, c.component()); + else if (valueForProperty("selection", c.component()) != null) + setValueForProperty("selection", null, c.component()); + } + if (associations.objectForKey("checked") != null) + setValueForProperty("checked", checked ? Boolean.TRUE : Boolean.FALSE, c.component()); + } + +}
\ No newline at end of file diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponent.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponent.java new file mode 100644 index 0000000..20d8b0a --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponent.java @@ -0,0 +1,1312 @@ +/* +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 + "</" + tagName + ">" ); + } + + 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 <standalone/> 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. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentContent.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentContent.java new file mode 100644 index 0000000..1544934 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentContent.java @@ -0,0 +1,45 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; + +/** + * Used to include a component's content inside another component. This way, you can create reusable components + * with content that surrounds the component it's in. In reality all it does is forward the appendToResponse + * method to the WOElement containing the HTML between the WEBOBJECT tags that define this element. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOComponentContent extends WODynamicElement { + + public WOComponentContent() { + super(); + } + + public WOComponentContent(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public void appendToResponse(WOResponse r, WOContext c) { + c.component().rootElement.appendToResponse(r, c); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentRequestHandler.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentRequestHandler.java new file mode 100644 index 0000000..9f79987 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentRequestHandler.java @@ -0,0 +1,229 @@ +/* +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; + + +/** +* An implementation of WORequestHandler that dispatches Component actions. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOComponentRequestHandler + extends WORequestHandler +{ + public WOResponse handleRequest( WORequest aRequest ) + { + WOApplication application = aRequest.application(); + WOContext context = WOContext.contextWithRequest( aRequest ); + WOResponse response = null; + + // no concurrent access is allowed to a user's session: + // a user/browser/machine with multiple requests will have to wait + synchronized ( aRequest.request.getSession() ) + { + try + { + application.awake(); + + WOSession session = null; + /* + // the NeXT way + String sessionID = aRequest.sessionID(); + if ( sessionID != null ) + { + session = application.restoreSessionWithID( sessionID, context ); + if ( session == null ) + { + response = application.handleSessionRestorationErrorInContext( context ); + } + } + else + { + session = application.createSessionForRequest( aRequest ); + if ( session == null ) + { + response = application.handleSessionCreationErrorInContext( context ); + } + else + { + session.setContext( context ); + } + } + */ + // the servlet way + String sessionID = aRequest.sessionID(); + if ( sessionID != null ) + { + session = application.restoreSessionWithID( sessionID, context ); + } + + if ( session == null ) + { + session = application.createSessionForRequest( aRequest ); + if ( session == null ) + { + response = application.handleSessionCreationErrorInContext( context ); + } + else + { + session.setContext( context ); + } + } + + context.setSession( session ); + + session.awake(); + + if ( response == null ) + { + + WOComponent page; + String contextID = aRequest.contextID(); + + if ( contextID != null ) + { + page = session.restorePageForContextID( contextID ); + } + else + { + page = application.pageWithName( aRequest.pageName(), context ); + } + + if ( page == null ) + { + //FIXME: should we call this a restoration error, or do we + // allow a bookmark with an expired context id to resume gracefully? + page = application.pageWithName( aRequest.pageName(), context ); + //!response = application.handlePageRestorationErrorInContext( context ); + } + //!else + { + context.pushElement( page ); //? needed? + page.ensureAwakeInContext( context ); //? shouldn't this be in WOApplication? + + // only take values from request if there are values in the request + System.out.println("should I takeValuesFromRequest ? " + ( aRequest.formValueKeys().count() > 0 )); + if ( aRequest.formValueKeys().count() > 0 ) + { + application.takeValuesFromRequest( aRequest, context ); + } + + // only invoke action if there is a sender id to invoke + WOActionResults result; + System.out.println("senderID: " + aRequest.senderID()); + if ( aRequest.senderID() != null ) + { + result = application.invokeAction( aRequest, context ); + } + else + { + result = null; + } + + if ( result == null || result == page ) // same page is returned + { + result = page; + + // generate response + response = new WOResponse(); + application.appendToResponse( response, context ); + page.sleep(); + } + else // different page is returned + if ( result instanceof WOComponent ) + { + page.sleep(); + page = (WOComponent) result; + page.ensureAwakeInContext( context ); + context.popElement(); // removes page + context.pushElement( page ); + + // generate response + response = new WOResponse(); + application.appendToResponse( response, context ); + page.sleep(); + } + else // WOResponse was returnd + { + response = (WOResponse) result; + } + + context.popElement(); + session.sleep(); + session.savePage( page ); + session.setContext( null ); + application.saveSessionForContext( context ); + } + } + } + catch ( Throwable t ) + { + response = application.handleException( t, context ); + } + + application.sleep(); + } + return response; + } +} + +/* + * $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.8 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.7 2003/01/17 20:34:57 mpowers + * Better handling for components and parents in the context's element stack. + * + * Revision 1.6 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.4 2003/01/14 15:51:09 mpowers + * No longer calling takeValues or invokeAction if there are no request params + * + * Revision 1.3 2003/01/13 22:24:34 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.1 2003/01/09 16:13:59 mpowers + * Implemented WOComponentRequestHandler: + * Bringing the request-response cycle more into conformance. + * + * Revision 1.2 2002/12/17 14:57:44 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:53:19 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:50 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOConditional.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOConditional.java new file mode 100644 index 0000000..124ca11 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOConditional.java @@ -0,0 +1,99 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; + +/** +* WOConditional renders whatever is inside its opening and closing tags + * only if a condition is met. + * Bindings are: + * <ul> + * <li>condition: a boolean property that indicates whether the contents of the element + * should be displayed, invoked, or passed the request form values.</li> + * <li>negate: if this is true, then the behavior of the element is reversed, showing its + * contents only if the condition is NOT met.</li> + * </ul> + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 893 $ + */ +public class WOConditional extends WODynamicElement { + + public boolean condition; + public boolean negate; + + protected WOConditional() { + super(); + } + + public WOConditional(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public void setCondition(boolean value) { + condition = value; + } + public boolean condition() { + return condition; + } + + public void setNegate(boolean value) { + negate = value; + } + public boolean negate() { + return negate; + } + + protected void pullValuesFromParent(WOComponent c) { + condition = booleanForProperty("condition", c); + negate = booleanForProperty("negate", c); + } + + public void takeValuesFromRequest(WORequest aRequest, WOContext aContext) { + if (rootElement == null) + return; + pullValuesFromParent(aContext.component()); + if ((condition && !negate) || (!condition && negate)) { + rootElement.takeValuesFromRequest(aRequest, aContext); + } + } + + public WOActionResults invokeAction(WORequest aRequest, WOContext aContext) { + if (rootElement == null) + return null; + pullValuesFromParent(aContext.component()); + WOActionResults el = null; + if ((condition && !negate) || (!condition && negate)) { + el = rootElement.invokeAction(aRequest, aContext); + } + return el; + } + + public void appendToResponse(WOResponse aResponse, WOContext aContext) { + if (rootElement == null) + return; + pullValuesFromParent(aContext.component()); + if ((condition && !negate) || (!condition && negate)) { + rootElement.appendToResponse(aResponse, aContext); + } + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOContext.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOContext.java new file mode 100644 index 0000000..4f774e6 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOContext.java @@ -0,0 +1,572 @@ +/* +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.IOException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** +* A pure java implementation of WOContext. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOContext +{ + private WOSession session; + private WORequest request; + private WOResponse response; + private List elementStack; + private boolean isInForm; + private boolean isDistributionEnabled; + private static final String EMPTY = ""; + private static final String SEP = "."; + private static final String ZERO = "0"; + private static final String HTTP = "http://"; + private static final String HTTPS = "https://"; + + // package access + WOComponent page; + WOComponent component; + StringBuffer elementID; + + private static volatile int contextCounter = 0; + private String contextID; + + /** + * Default constructor performs necessary initialization. + * Subclassers should override the static factory method below. + */ + public WOContext () + { + contextID = null; + elementID = new StringBuffer(); + elementStack = new LinkedList(); + } + + /** + * Preferred constructor performs necessary initialization. + * Subclassers should override this method. + */ + public WOContext (WORequest aRequest) + { + this(); + request = aRequest; + response = new WOResponse(); + } + + /** + * Simply calls the preferred constructor. + * Included for compatibility. + */ + public static WOContext contextWithRequest (WORequest aRequest) + { + String id = Integer.toString( contextCounter++ ); + WOContext result = new WOContext( aRequest ); + result.contextID = id; + return result; + } + + /** + * Returns the context id. + */ + public String contextID () + { + return contextID; + } + + /** + * Returns the sender id, or null if it doesn't exist. + */ + public String senderID () + { + return request.senderID(); + } + + /** + * Returns the element id, or null if it doesn't exist. + */ + public String elementID () + { + return elementID.toString(); + } + + /** + * Returns this context's application, or null if it doesn't exist. + */ + public WOApplication application () + { + return request.application(); + } + + /** + * Returns whether a session has been created for the associated request. + */ + public boolean hasSession () + { + return ( session != null ); + } + + /** + * Returns this context's session, creating one if it doesn't exist. + */ + public WOSession session () + { + if ( session == null ) + { + // so far we can't figure out how the direct action handler can avoid creating a session + throw new RuntimeException( "WOContext.session: Lazy instantiation not yet implemented." ); +/* + session = application().restoreSessionWithID( + request.sessionID(), this ); + if ( session == null ) + { + session = application().createSessionForRequest( request ); + session.setContext( this ); + } +*/ + } + return session; + } + + /** + * Package access only. + */ + void setSession( WOSession aSession ) + { + session = aSession; + } + + /** + * Returns this context's request. + */ + public WORequest request () + { + return request; + } + + /** + * Returns this context's response. + */ + public WOResponse response () + { + return response; + } + + /** + * Returns the current page. + */ + public WOComponent page () + { + return (WOComponent) elementStack.get( elementStack.size()-1 ); + } + + /** + * Returns the current component. + */ + public WOComponent component () + { + Object o; + Iterator i = elementStack.iterator(); + while ( i.hasNext() ) + { + o = i.next(); + if ( o instanceof WOComponent ) return (WOComponent) o; + } + return null; + } + + /** + * Returns the current component's parent. + */ + WOComponent parent () + { + Object o; + Iterator i = elementStack.iterator(); + if ( i.hasNext() ) + { + // skip current component + o = i.next(); + } + while ( i.hasNext() ) + { + o = i.next(); + if ( o instanceof WOComponent ) + { + return (WOComponent) o; + } + } + return null; + } + + /** + * Pushes an element onto the stack. + * Package access only. + */ + void pushElement( WOElement aComponent ) + { + elementStack.add( 0, aComponent ); + } + + /** + * Pops an element off the stack. + * Package access only. + */ + WOElement popElement() + { + return (WOElement) elementStack.remove(0); + } + + /** + * Returns whether the current context is in a form. + */ + public boolean isInForm () + { + return isInForm; + } + + /** + * Sets whether the current context is in a WOForm. + */ + public void setInForm (boolean inForm) + { + isInForm = inForm; + } + + /** + * Appends the specified string to the end of the element id. + * For example, if the element id is "0.1", sending "Test" + * changes the element id to "0.1.Test". + */ + public void appendElementIDComponent (String aString) + { + if ( elementID.length() > 0 ) + { + elementID.append( SEP ); + } + elementID.append( aString ); + } + + /** + * Appends a zero to the element id to represent the first + * new child component. For example, if the element id is "0.1", + * calling this changes the element id to "0.1.0". + */ + public void appendZeroElementIDComponent () + { + if ( elementID.length() > 0 ) + { + elementID.append( SEP ); + } + elementID.append( ZERO ); + } + + /** + * Increments the last component of the element id. + * For example, if the element id is "0.1", calling this + * changes the element id to "0.2". + */ + public void incrementLastElementIDComponent () + { + String last; + String id = elementID.toString(); + int index = id.lastIndexOf( SEP ); + if ( index == -1 ) + { + last = id; + } + else + { + last = id.substring( index + 1 ); + } + + deleteLastElementIDComponent(); + + try + { + appendElementIDComponent( + Integer.toString( Integer.parseInt( last ) + 1 ) ); + } + catch ( Exception exc ) + { + System.err.println( "Error parsing id: " + last ); + appendZeroElementIDComponent(); + } + //System.out.println( "WOContext: " + elementID ); + } + + /** + * Deletes the last component of the element id. + * For example, if the element id is "0.1", callling this + * changes the element id to "0". + */ + public void deleteLastElementIDComponent () + { + int index = elementID.toString().lastIndexOf( SEP ); + if ( index == -1 ) + { + elementID.setLength( 0 ); + } + else + { + elementID.setLength( index ); + } + } + + /** + * Deletes all components of the element id. + * This makes the element id an empty string. + */ + public void deleteAllElementIDComponents () + { + elementID.setLength( 0 ); + } + + /** + * Returns a URL for the named action with query parameters + * as specified by the dictionary. + */ + public String directActionURLForActionNamed ( + String anActionName, Map aQueryDict) + { + StringBuffer query = new StringBuffer(); + + try + { + String key; + Iterator i = aQueryDict.keySet().iterator(); + while ( i.hasNext() ) + { + key = i.next().toString(); + query.append( URI.encode( key, + URI.allowed_within_query ) ); + query.append( '=' ); + query.append( URI.encode( aQueryDict.get( key ).toString(), + URI.allowed_within_query ) ); + if ( i.hasNext() ) + { + query.append( '&' ); + } + } + } + catch ( IOException exc ) + { + // report error + System.err.println( + "directActionURLForActionNamed: " + anActionName + " : " + aQueryDict ); + System.err.println( exc ); + + // delete query string + query = new StringBuffer(); + } + + return urlWithRequestHandlerKey( + WOApplication.directActionRequestHandlerKey(), + anActionName, query.toString() ); + } + + /** + * Returns the complete URL for the current component action. + */ + public String componentActionURL () + { + StringBuffer buffer = new StringBuffer(); + buffer.append( request().applicationName() ); + buffer.append( '/' ); + buffer.append( WOApplication.application().componentRequestHandlerKey() ); + buffer.append( '/' ); + buffer.append( page().name() ); + buffer.append( '/' ); + buffer.append( contextID ); + buffer.append( '/' ); + buffer.append( elementID ); + return buffer.toString(); + } + + /** + * Returns a URL relative to the servlet for the specified + * request handler, action, and query string. + */ + public String urlWithRequestHandlerKey ( + String aRequestHandlerKey, String aPath, String aQueryString) + { + StringBuffer buffer = new StringBuffer(); + buffer.append( request().applicationName() ); + buffer.append( '/' ); + buffer.append( aRequestHandlerKey ); + buffer.append( '/' ); + buffer.append( aPath ); + if ( aQueryString != null && aQueryString.trim().length() > 0 ) + { + buffer.append( '?' ); + buffer.append( aQueryString ); + } + return buffer.toString(); + } + + /** + * Returns the complete URL for the specified request handler, + * path, and query string. isSecure determines the protocol: + * http or https. port is appended to the protocol, if zero, + * the port is omitted from the URL. + */ + public String completeURLWithRequestHandlerKey ( + String aRequestHandlerKey, String aPath, String aQueryString, + boolean isSecure, int port) + { + StringBuffer buffer = new StringBuffer(); + + if ( isSecure ) + { + buffer.append( HTTPS ); + } + else + { + buffer.append( HTTP ); + } + + buffer.append( request.applicationHost() ); + + if ( port != 0 && port != 80 ) + { + buffer.append( ':' ); + buffer.append( Integer.toString( port ) ); + } + + buffer.append( urlWithRequestHandlerKey( + aRequestHandlerKey, aPath, aQueryString ) ); + + return buffer.toString(); + } + + /** + * Sets whether distribution is enabled. + */ + public void setDistributionEnabled (boolean distributionEnabled) + { + isDistributionEnabled = distributionEnabled; + } + + /** + * Returns whether distribution is enabled. + */ + public boolean isDistributionEnabled () + { + return isDistributionEnabled; + } + + /** + * This method is not included in the WOContext specification. + * This implementation returns "" since only cookie ids are + * currently supported. + */ + String urlSessionPrefix () + { + return EMPTY; // "/" + sessionid; + } + + /** + * Returns the relative URL for the current page. + */ + String url () + { + throw new RuntimeException( "Not implemented yet." ); + } + + public String toString() + { + return "[WOContext@"+Integer.toHexString(System.identityHashCode(this)) + +":id=" + contextID +":page=" + page + ":component=" + component + ":element=" + elementID + "]"; + } +} + +/* + * $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.19 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.18 2003/02/21 16:40:23 mpowers + * Now reading port and smtp host from system properties. + * Implemented WOApplication.main. + * + * Revision 1.17 2003/01/24 20:12:54 mpowers + * Better parent determination. + * + * Revision 1.16 2003/01/21 22:27:02 mpowers + * Corrected context id usage. + * Implemented backtracking. + * + * Revision 1.15 2003/01/17 20:58:19 mpowers + * Fixed up WOHyperlink. + * + * Revision 1.14 2003/01/17 20:34:57 mpowers + * Better handling for components and parents in the context's element stack. + * + * Revision 1.13 2003/01/14 19:48:36 mpowers + * - fixes to property synchronization + * - forms now pass takeValuesFromRequest to children + * - fixes to action invocation + * - now supporting multipleSubmit in forms + * + * Revision 1.12 2003/01/13 22:24:44 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.11 2003/01/10 20:17:41 mpowers + * Component action urls are now working. + * + * 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:59 mpowers + * Implemented WOComponentRequestHandler: + * Bringing the request-response cycle more into conformance. + * + * Revision 1.4 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/12/17 14:57:42 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:53:04 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCookie.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCookie.java new file mode 100644 index 0000000..78427f0 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCookie.java @@ -0,0 +1,203 @@ +/* +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 net.wotonomy.foundation.NSDate; + +/** +* A pure java implementation of WOCookie that extends +* javax.servlet.httpd.Cookie for greater compatibility. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOCookie + extends javax.servlet.http.Cookie +{ + /** + * Default constructor. + */ + public WOCookie () + { + super( "", "" ); + } + + /** + * Constructs a cookie with the specified name and value. + */ + public WOCookie( String aName, String aValue ) + { + super( aName, aValue ); + } + + /** + * Constructs a cookie with the specified name and value. + * Also sets the path to the current application's path. + */ + public static WOCookie cookieWithName (String aName, String aValue) + { + WOCookie result = new WOCookie( aName, aValue ); + //TODO: Set the path to the current application's path. + return result; + } + + /** + * Constructs a cookie with the specified attributes. + */ + public static WOCookie cookieWithName (String aName, String aValue, + String aPath, String aDomain, NSDate expirationDate, boolean secure) + { + WOCookie result = new WOCookie( aName, aValue ); + result.setPath( aPath ); + result.setDomain( aDomain ); + result.setExpires( expirationDate ); + result.setSecure( secure ); + return result; + } + + /** + * Returns the name of the cookie. + */ + public String name () + { + return this.getName(); + } + + /** + * Sets the name of the cookie. + */ + public void setName (String aString) + { + // super.setName( aString ); + throw new RuntimeException( "Not yet implemented." ); + } + + /** + * Returns the value of the cookie. + */ + public String value () + { + return this.getValue(); + } + + /** + * Sets the value of the cookie. + */ + public void setValue (String aString) + { + super.setValue( aString ); + } + + /** + * Gets the domain of the cookie. + */ + public String domain () + { + return this.getDomain(); + } + + /** + * Sets the domain of the cookie. + */ + public void setDomain (String aString) + { + super.setDomain( aString ); + } + + /** + * Gets the path of the cookie. + */ + public String path () + { + return this.getPath(); + } + + /** + * Sets the path of the cookie. + */ + public void setPath (String aString) + { + super.setPath( aString ); + } + + /** + * Gets the expiration date of the cookie. + * If in the past, the cookie will persist until browser shutdown. + */ + public NSDate expires () + { + return new NSDate( this.getMaxAge() ); + } + + /** + * Sets the expiration date of the cookie. + */ + public void setExpires (NSDate aDate) + { + this.setMaxAge( (int) aDate.timeIntervalSinceNow() ); + } + + /** + * Returns whether the cookie will only be sent over a secure protocol. + */ + public boolean isSecure () + { + return this.getSecure(); + } + + /** + * Sets whether the cookie will only be sent over a secure protocol. + */ + public void setIsSecure (boolean isSecure) + { + this.setSecure( isSecure ); + } + + /** + * Returns the string as it appears in the HTTP header of the response. + * This would normally be called by WOResponse, but is handled automatically + * by the servlet implementation. + */ + public String headerString () + { + new RuntimeException( "Not implemented yet." ); + return null; + } +} + +/* + * $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.1.1.1 2000/12/21 15:53:04 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:50 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectAction.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectAction.java new file mode 100644 index 0000000..09d0131 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectAction.java @@ -0,0 +1,317 @@ +/* +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.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.servlet.http.HttpSession; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.ValueConverter; + +/** +* A pure java implementation of WODirectAction. +* This class provides a number of services to subclasses, +* including access to the query parameters, the request, +* the session, and logging and debugging facilities. +* A new instance of the class is created and then destroyed +* for every request-response cycle.<br><br> +* +* Subclass this to implement direct actions for your +* application. Subclasses would typically override the +* constructor to initialize state based on the request, +* and then provide additional methods that would be invoked +* based on the value at the end of the URI. <br><br> +* +* Example: "http://www/MyApp.woa/MyActions/search" would call +* the method named "searchAction" on the DirectAction subclass +* named "MyActions". If the subclass name is omitted, a subclass +* named "DirectAction" is assumed to exist within the application. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WODirectAction +{ + private WORequest request; + WOContext context; + + /** + * Default constructor. This is called implicitly by + * subclasses in all cases. Package access only. + */ + WODirectAction () + { + + } + + /** + * Request constructor. This is the constructor used + * to create an action in response to a user request. + * Override to perform any necessary initialization in + * your subclass. + */ + public WODirectAction (WORequest aRequest) + { + this(); + request = aRequest; + } + + /** + * Returns the response from the component named "Main". + */ + public WOActionResults defaultAction() + { + return pageWithName("Main").generateResponse(); + } + + /** + * Returns the WORequest object for the current request. + */ + public WORequest request () + { + return request; + } + + /** + * Returns the existing session, or null if no session exists. + */ + public WOSession existingSession () + { + //FIXME: this is incorrect + HttpSession session = request.servletRequest().getSession(); + if ( session == null ) return null; + WOSession wosession = new WOSession(); + wosession.setServletSession( session ); + return wosession; + } + + /** + * Returns the existing session if it exists. If no session + * exists, returns a newly created session. Note that if the + * user client does not support session ids/cookies, this will + * create a new session with each request. + */ + public WOSession session () + { + //FIXME: this is incorrect + WOSession wosession = new WOSession(); + wosession.setServletSession( + request.servletRequest().getSession( true ) ); + return wosession; + } + + /** + * Returns the named WOComponent. + */ + public WOComponent pageWithName (String aString) + { + return request.application().pageWithName( aString, context ); + } + + /** + * Appends "Action" to the specified string and tries to invoke + * method with that name and no arguments. Returns null if + * the method does not exist. If anAction is null, "defaultAction" + * will be assumed. + */ + public WOActionResults performActionNamed (String anAction) + { + if ( anAction == null ) anAction = "default"; + try + { + NSSelector sel = new NSSelector( anAction+"Action" ); + return (WOActionResults) sel.invoke( this ); + } + catch ( NoSuchMethodException exc ) + { + // returns below + } + catch ( InvocationTargetException exc ) + { + Throwable e = exc.getTargetException(); + exc.printStackTrace(); + throw new RuntimeException( e.toString() ); + } + catch ( Exception exc ) + { + exc.printStackTrace(); + throw new RuntimeException( exc.toString() ); + } + WOResponse error = new WOResponse(); + error.setStatus( 404 ); // not found + error.appendContentString( + "Could not find method named \"" + anAction + "Action\"." ); + return error; + } + + /** + * Assigns the arrays of form values for the specified keys + * to properties on this object with matching names whose type + * is NSArray or is convertable from a Collection. + */ + public void takeFormValueArraysForKeyArray (NSArray anArray) + { + if ( anArray == null ) return; + + Method m; + Object key; + Object value; + java.util.Enumeration e = anArray.objectEnumerator(); + while ( e.hasMoreElements() ) + { + key = e.nextElement(); + value = request.formValuesForKey( key.toString() ); + try + { + // obtain setter method for this class + m = Introspector.getPropertyWriteMethod( + this.getClass(), key.toString(), + new Class[] { Introspector.WILD } ); + if ( m != null ) + { + // if value isn't null, try to convert it to type + if ( value != null ) + { + Class[] paramTypes = m.getParameterTypes(); + if ( ! paramTypes[0].isAssignableFrom( + value.getClass() ) ) + { + //TODO: find a constructor whose parameter + // is assignable from value.getClass() + // and instantiate it in the place of value. + } + } + // set property to value + m.invoke( this, new Object[] { value } ); + } + } + catch ( Exception exc ) + { + // show error and continue + debugString( "WODirectAction.takeFormValuesForKeyArray: " + exc ); + } + } + + } + + /** + * Assigns the form values for the specified keys to properties + * on this object with matching names. + */ + public void takeFormValuesForKeyArray (NSArray anArray) + { + if ( anArray == null ) return; + + Method m; + Object key; + Object value; + java.util.Enumeration e = anArray.objectEnumerator(); + while ( e.hasMoreElements() ) + { + key = e.nextElement(); + value = request.formValueForKey( key.toString() ); + try + { + // obtain setter method for this class + m = Introspector.getPropertyWriteMethod( + this.getClass(), key.toString(), + new Class[] { Introspector.WILD } ); + if ( m != null ) + { + // if value isn't null, try to convert it to type + if ( value != null ) + { + Class[] paramTypes = m.getParameterTypes(); + Object convertedValue = + ValueConverter.convertObjectToClass( + value, paramTypes[0] ); + if ( convertedValue != null ) + { + value = convertedValue; + } + } + // set property to value + m.invoke( this, new Object[] { value } ); + } + } + catch ( Exception exc ) + { + // show error and continue + debugString( "WODirectAction.takeFormValuesForKeyArray: " + exc ); + } + } + + } + + /** + * Writes a message to the standard error stream. + */ + public static void logString (String aString) + { + System.err.println( aString ); + } + + /** + * Writes a message to the standard error stream + * if debugging is activated. + */ + public static void debugString (String aString) + { + // TODO: Check to see if debugging is enabled. + System.err.println( aString ); + } +} + +/* + * $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.5 2003/01/13 22:24:51 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.4 2003/01/09 21:16:48 mpowers + * Bringing request-response cycle more into conformance. + * + * Revision 1.3 2003/01/07 15:58:11 mpowers + * Implementing the request-response cycle. + * WOSession refactored to support distribution. + * + * Revision 1.2 2002/12/17 14:57:43 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:53:18 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:50 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectActionRequestHandler.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectActionRequestHandler.java new file mode 100644 index 0000000..eac826b --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectActionRequestHandler.java @@ -0,0 +1,221 @@ +/* +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 net.wotonomy.foundation.NSArray; + +/** +* An implementation of WORequestHandler that dispatches +* DirectActions. <br><br> +* +* See WODirectAction for the rules for parsing a request. +* In short, className defaults to "DirectAction", and the +* action defaults to "default". The action class is expected +* to have a constructor that takes a WORequest parameter. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WODirectActionRequestHandler + extends WORequestHandler +{ + public WOResponse handleRequest (WORequest aRequest) + { + WOResponse response = null; + + // no concurrent access is allowed to a user's session: + // a user/browser/machine with multiple requests will have to wait. + // NOTE: this forces a session creation for any direct action (!) + synchronized ( aRequest.request.getSession() ) + { + WOApplication application = aRequest.application(); + WOContext context = WOContext.contextWithRequest( aRequest ); + + String className = "DirectAction"; + String actionName = "default"; + NSArray path = aRequest.requestHandlerPathArray(); + if ( path.count() > 0 ) + { + className = path.objectAtIndex( 0 ).toString(); + if ( path.count() > 1 ) + { + actionName = path.objectAtIndex( path.count() - 1 ).toString(); + } + if ( path.count() > 2 ) + { + for ( int i = 1; i < path.count()-1; i++ ) + { + className = className + "." + + path.objectAtIndex( i ).toString(); + } + } + } + + //FIXME: sessions are supposed to be lazily created for direct actions + + WOSession session = null; + + String sessionID = aRequest.sessionID(); + if ( sessionID != null ) + { + session = application.restoreSessionWithID( sessionID, context ); + } + + if ( session == null ) + { + session = application.createSessionForRequest( aRequest ); + if ( session == null ) + { + response = application.handleSessionCreationErrorInContext( context ); + } + else + { + session.setContext( context ); + } + } + + context.setSession( session ); + + application.awake(); + session.awake(); + + try + { + + if ( response == null ) + { + Class c = null; + c = application.getLocalClass( className ); + if ( ( c == null ) && ( path.count() == 1 ) ) + { + actionName = className; + className = "DirectAction"; + c = application.getLocalClass( className ); + } + if ( c == null ) + { + throw new RuntimeException( + "Could not find class named \"" + className + + "\": " ); + } + java.lang.reflect.Constructor ctor = + c.getConstructor( new Class[] { WORequest.class } ); + + WODirectAction action = (WODirectAction) + ctor.newInstance( new Object[] { aRequest } ); + action.context = context; //HACK: how else can action call pageWithName? + + WOActionResults result = action.performActionNamed( actionName ); + if ( result instanceof WOComponent ) + { + //HACK: I'm not sure where this should have gone: seems hackish here. + context.pushElement( (WOComponent) result ); + ((WOComponent)result).ensureAwakeInContext( context ); + //context.popElement(); + } + response = result.generateResponse(); // calls session.savePage (?) + if ( result instanceof WOComponent ) + { + //HACK: I'm not sure where this should have gone: seems hackish here. + ((WOComponent)result).sleep(); + } + } + } + catch ( NoSuchMethodException exc ) + { + WOResponse error = new WOResponse(); + exc.printStackTrace(); + error.setStatus( 404 ); // not found + error.appendContentString( + "Could not find request constructor for class named \"" + + className + "\": " ); + response = error; + } + catch ( Exception exc ) + { + WOResponse error = new WOResponse(); + error.setStatus( 500 ); // error + if ( exc.getMessage() != null ) + { + error.appendContentString( exc.getMessage() ); + exc.printStackTrace(); + } + else + { + error.appendContentString( exc.toString() ); + exc.printStackTrace(); + } + response = error; + } + + session.sleep(); + session.setContext( null ); + application.saveSessionForContext( context ); + application.sleep(); + } + return response; + } + +} + +/* + * $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.10 2003/03/28 17:26:18 mpowers + * Implemented package support: Applications can now live in packages. + * Better support for locating package local classes. + * + * Revision 1.9 2003/01/19 22:33:26 mpowers + * Fixed problems with classpath and dynamic class loading. + * Dynamic elements now pass on ensureAwakeInContext. + * Parser how handles <standalone/> tags. + * + * Revision 1.8 2003/01/17 20:34:57 mpowers + * Better handling for components and parents in the context's element stack. + * + * Revision 1.7 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.5 2003/01/13 22:25:00 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.4 2003/01/09 21:16:49 mpowers + * Bringing request-response cycle more into conformance. + * + * Revision 1.2 2002/12/17 14:57:44 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:53:19 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:50 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODisplayGroup.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODisplayGroup.java new file mode 100644 index 0000000..bda1dd5 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODisplayGroup.java @@ -0,0 +1,2455 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Observable; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOKeyValueCoding; +import net.wotonomy.control.EOKeyValueCodingSupport; +import net.wotonomy.control.EOObjectStore; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOObserving; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.control.EOSortOrdering; +import net.wotonomy.control.OrderedDataSource; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSRange; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.Duplicator; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* WODisplayGroup manages a set of objects, +* allowing them to be sorted, batched, and filtered. +* WODisplay also acts as a bridge to the wotonomy's +* control package, including WODisplayGroup and +* EOEditingContext. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WODisplayGroup extends Observable + implements EOObserving, EOEditingContext.Editor, + java.io.Serializable +{ + /** + * Notification sent when the display group is about to fetch. + */ + public static final String DisplayGroupWillFetchNotification + = "DisplayGroupWillFetchNotification"; + + private static boolean + globalDefaultForValidatesChangesImmediately = true; + private static String + globalDefaultStringMatchFormat = "caseInsensitiveLike"; + private static String + globalDefaultStringMatchOperator = "%@*"; + + protected NSMutableArray allObjects; + protected NSArray allObjectsProxy; + protected NSMutableArray displayedObjects; + protected NSArray displayedObjectsProxy; + protected NSMutableArray selectedObjects; + protected NSArray selectedObjectsProxy; + protected NSMutableArray selectedIndexes; + + private String defaultStringMatchOperator; + private String defaultStringMatchFormat; + + private boolean validatesChangesImmediately; + private Object delegate; + private EODataSource dataSource; + private EOQualifier qualifier; + private NSMutableArray sortOrderings; + private NSArray sortOrderingsProxy; + + private NSArray localKeys; + private NSDictionary insertedObjectDefaultValues; + private boolean fetchesOnLoad; + private boolean selectsFirstObjectAfterFetch; + private boolean usesOptimisticRefresh; + private boolean inQueryMode; + + // change detection: package access for helper classes + boolean selectionChanged; + int updatedObjectIndex; + + // batching + private int batchIndex; + private int batchSize; + + // this property is not in the spec + private boolean compareByReference = false; + + private EOObserving lastGroupObserver; + + /** + * Creates a new display group. + */ + public WODisplayGroup() + { + validatesChangesImmediately = + globalDefaultForValidatesChangesImmediately(); + defaultStringMatchOperator = + globalDefaultStringMatchFormat(); + defaultStringMatchFormat = + globalDefaultStringMatchOperator(); + + allObjects = new ObservableArray( this ); + allObjectsProxy = NSArray.arrayBackedByList( allObjects ); + displayedObjects = new NSMutableArray(); + displayedObjectsProxy = NSArray.arrayBackedByList( displayedObjects ); + selectedObjects = new NSMutableArray(); + selectedObjectsProxy = NSArray.arrayBackedByList( selectedObjects ); + sortOrderings = new NSMutableArray(); + sortOrderingsProxy = NSArray.arrayBackedByList( sortOrderings ); + selectedIndexes = new NSMutableArray(); + + delegate = null; + dataSource = null; + qualifier = null; + + localKeys = new NSArray(); // not implemented + insertedObjectDefaultValues = new NSDictionary(); + fetchesOnLoad = false; // not implemented + selectsFirstObjectAfterFetch = false; + usesOptimisticRefresh = false; + inQueryMode = false; // not implemented + + selectionChanged = false; + updatedObjectIndex = -1; + + batchIndex = 0; + batchSize = 0; + } + + + + // specify optional data source + + /** + * Sets the data source that will be used by + * this display group. + */ + public void setDataSource ( EODataSource aDataSource ) + { + if ( ( dataSource != null ) + && ( dataSource.editingContext() != null ) ) + { + // un-register for notifications from existing parent store + NSNotificationCenter.defaultCenter().removeObserver( + this, null, dataSource.editingContext() ); + dataSource.editingContext().removeEditor( this ); + if ( dataSource.editingContext().messageHandler() == this ) + { + dataSource.editingContext().setMessageHandler( null ); + } + + } + + dataSource = aDataSource; + + if ( ( dataSource != null ) + && ( dataSource.editingContext() != null ) ) + { + // register for notifications from parent store + NSNotificationCenter.defaultCenter().addObserver( + this, new NSSelector( "objectsInvalidatedInEditingContext", + new Class[] { NSNotification.class } ), + null, dataSource.editingContext() ); + + // add ourselves as editor + dataSource.editingContext().addEditor( this ); + + // add ourselves as message handler if no such handler exists + if ( dataSource.editingContext().messageHandler() == null ) + { + dataSource.editingContext().setMessageHandler( this ); + } + } + } + + /** + * Returns the current data source backing this display group, + * or null if no dataSource is currently used. + */ + public EODataSource dataSource() + { + return dataSource; + } + + /** + * Returns the key by which this display group is bound a master + * display group, or null if this is not a detail display group. + */ + public String detailKey() + { + if ( dataSource instanceof PropertyDataSource ) + { + return ((PropertyDataSource)dataSource).key(); + } + return null; + } + + /** + * Sets the key by which this display group is bound a master + * display group. Does nothing if this is not a detail display group. + */ + public void setDetailKey( String aKey ) + { + if ( dataSource instanceof PropertyDataSource ) + { + ((PropertyDataSource)dataSource).setKey( aKey ); + } + } + + /** + * Returns whether the data source is a detail data source, + * suggesting that this is a detail display group. + */ + public boolean hasDetailDataSource() + { + return ( dataSource instanceof PropertyDataSource ); + } + + /** + * Returns the selected object in the master display group, + * or null if this is not a detail display group. + */ + public Object masterObject() + { + if ( dataSource instanceof PropertyDataSource ) + { + return ((PropertyDataSource)dataSource).source(); + } + return null; + } + + /** + * Sets the master object in the detail data source. + * Does nothing if there is no detail data source. + */ + public void setMasterObject( Object anObject ) + { + if ( dataSource instanceof PropertyDataSource ) + { + ((PropertyDataSource)dataSource).setSource( anObject ); + } + } + + // specify optional delegate + + /** + * Sets the display group delegate that + * will be used by this display group. + */ + public void setDelegate ( Object aDelegate ) + { + delegate = aDelegate; + } + + /** + * Returns the current delegate for this display group, + * or null if no delegate is currently set. + */ + public Object delegate() + { + return delegate; + } + + + + // display group configuration + + /** + * Returns the current string matching format. + * If not set, defaults to "%@*". + */ + public String defaultStringMatchFormat() + { + return defaultStringMatchFormat; + } + + /** + * Returns the current string matching operator. + * If not set, defaults to "caseInsensitiveLike". + */ + public String defaultStringMatchOperator() + { + return defaultStringMatchOperator; + } + + /** + * Sets the display group and associations to edit a + * "query by example" query object. This method is + * used for target/action connections. + */ + public void enterQueryMode ( Object aSender ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether this display group should immediate + * fetch when loaded. + */ + public boolean fetchesOnLoad() + { + return fetchesOnLoad; + } + + /** + * Returns whether this display group is in "query by + * example" mode. + */ + public boolean inQueryMode() + { + return inQueryMode; + } + + /** + * Returns a Map of default values that are applied + * to new objects that are inserted into the list. + */ + public NSDictionary insertedObjectDefaultValues() + { + return insertedObjectDefaultValues; + } + + /** + * Returns the keys that were declared when read from + * an external resource file. + */ + public NSArray localKeys() + { + return localKeys; + } + + /** + * Sets whether this display group will select the + * first object in the list after a fetch. + */ + public boolean selectsFirstObjectAfterFetch() + { + return selectsFirstObjectAfterFetch; + } + + /** + * Sets the default string matching format that + * will be used by this display group. + */ + public void setDefaultStringMatchFormat ( String aFormat ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the default string matching operator that + * will be used by this display group. + */ + public void setDefaultStringMatchOperator ( String anOperator ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets whether this display group will fetch objects + * from its data source on load. + */ + public void setFetchesOnLoad ( boolean willFetch ) + { + fetchesOnLoad = willFetch; + } + + /** + * Sets whether this display group is in "query by example" + * mode. If true, all associations will bind to a special + * "example" object. + */ + public void setInQueryMode ( boolean isInQueryMode ) + { + inQueryMode = isInQueryMode; + } + + /** + * Sets the mapping that contains the values that will + * be applied to new objects inserted into the display group. + */ + public void setInsertedObjectDefaultValues ( Map aMap ) + { + insertedObjectDefaultValues = new NSDictionary( aMap ); + } + + /** + * Sets the keys that are declared when instantiated from + * an external resource file. + */ + public void setLocalKeys ( List aKeyList ) + { + localKeys = new NSArray( (Collection) aKeyList ); + } + + /** + * Sets whether the first object in the list will be + * selected after a fetch. + */ + public void setSelectsFirstObjectAfterFetch ( + boolean selectsFirst ) + { + selectsFirstObjectAfterFetch = selectsFirst; + } + + /** + * Sets the order of the keys by which this display group + * will be ordered after a fetch or after a call to + * updateDisplayedObjects(). The elements in the display + * group will be sorted first by the first key, within + * the first key, by the second key, and so on. + */ + public void setSortOrderings ( List aList ) + { + sortOrderings.removeAllObjects(); + + Object o; + Iterator it = aList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + // handle the convenience of specifying just a key + if ( ! ( o instanceof EOSortOrdering ) ) + { + o = new EOSortOrdering( + o.toString(), EOSortOrdering.CompareAscending ); + } + sortOrderings.add( o ); + } + } + + /** + * Sets whether only changed objects are refreshed (optimistic), + * or whether all objects are refreshed (pessimistic, default). + * By default, when the display group receives notification that + * one of its objects has changed, updateDisplayedObjects is called. + */ + public void setUsesOptimisticRefresh ( boolean isOptimistic ) + { + usesOptimisticRefresh = isOptimistic; + } + + /** + * Sets whether changes made by associations are validated + * immediately, or when changes are saved. + */ + public void setValidatesChangesImmediately ( + boolean validatesImmediately ) + { + validatesChangesImmediately = validatesImmediately; + } + + /** + * Returns a read-only List of sort orderings for this display group. + */ + public NSArray sortOrderings() + { + return sortOrderingsProxy; + } + + /** + * Returns whether this display group refreshes only + * the changed objects or all objects on refresh. + */ + public boolean usesOptimisticRefresh() + { + return usesOptimisticRefresh; + } + + /** + * Returns whether this display group validates changes + * immediately. Otherwise, validation should occur when + * changes are saved. Default is the global default, + * which is initially true. + */ + public boolean validatesChangesImmediately() + { + return validatesChangesImmediately; + } + + + // qualification + + /** + * Returns a qualifier that will be applied all the objects + * in this display group to determine which objects will + * be displayed. + */ + public EOQualifier qualifier() + { + return qualifier; + } + + /** + * Returns a new qualifier built from the three query + * value maps: greater than, equal to, and less than. + */ + public EOQualifier qualifierFromQueryValues() + { + //TODO: assemble qualifier from query values + + return new EOQualifier() + { + // use inner class until we actually implement one + public EOQualifier qualifierWithBindings( + Map aMap, + boolean requireAll ) + { + return null; + } + public Throwable + validateKeysWithRootClassDescription( Class aClass ) + { + return null; + } + public boolean evaluateWithObject(Object o) + { + return false; + } + }; + } + + /** + * Calls qualifierFromQueryValues(), applies the result + * to the data source, and calls fetch(). + */ + public void qualifyDataSource() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Calls qualifierFromQueryValues(), sets the qualifier + * with setQualifier(), and calls updateDisplayedObjects(). + */ + public void qualifyDisplayGroup() + { + setQualifier( qualifierFromQueryValues() ); + updateDisplayedObjects(); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values. + */ + public NSDictionary queryBindingValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values. + */ + public NSMutableDictionary queryBindings() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values for a matching query. + */ + public NSMutableDictionary queryMatch() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values for a minimum value query. + */ + public NSMutableDictionary queryMin() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values for a maximum value query. + */ + public NSMutableDictionary queryMax() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to operator values. + */ + public NSMutableDictionary queryOperator() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a list containing all supported qualifier operators. + */ + public NSArray allQualifierOperators() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to operator values. + */ + public NSDictionary queryOperatorValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the qualifier that will be used by + * updateDisplayedObjects() to filter displayed objects. + */ + public void setQualifier ( EOQualifier aQualifier ) + { + qualifier = aQualifier; + } + + /** + * Sets the mapping that contains the mappings of keys + * to binding values. + */ + public void setQueryBindingValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to operator values. + */ + public void setQueryOperatorValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + + + // qualifier query values + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for equality. + */ + public NSDictionary equalToQueryValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for greater value. + */ + public NSDictionary greaterThanQueryValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for lesser value. + */ + public NSDictionary lessThanQueryValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the Map that contains the mappings of keys + * to query values that will be used to test for equality. + */ + public void setEqualToQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to query values that will be used to test for greater value. + */ + public void setGreaterThanQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to query values that will be used to test for lesser value. + */ + public void setLessThanQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Deprecated: returns true. + */ + public boolean endEditing() + { + return true; + } + + + // object management + + /** + * Returns a read-only List containing all objects managed by the display group. + * This includes those objects not visible due to disqualification. + */ + public NSArray allObjects() + { //System.out.println( "avoided allocation: allObjects" ); + return allObjectsProxy; + } + + /** + * Returns the total number of batches held by this display group. + */ + public int batchCount() + { + if ( batchSize < 1 ) return 1; + int count = displayedObjects.count(); + if ( count == 0 ) return 1; + return ((count-1) / batchSize) + 1; + } + + /** + * Returns the index of the currently displayed batch. + */ + public int currentBatchIndex() + { + return batchIndex; + } + + /** + * Sets the index of the currently displayed batch. + */ + public void setCurrentBatchIndex( int aBatchIndex ) + { + batchIndex = aBatchIndex; + updateDisplayedObjects(); + } + + /** + * Sets the displayed objects to the batch containing + * the first selected object, and updates the current + * batch index, and returns null to force a page reload. + * Displays the first batch is there is no selection. + */ + public Object displayBatchContainingSelectedObject() + { + if ( batchSize < 1 ) return null; + NSArray indexes = selectionIndexes(); + if ( indexes.count() > 0 ) + { + batchIndex = + ((Number)indexes.objectAtIndex( 0 )).intValue() / batchSize; + } + else + { + batchIndex = 0; + } + updateDisplayedObjects(); + return null; + } + + /** + * Sets the displayed objects to the next batch + * and updates the current batch index, and returns null + * to force a page reload. If there is no next batch + * the first batch is displayed. + */ + public Object displayNextBatch() + { + batchIndex = (batchIndex + 1) % batchCount(); + updateDisplayedObjects(); + return null; + } + + /** + * Sets the displayed objects to the next batch + * and updates the current batch index, and returns null + * to force a page reload. If there is no previous + * batch, the last batch is displayed. + */ + public Object displayPreviousBatch() + { + batchIndex--; + if ( batchIndex < 0 ) batchIndex = batchCount() - 1; + updateDisplayedObjects(); + return null; + } + + /** + * Returns whether the displayed objects have been batched. + */ + public boolean hasMultipleBatches() + { + return batchCount() > 1; + } + + /** + * Returns the one-based index within the displayed objects list + * of the first displayed object in the current batch. + */ + public int indexOfFirstDisplayedObject() + { + if ( batchSize < 1 ) return 1; + return batchIndex * batchSize + 1; + } + + /** + * Returns the one-based index within the displayed objects list + * of the first last object in the current batch. + */ + public int indexOfLastDisplayedObject() + { + if ( batchSize < 1 ) return displayedObjects.count(); + return Math.min( + ((batchIndex+1) * batchSize), + displayedObjects.count() ); + } + + /** + * Returns the number of objects per batch. + */ + public int numberOfObjectsPerBatch() + { + return batchSize; + } + + /** + * Returns the number of objects per batch. + */ + public void setNumberOfObjectsPerBatch( int aSize ) + { + batchSize = aSize; + updateDisplayedObjects(); + } + + /** + * Clears the current selection. + * @return True is the selection was cleared, + * False if the selection could not be cleared + * @see #setSelectionIndexes + */ + public boolean clearSelection() + { + Object result = notifyDelegate( + "displayGroupShouldChangeSelection", + new Class[] { WODisplayGroup.class, List.class }, + new Object[] { this, new NSArray( selectedObjects ) } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return false; + } + + selectionChanged = true; + willChange(); + + selectedObjects.removeAllObjects(); + selectedIndexes.removeAllObjects(); + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + + return true; + } + + /** + * Convenience for binding to a component action: + * calls deleteSelection() and then displayBatchContainingSelectedObject() + * and returns null, which is a suitable result from a component action. + */ + public Object delete() + { + deleteSelection(); + displayBatchContainingSelectedObject(); + return null; + } + + /** + * Deletes the object at the specified index, + * notifying the delegate before and after the operation, + * and then updating the selection if needed. + * @return True if delete was successful, false if the + * object was not deleted. + */ + public boolean deleteObjectAtIndex ( int anIndex ) + { + Object target = displayedObjects.objectAtIndex( anIndex ); + + Object result = notifyDelegate( + "displayGroupShouldDeleteObject", + new Class[] { WODisplayGroup.class, Object.class }, + new Object[] { this, target } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return false; + } + + deleteObjectAtIndexNoNotify( anIndex ); + + if ( dataSource != null ) + { + dataSource.deleteObject( target ); + } + + notifyDelegate( + "displayGroupDidDeleteObject", + new Class[] { WODisplayGroup.class, Object.class }, + new Object[] { this, target } ); + + return true; + } + + private void deleteObjectAtIndexNoNotify ( int anIndex ) + { + Object target = displayedObjects.objectAtIndex( anIndex ); + + int i; + + // remove from selected objects if necessary + i = indexOf( selectedObjects, target ); + if ( i != NSArray.NotFound ) + { + selectionChanged = true; + willChange(); // notify before removing + selectedObjects.removeObjectAtIndex( i ); + selectedIndexes.remove( new Integer( i ) ); // comps by value + } + else // notify - no selection change needed + { + willChange(); + } + + // remove from all objects + i = indexOf( allObjects, target ); + if ( i != NSArray.NotFound ) + { + allObjects.removeObjectAtIndex( i ); + } + else // otherwise should never happen + { +// throw new WotonomyException( +// "Displayed object not found in allObjects" ); + } + + // remove from displayed objects + displayedObjects.removeObjectAtIndex( anIndex ); + } + + /** + * Deletes the currently selected objects. + * This implementation calls deleteObjectAtIndex() for + * each index in the selection list, immediately returning + * false if any delete operation fails. + * @return True if all selected objects were deleted, + * false if any deletion failed. + */ + public boolean deleteSelection() + { + int i; + boolean result = true; + + Enumeration e = new NSArray( selectedObjects ).objectEnumerator(); + while ( e.hasMoreElements() ) + { + i = indexOf( displayedObjects, e.nextElement() ); + if ( i == NSArray.NotFound ) + { + // should never happen + throw new WotonomyException( + "Selected object not found in displayedObjects" ); + } + result = result && deleteObjectAtIndex( i ); + } + + return result; + } + + /** + * Returns a read-only List of all objects in the display group + * that are currently displayed by the associations. + */ + public NSArray displayedObjects() + { // System.out.println( "avoided allocation: displayedObjects" ); + if ( batchSize < 1 ) return displayedObjectsProxy; + return displayedObjectsProxy.subarrayWithRange( + new NSRange( batchIndex * batchSize, batchSize ) ); + } + + /** + * Requests a list of objects from the DataSource + * and calls setObjectArray to populate the list. + * More specifically, calls endEditing(), asks the + * delegate, fetches the objects, notifies the delegate, + * and populates the list. Returns null to force a + * page reload. + */ + public Object fetch() + { + endEditing(); + + if ( dataSource == null ) + { + return null; + } + + Object result = notifyDelegate( + "displayGroupShouldFetch", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return null; + } + + NSNotificationCenter.defaultCenter().postNotification( + DisplayGroupWillFetchNotification, this, new NSDictionary() ); + + NSArray objectList = dataSource.fetchObjects(); + + notifyDelegate( + "displayGroupDidFetchObjects", + new Class[] { WODisplayGroup.class, List.class }, + new Object[] { this, objectList } ); + + setObjectArray( objectList ); + + if ( ( selectsFirstObjectAfterFetch ) && ( displayedObjects.size() > 0 ) ) + { + setSelectionIndexes( new NSArray( new Integer( 0 ) ) ); + } + + return null; + } + + /** + * Convenience to call insertNewObjectAtIndex with the current selection plus one, + * or at the end of the list if there is no selection. + * Returns null to force a page reload. + */ + public Object insert() + { + NSArray indexes = selectionIndexes(); + int size = indexes.count(); + if ( size == 0 ) + { + insertNewObjectAtIndex( displayedObjects.count() ); + } + else + { + insertNewObjectAtIndex( + ((Number)selectedIndexes.objectAtIndex( size-1 )).intValue()+1 ); + } + return null; + } + + /** + * Creates a new object at the specified index. + * Calls insertObjectAtIndex() with the result + * from sending createObject() to the data source. + * @return the newly created object. + */ + public Object insertNewObjectAtIndex ( int anIndex ) + { + Object result = null; + if ( dataSource != null ) + { + result = dataSource.createObject(); + } + if ( result != null ) + { + if ( insertedObjectDefaultValues != null ) + { + Duplicator.writePropertiesForObject( + insertedObjectDefaultValues, result ); + } + insertObjectAtIndex( result, anIndex ); + } + else // create failed + { + if ( delegate() != null ) + { + NSSelector selector = new NSSelector( + "displayGroupCreateObjectFailed", + new Class[] { WODisplayGroup.class, EODataSource.class } ); + if ( selector.implementedByObject( delegate() ) ) + { + try + { + selector.invoke( delegate(), new Object[] { this, dataSource } ); + return result; + } + catch ( Exception exc ) + { + System.err.println( "Error notifying delegate: displayGroupCreateObjectFailed" ); + exc.printStackTrace(); + } + } + } + } + return result; + } + + /** + * Inserts the specified object into the list at + * the specified index. + */ + public void insertObjectAtIndex ( Object anObject, int anIndex ) + { + Object result = notifyDelegate( + "displayGroupShouldInsertObject", + new Class[] { WODisplayGroup.class, Object.class, int.class }, + new Object[] { this, anObject, new Integer(anIndex) } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return; + } + + updatedObjectIndex = anIndex; + willChange(); + + int i; + + // add to all objects + if ( anIndex == displayedObjects.size() ) + { + allObjects.addObject( anObject ); + } + else // insert before same object + { + Object target = displayedObjects.objectAtIndex( anIndex ); + int targetIndex = indexOf( allObjects, target ); + if ( targetIndex != NSArray.NotFound ) + { + allObjects.insertObjectAtIndex( anObject, targetIndex ); + } + else // should never happen + { + throw new WotonomyException( + "Could not find displayed object in all objects list: " + + target ); + } + } + + // add to displayed objects + displayedObjects.insertObjectAtIndex( anObject, anIndex ); + + if ( dataSource != null ) + { + if ( dataSource instanceof OrderedDataSource ) + { + ((OrderedDataSource)dataSource).insertObjectAtIndex( + anObject, anIndex ); + } + else + { + dataSource.insertObject( anObject ); + } + } + + notifyDelegate( + "displayGroupDidInsertObject", + new Class[] { WODisplayGroup.class, Object.class }, + new Object[] { this, anObject } ); + } + + /** + * Sets contentsChanged to true and notifies all observers. + */ + public void redisplay() + { + willChange(); + } + + /** + * Sets the selection to the next displayed object after the current + * selection. If the last object is selected, or if no object + * is selected, then the first object becomes selected. + * If multiple items are selected, the first selected item is + * considered the selected item for the purposes of this method. + * Does not call redisplay(). + * @return null to force a page reload. + */ + public Object selectNext() + { + int count = displayedObjects.count(); + if ( count == 0 ) return null; + if ( count == 1 ) + { + selectObject( displayedObjects.objectAtIndex( 0 ) ); + return null; + } + + int i = -1; + Object selectedObject = selectedObject(); + if ( selectedObject != null ) + { + i = indexOf( displayedObjects, selectedObject ); + } + if ( i == NSArray.NotFound ) i = -1; + + // select next object + i++; + if ( i != displayedObjects.count() ) + { + // set to next object + selectedObject = displayedObjects.objectAtIndex( i ); + } + else // out of range + { + // set to null + selectedObject = displayedObjects.objectAtIndex( 0 ); + } + + selectObject( selectedObject ); + return null; + } + + /** + * Sets the selection to the specified object. + * If the specified object is null or does not exist + * in the list of displayed objects, the selection + * will be cleared. + * @return true if the object was selected. + */ + public boolean selectObject ( Object anObject ) + { + if ( ( anObject == null ) || + ( indexOf( displayedObjects, anObject ) + == NSArray.NotFound ) ) + { + clearSelection(); + return false; + } + + selectObjectsIdenticalTo( new NSArray( new Object[] { anObject } ) ); + return true; + } + + /** + * Sets the selection to the specified objects. + * If the specified list is null or if none of the objects + * in the list exist in the list of displayed objects, the + * selection will be cleared. + * @return true if all specified objects were selected. + */ + public boolean selectObjectsIdenticalTo ( List anObjectList ) + { + // optimization: check for resetting of selection + if ( ( anObjectList != null ) && ( selectedObjects.size() == anObjectList.size() ) ) + { + boolean identical = true; + int size = selectedObjects.size(); + for ( int i = 0; ( i < size ) && identical; i++ ) + { + // compare by reference + if ( anObjectList.get( i ) != selectedObjects.get( i ) ) + { + identical = false; + } + else if ( displayedObjects.indexOfIdenticalObject( + anObjectList.get( i ) ) == NSArray.NotFound ) + { + identical = false; + } + } + if ( identical ) + { + return true; + } + } + + Object result = notifyDelegate( + "displayGroupShouldChangeSelection", + new Class[] { WODisplayGroup.class, List.class }, + new Object[] { this, anObjectList } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + // need to notify the calling component + // to revert back to the previous selection + selectionChanged = true; + willChange(); + return false; + } + + int i; + selectionChanged = true; + willChange(); + Object o; + selectedObjects.removeAllObjects(); + selectedIndexes.removeAllObjects(); + Iterator it = anObjectList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + if ( ( i = displayedObjects.indexOfIdenticalObject( o ) ) + != NSArray.NotFound ) + { + selectedObjects.addObject( o ); + selectedIndexes.addObject( new Integer( i ) ); + } + } + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + + return true; + } + + /** + * Calls selectObjectsIdenticalTo and if false is returned + * and selectFirstIfNoMatch is true, selects the first object. + */ + public boolean selectObjectsIdenticalToSelectFirstOnNoMatch( + List anObjectList, boolean selectFirstIfNoMatch ) + { + if ( selectObjectsIdenticalTo( anObjectList ) ) + { + return true; + } + if ( selectFirstIfNoMatch ) + { + clearSelection(); + selectNext(); + return true; + } + return false; + } + + /** + * Sets the selection to the previous displayed object before the current + * selection. If the first object is selected, or if no object + * is selected, then the last object becomes selected. + * If multiple items are selected, the first selected item is + * considered the selected item for the purposes of this method. + * Does not call redisplay(). + * @return null to force a page reload. + */ + public Object selectPrevious() + { + int i = displayedObjects.count(); + if ( i == 0 ) return null; + if ( i == 1 ) + { + selectObject( displayedObjects.objectAtIndex( 0 ) ); + return null; + } + + Object selectedObject = selectedObject(); + if ( selectedObject != null ) + { + i = indexOf( displayedObjects, selectedObject ); + } + if ( i == NSArray.NotFound ) i = displayedObjects.count(); + + // select next object + i--; + if ( i < 0 ) + { + // out of range - select last object + i = displayedObjects.count() - 1; + } + + selectObject( displayedObjects.objectAtIndex( i ) ); + return null; + } + + /** + * Returns the currently selected object, or null if + * there is no selection. + */ + public Object selectedObject() + { + if ( selectedObjects.count() == 0 ) + { + return null; + } + return selectedObjects.objectAtIndex( 0 ); + } + + /** + * Returns a read-only List containing all selected objects, if any. + * Returns an empty list if no objects are selected. + */ + public NSArray selectedObjects() + { // System.out.println( "avoided allocation: selectedObjects" ); + return selectedObjectsProxy; + } + + /** + * Returns a read-only List containing the indexes of all selected + * objects, if any. The list contains instances of + * java.lang.Number; call intValue() to retrieve the index. + */ + public NSArray selectionIndexes() + { +// return selectedIndexes; + int i; + NSMutableArray result = new NSMutableArray(); + Enumeration e = selectedObjects.objectEnumerator(); + while ( e.hasMoreElements() ) + { + i = indexOf( displayedObjects, e.nextElement() ); + if ( i != NSArray.NotFound ) + { + result.addObject( new Integer( i ) ); + } + else + { + System.err.println( + "Should never happen: selected objects not in displayed objects" ); + new RuntimeException().printStackTrace( System.err ); + } + } + return result; + } + + /** + * Sets the objects managed by this display group. + * updateDisplayedObjects() is called to filter the + * display objects. The previous selection will be + * maintained if possible. The data source is not + * notified. + */ + public void setObjectArray ( List anObjectList ) + { + if ( anObjectList == null ) anObjectList = new NSArray(); + + Object result = notifyDelegate( + "displayGroupDisplayArrayForObjects", + new Class[] { WODisplayGroup.class, List.class }, + new Object[] { this, anObjectList } ); + if ( result != null ) + { + anObjectList = (List) result; + } + + willChange(); + + NSArray oldSelectedObjects = new NSArray( selectedObjects ); // copy + + // reset allObjects to new list + allObjects.removeAllObjects(); + allObjects.addObjectsFromArray( anObjectList ); + + // update the displayed object list + updateDisplayedObjects(); + + // restore the selection if possible + selectObjectsIdenticalTo( oldSelectedObjects ); + + batchIndex = 0; + displayBatchContainingSelectedObject(); + } + + /** + * Sets the currently selected object, or clears the + * selection if the object is not found or is null. + * Note: it's not clear how this differs from + * selectObject in the spec. It is recommended that + * you call selectObject for now. + */ + public void setSelectedObject ( Object anObject ) + { + selectObject( anObject ); + } + + /** + * Sets the current selection to the specified objects. + * The previous selection is cleared, and any objects + * in the display group that are in the specified list + * are then selected. If no items in the specified list + * are found in the display group, then the selection is + * effectively cleared. + * Note: it's not clear how this differs from + * selectObjectsIdenticalTo in the spec. + * It is recommended that you call that method for now. + */ + public void setSelectedObjects ( List aList ) + { + selectObjectsIdenticalTo( aList ); + } + + /** + * Sets the current selection to the objects at the + * specified indexes. Items in the list are assumed + * to be instances of java.lang.Number. + * The previous selection is cleared, and any objects + * in the display group that are in the specified list + * are then selected. If no items in the specified list + * are found in the display group, then the selection is + * effectively cleared. + */ + public boolean setSelectionIndexes ( List aList ) + { + Object o; + int index; + NSMutableArray objects = new NSMutableArray(); + Iterator it = aList.iterator(); + while ( it.hasNext() ) + { + index = ((Number)it.next()).intValue(); + if ( index < displayedObjects.count() ) + { + o = displayedObjects.objectAtIndex( index ); + if ( o != null ) + { + objects.add( o ); + } + } + } + return selectObjectsIdenticalTo( objects ); + } + + /** + * Applies the qualifier to all objects and sorts + * the results to update the list of displayed objects. + * Observing associations are notified to reflect the changes. + */ + public void updateDisplayedObjects() + { + updatedObjectIndex = -1; + willChange(); + + displayedObjects.removeAllObjects(); + + displayedObjects.addObjectsFromArray( allObjects ); + + // apply qualifier, if any + if ( qualifier != null ) + { + EOQualifier.filterArrayWithQualifier( + displayedObjects, qualifier ); + } + + // apply sort orderings, if any + NSArray orderings = sortOrderings(); + if ( orderings != null ) + { + if ( orderings.count() > 0 ) + { + selectionChanged = true; + willChange(); + EOSortOrdering.sortArrayUsingKeyOrderArray( + displayedObjects, orderings ); + } + } + + // make sure the selectedObjects is a subset of displayedObjects + int i; + Object o; + Iterator it = new LinkedList( selectedObjects ).iterator(); + boolean removeflag = false; + selectedIndexes.removeAllObjects(); + while ( it.hasNext() ) + { + o = it.next(); + if ( ( i = displayedObjects.indexOfIdenticalObject( o ) ) + == NSArray.NotFound ) + { + selectedObjects.removeIdenticalObject( o ); + removeflag = true; + } + else + { + selectedIndexes.addObject( new Integer( i ) ); + } + } + + //Note: it is important to put the + //selectionChanged = true line below remove. + if (removeflag) + { + selectionChanged = true; + willChange(); + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + } + } + + /** + * Returns the index of the changed object. If more than + * one object has changed, -1 is returned. + */ + public int updatedObjectIndex() + { + return updatedObjectIndex; + } + + // getting and setting values in objects + + /** + * Returns a value on the selected object for the specified key. + */ + public Object selectedObjectValueForKey ( String aKey ) + { + Object selectedObject = selectedObject(); + if ( selectedObject == null ) return null; + return valueForObject( selectedObject, aKey ); + } + + /** + * Sets the specified value for the specified key on + * all selected objects. + */ + public boolean setSelectedObjectValue ( + Object aValue, String aKey ) + { + Object selectedObject = selectedObject(); + if ( selectedObject == null ) return false; + return setValueForObject( aValue, selectedObject, aKey ); + } + + /** + * Sets the specified value for the specified key on + * the specified object. Validations may be triggered, + * and error dialogs may appear to the user. + * @return True if the value was set successfully, + * false if the value could not be set and the update + * operation should not continue. + */ + public boolean setValueForObject ( + Object aValue, Object anObject, String aKey ) + { + // notify object's observers: + // this includes us, and will notify our observers + EOObserverCenter.notifyObserversObjectWillChange( anObject ); + + //TODO: if key is null, need to remove old object + // and add new object instead of simply replacing it. + + try + { + if ( anObject instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)anObject).takeValueForKey( aValue, aKey ); + } + else + { + EOKeyValueCodingSupport.takeValueForKey( anObject, aValue, aKey ); + } + } + catch ( RuntimeException exc ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { WODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", exc.getMessage() } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + throw exc; + } + return false; + } + + notifyDelegate( + "displayGroupDidSetValueForObject", + new Class[] { WODisplayGroup.class, Object.class, Object.class, String.class }, + new Object[] { this, aValue, anObject, aKey } ); + + return true; + } + + /** + * Calls setValueForObject() for the object at + * the specified index. + */ + public boolean setValueForObjectAtIndex ( + Object aValue, int anIndex, String aKey ) + { + return setValueForObject( + aValue, displayedObjects.objectAtIndex( anIndex ), aKey ); + } + + /** + * Returns the value for the specified key on the specified object. + */ + public Object valueForObject ( Object anObject, String aKey ) + { + // empty string is considered the identity property + if ( aKey == null ) return anObject; + if ( aKey.equals( "" ) ) return anObject; + + try + { + if ( anObject instanceof EOKeyValueCoding ) + { + return ((EOKeyValueCoding)anObject).valueForKey( aKey ); + } + else + { + return EOKeyValueCodingSupport.valueForKey( anObject, aKey ); + } + } + catch ( RuntimeException exc ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { WODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", exc.getMessage() } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + throw exc; + } + return null; + } + } + + /** + * Calls valueForObject() for the object at the specified index. + * Returns null if out of bounds. + */ + public Object valueForObjectAtIndex ( int anIndex, String aKey ) + { + if ( displayedObjects.count() <= anIndex ) return null; + Object o = displayedObjects.objectAtIndex( anIndex ); + return valueForObject( o, aKey ); + } + + /** + * Prints out the list of displayed objects. + */ + public String toString() + { + return displayedObjects.toString(); + } + + + /** + * Handles notifications from the data source's editing context, + * looking for InvalidatedAllObjectsInStoreNotification and + * ObjectsChangedInEditingContextNotification, refetching in + * the former case and updating displayed objects in the latter. + * Note: This method is not in the public specification. + */ + public void objectsInvalidatedInEditingContext( NSNotification aNotification ) + { + if ( EOObjectStore.InvalidatedAllObjectsInStoreNotification + .equals( aNotification.name() ) ) + { + Object result = notifyDelegate( + "displayGroupShouldRefetch", + new Class[] { WODisplayGroup.class, NSNotification.class }, + new Object[] { this, aNotification } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + fetch(); + } + } + else + if ( EOEditingContext.ObjectsChangedInEditingContextNotification + .equals( aNotification.name() ) ) + { + Object result = notifyDelegate( + "displayGroupShouldRedisplay", + new Class[] { WODisplayGroup.class, NSNotification.class }, + new Object[] { this, aNotification } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + int index; + Enumeration e; + boolean didChange = false; + NSDictionary userInfo = aNotification.userInfo(); + + // inserts are ignored + + // mark updated objects as updated + NSArray updates = (NSArray) userInfo.objectForKey( + EOObjectStore.UpdatedKey ); + e = updates.objectEnumerator(); + while ( e.hasMoreElements() ) + { + index = indexOf( displayedObjects, e.nextElement() ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "WODisplayGroup: updated: " + index ); + if ( ! didChange ) + { + didChange = true; + willChange(); + updatedObjectIndex = index; + } + else + { + updatedObjectIndex = -1; + } + } + } + + // treat invalidated objects as updated + NSArray invalidates = (NSArray) userInfo.objectForKey( + EOObjectStore.InvalidatedKey ); + e = invalidates.objectEnumerator(); + while ( e.hasMoreElements() ) + { + index = indexOf( displayedObjects, e.nextElement() ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "WODisplayGroup: invalidated: " + index ); + if ( ! didChange ) + { + didChange = true; + willChange(); + updatedObjectIndex = index; + } + else + { + updatedObjectIndex = -1; + } + } + } + + // remove deletes from display group if they exist + NSArray deletes = (NSArray) userInfo.objectForKey( + EOObjectStore.DeletedKey ); + e = deletes.objectEnumerator(); + Object o; + while ( e.hasMoreElements() ) + { + o = e.nextElement(); + index = indexOf( displayedObjects, o ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "WODisplayGroup: deleted: " + o ); + deleteObjectAtIndexNoNotify( index ); + } + } + + if ( !usesOptimisticRefresh() ) + { + updateDisplayedObjects(); + } + } + } + + } + + // static methods + + /** + * Specifies the default behavior for whether changes + * should be validated immediately for all display groups. + */ + public static boolean + globalDefaultForValidatesChangesImmediately() + { + return globalDefaultForValidatesChangesImmediately; + } + + /** + * Specifies the default string matching format for all + * display groups. + */ + public static String globalDefaultStringMatchFormat() + { + return globalDefaultStringMatchFormat; + } + + /** + * Specifies the default string matching operator for all + * display groups. + */ + public static String globalDefaultStringMatchOperator() + { + return globalDefaultStringMatchOperator; + } + + /** + * Sets the default behavior for validating changes + * for all display groups. + */ + public static void + setGlobalDefaultForValidatesChangesImmediately ( + boolean validatesImmediately ) + { + globalDefaultForValidatesChangesImmediately = + validatesImmediately; + } + + /** + * Sets the default string matching format that + * will be used by all display groups. + */ + public static void + setGlobalDefaultStringMatchFormat ( String aFormat ) + { + globalDefaultStringMatchFormat = aFormat; + } + + /** + * Sets the default string matching operator that + * will be used by all display groups. + */ + public static void + setGlobalDefaultStringMatchOperator ( String anOperator ) + { + globalDefaultStringMatchOperator = anOperator; + } + + /** + * Needed because we don't inherit from NSObject. + * Calls EOObserverCenter.notifyObserversObjectWillChange. + */ + protected void willChange() + { + EOObserverCenter.notifyObserversObjectWillChange( this ); + } + + /** + * Returns the index of the specified object in the + * specified NSArray, comparing by value or by reference + * as determined by the private instance variable + * compareByReference. If not found, returns NSArray.NotFound. + */ + private int indexOf( NSArray anArray, Object anObject ) + { + if ( compareByReference ) + { + return anArray.indexOfIdenticalObject( anObject ); + } + else + { + return anArray.indexOf( anObject ); + } + } + + // interface EOObserving + + /** + * Receives notifications of changes from objects that + * are managed by this display group. This implementation + * sets updatedObjectIndex as appropriate. + */ + public void objectWillChange(Object anObject) + { + int index = indexOf( displayedObjects, anObject ); + if ( index != NSArray.NotFound ) + { + updatedObjectIndex = index; + willChange(); + } + } + + // interface EOEditingContext.Editor + + /** + * Called before the editing context begins to save changes. + * This implementation calls endEditing(). + */ + public void editingContextWillSaveChanges( + EOEditingContext anEditingContext ) + { + endEditing(); + } + + /** + * Called to determine whether this editor has changes + * that have not been committed to the object in the context. + * This implementation returns false. + */ + public boolean editorHasChangesForEditingContext( + EOEditingContext anEditingContext ) + { + return false; + } + + // interface EOEditingContext.MessageHandler + + /** + * Called to display a message for an error that occurred + * in the specified editing context. If the delegate allows, + * this implementation writes a message to the standard output. + * Override to customize. + */ + public void editingContextPresentErrorMessage( + EOEditingContext anEditingContext, + String aMessage ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { WODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", aMessage } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + System.out.println( aMessage ); + } + } + + /** + * Called by the specified object store to determine whether + * fetching should continue, where count is the current count + * and limit is the limit as specified by the fetch specification. + * This implementation returns true. Override to customize. + */ + public boolean editingContextShouldContinueFetching( + EOEditingContext anEditingContext, + int count, + int limit, + EOObjectStore anObjectStore ) + { + return true; + } + + /** + * Sends the specified message to the delegate. + * Returns the return value of the method, + * or null if no return value or no delegate + * or no implementation. + */ + private Object notifyDelegate( + String aMethodName, Class[] types, Object[] params ) + { + try + { + Object delegate = delegate(); + if ( delegate == null ) return null; + return NSSelector.invoke( + aMethodName, types, delegate, params ); + } + catch ( NoSuchMethodException e ) + { + // ignore: not implemented + } + catch ( Exception exc ) + { + // log to standard error + System.err.println( + "Error while messaging delegate: " + + delegate + " : " + aMethodName ); + exc.printStackTrace(); + } + + return null; + } + + /** + * DisplayGroups can delegate important decisions to a Delegate. + * Note that DisplayGroup doesn't require its delegates to implement + * this interface: rather, this interface defines the methods that + * DisplayGroup will attempt to invoke dynamically on its delegate. + * The delegate may choose to implement only a subset of the methods + * on the interface. + */ + public interface Delegate + { + /** + * Called when the specified data source fails + * to create an object for the specified display group. + */ + void displayGroupCreateObjectFailed ( + WODisplayGroup aDisplayGroup, + EODataSource aDataSource ); + + /** + * Called after the specified display group's + * data source is changed. + */ + void displayGroupDidChangeDataSource ( + WODisplayGroup aDisplayGroup ); + + /** + * Called after a change occurs in the specified + * display group's selected objects. + */ + void displayGroupDidChangeSelectedObjects ( + WODisplayGroup aDisplayGroup ); + + /** + * Called after the specified display group's + * selection has changed. + */ + void displayGroupDidChangeSelection ( + WODisplayGroup aDisplayGroup ); + + /** + * Called after the specified display group has + * deleted the specified object. + */ + void displayGroupDidDeleteObject ( + WODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called after the specified display group + * has fetched the specified object list. + */ + void displayGroupDidFetchObjects ( + WODisplayGroup aDisplayGroup, + List anObjectList ); + + /** + * Called after the specified display group + * has inserted the specified object into + * its internal object list. + */ + void displayGroupDidInsertObject ( + WODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called after the specified display group + * has set the specified value for the specified + * object and key. + */ + void displayGroupDidSetValueForObject ( + WODisplayGroup aDisplayGroup, + Object aValue, + Object anObject, + String aKey ); + + /** + * Called by the specified display group to + * determine what objects should be displayed + * for the objects in the specified list. + * @return An NSArray containing the objects + * to be displayed for the objects in the + * specified list. + */ + NSArray displayGroupDisplayArrayForObjects ( + WODisplayGroup aDisplayGroup, + List aList ); + + /** + * Called by the specified display group before + * it attempts to change the selection. + * @return True to allow the selection to change, + * false otherwise. + */ + boolean displayGroupShouldChangeSelection ( + WODisplayGroup aDisplayGroup, + List aSelectionList ); + + /** + * Called by the specified display group before + * it attempts to delete the specified object. + * @return True to allow the object to be deleted + * false to prevent the deletion. + */ + boolean displayGroupShouldDeleteObject ( + WODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called by the specified display group before + * it attempts display the specified alert to + * the user. + * @return True to allow the message to be + * displayed, false if you want to handle the + * alert yourself and suppress the display group's + * notification. + */ + boolean displayGroupShouldDisplayAlert ( + WODisplayGroup aDisplayGroup, + String aTitle, + String aMessage ); + + /** + * Called by the specified display group before + * it attempts fetch objects. + * @return True to allow the fetch to take place, + * false to prevent the fetch. + */ + boolean displayGroupShouldFetch ( + WODisplayGroup aDisplayGroup ); + + /** + * Called by the specified display group before + * it attempts to insert the specified object. + * @return True to allow the object to be inserted + * false to prevent the insertion. + */ + boolean displayGroupShouldInsertObject ( + WODisplayGroup aDisplayGroup, + Object anObject, + int anIndex ); + + /** + * Called by the specified display group when + * it receives the specified + * ObjectsChangedInEditingContextNotification. + * @return True to allow the display group to + * update the display (recommended), false + * to prevent the update. + */ + boolean displayGroupShouldRedisplay ( + WODisplayGroup aDisplayGroup, + NSNotification aNotification ); + + /** + * Called by the specified display group when + * it receives the specified + * InvalidatedAllObjectsInStoreNotification. + * @return True to allow the display group to + * refetch (recommended), false to prevent the + * refetch. + */ + boolean displayGroupShouldRefetch ( + WODisplayGroup aDisplayGroup, + NSNotification aNotification ); + + } + +} + +/* + * $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.6 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.5 2003/01/22 23:01:06 mpowers + * Better handling for index out of bounds. + * + * Revision 1.4 2003/01/21 22:27:02 mpowers + * Corrected context id usage. + * Implemented backtracking. + * + * Revision 1.3 2003/01/21 17:54:01 mpowers + * Batch indices are one-based, not zero-based. + * + * Revision 1.2 2003/01/21 14:38:42 mpowers + * Fixed batching. + * + * Revision 1.1 2003/01/18 23:30:42 mpowers + * WODisplayGroup now compiles. + * + * Revision 1.46 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.45 2002/10/24 18:20:20 mpowers + * Because NSArray is read-only, we are returning our internal representations + * to callers of allObjects(), displayedObjects(), and selectedObjects(). + * + * Revision 1.44 2002/08/06 18:20:25 mpowers + * Now posting DisplayGroupWillFetch notifications before fetch. + * Implemented support for usesOptimisticRefresh. + * No longer supporting inserted/updated/deleted lists: not part of spec. + * + * Revision 1.43 2002/05/17 15:01:49 mpowers + * Implemented dynamic lookup of delegate methods so delegates no longer + * need to implement the DisplayGroup.Delegate interface. + * + * Revision 1.42 2002/03/26 21:46:06 mpowers + * Contributing EditingContext as a java-friendly convenience. + * + * Revision 1.41 2002/03/11 03:17:56 mpowers + * Provided control point for coalesced changes. + * + * Revision 1.40 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.39 2002/02/19 22:26:04 mpowers + * Implemented EOEditingContext.MessageHandler support. + * + * Revision 1.38 2002/02/19 16:37:38 mpowers + * Implemented support for EOEditingContext.Editor + * + * Revision 1.37 2001/12/11 22:17:48 mpowers + * Now properly handling exceptions in valueForObject. + * No longer trying to retain selection based only on index. + * + * Revision 1.36 2001/11/08 21:42:00 mpowers + * Now we know what to do with shouldRefetch and shouldRedisplay. + * + * Revision 1.35 2001/11/04 18:26:58 mpowers + * Fixed bug where exceptions were not properly reported when updating + * a value and the display group did not have a delegate. + * + * Revision 1.34 2001/11/02 20:59:36 mpowers + * Now correctly ensuring selected objects are a subset of displayed objects. + * + * Revision 1.33 2001/10/30 22:56:45 mpowers + * Added support for EOQualifier. + * + * Revision 1.32 2001/10/23 22:27:53 mpowers + * Now running at ObserverPrioritySixth. + * + * Revision 1.31 2001/10/23 18:45:05 mpowers + * Rolling back changes. + * + * Revision 1.28 2001/08/22 19:23:41 mpowers + * No longer asserting objects in all objects list. + * + * Revision 1.27 2001/07/30 16:17:01 mpowers + * Minor code cleanup. + * + * Revision 1.26 2001/07/10 22:49:07 mpowers + * Fixed bug in optimization for selectObjectsIdenticalTo (found by Dongzhi). + * + * Revision 1.25 2001/06/19 15:40:21 mpowers + * Now only changing the selection if the new selection is different + * from the old. + * + * Revision 1.24 2001/05/24 17:36:15 mpowers + * Fixed problem with selectedObjectsIdenticalTo: it was using compare + * by value instead of compare by reference. + * + * Revision 1.23 2001/05/18 21:09:19 mpowers + * Now throwing exceptions if the delegate cannot handle error from update. + * + * Revision 1.22 2001/05/14 15:26:12 mpowers + * Now checking for null delegate before and after selection change. + * + * Revision 1.21 2001/05/08 18:47:34 mpowers + * Minor fixes for d3. + * + * Revision 1.20 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.19 2001/04/13 16:38:09 mpowers + * Alpha3 release. + * + * Revision 1.18 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.17 2001/03/29 03:31:13 mpowers + * No longer using Introspector. + * + * Revision 1.16 2001/02/27 03:32:18 mpowers + * Implemented default values for new objects. + * + * Revision 1.15 2001/02/27 02:11:17 mpowers + * Now throwing exception when cloning fails. + * Removed debugging printlns. + * + * Revision 1.14 2001/02/26 22:41:51 mpowers + * Implemented null placeholder classes. + * Duplicator now uses NSNull. + * No longer catching base exception class. + * + * Revision 1.13 2001/02/26 15:53:22 mpowers + * Fine-tuning notification firing. + * Child display groups now update properly after parent save or invalidate. + * + * Revision 1.12 2001/02/22 20:55:06 mpowers + * Implemented notification handling. + * + * Revision 1.11 2001/02/21 20:40:42 mpowers + * setObjectArray now falls back to index when trying to retain the + * same selection. + * + * Revision 1.10 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.9 2001/02/17 17:23:49 mpowers + * More changes to support compiling with jdk1.1 collections. + * + * Revision 1.8 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.7 2001/01/24 16:35:37 mpowers + * Improved documentation on TreeAssociation. + * SortOrderings are now inherited from parent nodes. + * Updates after sorting are still lost on TreeController. + * + * Revision 1.6 2001/01/24 14:23:05 mpowers + * Added support for OrderedDataSource. + * + * Revision 1.5 2001/01/12 17:21:37 mpowers + * Implicit creation of EOSortOrderings now happens in setSortOrderings. + * + * Revision 1.4 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.3 2001/01/10 22:49:44 mpowers + * Implemented similarly named selection methods instead of + * throwing exceptions. + * + * Revision 1.2 2001/01/09 20:12:52 mpowers + * Moved inner classes to package access. + * + * Revision 1.1.1.1 2000/12/21 15:48:20 mpowers + * Contributing wotonomy. + * + * Revision 1.21 2000/12/20 16:25:39 michael + * Added log to all files. + * + * Revision 1.20 2000/12/15 15:04:42 michael + * Added doc. + * + * Revision 1.19 2000/12/11 13:32:48 michael + * Finish the much better TreeAssociation implementation. + * TreeAssociation now has no gui dependencies. + * + * Revision 1.18 2000/12/05 17:41:46 michael + * Broadcasts selection change after delegate refuses selection change + * so the initiating association gets refreshed. + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODynamicElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODynamicElement.java new file mode 100644 index 0000000..6e449c3 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODynamicElement.java @@ -0,0 +1,208 @@ +/* +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.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* The base class for dynamic WOElements. Dynamic elements +* are expected to do something useful with user-entered data +* in the request and with any binding associations with the +* context's current WOComponent. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public abstract class WODynamicElement + extends WOElement +{ + protected String name; + protected WOElement rootElement; + protected NSDictionary associations; + + /** + * The default constructor. + */ + protected WODynamicElement () + { + name = null; + associations = new NSMutableDictionary(); + rootElement = null; + } + + /** + * Required constructor specifying the class name of the component, + * a map of associations, and the root element of the tree that + * contains this element (which may be null). The map keys + * correspond to properties of this element, and the values are + * associations to be applied to the context's current component. + */ + public WODynamicElement ( + String aName, NSDictionary anAssociationMap, WOElement aRootElement) + { + this(); + name = aName; + associations = anAssociationMap; + rootElement = aRootElement; + } + + /** + * 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) + { + if ( rootElement != null ) + { + rootElement.ensureAwakeInContext( aContext ); + } + } + + /** + * Use this method to get a map with the properties that start with + * a question mark. These are supposed to go at the end of a URL, and it is + * very useful for components that generate URLs, specially with direct + * actions. + * @param c The component where the values of the properties have to be + * retrieved from. + */ + Map urlFields(WOComponent c) { + HashMap map = new HashMap(associations.count()); + Enumeration enumeration = associations.keyEnumerator(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + if (key.charAt(0) == '?') { + map.put(key.substring(1), valueForProperty(key, c)); + } + } + return map; + } + + /** Convenience method for getting the value of an association. */ + Object valueForProperty(String key, WOComponent c) { + WOAssociation a = (WOAssociation)associations.objectForKey(key); + if (a != null) + return a.valueInComponent(c); + return null; + } + + /** Convenience method for getting the string value of an association. */ + String stringForProperty(String key, WOComponent c) { + WOAssociation a = (WOAssociation)associations.objectForKey(key); + Object result = null; + if (a != null) result = a.valueInComponent(c); + if ( result == null ) return null; + return result.toString(); + } + + /** Convenience method for getting the string value of an association. */ + boolean booleanForProperty(String key, WOComponent c) { + WOAssociation a = (WOAssociation)associations.objectForKey(key); + Object result = null; + if (a != null) result = a.valueInComponent(c); + if ( result == null ) return false; + if ( result.toString().toLowerCase().equals( "true" ) ) return true; + return Boolean.TRUE.equals( result ); + } + + /** Convenience method for setting the value of an association. */ + void setValueForProperty(String key, Object value, WOComponent c) { + WOAssociation a = (WOAssociation)associations.objectForKey(key); + if ( a != null && a.isValueSettable() ) + a.setValue(value, c); + } + + /** this method composes a String suitable for inclusion inside a HTML tag. It includes + the key-value pairs of all the associations not mentioned in the standardProperties + parameter. This is very useful for including extra properties in tags without having to worry + if the HTML specification has changed or if non-standard tags are being used. + @param c The component where the associations' values should be retrieved from. + @param standardProperties An array of Strings with all the associations that should be + excluded from the resulting string. */ + String additionalHTMLProperties(WOComponent c, NSArray standardProperties) { + Enumeration enumeration = associations.keyEnumerator(); + StringBuffer buf = new StringBuffer(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + if (!(standardProperties.containsObject(key) || key.charAt(0)=='?')) { + buf.append(' '); + buf.append(key); + buf.append("=\""); + buf.append(valueForProperty(key, c)); + buf.append('\"'); + } + } + return buf.toString(); + } + + /** + * This method is called to retrieve user-entered data from + * the request. WOElements should retrieve data from the + * request based on their elementID and set values in the + * context's current WOComponent, typically those values that + * are associated with the element in the binding. This + * implementation does nothing. + */ + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + + } + + /** + * This method is called on all objects and elements of the + * application until a non-null value is returned. + * WOElements should first check to see if they are the + * target of an action by checking the WOContext's senderID + * to see if it matches this element's elementID. + * If this element is the target, it should perform an + * appropriate action on the context's current WOComponent, + * usually the action specified in the binding, and return + * the result of that action. This implementation returns null. + */ + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + return null; + } + + /** + * This method is called on all elements of the content tree + * to build a response to a user request. The message should + * be forwarded to any child elements so that the entire tree + * is traversed. This implementation does nothing. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + // does nothing + } + + public WOResponse generateResponse() + { + return null; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOElement.java new file mode 100644 index 0000000..11944d3 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOElement.java @@ -0,0 +1,97 @@ +/* +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.Serializable; + +import net.wotonomy.foundation.NSDictionary; + +/** +* This class represents a static or dynamic portion of the +* content returned to a request. Each request walks a tree +* of WOElements to generate a response. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public abstract class WOElement implements WOActionResults, Serializable +{ + NSDictionary associations; + + /** + * Default constructor. Performs necessary initialization. + */ + public WOElement() + { + } + + /** + * This method is called to retrieve user-entered data from + * the request. WOElements should retrieve data from the + * request based on their elementID and set values in the + * context's current WOComponent, typically those values that + * are associated with the element in the binding. This + * implementation does nothing. + */ + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + // does nothing + } + + /** + * This method is called on all objects and elements of the + * application until a non-null value is returned. + * WOElements should first check to see if they are the + * target of an action by checking the WOContext's senderID + * to see if it matches this element's elementID. + * If this element is the target, it should perform an + * appropriate action on the context's current WOComponent, + * usually the action specified in the binding, and return + * the result of that action. This implementation returns null. + */ + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + return null; + } + + /** + * This method is called on all elements of the content tree + * to build a response to a user request. The message should + * be forwarded to any child elements so that the entire tree + * is traversed. This implementation does nothing. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + // does nothing + } + + /** + * 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) + { + // does nothing + } + + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOForm.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOForm.java new file mode 100644 index 0000000..887d1a3 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOForm.java @@ -0,0 +1,124 @@ +/* + 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 net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* Implements a FORM element with dynamic bindings. +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOForm extends WODynamicElement { + + public WOForm() { + super(); + } + + public WOForm(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public String href(WOContext c) { + return (String)valueForProperty("href", c.component()); + } + + public String directActionName(WOContext c) { + return (String)valueForProperty("directAction", c.component()); + } + + public String directActionClass(WOContext c) { + return (String)valueForProperty("actionClass", c.component()); + } + + public boolean multipleSubmit(WOContext c) { + return booleanForProperty("multipleSubmit", c.component()); + } + + public void takeValuesFromRequest ( + WORequest aRequest, WOContext aContext) + { + if ( rootElement != null ) + { + rootElement.takeValuesFromRequest( aRequest, aContext ); + } + } + + public void appendToResponse(WOResponse r, WOContext c) { + //Append the opening tag + r.appendContentString("<FORM"); + String link = href(c); + //Append the href, if present + if (link != null) { + r.appendContentString(" HREF=\""); + r.appendContentString(link); + r.appendContentString("\""); + link = null; + } else { + link = directActionName(c); + } + + //otherwise, append Direct Action + if (link != null) { + r.appendContentString(" HREF=\""); + if (directActionClass(c) != null) + link = directActionClass(c) + "/" + link; + r.appendContentString(c.directActionURLForActionNamed(link, urlFields(c.component()))); + r.appendContentString("\""); + link = null; +// } else if (associations.objectForKey("action") != null) { + } else { + //finally, append action + r.appendContentString(" action=\""); + r.appendContentString(c.componentActionURL()); + r.appendContentString("\""); + } //else +// now defaulting to action if not specified(?): WOBuilder does generate WOForms without any bindings(!) +// throw new IllegalArgumentException("You must use one of directActionName, action, or href."); + + //Append any additional properties + link = additionalHTMLProperties(c.component(), new NSArray(new Object[]{ + "href", "action", "directActionName", "actionClass", "multipleSubmit" })); + if (link.length() > 0) + r.appendContentString(link); + r.appendContentString(">"); + //Notify that we're inside a form now + c.setInForm(true); + //Append the inner template + rootElement.appendToResponse(r, c); + //Close the tag + r.appendContentString("</FORM>"); +// c.deleteLastElementIDComponent(); + c.setInForm(false); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + //We only process the request if it's not a multipleSubmit (otherwise we leave it to the buttons) + if (!multipleSubmit(c) + && c.elementID().equals(c.senderID()) + && associations.objectForKey("action") != null ) { + return (WOActionResults)valueForProperty("action", c.component()); + } + WOActionResults res = rootElement.invokeAction(r, c); + return res; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOFrame.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOFrame.java new file mode 100644 index 0000000..30dd4bc --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOFrame.java @@ -0,0 +1,60 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +public class WOFrame extends WODynamicElement { + + public WOFrame() { + super(); + } + + public WOFrame(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + public String frameName(WOContext c) { + String x = (String)valueForProperty("name", c.component()); + if (x != null) + return x; + return c.elementID(); + } + + public String url(WOContext c) { + //Check if the href property is set + String href = stringForProperty("href", c.component()); + if (href != null) + return href; + href = stringForProperty("pageName", c.component()); + if (href != null || associations.objectForKey("action") != null) { //write this component's URL + return c.componentActionURL(); + } + href = stringForProperty("directActionName", c.component()); + if (href != null) { //compose the direct action URL + String fullActionName = stringForProperty("actionClass", c.component()); + if (fullActionName != null) + fullActionName = fullActionName + "/" + href; + else + fullActionName = href; + return c.directActionURLForActionNamed(fullActionName, + urlFields(c.component())); + } + //Coded needed here to support filename/framework and data/mimeType. + return null; + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString("<FRAME NAME=\""); + r.appendContentString(frameName(c)); + r.appendContentString("\" SRC=\""); + r.appendContentString(url(c)); + r.appendContentString("\""); + String moreFields = additionalHTMLProperties(c.component(), new NSArray(new Object[]{ + "name", "href", "pageName", "directActionName", "actionClass" })); + if (moreFields != null && moreFields.length() > 0) + r.appendContentString(moreFields); + r.appendContentString(">"); + } + +}
\ No newline at end of file diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericContainer.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericContainer.java new file mode 100644 index 0000000..9af5460 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericContainer.java @@ -0,0 +1,61 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; + +/** + * Used to dynamically generate HTML containers (elements with opening and closing tags and something in between). + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOGenericContainer extends WOGenericElement { + + public WOGenericContainer() { + super(); + } + + public WOGenericContainer(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public void appendToResponse(WOResponse r, WOContext c) { + super.appendToResponse(r, c); + rootElement.appendToResponse(r, c); + r.appendContentString("</"); + r.appendContentString(elementName(c)); + r.appendContentString(">"); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + super.takeValuesFromRequest( r, c ); + rootElement.takeValuesFromRequest(r, c); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + WOActionResults result = super.invokeAction( r, c ); + if ( result == null ) + { + result = rootElement.invokeAction(r, c); + } + return result; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericElement.java new file mode 100644 index 0000000..8894428 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericElement.java @@ -0,0 +1,95 @@ +/* + 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 net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** + * Used to generate any HTML element dynamically. It only creates the opening tag without a closing tag. + * To generate HTML elements that have opening and closing tags, as well as some content in between, + * use WOGenericContainer instead. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOGenericElement extends WODynamicElement { + + static NSArray bindings = new NSArray( new Object[] + { "elementName", "omitTags", "elementID", "otherTagString", + "formValue", "formValues", "invokeAction" } ); + + public WOGenericElement() { + super(); + } + + public WOGenericElement(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public String elementName(WOContext c) { + String x = (String)valueForProperty("elementName", c.component()); + if (x != null) + return x; + return c.elementID(); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + if ( c.elementID().equals( c.senderID() ) ) + { + Object value; + value = r.formValueForKey( c.elementID() ); + setValueForProperty( "formValue", value, c.component() ); + value = r.formValuesForKey( c.elementID() ); + setValueForProperty( "formValues", value, c.component() ); + } + } + + public WOActionResults invokeAction(WORequest r, WOContext c) + { + WOActionResults result = null; + String action = stringForProperty( "invokeAction", c.component() ); + if ( action != null && c.elementID().equals( c.senderID() ) ) + { + result = c.component().performAction( action ); + } + return result; + } + + public void appendToResponse(WOResponse r, WOContext c) { + WOComponent component = c.component(); + if ( !booleanForProperty( "omitTags", component ) ) + { + r.appendContentString("<"); + r.appendContentString(elementName(c)); + String other = stringForProperty( "otherTagString", component ); + if ( other != null ) + { + r.appendContentString( " " ); + r.appendContentString( other ); + } + String add = additionalHTMLProperties(component, bindings); + if (add.length() > 0) + r.appendContentString(add); + r.appendContentString(">"); + } + setValueForProperty( "elementID", c.elementID(), component ); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHiddenField.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHiddenField.java new file mode 100644 index 0000000..c5d5711 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHiddenField.java @@ -0,0 +1,42 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; + +/** +* Used to dynamically generate a hidden field within a form. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOHiddenField extends WOTextField { + + public WOHiddenField() { + super(); + } + + public WOHiddenField(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHyperlink.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHyperlink.java new file mode 100644 index 0000000..d0f3ff7 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHyperlink.java @@ -0,0 +1,249 @@ +/* + 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.util.Iterator; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* WOHyperlink renders a dynamically generated hyperlink in the output. + * Bindings are: + * <ul> + * <li>string: a string to be included between the hyperlink tags (optional).</li> + * <li>escapeHTML: a property returning a value convertable to a Boolean + * indicating whether the any html characters in the output should be + * escaped so they are shown as html characters rather than interpreted + * as html.</li> + * <li>href: The URL that the hyperlink should point to.</li> + * <li>pageName: The name of the WOComponent that the hyperlink should point to.</li> + * <li>directActionName: The name of the direct action to call when the link is activated.</li> + * <li>actionClass: The name of the WODirectAction subclass where the direct action resides.</li> + * <li>anchorName: The name of the link, for anchor tags.</li> + * <li>action: A pointer to a method on the component that contains this element. If the link is activated, + * the method will be called. + * <li>ref: The name of the anchor to go to inside the resulting page.</li> + * </ul> + * + * The href, pageName and directActionName/actionClass and name properties are mutually exclusive and you should + * only use at most one of them simultaneously. + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOHyperlink extends WODynamicElement { + + protected String string; + protected String href; + protected String pageName; + protected String directActionName; + protected String actionClass; + protected String action; + protected boolean escapeHTML; + protected String anchorName; + protected String ref; + + protected WOHyperlink() { + super(); + } + + public WOHyperlink(String aName, NSDictionary aMap, WOElement aRootElement) { + super(aName, aMap, aRootElement); + escapeHTML = true; + } + + public void setString(String value) { + string = value; + } + public String string() { + return string; + } + + public void setHref(String value) { + href = value; + } + public String href() { + return href; + } + + public void setAnchorName(String value) { + anchorName = value; + } + public String anchorName() { + return anchorName; + } + + public void setPageName(String value) { + pageName = value; + } + public String pageName() { + return pageName; + } + + public void setDirectActionName(String value) { + directActionName = value; + } + public String directActionName() { + return directActionName; + } + + public void setActionClass(String value) { + actionClass = value; + } + public String actionClass() { + return actionClass; + } + + /** Sets the escapeHTML property. */ + public void setEscapeHTML(boolean escape) { + escapeHTML = escape; + } + + /** If true, inserts escape codes in to the <B>string</B> string so + * that HTML special characters (greater-than, less-than, etc.) + * appear correctly. If false, those characters will get + * interpreted by the browser. Defaults to true. + */ + public boolean escapeHTML() { + return escapeHTML; + } + + public String actionURL(WOContext c) { + //Check if the href property is set + if (href() != null) { + return href(); + } else if (pageName() != null || associations.objectForKey("action") != null) { //write this component's URL + StringBuffer retval = new StringBuffer(c.componentActionURL()); + Map addFields = urlFields(c.component()); + if (addFields.size() > 0) { + Iterator enumeration = addFields.keySet().iterator(); + retval.append('?'); + while (enumeration.hasNext()) { + String encoding = c.response() != null ? c.response().contentEncoding() : c.request().contentEncoding(); + String key = (String)enumeration.next(); + try { + retval.append(java.net.URLEncoder.encode(key, encoding)); + } catch (java.io.UnsupportedEncodingException ex) { + retval.append(key); + } + retval.append("="); + try { + retval.append(java.net.URLEncoder.encode(addFields.get(key).toString(), encoding)); + } catch (java.io.UnsupportedEncodingException e) { + retval.append(addFields.get(key).toString()); + } + if (enumeration.hasNext()) + retval.append('&'); + } + } + return retval.toString(); + } else if (directActionName() != null) { //compose the direct action URL + String fullActionName = null; + if (actionClass() != null ) + fullActionName = actionClass() + "/" + directActionName(); + else + fullActionName = directActionName(); + return c.directActionURLForActionNamed(fullActionName, urlFields(c.component())); + } + return null; + } + + protected void pullValuesFromParent(WOComponent c) { + string = stringForProperty("string", c); + href = stringForProperty("href", c); + pageName = stringForProperty("pageName", c); + directActionName = stringForProperty("directActionName", c); + actionClass = stringForProperty("actionClass", c); + //action = stringForProperty("action", c); + escapeHTML = booleanForProperty("escapeHTML", c); + anchorName = stringForProperty("anchorName", c); + ref = stringForProperty("ref", c); + } + + public void appendToResponse(WOResponse r, WOContext c) { + pullValuesFromParent( c.component() ); + r.appendContentString("<A"); + boolean closeQuotes = false; + //Check if the href property is set + String _href = actionURL(c); + if (_href != null) { + closeQuotes = true; + r.appendContentString(" HREF=\""); + r.appendContentString(_href); + } else if (anchorName() != null) { + r.appendContentString(" NAME=\""); + r.appendContentString(anchorName()); + closeQuotes = true; + } + if (ref != null) { + if (!closeQuotes) { + r.appendContentString(" HREF=\"#"); + closeQuotes = true; + } else + r.appendContentString("#"); + r.appendContentString(ref); + } + if (closeQuotes) + r.appendContentString("\""); + r.appendContentString(additionalHTMLProperties(c.component(), new NSArray(new Object[]{ + "name", "href", "pageName", "action", "directActionName", "actionClass", "anchorName", + "escapeHTML", "string" }))); + r.appendContentString(">"); + //Append the string if present + if (string() != null) { + if (escapeHTML()) + r.appendContentHTMLString(string()); + else + r.appendContentString(string()); + } + //If there is a template, call appendToResponse on it + if (rootElement != null) { + rootElement.appendToResponse(r, c); + } + //Close the tag + r.appendContentString("</A>"); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + System.out.println("invoke action with elementID=" + c.elementID() + " senderID=" + c.senderID()); + //Check if this element is the target + if (c.senderID().equals(c.elementID())) { + if (pageName() != null) + { + return WOApplication.application().pageWithName(pageName(), r); + } + else + { + WOAssociation ass = (WOAssociation) associations.objectForKey("action"); + if ( ass != null && ass.path != null ) //?? + return (WOActionResults)c.component().performAction( ass.path ); + } + } + return null; + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + System.out.println("takeValuesFromRequest elementID=" + c.elementID() + " senderID=" + c.senderID()); + super.takeValuesFromRequest(r, c); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImage.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImage.java new file mode 100644 index 0000000..2673cd1 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImage.java @@ -0,0 +1,150 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; + +/** +* WOImage renders a dynamically generated IMG tag. The URL for the image SRC can be +* static, or it can be generated dynamically to return a NSData object (with a mime-type) +* or even return the contents of a file (not implemented yet). +* +* Bindings are: +* <UL><LI>src: A static URL for the image source.</li> +* <li>data: A NSData object with the image content. Must be used with mimeType.</li> +* <li>mimeType: The MIME type for the image data. Can be used with filename or data bindings.</li> +* <li>filename: The path to a file containing an image.</li> +* <li>framework: The optional framework from whence the image should be retrieved (used in conjunction with filename).</li> +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOImage extends WODynamicElement { + + protected String src; + protected String filename; + protected String framework; + protected NSData data; + protected String mimeType; + + protected WOImage() { + super(); + } + + public WOImage(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public void setSrc(String value) { + src = value; + } + public String src() { + return src; + } + + public void setFilename(String value) { + filename = value; + } + + public String filename() { + return filename; + } + + public void setFramework(String value) { + framework = value; + } + public String framework() { + return framework; + } + + public void setData(NSData value) { + data = value; + } + public NSData data() { + return data; + } + + public void setMimeType(String value) { + mimeType = value; + } + public String mimeType() { + return mimeType(); + } + + public String sourceURL(WOContext c) { + if (associations.objectForKey("src") != null) + return (String)valueForProperty("src", c.component()); + if (associations.objectForKey("data") != null) { + return c.componentActionURL(); + } + if (associations.objectForKey("filename") != null) { + WOComponent component = c.component(); + + String framework = stringForProperty("framework", component); + String filename = stringForProperty( "filename", component ); + if ( filename != null && framework == null ) + { + if ( filename.startsWith("/" ) ) + { + int i = filename.lastIndexOf( "/" ); + if ( i > 0 ) + { + framework = filename.substring( 0, i ); + if ( i < filename.length() ) + { + filename = filename.substring( i+1 ); + } + } + } + else + { + // just until we figure out how we're handling bundles/localization + framework = component.frameworkName(); + if ( framework != null ) + { + framework = framework + '/' + component.name() + ".wo"; + } + else + { + framework = '/' + component.name() + ".wo"; + } + } + } + return WOApplication.application().resourceManager().urlForResourceNamed( + filename, framework, c.request().browserLanguages(), c.request() ); + } + return "NO SOURCE"; + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString("<IMG SRC=\""); + r.appendContentString(sourceURL(c)); + r.appendContentString("\""); + r.appendContentString(additionalHTMLProperties(c.component(), new NSArray(new Object[]{ + "src", "filename", "framework", "data", "mimeType" }))); + r.appendContentString(">"); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + if (c.senderID().equals(c.elementID())) { + Object data = valueForProperty("data", c.component()); + if (data instanceof byte[]) data = new NSData( (byte[]) data ); + if (data instanceof NSData) { + String mt = stringForProperty("mimeType", c.component()); + if (mt == null) + throw new IllegalArgumentException("WOImage: No mimeType specified for data."); + WOResponse img = new WOResponse(); + img.setContent((NSData)data); + img.setHeader(mt, "content-type"); + return img; + } else if (filename() != null) { + //will this thing use frameworks, or regular JAR files with resources? + // mp: both/either, depending on which resource manager implementation + } + } + return null; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImageButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImageButton.java new file mode 100644 index 0000000..7c9f22e --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImageButton.java @@ -0,0 +1,58 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** + * WOImageButton renders a dynamically generated IMG tag or an INPUT tag, depending on whether the + * element is inside a WOForm (in which case an INPUT of type IMAGE is generated) or not (in which + * case the equivalent of a WOActiveImage) + * + * Bindings are: + * <UL> + * <LI>src: A static URL for the image source.</li> + * <li>data: A NSData object with the image content. Must be used with mimeType.</li> + * <li>mimeType: The MIME type for the image data. Can be used with filename or data bindings.</li> + * <li>filename: The path to a file containing an image.</li> + * <li>framework: The framework where the image should be retrieved from (used in conjunction with filename).</li> + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOImageButton extends WOImage { + + protected WOImageButton() { + super(); + } + + public WOImageButton(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public String buttonName(WOContext c) { + String x = (String)valueForProperty("name", c.component()); + if (x != null) + return x; + return c.elementID(); + } + + public void appendToResponse(WOResponse r, WOContext c) { + if (c.isInForm()) { + //generate an INPUT + r.appendContentString("<INPUT TYPE=IMAGE NAME=\""); + r.appendContentString(buttonName(c)); + r.appendContentString("\" SRC=\""); + r.appendContentString(sourceURL(c)); + r.appendContentString("\""); + r.appendContentString(additionalHTMLProperties(c.component(), new NSArray(new Object[]{ + "name", "action", "src", "filename", "framework", "data", "mimeType" }))); + r.appendContentString(">"); + } else { + //generate a WOActiveImage + new WOActiveImage(name, associations, rootElement).appendToResponse(r, c); + } + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOInput.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOInput.java new file mode 100644 index 0000000..a8c7daa --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOInput.java @@ -0,0 +1,102 @@ + +package net.wotonomy.web; + +import java.text.DecimalFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +public abstract class WOInput extends WODynamicElement { + + public WOInput() { + super(); + } + + public WOInput(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected abstract String inputType(); + protected abstract Object value(WOContext c); + + protected NSMutableArray additionalAttributes() { + return new NSMutableArray(new Object[]{ + "disabled", "type", "value", "name" + }); + } + + public String inputName(WOContext c) { + String x = (String)valueForProperty("name", c.component()); + if (x != null) + return x; + return c.elementID(); + } + + protected boolean disabled(WOContext c) { + return booleanForProperty("disabled", c.component()); + } + + protected void appendExtras(WOResponse r, WOContext c) { + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString("<INPUT TYPE=\""); + r.appendContentString(inputType()); + r.appendContentString("\" NAME=\""); + r.appendContentString(inputName(c)); + r.appendContentString("\" VALUE=\""); + r.appendContentString(value(c).toString()); + r.appendContentString("\""); + String moreFields = additionalHTMLProperties(c.component(), additionalAttributes()); + if (moreFields != null && moreFields.length() > 0) + r.appendContentString(moreFields); + appendExtras(r, c); + if (disabled(c)) { + r.appendContentString(" DISABLED"); + } + r.appendContentString(">"); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + if (disabled(c)) + return; + Object val = r.formValueForKey(inputName(c)); + WOAssociation va = (WOAssociation)associations.objectForKey("value"); + if (val != null && va != null && va.isValueSettable()) + setValueForProperty("value", val, c.component()); + } + + /** Formats a value as a date or number. Checks for + * numberformat or dateformat associations; if one of them + * exists, the value is formatter using the specified pattern. + * @param value The value to format. + * @return The original object, or a date or number if the + * receiver has a numberformat or dateformat association. + */ + protected Object formattedValue(Object value, WOComponent c) { + //Format the value in case of number + String pattern = (String)valueForProperty("numberformat", c); + if (pattern != null) { + DecimalFormat fmt = new DecimalFormat(pattern); + try { + return fmt.parse(value.toString()); + } catch (ParseException e) { + return value; + } + } + //Format the value in case of date + pattern = (String)valueForProperty("dateformat", c); + if (pattern != null) { + SimpleDateFormat fmt = new SimpleDateFormat(pattern); + try { + return fmt.parse(value.toString()); + } catch (ParseException e) { + return value; + } + } + return value; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMailDelivery.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMailDelivery.java new file mode 100644 index 0000000..eccc489 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMailDelivery.java @@ -0,0 +1,54 @@ +/* +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.util.List; + +/** +* A pure java implementation of WOMailDelivery. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public abstract class WOMailDelivery +{ + private static WOMailDelivery sharedInstance; + + protected WOMailDelivery () + { + } + + public static WOMailDelivery sharedInstance () + { + if ( sharedInstance == null ) + { +// sharedInstance = new WOMailDelivery(); + } + return sharedInstance; + } + + public abstract String composePlainTextEmail ( + String aSender, List aToList, List aCcList, + String aSubject, String aMessage, boolean sendImmediately ); + public abstract String composeComponentEmail ( + String aSender, List aToList, List aCcList, + String aSubject, WOComponent aComponent, boolean sendImmediately ); + public abstract void sendEmail (String aMailMessage); +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMessage.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMessage.java new file mode 100644 index 0000000..be76be1 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMessage.java @@ -0,0 +1,333 @@ +/* +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 net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableData; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* A pure java implementation of WOResponse. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOMessage { + + protected String _contentEncoding = "ISO8859_1"; + protected NSMutableDictionary _headers = new NSMutableDictionary(); + protected NSMutableDictionary _cookies = new NSMutableDictionary(); + private NSMutableDictionary _userInfo = new NSMutableDictionary(); + protected NSMutableData _contentData = new NSMutableData(); + + public WOMessage() { + super(); + } + + /** + * Sets the content encoding for the response. + */ + public void setContentEncoding (String encoding) + { + _contentEncoding = encoding; + } + + /** + * Gets the current content encoding for the response. + */ + public String contentEncoding () { + return _contentEncoding; + } + + /** + * Sets the specified array of values as headers under + * the specified key. + */ + public void setHeaders (NSArray headerArray, String aKey) { + _headers.setObjectForKey( headerArray, aKey ); + } + + /** + * Sets the specified header value for the specified key. + */ + public void setHeader (String aValue, String aKey) { + _headers.setObjectForKey( new NSArray( aValue ), aKey ); + } + + /** + * Returns an array of all the header keys that have been + * set in the response. + */ + public NSArray headerKeys () { + return _headers.allKeys(); + } + + /** + * Returns an array of all the header values for the specified key, + * or null if the key does not exist. + */ + public NSArray headersForKey (String aKey) { + return (NSArray)_headers.objectForKey( aKey ); + } + + /** + * Returns one header value for the specified key. + * Provided as a convenience, since most header keys + * will have a single value. + */ + public String headerForKey (String aKey) { + NSArray values = (NSArray)_headers.objectForKey( aKey ); + if ( values != null && values.count() > 0 ) { + return values.objectAtIndex( 0 ).toString(); + } + return null; + } + + /** + * Sets the content of the response to the bytes represented + * by the specified data object. + */ + public void setContent(NSData aData) { + _contentData.setData( aData ); + setHeader(Integer.toString(aData.length()), "content-length"); + } + + /** + * Retrieves the current content of the response. + */ + public NSData content() { + return _contentData; + } + + /** + * Sets the current user info dictionary. These values + * are for application-specific uses and are available to + * other actions and components in the request-response cycle. + */ + public void setUserInfo (NSDictionary aDict) { + _userInfo = new NSMutableDictionary( aDict ); + } + + /** + * Gets the current user info dictionary. These values + * are for application-specific uses are are available to + * other actions and components in the request-response cycle. + */ + public NSDictionary userInfo () { + return new NSDictionary(_userInfo); + } + + /** + * Appends the bytes in the specified data object to the response. + */ + public void appendContentData (NSData aData) + { + _contentData.appendData( aData ); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Appends the specified byte to the response. + */ + public void appendContentCharacter (char character) { + _contentData.appendByte((byte)character); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Appends the specified string to the response. + * Any special characters will not be escaped. + * The string will be encoded in the current content encoding. + */ + public void appendContentString (String aString) + { + _contentData.appendData( new NSData( aString.getBytes() ) ); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Appends the specified string containing HTML to the response. + * Any special characters will be escaped appropriately. + * The string will be encoded in the current content encoding. + */ + public void appendContentHTMLString (String aString) + { + _contentData.appendData( + new NSData( stringByEscapingHTMLString( + aString ).getBytes() ) ); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Appends the specified string containing HTML to the response. + * Any special characters will be escaped appropriately. + * This method escapes tabs and new-line characters as well. + * The string will be encoded in the current content encoding. + */ + public void appendContentHTMLAttributeValue (String aString) + { + _contentData.appendData ( + new NSData( stringByEscapingHTMLAttributeValue( + aString ).getBytes() ) ); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Adds the specified cookie to the response. + */ + public void addCookie (WOCookie aCookie) + { + _cookies.setObjectForKey( aCookie, aCookie.name() ); + } + + /** + * Removes the specified cookie from the response. + */ + public void removeCookie (WOCookie aCookie) + { + _cookies.removeObjectForKey( aCookie.name() ); + } + + /** + * Returns an array of cookies currently being sent with the response. + * Contains whatever cookies have previously been set in this response. + */ + public NSArray cookies () + { + return _cookies.allValues(); + } + + /** + * Sets the HTTP version header in the response. + */ + public void setHTTPVersion (String aString) + { + setHeader( aString, "Protocol"); + } + + /** + * Gets the current HTTP version header for the response. + * Because servlet responses do not allow read access + * to headers, this method returns null if setHTTPVersion + * has not been called. + */ + public String httpVersion () + { + return headerForKey( "Protocol" ); + } + + /** + * Returns a sting containing the contents of the specified + * string after escaping all special HTML characters. + */ + public static String stringByEscapingHTMLString + (String aString) + { + int len = aString.length(); + StringBuffer result = new StringBuffer(); + char[] buf = new char[ len ]; + aString.getChars( 0, len, buf, 0 ); + for ( int i = 0; i < len; i++ ) + { + if ( buf[i] == '&' ) + { + result.append( "&" ); + } + else + if ( buf[i] == '\\' ) + { + result.append( """ ); + } + else + if ( buf[i] == '<' ) + { + result.append( "<" ); + } + else + if ( buf[i] == '>' ) + { + result.append( ">" ); + } + else + { + result.append( buf[i] ); + } + } + return result.toString(); + } + + /** + * Returns a sting containing the contents of the specified + * string after escaping all special HTML characters. + * This method escapes tabs and new-line characters as well. + */ + public static String stringByEscapingHTMLAttributeValue + (String aString) + { + int len = aString.length(); + StringBuffer result = new StringBuffer(); + char[] buf = new char[ len ]; + aString.getChars( 0, len, buf, 0 ); + for ( int i = 0; i < len; i++ ) + { + if ( buf[i] == '&' ) + { + result.append( "&" ); + } + else + if ( buf[i] == '\\' ) + { + result.append( """ ); + } + else + if ( buf[i] == '<' ) + { + result.append( "<" ); + } + else + if ( buf[i] == '>' ) + { + result.append( ">" ); + } + else + if ( buf[i] == '\t' ) + { + result.append( "	" ); + } + else + if ( buf[i] == '\n' ) + { + result.append( " " ); + } + else + if ( buf[i] == '\r' ) + { + result.append( " " ); + } + else + { + result.append( buf[i] ); + } + } + return result.toString(); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOParentElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOParentElement.java new file mode 100644 index 0000000..40ee618 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOParentElement.java @@ -0,0 +1,178 @@ +/* +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.util.Iterator; +import java.util.List; + +import net.wotonomy.foundation.NSMutableArray; + +/** +* This class represents a parent node in an element tree. +* It has no content in itself, and exists only to forward +* messages to each of its children, in turn. +* Package access only, as it is not in the specification. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +class WOParentElement extends WOElement +{ + NSMutableArray children; + + /** + * Default constructor. + */ + public WOParentElement() + { + children = new NSMutableArray(); + } + + /** + * Returns an element with the specified children. + */ + public WOParentElement( List childElements ) + { + this(); + children.addAll( childElements ); + } + + /** + * 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) + { + WOElement element; + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + element = (WOElement) it.next(); + aContext.pushElement( element ); + element.ensureAwakeInContext( aContext ); + aContext.popElement(); + } + } + + /** + * Forwards this message to all child elements. + */ + public void takeValuesFromRequest ( + WORequest aRequest, WOContext aContext) + { + WOElement element; + +// aContext.incrementLastElementIDComponent(); + aContext.appendZeroElementIDComponent(); + + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + element = (WOElement) it.next(); + aContext.pushElement( element ); + aContext.incrementLastElementIDComponent(); + element.takeValuesFromRequest( aRequest, aContext ); + aContext.popElement(); + } + + aContext.deleteLastElementIDComponent(); + } + + /** + * Forwards this message to all child elements, + * returning the first non-null result. + */ + public WOActionResults invokeAction ( + WORequest aRequest, WOContext aContext) + { + WOElement element; + +// aContext.incrementLastElementIDComponent(); + aContext.appendZeroElementIDComponent(); + + WOActionResults result = null; + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + element = (WOElement) it.next(); + aContext.pushElement( element ); + aContext.incrementLastElementIDComponent(); + result = element.invokeAction( aRequest, aContext ); + aContext.popElement(); + if ( result != null ) break; + } + + aContext.deleteLastElementIDComponent(); + return result; + } + + /** + * Forwards this message to all child elements. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + WOElement element; + +// aContext.incrementLastElementIDComponent(); + aContext.appendZeroElementIDComponent(); + + // for each child element + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + element = (WOElement) it.next(); + aContext.pushElement( element ); + aContext.incrementLastElementIDComponent(); + + // forward the message + element.appendToResponse( + aResponse, aContext ); + + aContext.popElement(); + + } + aContext.deleteLastElementIDComponent(); + } + + public WOResponse generateResponse() + { + WOResponse r = new WOResponse(); + return r; + } + + public String toString() + { + StringBuffer result = new StringBuffer(); + result.append( "[WOParentElement: " ); + // for each child element + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + result.append( "[ " ); + result.append( it.next().toString() ); + result.append( " ]" ); + } + result.append( " ]" ); + return result.toString(); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPasswordField.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPasswordField.java new file mode 100644 index 0000000..2d4f16e --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPasswordField.java @@ -0,0 +1,20 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +public class WOPasswordField extends WOTextField { + + public WOPasswordField() { + super(); + } + + public WOPasswordField(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "PASSWORD"; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPopUpButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPopUpButton.java new file mode 100644 index 0000000..a73636c --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPopUpButton.java @@ -0,0 +1,135 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCodingAdditions; + +public class WOPopUpButton extends WOInput { + + public WOPopUpButton() { + super(); + } + + public WOPopUpButton(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "SELECT"; + } + + protected int inputSize() { + return 1; + } + + protected NSArray list(WOContext c) { + NSArray l = (NSArray)valueForProperty("list", c.component()); + if (l == null) + l = NSArray.EmptyArray; + return l; + } + + protected void setItem(Object v, WOContext c) { + if (associations.objectForKey("item") == null) + return; + setValueForProperty("item", v, c.component()); + } + protected Object item(WOContext c) { + return valueForProperty("item", c.component()); + } + + protected void setSelection(Object v, WOContext c) { + if (associations.objectForKey("selection") == null) + return; + setValueForProperty("selection", v, c.component()); + } + protected Object selection(WOContext c) { + return valueForProperty("selection", c.component()); + } + + public Object value(WOContext c) { + return null; + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString("<SELECT NAME=\""); + r.appendContentString(inputName(c)); + r.appendContentString("\" SIZE="); + r.appendContentString(Integer.toString(inputSize())); + r.appendContentString(">"); + java.util.Enumeration numerador = list(c).objectEnumerator(); + String displayKey = stringForProperty("displayString", c.component()); + String valueKey = stringForProperty("value", c.component()); + Object sel = selection(c); + if (sel == null) + sel = item(c); + int pos = 0; + while (numerador.hasMoreElements()) { + Object item = numerador.nextElement(); + setItem(item, c); + r.appendContentString("<OPTION "); + //Append the "SELECTED" attribute if it's the selected item + if (sel != null && item.equals(sel)) + r.appendContentString("SELECTED "); + r.appendContentString("VALUE=\""); + //Append the value + if (valueKey != null && item instanceof NSKeyValueCodingAdditions) { + Object val = ((NSKeyValueCodingAdditions)item).valueForKeyPath(valueKey); + if (val == null) + val = "null"; + r.appendContentString(val.toString()); + } else + r.appendContentString(Integer.toString(pos)); + r.appendContentString("\">"); + //Append display string + if (displayKey != null && item instanceof NSKeyValueCodingAdditions) { + Object ds = ((NSKeyValueCodingAdditions)item).valueForKeyPath(displayKey); + if (ds == null) + ds = ""; + r.appendContentString(ds.toString()); + } else + r.appendContentString(item.toString()); + r.appendContentString("\n"); + pos++; + } + r.appendContentString("</SELECT>"); + } + + protected void select(Object v, WOContext c) { + if (associations.objectForKey("selection") != null) { + setSelection(v, c); + return; + } + if (associations.objectForKey("item") != null) { + setItem(v,c); + } + } + public void takeValuesFromRequest(WORequest r, WOContext c) { + Object val = r.formValueForKey(inputName(c)); + if (val == null) + return; + NSArray list = list(c); + String valueKey = stringForProperty("value", c.component()); + //If no value binding, just get the index + if (valueKey == null) { + int pos = Integer.parseInt(val.toString()); + val = list.objectAtIndex(pos); + select(val, c); + return; + } + //If value binding is present, lookup the value + java.util.Enumeration numerador = list.objectEnumerator(); + while (numerador.hasMoreElements()) { + Object o = numerador.nextElement(); + if (o instanceof NSKeyValueCodingAdditions) { + Object x = ((NSKeyValueCodingAdditions)o).valueForKeyPath(valueKey); + if (x != null && x.equals(val)) { + select(o, c); + return; + } + } + } + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORadioButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORadioButton.java new file mode 100644 index 0000000..386218c --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORadioButton.java @@ -0,0 +1,20 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +public class WORadioButton extends WOCheckBox { + + public WORadioButton() { + super(); + } + + public WORadioButton(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "RADIO"; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORepetition.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORepetition.java new file mode 100644 index 0000000..afa118a --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORepetition.java @@ -0,0 +1,179 @@ +/* + 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.util.Collection; +import java.util.Iterator; + +import net.wotonomy.foundation.NSDictionary; + +public class WORepetition extends WODynamicElement { + + protected int count; + protected int index; + protected Object item; + protected Collection list; + protected Iterator iterator; + + protected WORepetition() { + super(); + } + + public WORepetition(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public void setCount(int value) { + count = value; + } + + public int count() { + return count; + } + + public void setIndex(int value) { + index = value; + } + public int index() { + return index; + } + + public void setItem(Object value) { + item = value; + } + public Object item() { + return item; + } + + public void setList(Collection value) { + list = value; + } + public Collection list() { + return list; + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + c.appendZeroElementIDComponent(); + pullValuesFromParent(c.component()); + index = 0; + WOAssociation countAsoc = (WOAssociation)associations.objectForKey("count"); + item = getNextItem(); + while (item != null) { + pushValuesToParent(c.component()); + rootElement.takeValuesFromRequest(r, c); + index++; + c.incrementLastElementIDComponent(); + if (countAsoc != null && index >= count()) + item = null; + else + item = getNextItem(); + } + iterator = null; + c.deleteLastElementIDComponent(); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + c.appendZeroElementIDComponent(); + pullValuesFromParent(c.component()); + index = 0; + WOAssociation countAsoc = (WOAssociation)associations.objectForKey("count"); + item = getNextItem(); + while (item != null) { + pushValuesToParent(c.component()); + WOActionResults e = rootElement.invokeAction(r, c); + if (e != null) + { + iterator = null; + return e; + } + index++; + c.incrementLastElementIDComponent(); + if (countAsoc != null && index >= count()) + item = null; + else + item = getNextItem(); + } + iterator = null; + c.deleteLastElementIDComponent(); + return null; + } + + public void appendToResponse(WOResponse r, WOContext c) { + c.appendZeroElementIDComponent(); + pullValuesFromParent(c.component()); + index = 0; + WOAssociation countAsoc = (WOAssociation)associations.objectForKey("count"); + item = getNextItem(); + while (item != null) { + pushValuesToParent(c.component()); + rootElement.appendToResponse(r, c); + index++; + c.incrementLastElementIDComponent(); + if (countAsoc != null || index < count()) + item = null; + else + item = getNextItem(); + } + iterator = null; + c.deleteLastElementIDComponent(); + } + + protected Object getNextItem() { + if ( iterator == null ) + { + if ( list == null ) return null; + iterator = list.iterator(); + } + if ( iterator.hasNext() ) + { + return iterator.next(); + } + return null; + } + + protected void pullValuesFromParent(WOComponent c) { + Object value; + + value = valueForProperty("count", c); + if ( value instanceof Number ) + { + setCount( ((Number)value).intValue() ); + } + else + { + setCount( -1 ); + } + + value = valueForProperty("list", c); + if ( value instanceof Collection ) + { + setList( (Collection) value ); + } + else + { + setList( null ); + } + } + + protected void pushValuesToParent(WOComponent c) { + setValueForProperty("index", new Integer(index), c); + setValueForProperty("item", item, c); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequest.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequest.java new file mode 100644 index 0000000..7d71223 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequest.java @@ -0,0 +1,586 @@ +/* +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.InputStream; +import java.util.Enumeration; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableData; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* A pure java implementation of WORequest. +* This implementation is backed by an HttpServletRequest, +* and thus does not support application-specific subclassing. +* Future implementations may remove this limitation. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WORequest extends WOResponse +{ + HttpServletRequest request; + private WOApplication application; + private NSArray languages; + private String requestHandlerKey; + private String requestHandlerPath; + private String pageName; + private String contextID; + private String senderID; + private String defaultFormValueEncoding; + + /** + * Parameterless constructor which should not be called. + */ + public WORequest () + { + throw new RuntimeException( + "This constructor is not yet supported." ); + } + + /** + * Standard constructor. Method and URL are required. + * HeaderMap is a map of header names to arrays containing + * one or more values. Data is the content of the request. + * UserInfo contains application-defined paramters that get + * passed to all objects involved in dispatching the request. + */ + public WORequest + (String aMethod, String aURL, String aProtocolName, + NSDictionary headerMap, NSData aData, NSDictionary userInfo) + { + throw new RuntimeException( + "This constructor is not yet supported." ); + } + + /** + * The only supported constructor for this implementation. + * This WORequest will wrap the specified servlet request. + */ + public WORequest( HttpServletRequest aRequest, WOApplication anApplication ) + { + request = aRequest; + application = anApplication; + + languages = null; + senderID = null; + pageName = null; + contextID = null; + defaultFormValueEncoding = "ISO8859_1"; + requestHandlerKey = WOApplication.directActionRequestHandlerKey(); + requestHandlerPath = request.getServletPath(); // request.getPathInfo(); + String remainder = requestHandlerPath; + if ( requestHandlerPath == null ) requestHandlerPath = ""; + if ( requestHandlerPath.length() > 0 ) + { + int i; + + // get request handler key + i = requestHandlerPath.indexOf( "/", 1 ); + if ( i != -1 ) + { + requestHandlerKey = requestHandlerPath.substring( 1, i ); + requestHandlerPath = requestHandlerPath.substring( i ); + } + + Enumeration e = requestHandlerPathArray().objectEnumerator(); + // skipping session ID for now + if ( e.hasMoreElements() ) + { + pageName = e.nextElement().toString(); + } + if ( e.hasMoreElements() ) + { + contextID = e.nextElement().toString(); + } + if ( e.hasMoreElements() ) + { + senderID = e.nextElement().toString(); + } + } + } + + /** + * Returns the HTTP method of the request: "GET" or "POST", + * or possibly "PUT". + */ + public String method () + { + return request.getMethod(); + } + + /** + * Returns the Uniform Resource Identifier, which is the part + * of the request URL after the protocol name (after the port + * number) and before the query parameters (before the question mark). + */ + public String uri () + { + return request.getRequestURI(); + } + + /** + * Returns the name of the protocol (presumably HTTP) and the + * version used by the client as sent in the request headers. + */ + public String httpVersion () + { + return request.getProtocol(); + } + + /** + * Returns an array of the header names in this request + * in no particular order. + */ + public NSArray headerKeys () + { + return arrayFromEnumeration( request.getHeaderNames() ); + } + + /** + * Returns an array of the header values for the specified key + * in no particular order. + */ + public NSArray headersForKey (String aString) + { + return arrayFromEnumeration( request.getHeaders( aString ) ); + } + + /** + * Returns one value for the specified header key. + * Provided as a convenience since most headers have + * only one value. Returns null if not found. + */ + public String headerForKey (String aKey) + { + return request.getHeader( aKey ); + } + + /** + * Returns the content of the request, or null if no content. + * The type of the content is determined by the "content-type" header. + * On error, null is returned. + */ + public NSData content () + { + //TODO: This is broken! + NSMutableData data = new NSMutableData(); + try + { + byte[] buf = new byte[ request.getContentLength() ]; + InputStream is = request.getInputStream(); + + // this is so very not efficient + int len; + while ( ( len = is.read( buf ) ) > -1 ) + { + // copies data twice... + data.appendData( new NSData( buf, 0, len ) ); + } + } + catch ( Exception exc ) + { + return null; + } + return data; + } + + /** + * Returns the application-specific userInfo dictionary. + * This implementation returns the servlet attribute map. + */ + public NSDictionary userInfo () + { + //TODO: Test this logic. + Object key, value; + NSMutableDictionary info = new NSMutableDictionary(); + java.util.Enumeration e = request.getAttributeNames(); + while ( e.hasMoreElements() ) + { + key = e.nextElement(); + value = request.getAttribute( e.nextElement().toString() ); + if ( value != null ) + { + info.setObjectForKey( value, key ); + } + } + return info; + } + + /** + * Returns the items in the request handler path parsed + * by the "/" delimiter and put into an array. + */ + public NSArray requestHandlerPathArray () + { + return NSArray.componentsSeparatedByString( requestHandlerPath(), "/" ); + } + + /** + * Returns the client's preferred languages in decreasing + * order of preference. The strings are returned in java + * Locale format, meaning dashes (-) are converted to + * underbars (_): see java.util.Locale.toString(). + */ + public NSArray browserLanguages () + { + if ( languages == null ) + { + languages = arrayFromEnumeration( request.getLocales() ); + } + return languages; + } + + /** + * Returns the portion of the URI specifying the engine within which + * this web application is running. This is important for generating + * URLs to be used in the content of the response. + */ + public String adaptorPrefix () + { + //TODO: Test this logic. + String name = request.getServletPath(); + return name; + +// String uri = request.getRequestURI(); +// int end = uri.indexOf( request.getPathInfo() ); +// return uri.substring( request.getContextPath().length(), end ); + } + + /** + * Returns the application name as specified in the request URI. + * Note that wotonomy web applications do not typically have a .woa + * extension, but the extension will be included for those that do. + */ + public String applicationName () + { + return request.getContextPath(); + } + + /** + * Returns the id of the application instance that is needed to + * service this request. -1 indicates that the request is not + * specific to a particular instance of the application. + * This implementation currently returns -1. + */ + public int applicationNumber () + { + return -1; + } + + /** + * Returns the portion of the URI that specifies which request handler + * should handle the request. + */ + public String requestHandlerKey () + { + return requestHandlerKey; + } + + /** + * Returns the portion of the URI that specifies path information + * for the request, not including the query string. + */ + public String requestHandlerPath () + { + return requestHandlerPath; + } + + /** + * Returns the unique identifier for the sessions associated with + * this request, or null if no session is found. + */ + public String sessionID () + { + HttpSession session = request.getSession( false ); + if ( session == null ) return null; + return session.getId(); + } + + /** + * Returns an array containing all the form keys in the request. + */ + public NSArray formValueKeys () + { + return arrayFromEnumeration( request.getParameterNames() ); + } + + /** + * Returns an array of the values for the specified key in + * no particular order. + */ + public NSArray formValuesForKey (String aKey) + { + NSMutableArray result = new NSMutableArray(); + String[] values = request.getParameterValues( aKey ); + if ( values != null ) + { + for ( int i = 0; i < values.length; i++ ) + { + result.addObject( values[i] ); + } + } + return result; + } + + /** + * Returns one value for the specified key. + * Provided as a convenience since most keys have + * only one value. Returns null if not found. + */ + public Object formValueForKey (String aKey) + { + return request.getParameter( aKey ); + } + + /** + * Returns the value for the specified key, as a String. + */ + public String stringFormValueForKey(String key) { + Object x = formValueForKey(key); + if (x != null) + return x.toString(); + return null; + } + + /** + * Returns a dictionary containing all the key-value + * mappings in the request. + */ + public NSDictionary formValues () + { + NSMutableDictionary result = new NSMutableDictionary (); + java.util.Enumeration e = request.getParameterNames(); + String key; + while ( e.hasMoreElements() ) + { + key = e.nextElement().toString(); + result.setObjectForKey( formValuesForKey( key ), key ); + } + return result; + } + + /** + * Returns whether the request is from a java-based client component. + * This implementation returns false. + */ + public boolean isFromClientComponent () + { + return false; + } + + /** + * Returns an array of cookie values for the specified key in + * no particular order. + */ + public NSArray cookieValuesForKey (String aKey) + { + // TODO: Test this logic. + NSMutableArray result = new NSMutableArray(); + Cookie[] cookies = request.getCookies(); + if ( cookies == null ) return result; + + for ( int i = 0; i < cookies.length; i++ ) + { + if ( cookies[i].getName().equals( aKey ) ) + { + result.addObject( cookies[i].getValue() ); + } + } + + return result; + } + + /** + * Returns one value for the specified cookie key. + * Provided as a convenience since most cookies have + * only one value. Returns null if not found. + */ + public String cookieValueForKey (String aKey) + { + // TODO: Test this logic. + Cookie[] cookies = request.getCookies(); + if ( cookies == null ) return null; + + for ( int i = 0; i < cookies.length; i++ ) + { + if ( cookies[i].getName().equals( aKey ) ) + { + return cookies[i].getValue(); + } + } + + return null; + } + + /** + * Returns a dictionary of cookie key-value mappings. + */ + public NSDictionary cookieValues () + { + // TODO: Test this logic. + NSMutableDictionary result = new NSMutableDictionary(); + Cookie[] cookies = request.getCookies(); + if ( cookies == null ) return result; + + NSMutableArray value; + for ( int i = 0; i < cookies.length; i++ ) + { + value = (NSMutableArray) result.objectForKey( + cookies[i].getName() ); + if ( value == null ) + { + value = new NSMutableArray(); + result.setObjectForKey( + cookies[i].getValue(), cookies[i].getName() ); + } + value.addObject( cookies[i].getValue() ); + } + + return result; + } + + /** + * Sets the default character encoding. + */ + public void setDefaultFormValueEncoding (String encoding) + { + defaultFormValueEncoding = encoding; + } + + /** + * Returns the default form value encoding ("ISO8859_1"). + */ + public String defaultFormValueEncoding () + { + return defaultFormValueEncoding; + } + + /** + * Sets whether the appropriate encoding scheme for decoding + * the form values will be automatically determined. + */ + public void setFormValueEncodingDetectionEnabled (boolean enabled) + { + throw new RuntimeException( "Not yet implemented." ); + } + + /** + * Gets whether the appropriate encoding scheme for decoding + * the form values is currently automatically determined. + */ + public boolean isFormValueEncodingDetectionEnabled () + { + throw new RuntimeException( "Not yet implemented." ); + } + + /** + * Gets the current method used for decoding form values. + */ + public int formValueEncoding () + { + throw new RuntimeException( "Not yet implemented." ); + } + + /** + * Returns the host name of the server that is the target of this request. + * NOTE: This method is not published in the WORequest specification. + */ + String applicationHost () + { + // FIXME: this should call WOApplication.hostname(); + return request.getServerName(); + } + + /** + * Returns the port of the server that is the target of this request. + * NOTE: This method is not published in the WORequest specification. + */ + int port() + { + // FIXME: this should call WOApplication.port(); + return request.getServerPort(); + } + + /** + * Returns the backing HttpServletRequest. + */ + HttpServletRequest servletRequest () + { + return request; + } + + /** + * Returns the application that was the target of this request. + * This method is not published in the WORequest specification. + */ + WOApplication application () + { + return application; + } + + /** + * This method is not published in the WORequest specification. + * This sender id from the URI, which is the id of the element + * that generated this request. + */ + String senderID () + { + return senderID; + } + + /** + * This method is not published in the WORequest specification. + * This returns the context id from the URI. + */ + String contextID () + { + return contextID; + } + + /** + * This method is not published in the WORequest specification. + */ + String pageName () + { + return pageName; + } + + /** + * Convenience method to populate an NSArray from an Enumeration. + */ + private static NSArray arrayFromEnumeration( java.util.Enumeration e ) + { + NSMutableArray result = new NSMutableArray(); + while ( e.hasMoreElements() ) + { + result.addObject( e.nextElement() ); + } + return result; + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequestHandler.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequestHandler.java new file mode 100644 index 0000000..957cf25 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequestHandler.java @@ -0,0 +1,68 @@ +/* +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 net.wotonomy.foundation.internal.NetworkClassLoader; + +/** +* A pure java implementation that corresponds +* to the abstract class WORequestHandler. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public abstract class WORequestHandler +{ + private NetworkClassLoader loader; + + /** + * Default constructor. + */ + public WORequestHandler() + { + loader = new NetworkClassLoader( WOApplication.application().getClass().getClassLoader() ); + } + + /** + * Dispatches the request and returns the response. + */ + abstract public WOResponse handleRequest (WORequest aRequest); + + /** + * Gets the specified class, loading it if it has not already been + * loaded or if it has been modified. Returns null if not found. + * Because this method is not in the specification, it has only + * package access. + */ + Class loadClass( String aName ) + { + try + { + return loader.loadClass( aName, true ); + } + catch ( ClassNotFoundException cnfe ) + { + return null; + } + } + +// void lock () +// void unlock () +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResetButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResetButton.java new file mode 100644 index 0000000..8428681 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResetButton.java @@ -0,0 +1,50 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; + +/** +* Implements a reset button with dynamic bindings. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOResetButton extends WOInput { + + public WOResetButton() { + super(); + } + + public WOResetButton(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + protected String inputType() { + return "RESET"; + } + + protected Object value(WOContext c) { + Object v = valueForProperty("value", c.component()); + if (v == null) + return "Reset"; + return v; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceManager.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceManager.java new file mode 100644 index 0000000..c69c9db --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceManager.java @@ -0,0 +1,489 @@ +/* +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.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSKeyValueCoding; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.internal.PropertyListParser; + +/** +* Manages all resources vended by the application. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOResourceManager +{ + private NSMutableDictionary resourceCache; + private NSMutableDictionary dynamicDataCache; + private NSMutableDictionary stringTableCache; + private Map localeCache; // used for wo-style i18n + private Locale californiaLocale; // used for wo-style i18n + + /** + * Constructor is only accessible to subclasses. + */ + protected WOResourceManager() + { + resourceCache = new NSMutableDictionary(); + dynamicDataCache = new NSMutableDictionary(); + stringTableCache = new NSMutableDictionary(); + localeCache = new HashMap(); + californiaLocale = new Locale( "en", "US" ); + localeCache.put( "en", californiaLocale ); + } + + /** + * Returns the raw data corresponding to the specified resource. + * Any data retrieved by this method will be placed in the + * resource manager's global cache. + */ + public byte[] bytesForResourceNamed(String aFileName, + String aFrameworkName, + NSArray aLanguagesList) + { + String mash = aFileName + aFrameworkName; + if ( aLanguagesList != null ) + { + mash = mash + aLanguagesList.componentsJoinedByString(":"); + } + + byte[] result = (byte[]) resourceCache.objectForKey( mash ); + if ( result == null ) + { + InputStream input = inputStreamForResourceNamed( + aFileName, aFrameworkName, aLanguagesList ); + if ( input != null ) + { + try + { + int c; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + while ( ( c = input.read() ) != -1 ) + { + output.write( c ); + } + output.flush(); + input.close(); + output.close(); + result = output.toByteArray(); + synchronized ( resourceCache ) + { + resourceCache.setObjectForKey( result, mash ); + } + } + catch ( Throwable t ) + { + System.err.println( "WOResourceManager: Error reading bytes: " + aFileName ); + t.printStackTrace(); + } + } + } + return result; + } + + /** + * Returns the content type corresponding to the specified resource. + * This implementation recognizes gif, jpg, png, html, and xml extensions. + * Otherwise, "text/plain" is returned. + */ + public String contentTypeForResourceNamed(String aResourcePath) + { + if ( aResourcePath.endsWith( ".gif" ) ) return "image/gif"; + if ( aResourcePath.endsWith( ".jpg" ) ) return "image/jpeg"; + if ( aResourcePath.endsWith( ".png" ) ) return "image/png"; + if ( aResourcePath.endsWith( ".html" ) ) return "text/html"; + if ( aResourcePath.endsWith( ".xml" ) ) return "text/xml"; + return "text/plain"; + } + + /** + * Returns a url to be used when errors occur while retrieving a resource. + */ + public String errorMessageUrlForResourceNamed(String aResourceName, + String aFrameworkName) + { + if ( aResourceName == null ) aResourceName = "null"; + if ( aFrameworkName == null ) + { + return "/ERROR/NOT_FOUND/app=" + + WOApplication.application().name() + + "/filename=" + aResourceName; + } + else + { + return "/ERROR/NOT_FOUND/framework=" + + aFrameworkName + "/filename=" + aResourceName; + } + + } + + /** + * Clears all cached system-wide resource data. + */ + public void flushDataCache() + { + synchronized ( resourceCache ) + { + resourceCache.removeAllObjects(); + } + synchronized ( dynamicDataCache ) + { + dynamicDataCache.removeAllObjects(); + } + synchronized ( stringTableCache ) + { + stringTableCache.removeAllObjects(); + } + } + + /** + * Returns the file-system path for the specified resource. + * Deprecated and not implemented. + * @deprecated Use inputStreamForResourceNamed instead. + */ + public String pathForResourceNamed(String aResourceName, + String aFrameworkName, + NSArray aLanguagesList) + { + throw new RuntimeException( "ResourceManager.pathForResourceNamed: deprecated" ); + } + + /** + * Removes the data from the dynamic data cache for the specified session. + * If aSession is null, the data is removed from the application-wide + * data cache. + */ + public void removeDataForKey(String aKey, + WOSession aSession) + { + if ( aSession != null ) + { + if ( aSession.dynamicDataCache != null ) + { + aSession.dynamicDataCache.removeObjectForKey( aKey ); + } + } + else + { + synchronized ( dynamicDataCache ) + { + dynamicDataCache.removeObjectForKey( aKey ); + } + } + } + + /** + * Sets the data in the dynamic data cache for the specified session. + * If aSession is null, the data is placed in the application-wide + * data cache. If the key is a system-generated key, the data will + * be removed by calling removeData() the next time it is requested. + */ + public void setData(NSData someData, + String key, + String type, + WOSession aSession) + { + if ( aSession != null ) + { + if ( aSession.dynamicDataCache != null ) + { + aSession.dynamicDataCache.setObjectForKey( + new TypedData( type, someData ), key ); + } + } + else + { + synchronized ( dynamicDataCache ) + { + dynamicDataCache.setObjectForKey( + new TypedData( type, someData ), key ); + } + } + } + + /** + * Returns a localized string from a property list for + * a given key. If the key doesn't exist, aDefaultValue + * is returned. + */ + public String stringForKey(String aKey, + String aFileName, + String aDefaultValue, + String aFrameworkName, + NSArray aLanguagesList) + { + + String mash = aFileName + aFrameworkName; + if ( aLanguagesList != null ) + { + mash = mash + aLanguagesList.componentsJoinedByString(":"); + } + + Object table = stringTableCache.objectForKey( mash ); + if ( table == null ) + { + try + { + InputStream input = (InputStream) inputStreamForResourceNamed( + aFileName, aFrameworkName, aLanguagesList); + if ( input != null ) + { + Reader reader = + new BufferedReader(new InputStreamReader(input)); + table = PropertyListParser.propertyListFromReader( reader ); + synchronized ( stringTableCache ) + { + stringTableCache.setObjectForKey( table, mash ); + } + } + } + catch ( IOException ioe ) + { + System.err.println( "WOResourceManager: error reading: " + aFileName ); + ioe.printStackTrace(); + } + catch ( Throwable t ) + { + // could not parse + System.err.println( "WOResourceManager: could not parse: " + aFileName ); + System.err.println( t ); + } + } + + Object result = null; + if ( table != null ) + { + result = NSKeyValueCoding.DefaultImplementation.valueForKey( table, aKey ); + } + if ( result == null ) + { + result = aDefaultValue; + } + else + { + result = result.toString(); + } + + return (String) result; + } + + /** + * Returns a url that invokes the resource manager for the + * specified resource. + */ + public String urlForResourceNamed(String aResourceName, + String aFrameworkName, + NSArray aLanguagesList, + WORequest aRequest) + { + StringBuffer buffer = new StringBuffer(); + if ( aFrameworkName == null ) + { + aFrameworkName = "application"; + } + buffer.append( aRequest.applicationName() ); + buffer.append( '/' ); + buffer.append( WOApplication.resourceRequestHandlerKey() ); + if ( !aFrameworkName.startsWith("/") ) + { + buffer.append( '/' ); + } + buffer.append( aFrameworkName ); + buffer.append( '/' ); + buffer.append( aResourceName ); + return buffer.toString(); + } + + /** + * Returns an input for the raw resource. Data returned by + * this method will not be put in the resource manager's global cache. + */ + public InputStream inputStreamForResourceNamed(String aResourceName, + String aFrameworkName, + NSArray aLanguagesList) + { + if ( aResourceName == null ) return null; + InputStream result = null; + + StringBuffer path = new StringBuffer(); + path.append( '/' ); + if ( aFrameworkName != null ) + { + path.append( aFrameworkName ).append( '/' ); + } + + int i = aResourceName.lastIndexOf( "." ); + if ( i != -1 ) + path.append( aResourceName.substring( 0, i ) ); + else + path.append( aResourceName ); + + String location = path.toString(); + if ( aLanguagesList != null ) + { + String language; + Locale locale; + HashSet tried = new HashSet(5); + Enumeration e = aLanguagesList.objectEnumerator(); + while ( e.hasMoreElements() && result == null ) + { + language = e.nextElement().toString(); + + // look for java-style localization + if ( i != -1 ) + { + result = getStream( location + '_' + + language + aResourceName.substring( i ) ); + } + else // no dot extension + { + result = getStream( location + '_' + language ); + } + + // look for wo-style localization + if ( result == null ) + { + locale = (Locale) localeCache.get( language ); + if ( locale == null ) + { + if ( language.length() == 5 ) + { + locale = new Locale( + language.substring( 0, 2 ), + language.substring( 3, 5 ) ); + } + else + { + locale = new Locale( language, "" ); + } + synchronized ( localeCache ) + { + localeCache.put( language, locale ); + } + } + + language = '/'+locale.getDisplayLanguage( californiaLocale )+".lproj"; + if ( !tried.contains( language ) ) + { + if ( aFrameworkName != null ) + { + int j = aFrameworkName.length()+1; + path.insert( j, language ); + result = getStream( path.toString() + aResourceName.substring( i ) ); + path.delete( j, j+language.length() ); + } + else + { + result = getStream( language + path.toString() + aResourceName.substring( i ) ); + } + tried.add( language ); + } + } + } + } + + // look for file in package + if ( result == null ) + { + if ( i != -1 ) + { + result = getStream( path.append( + aResourceName.substring( i ) ).toString() ); + } + else // no dot extension + { + result = getStream( location ); + } + } + + return result; + } + + private static final InputStream getStream( String path ) + { //System.out.println( "getStream: " + path ); + InputStream input = + WOApplication.application().getClass().getResourceAsStream( path ); + if ( input == null ) + { + // in case the local class loader doesn't delegate to its parent + input = ClassLoader.getSystemResourceAsStream( path ); + } + return input; + } + + private static final class TypedData + { + String type; + NSData data; + + public TypedData( String aType, NSData aData ) + { + type = aType; + data = aData; + } + } +} + +/* + * $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.5 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.4 2003/02/28 22:58:57 mpowers + * Added support for wo-style localization (*.lproj). + * + * Revision 1.3 2003/02/21 16:40:24 mpowers + * Now reading port and smtp host from system properties. + * Implemented WOApplication.main. + * + * Revision 1.2 2003/01/28 19:33:52 mpowers + * Implemented the rest of WOResourceManager. + * Implemented support for java-style i18n. + * Components now use the resource manager to load templates. + * + * Revision 1.1 2003/01/27 15:08:00 mpowers + * Implemented WOResourceManager, using java resources for now. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceRequestHandler.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceRequestHandler.java new file mode 100644 index 0000000..4d25128 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceRequestHandler.java @@ -0,0 +1,122 @@ +/* +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.util.Enumeration; + +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* An implementation of WORequestHandler that +* retrieves resources from the deployed application. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOResourceRequestHandler + extends WORequestHandler +{ + private NSMutableDictionary resourceCache; + private WOResourceManager resourceManager; + + public WOResourceRequestHandler() + { + //TODO: should probably have some kind of limit on the cache + resourceCache = new NSMutableDictionary(); + resourceManager = WOApplication.application().resourceManager(); + } + + public WOResponse handleRequest( WORequest aRequest ) + { + WOResponse response = new WOResponse(); + + StringBuffer buf = new StringBuffer(); + + //TODO: this is just to get things working... + String framework = null; + Enumeration e = aRequest.requestHandlerPathArray().objectEnumerator(); + if ( e.hasMoreElements() ) + { + framework = e.nextElement().toString(); + if ( framework.equals( "application" ) ) + { + buf.append('/').append( framework ); + framework = null; + } + } + if ( e.hasMoreElements() ) + { + buf.append( e.nextElement() ); + } + while ( e.hasMoreElements() ) + { + buf.append('/').append( e.nextElement() ); + } + + String resource; + if ( buf.length() > 0 ) + { + resource = buf.toString(); + byte[] data = resourceManager.bytesForResourceNamed( + resource, framework, aRequest.browserLanguages() ); + if ( data != null ) + { + response.setHeader( + resourceManager.contentTypeForResourceNamed( resource ), + "Content-Type" ); + response.setContent( new NSData( data ) ); + return response; + } + } + response.setStatus( 404 ); // not found + return response; + } +} + +/* + * $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.6 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.5 2003/01/27 15:08:00 mpowers + * Implemented WOResourceManager, using java resources for now. + * + * Revision 1.4 2003/01/22 23:01:36 mpowers + * Better handling for request handler path. + * Better support for resources with absolute path. + * + * Revision 1.3 2003/01/20 16:18:22 mpowers + * Fixed class loading issues with resource retrieval. + * + * Revision 1.2 2003/01/17 20:58:20 mpowers + * Fixed up WOHyperlink. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceURL.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceURL.java new file mode 100644 index 0000000..862c6dc --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceURL.java @@ -0,0 +1,31 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; + +public class WOResourceURL extends WODynamicElement { + + public WOResourceURL() { + super(); + } + + public WOResourceURL(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + public void appendToResponse(WOResponse r, WOContext c) { + String fname = stringForProperty("filename", c.component()); + if (fname != null) { + String fwk = stringForProperty("framework", c.component()); + return; + } + NSData data = (NSData)valueForProperty("data", c.component()); + if (data != null) { + String mime = stringForProperty("mimeType", c.component()); + String key = stringForProperty("key", c.component()); + return; + } + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResponse.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResponse.java new file mode 100644 index 0000000..d9fbade --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResponse.java @@ -0,0 +1,184 @@ +/* +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.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +import net.wotonomy.foundation.NSArray; + +/** +* A pure java implementation of WOResponse. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOResponse extends WOMessage + implements WOActionResults +{ + protected static String defaultEncoding; + private static SimpleDateFormat htmlDateFormat; + static + { + defaultEncoding = "ISO8859_1"; + htmlDateFormat = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss z" ); + htmlDateFormat.setTimeZone( + TimeZone.getTimeZone( "GMT" ) ); + } + + private int status; + + /** + * Parameterless constructor which should not be called. + */ + public WOResponse () + { + status = -1; // -1 indicates not yet set + } + + /** + * Sets the status code of the response. + * You should use the constants defined in HttpServletResponse. + */ + public void setStatus (int code) + { + status = code; + } + + /** + * Gets the current status code for the response. + */ + public int status () + { + return status; + } + + /** + * Sets a header in the response to disable client caching. + * (Whether this works depends on the client implementation.) + */ + public void disableClientCaching () + { + String dateString = htmlDateFormat.format( new Date() ); + setHeader( dateString, "Date" ); + setHeader( dateString, "Expires" ); + setHeader( "no-cache", "Pragma" ); + setHeaders( new NSArray( new Object[] { + "private", "no-cache", "max-age = 0" } ), "Cache-Control" ); + //System.out.println( "disableClientCaching: " + dateString ); + } + + /** + * Returns this object. (Implements the WOActionResults interface.) + */ + public WOResponse generateResponse() + { + return this; + } + + /** + * Generates the response using the specified servlet response, + * but does not flush the buffer. The caller is responsible + * for sending the response to the client. <br><br> + * Note that this method is currently responsible for setting + * the content type to "text/html". As far as I can tell, WORequests + * have no way to set the content type and therefore must always + * be of type "text/html". + */ + void generateServletResponse + (javax.servlet.http.HttpServletResponse response) + { + if ( WOApplication.application().isPageRefreshOnBacktrackEnabled() ) + { + disableClientCaching(); + } + + String key; + java.util.Enumeration e, f; + + // set content type: might be overwritten by headers below + response.setContentType( "text/html" ); + + // set status + if ( status != -1 ) + { + response.setStatus( status ); + } + + // set headers + f = _headers.allKeys().objectEnumerator(); + while ( f.hasMoreElements() ) + { + key = f.nextElement().toString(); + e = ((NSArray)_headers.objectForKey( key )).objectEnumerator(); + if ( e.hasMoreElements() ) + { + // overwrite existing header + response.setHeader( key, e.nextElement().toString() ); + } + while ( e.hasMoreElements() ) + { + response.addHeader( key, e.nextElement().toString() ); + } + } + + // set cookies + e = _cookies.allValues().objectEnumerator(); + while ( e.hasMoreElements() ) + { + response.addCookie( (WOCookie) e.nextElement() ); + } + + try + { + // write content + response.getOutputStream().write(_contentData.bytes() ); + if ( status > 299 ) + { + response.sendError( status ); + } + } + catch ( Exception exc ) + { + throw new RuntimeException( + "Error writing response: " + exc ); + } + } + + + /** + * Returns the current default encoding seting. + */ + public static String defaultEncoding () + { + return defaultEncoding; + } + + /** + * Sets the default encoding setting. + */ + public static void setDefaultEncoding (String encoding) + { + defaultEncoding = encoding; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOServletSessionStore.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOServletSessionStore.java new file mode 100644 index 0000000..0f2898c --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOServletSessionStore.java @@ -0,0 +1,188 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2003 Intersect Software Corp. + +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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; + +import javax.servlet.http.HttpSession; + + +/** +* An implementation of WOSessionStore that stores WOSessions +* inside of HttpSessions, to take advantage of the servlet +* containers built-in distribution capabilities. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOServletSessionStore extends WOSessionStore +{ + private static final String ServletSessionKey = "WOServletSessionStoreKey"; + + /** + * This implementation currently does nothing and returns null, as we rely on the + * servlet container to dispose of the HttpSession which contains our WOSession. + */ + public WOSession removeSessionWithID(String sessionID) + { + return null; + } + + /** + * Returns the WOSession for the specified ID from the store. + * The sessionID parameter is ignored, and the session is removed from + * the store until saveSessionForContext is called. + */ + public WOSession restoreSessionWithID(String sessionID, WORequest aRequest) + { + WOSession result = null; + HttpSession servletSession = aRequest.servletRequest().getSession(); + if ( servletSession != null ) + { + try + { + result = (WOSession) servletSession.getAttribute( ServletSessionKey ); + if ( result != null ) + { + // if the servlet's class loader has changed, we need to reload + if ( result.getClass().getClassLoader() != + WOApplication.application().getClass().getClassLoader() ) + { + throw new ClassCastException( result.getClass().toString() ); + } + } + } + catch ( ClassCastException exc ) + { + // we're having an issue with the container's class loader: + // try serializing and deserializing to see if it is reloaded correctly + try + { + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ObjectOutputStream output = new ObjectOutputStream( bytes ); + Object o = servletSession.getAttribute( ServletSessionKey ); + output.writeObject( o ); + output.flush(); + output.close(); + + System.out.println( + "WOServletSessionStore: reloaded session with size: " + + bytes.toByteArray().length ); + + ObjectInputStream input = new CustomObjectInputStream( + new ByteArrayInputStream( bytes.toByteArray() ), + WOApplication.application().getClass().getClassLoader() ); + + o = input.readObject(); + input.close(); + + // try it again + result = (WOSession) o; + } + catch ( Exception e ) + { + e.printStackTrace(); + // give up: remove the attribute and allow a new session + servletSession.removeAttribute( ServletSessionKey ); + } + } + + if ( result != null ) + { + servletSession.removeAttribute( ServletSessionKey ); + } + } + //System.out.println( "restoreSessionWithID: " + sessionID + " : " + result ); + return result; + } + + /** + * Places the context's session into the store. + */ + public void saveSessionForContext(WOContext context) + { + //System.out.println( "saveSessionForContext: " + context + " : " + context.session() ); + context.request().servletRequest().getSession( true ).setAttribute( + ServletSessionKey, context.session() ); + } + + /** + * Needed to specify a class loader which may be different that the + * one used to load the ObjectInputStream class. + */ + private static class CustomObjectInputStream extends ObjectInputStream + { + ClassLoader loader; + + public CustomObjectInputStream( + InputStream anInputStream, ClassLoader aClassLoader ) + throws java.io.IOException, java.io.StreamCorruptedException + { + super( anInputStream ); + loader = aClassLoader; + } + + protected Class resolveClass(ObjectStreamClass v) + throws IOException, ClassNotFoundException + { + return loader.loadClass( v.getName() ); + } + } +} + +/* + * $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.7 2003/01/22 23:00:32 mpowers + * Now keeping the most recent page around. + * + * Revision 1.6 2003/01/21 17:53:16 mpowers + * Now handling reloading when wotonomy is on the system class path. + * + * Revision 1.5 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.3 2003/01/14 16:05:19 mpowers + * Removed extraneous printlns. + * + * Revision 1.2 2003/01/13 22:25:07 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.1 2003/01/07 20:48:24 mpowers + * Implemented WOSessionStore and WOServletSessionStore. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSession.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSession.java new file mode 100644 index 0000000..e187567 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSession.java @@ -0,0 +1,528 @@ +/* +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.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpSession; + +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.KeyValueCodingUtilities; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDate; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCodingAdditions; +import net.wotonomy.foundation.NSKeyValueCodingSupport; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* A pure java implementation of WOSession. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOSession implements Serializable, NSKeyValueCodingAdditions +{ + //NOTE: need to set this when deserialized and on creation + transient private HttpSession session; + + // the current context + transient private WOContext context; + + // the last requested page: an optimization + transient private WOComponent currentPage; + // the last requested page's context id + transient private String currentContextID; + + //FIXME: transient until ec's implement serializable + private transient EOEditingContext defaultEditingContext; + + private NSMutableDictionary state; + private NSMutableDictionary pages; + private NSMutableDictionary permanentPages; + private NSMutableArray stateStack; + private NSMutableArray pageStack; + private NSMutableArray permanentPageStack; + private boolean terminating; + + // used by WOResourceManager to cache dynamic resources + transient NSMutableDictionary dynamicDataCache; + + public static final String WOSessionDidTimeOutNotification + = "WOSessionDidTimeOutNotification"; + public static final String WOSessionDidRestoreNotification + = "WOSessionDidRestoreNotification"; + public static final String WOSessionDidCreateNotification + = "WOSessionDidCreateNotification"; + + /** + * Default constructor. This is called implicitly by + * subclasses in all cases. + */ + public WOSession () + { + session = null; + state = new NSMutableDictionary(); + pages = new NSMutableDictionary(); + permanentPages = new NSMutableDictionary(); + stateStack = NSMutableArray.mutableArrayBackedByList( new LinkedList() ); + pageStack = NSMutableArray.mutableArrayBackedByList( new LinkedList() ); + permanentPageStack = NSMutableArray.mutableArrayBackedByList( new LinkedList() ); + defaultEditingContext = null; + terminating = false; + } + + /** + * Package method to initialize the backing session. + */ + void setServletSession( HttpSession aSession ) + { + session = aSession; + } + + /** + * Package method to set the current context. + */ + void setContext( WOContext aContext ) + { + context = aContext; + } + + /** + * Returns the id of the current session. If no session + * currently exists, return null. + */ + public String sessionID () + { + if ( session != null ) + { + return session.getId(); + } + return null; + } + + /** + * Sets whether distribution is currently enabled. + * This method is not implemented by this implementation + * as the servlet container manages distribution. + */ + public void setDistributionEnabled (boolean enabled) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether the session is part of a distributed application. + * This implementation always returns false. + */ + public boolean isDistributionEnabled () + { + return false; + } + + /** + * Sets whether session ids should be stored in cookies. + * This method is not implemented in this implementation + * as the servlet container manages sessions with cookies. + */ + public void setStoresIDsInCookies (boolean cookies) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether session ids are currently stored in cookies. + * This implementation always returns true. + */ + public boolean storesIDsInCookies () + { + return true; + } + + /** + * Returns the current expiration date for cookies that store session ids. + */ + public NSDate expirationDateForIDCookies () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets whether session ids should be stored in urls. + * This method is not implemented in this implementation + * as the servlet container manages sessions with cookies. + */ + public void setStoresIDsInURLs (boolean urls) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether session ids are currently stored in urls. + * This implementation always returns false. + */ + public boolean storesIDsInURLs () + { + return false; + } + + /** + * Returns the current domain for cookies containing session ids. + */ + public String domainForIDCookies () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Terminates this session after the completion of the current response. + */ + public void terminate () + { + terminating = true; + session.invalidate(); + } + + /** + * Returns whether the current session will terminate at the completion + * of the current response. + */ + public boolean isTerminating () + { + return terminating; + } + + /** + * Sets the number of seconds after the last request before + * the session should be terminated. + */ + public void setTimeOut (double timeout) + { + session.setMaxInactiveInterval( (int) timeout ); + } + + /** + * Returns the number of seconds after the last request before + * the session should be terminated. + */ + public double timeOut () + { + return session.getMaxInactiveInterval(); + } + + /** + * Sets the languages for which this session has been localized, + * in order of preference. The application will be responsible for + * localizing the content based on the languages found in this array. + */ + public void setLanguages (NSArray anArray) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns the languages for which this session has been localized, + * in order of preference. The application will be responsible for + * localizing the content based on the languages found in this array. + */ + public NSArray languages () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Stores the specified key-value pair in the session. + */ + public void setObjectForKey (Object anObject, String aKey) + { + state.setObjectForKey( anObject, aKey ); + } + + /** + * Returns the session value associated with the specified key. + */ + public Object objectForKey (String aKey) + { + return state.objectForKey( aKey ); + } + + /** + * Removes the session value associated with the specified key. + */ + public void removeObjectForKey (String aKey) + { + state.removeObjectForKey( aKey ); + } + + /** + * Returns the context for the current request. + */ + public WOContext context () + { + return context; + } + + /** + * Invoked at the beginning of the request-response cycle. + * Override to perform any kind of initialization at the + * start of a request. This implementation does nothing. + */ + public void awake () + { + + } + + /** + * Invoked by the Application to extract user-assigned balues + * and assign them to attributes. This implementation calls + * takeValuesFromRequest on the top-level component. + */ + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + context().component().takeValuesFromRequest( aRequest, aContext ); + } + + /** + * Invoked by the Application to determine which component is the + * intended recipient of the user's action. This implementation calls + * invokeAction on the top-level component. + */ + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + return context().component().invokeAction( aRequest, aContext ); + } + + /** + * Invoked by the Application to generate the content of the response. + * This implementation calls appendToResponse on the top-level component. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + context().component().appendToResponse( aResponse, aContext ); + } + + /** + * Invoked at the end of the request-response cycle. + * Override to perform any kind of clean-up at the + * end of a request. This implementation does nothing. + */ + public void sleep () + { + + } + + /** + * Returns a list of pages accessed by this session in order + * of their access and named by calling WOComponent.descriptionForResponse. + */ + public NSArray statistics () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Puts this page in the session's page cache using the current + * context id as the key. + */ + public void savePage (WOComponent aComponent) + { + currentPage = aComponent; + currentContextID = context.contextID(); + + if ( pages.objectForKey( currentContextID ) == null ) + { + byte[] data = KeyValueCodingUtilities.freeze( + aComponent, defaultEditingContext(), aComponent, true ); + System.out.println( "WOSession.savePage: " + currentContextID + " : " + data.length ); + + pages.setObjectForKey( data, currentContextID ); + pageStack.addObject( currentContextID ); + if ( pageStack.count() > context().application().pageCacheSize() ) + { + String id = pageStack.remove( 0 ).toString(); // removeObjectAtIndex + System.out.println( "WOSession.savePage: removing from cache: " + id ); + pages.removeObjectForKey( id ); + } + } + //System.out.println( "savePage: " + this + " : " + id + " : " + pages ); + } + + /** + * Returns the page in the session's page cache corresponding to + * the specified context id. Any special permanent caches are + * searched before the standard page cache. + */ + public WOComponent restorePageForContextID (String anID) + { + if ( anID == null ) return null; + if ( anID.equals( currentContextID ) ) return currentPage; + + WOComponent result = null; + byte[] data = (byte[]) permanentPages.objectForKey( anID ); + if ( data == null ) data = (byte[]) pages.objectForKey( anID ); + if ( data != null ) + { + System.out.println( "WOSession.restorePageForContextID: " + anID + " : " + data.length ); + result = (WOComponent) KeyValueCodingUtilities.thaw( + data, defaultEditingContext(), + WOApplication.application().getClass().getClassLoader(), true ); + } + //System.out.println( "restorePageForContextID: " + this + " : " + anID + " : " + result + " : " + pages ); + return result; + } + + /** + * Puts this page in the special cache is will not get automatically + * flushed like the session page cache. Use this if the page + * is likely to be around for a while, specifically pages within + * frames. + */ + public void savePageInPermanentCache (WOComponent aComponent) + { + currentPage = aComponent; + currentContextID = context.contextID(); + + if ( permanentPages.objectForKey( currentContextID ) == null ) + { + byte[] data = KeyValueCodingUtilities.freeze( + aComponent, defaultEditingContext(), aComponent, true ); + //System.out.println( "WOSession.savePageInPermanentCache: " + // + currentContextID + " : " + data.length ); + + permanentPages.setObjectForKey( data, currentContextID ); + permanentPageStack.addObject( currentContextID ); + if ( permanentPageStack.count() > context().application().pageCacheSize() ) + { + String id = permanentPageStack.remove( 0 ).toString(); // removeObjectAtIndex + permanentPages.removeObjectForKey( id ); + } + } + } + + /** + * Writes a message to the standard error stream. + */ + public static void logString (String aString) + { + System.err.println( aString ); + } + + /** + * Writes a message to the standard error stream + * if debugging is activated. + */ + public static void debugString (String aString) + { + // TODO: Check to see if debugging is enabled. + System.err.println( aString ); + } + + /** + * Returns the default editing context used by this session. + * Defaults to null. + */ + public EOEditingContext defaultEditingContext () + { + return defaultEditingContext; + } + + /** + * Sets the default editing context used by this session. + */ + public void setDefaultEditingContext (EOEditingContext aContext) + { + defaultEditingContext = aContext; + } + + // interface NSKeyValueCodingAdditions + + 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 ); + Object result = objectForKey( aKey ); + if ( result == null ) + result = NSKeyValueCodingSupport.valueForKey( this, aKey ); + return result; + } + + public void takeValueForKey (Object aValue, String aKey) + { // System.out.println( "takeValueForKey: " + aKey + " : " + aValue + "->" + this ); + setObjectForKey( aValue, aKey ); + } + + public Object storedValueForKey (String aKey) + { + Object result = objectForKey( aKey ); + if ( result == null ) + NSKeyValueCodingSupport.storedValueForKey( this, aKey ); + return result; + } + + public void takeStoredValueForKey (Object aValue, String aKey) + { + setObjectForKey( aValue, aKey ); + } + + public Object handleQueryWithUnboundKey (String aKey) + { + return NSKeyValueCodingSupport.handleQueryWithUnboundKey( this, aKey ); + } + + public void handleTakeValueForUnboundKey (Object aValue, String aKey) + { + NSKeyValueCodingSupport.handleTakeValueForUnboundKey( this, aValue, aKey ); + } + + public void unableToSetNullForKey (String aKey) + { + NSKeyValueCodingSupport.unableToSetNullForKey( this, aKey ); + } + + public Object validateTakeValueForKeyPath (Object aValue, String aKey) + { + throw new RuntimeException( "Not implemented yet." ); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSessionStore.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSessionStore.java new file mode 100644 index 0000000..f91a433 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSessionStore.java @@ -0,0 +1,93 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2003 Intersect Software Corp. + +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; + +/** +* An abstract class defining the requirements for persisting +* session state across user transactions. Used by WOApplication +* to persist sessions between requests. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public abstract class WOSessionStore +{ + private static WOSessionStore serverSessionStore = null; + + /** + * Returns the default session store used by WOApplication. + */ + public static WOSessionStore serverSessionStore() + { + if ( serverSessionStore == null ) + { + serverSessionStore = new WOServletSessionStore(); + } + return serverSessionStore; + } + + /** + * Called by WOApplication after the request-response cycle has ended. + * The context's session will again be available for subsequent requests. + */ + public final void checkInSessionForContext(WOContext aContext) + { + saveSessionForContext( aContext ); + } + + /** + * Returns the session with the specified id for the specified request, + * or null if none exist. Subsequent calls for the same id will return + * null until the session is checked in again. + * Called by WOApplication before the request-response cycle starts. + */ + public final WOSession checkOutSessionWithID(String sessionID, WORequest aRequest) + { + return restoreSessionWithID( sessionID, aRequest ); + } + + /** + * Removes the WOSession for the specified ID from the store and returns it. + */ + public abstract WOSession removeSessionWithID(String sessionID); + + /** + * Returns the WOSession for the specified ID from the store. + */ + public abstract WOSession restoreSessionWithID(String sessionID, + WORequest aRequest); + + /** + * Places the context's session into the store. + */ + public abstract void saveSessionForContext(WOContext context); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/01/07 20:48:29 mpowers + * Implemented WOSessionStore and WOServletSessionStore. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOStaticElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOStaticElement.java new file mode 100644 index 0000000..308cd20 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOStaticElement.java @@ -0,0 +1,70 @@ +/* +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; + +/** +* This class represents a static portion of a web page. +* Package access only, as it is not in the specification. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +class WOStaticElement extends WOElement +{ + String content; + + /** + * Default constructor. + */ + public WOStaticElement() + { + content = null; + } + + /** + * Returns a static element representing the specified content. + */ + public WOStaticElement( String aContentString ) + { + this(); + content = aContentString; + } + + /** + * Overridden to append the content string.. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + aResponse.appendContentString( content ); + } + + + public WOResponse generateResponse() + { + WOResponse r = new WOResponse(); + if (content != null) + { + r.appendContentString(content); + } + return r; + } + + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOString.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOString.java new file mode 100644 index 0000000..ef5b771 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOString.java @@ -0,0 +1,136 @@ +/* +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.text.Format; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSNumberFormatter; +import net.wotonomy.foundation.NSTimestampFormatter; + +/** +* WOString renders a dynamically generated string in the output. +* Bindings are: +* <ul> +* <li>value: a property returning a value which will be rendered as the +* output. If a formatter is not used, then the value must be convertable +* to a String with toString().</li> +* <li>escapeHTML: a property returning a value convertable to a Boolean +* indicating whether the any html characters in the output should be +* escaped so they are shown as html characters rather than interpreted +* as html.</li> +* <li>formatter: a property returning a Format object that will be +* used to format the value into a String.</li> +* <li>dateformat: a property returning a DateFormat object that will be +* used to format the value into a String.</li> +* <li>numberformat: a property returning a NumberFormat object that will be +* used to format the value into a String. Not yet implemented.</li> +* <li>valueWhenEmpty: a property returning a String that will be used +* in place of an empty or null value. Not yet implemented.</li> +* </ul> +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOString extends WODynamicElement +{ + protected Object value; + protected boolean escapeHTML; + protected Format formatter; + protected String dateformat; + protected String numberformat; + protected Object valueWhenEmpty; + + /** + * The default constructor. + */ + protected WOString () + { + } + + public WOString ( + String aName, NSDictionary anAssociationMap, WOElement aRootElement) + { + super( aName, anAssociationMap, aRootElement ); + escapeHTML = true; + } + + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + WOComponent c = aContext.component(); + numberformat = stringForProperty("numberformat", c ); + dateformat = stringForProperty("dateformat", c ); + formatter = (Format) valueForProperty("formatter", c ); + escapeHTML = booleanForProperty("escapeHTML", c ); + value = valueForProperty("value", c ); + valueWhenEmpty = valueForProperty("valueWhenEmpty", c ); + + Object result = value; + if ( result != null ) + { + if ( formatter != null ) + { + try + { + result = formatter.format( result ); + } + catch ( IllegalArgumentException exc ) + { + } + } + if ( dateformat != null ) + { + try + { + result = new NSTimestampFormatter( dateformat ).format( result ); + } + catch ( IllegalArgumentException exc ) + { + } + } + if ( numberformat != null ) + { + try + { + result = new NSNumberFormatter( numberformat ).format( result ); + } + catch ( IllegalArgumentException exc ) + { + } + } + } + if ( result == null ) + { + result = valueWhenEmpty; + if ( result == null ) + { + result = "nil"; + } + } + if ( escapeHTML ) + { + aResponse.appendContentHTMLString( result.toString() ); + } + else + { + aResponse.appendContentString( result.toString() ); + } + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSubmitButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSubmitButton.java new file mode 100644 index 0000000..05824f3 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSubmitButton.java @@ -0,0 +1,70 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +/** +* Implements a submit button with dynamic bindings. +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOSubmitButton extends WOInput { + + public WOSubmitButton() { + super(); + } + + public WOSubmitButton(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + protected String inputType() { + return "SUBMIT"; + } + + protected Object value(WOContext c) { + Object v = valueForProperty("value", c.component()); + if (v == null) { + return "Submit"; + } + return v; + } + + protected NSMutableArray additionalAttributes() { + NSMutableArray a = super.additionalAttributes(); + a.add("action"); + return a; + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + if (disabled(c)) + return null; + //It's useless to check the senderID here because it's going to be for the form always. + //So we check if the formValues contain the button's name (which means it was pressed) + if (r.formValueForKey(inputName(c)) != null) { + if (associations.objectForKey("action") != null) + return (WOActionResults)valueForProperty("action", c.component()); + } + return null; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSwitchComponent.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSwitchComponent.java new file mode 100644 index 0000000..d2e373a --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSwitchComponent.java @@ -0,0 +1,112 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + +public class WOSwitchComponent extends WODynamicElement { + + private NSMutableDictionary elements; + private String currentName; + private WOElement currentElement; + + protected WOSwitchComponent() + { + super(); + elements = new NSMutableDictionary(); + } + + public WOSwitchComponent(String aName, NSDictionary aMap, WOElement template) + { + super(aName, aMap, template); + elements = new NSMutableDictionary(); + } + + private WOElement getCurrentElement( WOContext c ) + { + String name = stringForProperty( "WOComponentName", c.component() ); + if ( name == null ) return null; + + if ( currentElement != null && name.equals( currentName ) ) return currentElement; + + currentName = name; + currentElement = (WOElement) elements.objectForKey( name ); + if ( currentElement == null ) + { + currentElement = WOApplication.application().pageWithName( name, c ); + if ( currentElement != null ) + { + currentElement.associations = associations; + elements.setObjectForKey( currentElement, name ); + } + } + + return currentElement; + } + + void ensureAwakeInContext (WOContext aContext) + { + if ( aContext != null ) + { + WOElement element = getCurrentElement( aContext ); + if ( element != null ) + { + aContext.pushElement( element ); + element.ensureAwakeInContext( aContext ); + aContext.popElement(); + } + } + } + + public void takeValuesFromRequest(WORequest r, WOContext c) + { + WOElement element = getCurrentElement( c ); + if ( element != null ) + { + c.pushElement( element ); + element.takeValuesFromRequest( r, c ); + c.popElement(); + } + } + + public WOActionResults invokeAction(WORequest r, WOContext c) + { + WOActionResults result = null; + WOElement element = getCurrentElement( c ); + if ( element != null ) + { + c.pushElement( element ); + result = element.invokeAction( r, c ); + c.popElement(); + } + return result; + } + + public void appendToResponse(WOResponse r, WOContext c) + { + WOElement element = getCurrentElement( c ); + if ( element != null ) + { + c.pushElement( element ); + element.appendToResponse( r, c ); + c.popElement(); + } + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOText.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOText.java new file mode 100644 index 0000000..008fda8 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOText.java @@ -0,0 +1,72 @@ +/* + 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 net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* Implements a TEXTAREA element, with dynamic bindings. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOText extends WOInput { + + public WOText() { + super(); + } + + public WOText(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + protected String inputType() { + return "TEXTAREA"; + } + + protected Object value(WOContext c) { + Object fieldValue = valueForProperty("value", c.component()); + if (fieldValue == null) { + fieldValue = ""; + } + return formattedValue(fieldValue, c.component()); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + Object val = r.formValueForKey(inputName(c)); + if ( val != null ) + setValueForProperty("value", formattedValue(val, c.component()), c.component()); + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString("<TEXTAREA NAME=\""); + r.appendContentString(inputName(c)); + r.appendContentString("\""); + String moreFields = additionalHTMLProperties(c.component(), new NSArray(new Object[]{ + "name", "value" })); + if (moreFields != null && moreFields.length() > 0) + r.appendContentString(moreFields); + r.appendContentString(">"); + moreFields = value(c).toString(); + if (moreFields != null && moreFields.length() > 0) + r.appendContentString(moreFields); + r.appendContentString("</TEXTAREA>"); + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOTextField.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOTextField.java new file mode 100644 index 0000000..4233ad4 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOTextField.java @@ -0,0 +1,59 @@ +/* + 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 net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +/** +* Implements an INPUT tag of type TEXT, with dynamic bindings. +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOTextField extends WOInput { + + public WOTextField() { + super(); + } + + public WOTextField(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "TEXT"; + } + + protected Object value(WOContext c) { + Object fieldValue = valueForProperty("value", c.component()); + if (fieldValue == null) { + fieldValue = ""; + } + return formattedValue(fieldValue, c.component()); + } + + protected NSMutableArray additionalAttributes() { + NSMutableArray a = super.additionalAttributes(); + a.addObject("dateformat"); + a.addObject("numberformat"); + return a; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/package.html b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/package.html new file mode 100644 index 0000000..10bef50 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/package.html @@ -0,0 +1,11 @@ +<body> +<p> +An implementation of the WebObjects +framework. It tries to be API-compatible to the +maximum extent possible. +</p> +<p> +This package has dependencies on the foundation +and util packages. +</p> +</body> diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/util/BrowserLauncher.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/util/BrowserLauncher.java new file mode 100644 index 0000000..777d4a1 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/util/BrowserLauncher.java @@ -0,0 +1,665 @@ +//package edu.stanford.ejalbert; +package net.wotonomy.web.util; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** <p> BrowserLauncher is a class that provides one static method, + * openURL, which opens the default web browser for the current user + * of the system to the given URL. It may support other protocols + * depending on the system -- mailto, ftp, etc. -- but that has not + * been rigorously tested and is not guaranteed to work. </p> + * + * <p> Yes, this is platform-specific code, and yes, it may rely on + * classes on certain platforms that are not part of the standard JDK. + * What we're trying to do, though, is to take something that's + * frequently desirable but inherently platform-specific -- opening a + * default browser -- and allow programmers (you, for example) to do + * so without worrying about dropping into native code or doing + * anything else similarly evil. </p> + * + * <p> Anyway, this code is completely in Java and will run on all JDK + * 1.1-compliant systems without modification or a need for additional + * libraries. All classes that are required on certain platforms to + * allow this to run are dynamically loaded at runtime via reflection + * and, if not found, will not cause this to do anything other than + * returning an error when opening the browser. </p> + * + * <p> There are certain system requirements for this class, as it's + * running through Runtime.exec(), which is Java's way of making a + * native system call. Currently, this requires that a Macintosh have + * a Finder which supports the GURL event, which is true for Mac OS + * 8.0 and 8.1 systems that have the Internet Scripting AppleScript + * dictionary installed in the Scripting Additions folder in the + * Extensions folder (which is installed by default as far as I know + * under Mac OS 8.0 and 8.1), and for all Mac OS 8.5 and later + * systems. On Windows, it only runs under Win32 systems (Windows 95, + * 98, and NT 4.0, as well as later versions of all). On other + * systems, this drops back from the inherently platform-sensitive + * concept of a default browser and simply attempts to launch Netscape + * via a shell command. </p> + * + * <p> This code is Copyright 1999-2001 by Eric Albert + * (ejalbert@cs.stanford.edu) and may be redistributed or modified in + * any form without restrictions as long as the portion of this + * comment from this paragraph through the end of the comment is not + * removed. The author requests that he be notified of any + * application, applet, or other binary that makes use of this code, + * but that's more out of curiosity than anything and is not required. + * This software includes no warranty. The author is not repsonsible + * for any loss of data or functionality or any adverse or unexpected + * effects of using this software. </p> + * + * Credits: <br> Steven Spencer, JavaWorld magazine (<a + * href="http://www.javaworld.com/javaworld/javatips/jw-javatip66.html">Java + * Tip 66</a>) <br> Thanks also to Ron B. Yeh, Eric Shapiro, Ben + * Engber, Paul Teitlebaum, Andrea Cantatore, Larry Barowski, Trevor + * Bedzek, Frank Miedrich, and Ron Rabakukk + * + * + * original author Eric Albert (<a + * href="mailto:ejalbert@cs.stanford.edu">ejalbert@cs.stanford.edu</a>) + * author's version 1.4b1 (Released June 20, 2001) + * @author Copyright 2001, Intersect Software Corporation + * @version $Revision: 905 $ $Date: 2006-02-18 20:44:03 -0500 (Sat, 18 Feb 2006) $ + */ + +public class BrowserLauncher { + /** The Java virtual machine that we are running on. Actually, in + * most cases we only care about the operating system, but some + * operating systems require us to switch on the VM. */ + private static int jvm; + + /** The browser for the system */ + private static Object browser; + + /** Caches whether any classes, methods, and fields that are not + * part of the JDK and need to be dynamically loaded at runtime + * loaded successfully. <p> Note that if this is + * <code>false</code>, <code>openURL()</code> will always return + * an IOException. */ + private static boolean loadedWithoutErrors; + + /** The com.apple.mrj.MRJFileUtils class */ + private static Class mrjFileUtilsClass; + + /** The com.apple.mrj.MRJOSType class */ + private static Class mrjOSTypeClass; + + /** The com.apple.MacOS.AEDesc class */ + private static Class aeDescClass; + + /** The <init>(int) method of com.apple.MacOS.AETarget */ + private static Constructor aeTargetConstructor; + + /** The <init>(int, int, int) method of com.apple.MacOS.AppleEvent */ + private static Constructor appleEventConstructor; + + /** The <init>(String) method of com.apple.MacOS.AEDesc */ + private static Constructor aeDescConstructor; + + /** The findFolder method of com.apple.mrj.MRJFileUtils */ + private static Method findFolder; + + /** The getFileCreator method of com.apple.mrj.MRJFileUtils */ + private static Method getFileCreator; + + /** The getFileType method of com.apple.mrj.MRJFileUtils */ + private static Method getFileType; + + /** The openURL method of com.apple.mrj.MRJFileUtils */ + private static Method openURL; + + /** The makeOSType method of com.apple.MacOS.OSUtils */ + private static Method makeOSType; + + /** The putParameter method of com.apple.MacOS.AppleEvent */ + private static Method putParameter; + + /** The sendNoReply method of com.apple.MacOS.AppleEvent */ + private static Method sendNoReply; + + /** Actually an MRJOSType pointing to the System Folder on a Macintosh */ + private static Object kSystemFolderType; + + /** The keyDirectObject AppleEvent parameter type */ + private static Integer keyDirectObject; + + /** The kAutoGenerateReturnID AppleEvent code */ + private static Integer kAutoGenerateReturnID; + + /** The kAnyTransactionID AppleEvent code */ + private static Integer kAnyTransactionID; + + /** The linkage object required for JDirect 3 on Mac OS X. */ + private static Object linkage; + + /** The framework to reference on Mac OS X */ + private static final String JDirect_MacOSX = "/System/Library/Frameworks/Carbon.framework/Frameworks/HIToolbox.framework/HIToolbox"; + + /** JVM constant for MRJ 2.0 */ + private static final int MRJ_2_0 = 0; + + /** JVM constant for MRJ 2.1 or later */ + private static final int MRJ_2_1 = 1; + + /** JVM constant for Java on Mac OS X 10.0 (MRJ 3.0) */ + private static final int MRJ_3_0 = 3; + + /** JVM constant for MRJ 3.1 */ + private static final int MRJ_3_1 = 4; + + /** JVM constant for any Windows NT JVM */ + private static final int WINDOWS_NT = 5; + + /** JVM constant for any Windows 9x JVM */ + private static final int WINDOWS_9x = 6; + + /** JVM constant for any other platform */ + private static final int OTHER = -1; + + /** The file type of the Finder on a Macintosh. Hardcoding + * "Finder" would keep non-U.S. English systems from working + * properly. */ + private static final String FINDER_TYPE = "FNDR"; + + /** The creator code of the Finder on a Macintosh, which is needed + * to send AppleEvents to the application. */ + private static final String FINDER_CREATOR = "MACS"; + + /** The name for the AppleEvent type corresponding to a GetURL event. */ + private static final String GURL_EVENT = "GURL"; + + /** The first parameter that needs to be passed into + * Runtime.exec() to open the default web browser on Windows. */ + private static final String FIRST_WINDOWS_PARAMETER = "/c"; + + /** The second parameter for Runtime.exec() on Windows. */ + private static final String SECOND_WINDOWS_PARAMETER = "start"; + + /** The third parameter for Runtime.exec() on Windows. This is a + * "title" parameter that the command line expects. Setting this + * parameter allows URLs containing spaces to work. */ + private static final String THIRD_WINDOWS_PARAMETER = "\"\""; + + /** The shell parameters for Netscape that opens a given URL in an + * already-open copy of Netscape on many command-line systems. */ + private static final String NETSCAPE_REMOTE_PARAMETER = "-remote"; + //private static final String NETSCAPE_OPEN_PARAMETER_START = "'openURL("; + //private static final String NETSCAPE_OPEN_PARAMETER_END = ")'"; + private static final String NETSCAPE_OPEN_PARAMETER_START = "openURL("; + private static final String NETSCAPE_OPEN_PARAMETER_END = ")"; + + /** The message from any exception thrown throughout the + * initialization process. */ + private static String errorMessage; + + /** An initialization block that determines the operating system + * and loads the necessary runtime data. */ + static { + loadedWithoutErrors = true; + String osName = System.getProperty("os.name"); + if (osName.startsWith("Mac OS")) { + String mrjVersion = System.getProperty("mrj.version"); + String majorMRJVersion = mrjVersion.substring(0, 3); + try { + double version = Double.valueOf(majorMRJVersion).doubleValue(); + if (version == 2) { + jvm = MRJ_2_0; + } else if (version >= 2.1 && version < 3) { + // Assume that all 2.x versions of MRJ work the + // same. MRJ 2.1 actually works via + // Runtime.exec() and 2.2 supports that but has an + // openURL() method as well that we currently + // ignore. + jvm = MRJ_2_1; + } else if (version == 3.0) { + jvm = MRJ_3_0; + } else if (version >= 3.1) { + // Assume that all 3.1 and later versions of MRJ + // work the same. + jvm = MRJ_3_1; + } else { + loadedWithoutErrors = false; + errorMessage = "Unsupported MRJ version: " + version; + } + } catch (NumberFormatException nfe) { + loadedWithoutErrors = false; + errorMessage = "Invalid MRJ version: " + mrjVersion; + } + } else if (osName.startsWith("Windows")) { + if (osName.indexOf("9") != -1) { + jvm = WINDOWS_9x; + } else { + jvm = WINDOWS_NT; + } + } else { + jvm = OTHER; + } + + if (loadedWithoutErrors) { // if we haven't hit any errors yet + loadedWithoutErrors = loadClasses(); + } + } + + /** This class should be never be instantiated; this just ensures so. */ + private BrowserLauncher() { } + + /** Called by a static initializer to load any classes, fields, + * and methods required at runtime to locate the user's web + * browser. + * @return <code>true</code> if all intialization succeeded + * <code>false</code> if any portion of the initialization failed */ + private static boolean loadClasses() { + switch (jvm) { + case MRJ_2_0: + try { + Class aeTargetClass = Class.forName("com.apple.MacOS.AETarget"); + Class osUtilsClass = Class.forName("com.apple.MacOS.OSUtils"); + Class appleEventClass = Class.forName + ("com.apple.MacOS.AppleEvent"); + Class aeClass = Class.forName("com.apple.MacOS.ae"); + aeDescClass = Class.forName("com.apple.MacOS.AEDesc"); + + aeTargetConstructor = aeTargetClass.getDeclaredConstructor + (new Class [] { int.class }); + appleEventConstructor = appleEventClass.getDeclaredConstructor + (new Class[] { int.class, int.class, aeTargetClass, + int.class, int.class }); + aeDescConstructor = aeDescClass.getDeclaredConstructor + (new Class[] { String.class }); + + makeOSType = osUtilsClass.getDeclaredMethod + ("makeOSType", new Class [] { String.class }); + putParameter = appleEventClass.getDeclaredMethod + ("putParameter", new Class[] { int.class, aeDescClass }); + sendNoReply = appleEventClass.getDeclaredMethod + ("sendNoReply", new Class[] { }); + + Field keyDirectObjectField = aeClass.getDeclaredField + ("keyDirectObject"); + keyDirectObject = (Integer) keyDirectObjectField.get(null); + Field autoGenerateReturnIDField = appleEventClass + .getDeclaredField("kAutoGenerateReturnID"); + kAutoGenerateReturnID = (Integer) autoGenerateReturnIDField + .get(null); + Field anyTransactionIDField = appleEventClass.getDeclaredField + ("kAnyTransactionID"); + kAnyTransactionID = (Integer) anyTransactionIDField.get(null); + } catch (ClassNotFoundException cnfe) { + errorMessage = cnfe.getMessage(); + return false; + } catch (NoSuchMethodException nsme) { + errorMessage = nsme.getMessage(); + return false; + } catch (NoSuchFieldException nsfe) { + errorMessage = nsfe.getMessage(); + return false; + } catch (IllegalAccessException iae) { + errorMessage = iae.getMessage(); + return false; + } + break; + case MRJ_2_1: + try { + mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils"); + mrjOSTypeClass = Class.forName("com.apple.mrj.MRJOSType"); + Field systemFolderField = mrjFileUtilsClass.getDeclaredField + ("kSystemFolderType"); + kSystemFolderType = systemFolderField.get(null); + findFolder = mrjFileUtilsClass.getDeclaredMethod + ("findFolder", new Class[] { mrjOSTypeClass }); + getFileCreator = mrjFileUtilsClass.getDeclaredMethod + ("getFileCreator", new Class[] { File.class }); + getFileType = mrjFileUtilsClass.getDeclaredMethod + ("getFileType", new Class[] { File.class }); + } catch (ClassNotFoundException cnfe) { + errorMessage = cnfe.getMessage(); + return false; + } catch (NoSuchFieldException nsfe) { + errorMessage = nsfe.getMessage(); + return false; + } catch (NoSuchMethodException nsme) { + errorMessage = nsme.getMessage(); + return false; + } catch (SecurityException se) { + errorMessage = se.getMessage(); + return false; + } catch (IllegalAccessException iae) { + errorMessage = iae.getMessage(); + return false; + } + break; + case MRJ_3_0: + try { + Class linker = Class.forName("com.apple.mrj.jdirect.Linker"); + Constructor constructor = linker.getConstructor + (new Class[]{ Class.class }); + linkage = constructor.newInstance(new Object[] + { BrowserLauncher.class }); + } catch (ClassNotFoundException cnfe) { + errorMessage = cnfe.getMessage(); + return false; + } catch (NoSuchMethodException nsme) { + errorMessage = nsme.getMessage(); + return false; + } catch (InvocationTargetException ite) { + errorMessage = ite.getMessage(); + return false; + } catch (InstantiationException ie) { + errorMessage = ie.getMessage(); + return false; + } catch (IllegalAccessException iae) { + errorMessage = iae.getMessage(); + return false; + } + break; + case MRJ_3_1: + try { + mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils"); + openURL = mrjFileUtilsClass.getDeclaredMethod + ("openURL", new Class[] { String.class }); + } catch (ClassNotFoundException cnfe) { + errorMessage = cnfe.getMessage(); + return false; + } catch (NoSuchMethodException nsme) { + errorMessage = nsme.getMessage(); + return false; + } + break; + default: + break; + } + return true; + } + + /** Attempts to locate the default web browser on the local + * system. Caches results so it only locates the browser once * + * for each use of this class per JVM instance. + * @return The browser for the system. Note that this may not be + * what you would consider to be a standard web browser; instead, + * it's the application that gets called to open the default web + * browser. In some cases, this will be a non-String object that + * provides the means of calling the default browser. */ + private static Object locateBrowser() { + if (browser != null) { + return browser; + } + switch (jvm) { + case MRJ_2_0: + try { + Integer finderCreatorCode = (Integer) makeOSType.invoke + (null, new Object[] { FINDER_CREATOR }); + Object aeTarget = aeTargetConstructor.newInstance + (new Object[] { finderCreatorCode }); + Integer gurlType = (Integer) makeOSType.invoke + (null, new Object[] { GURL_EVENT }); + Object appleEvent = appleEventConstructor.newInstance + (new Object[] { gurlType, gurlType, aeTarget, + kAutoGenerateReturnID, kAnyTransactionID }); + // Don't set browser = appleEvent because then the + // next time we call locateBrowser(), we'll get the + // same AppleEvent, to which we'll already have added + // the relevant parameter. Instead, regenerate the + // AppleEvent every time. There's probably a way to + // do this better; if any has any ideas, please let me + // know. + return appleEvent; + } catch (IllegalAccessException iae) { + browser = null; + errorMessage = iae.getMessage(); + return browser; + } catch (InstantiationException ie) { + browser = null; + errorMessage = ie.getMessage(); + return browser; + } catch (InvocationTargetException ite) { + browser = null; + errorMessage = ite.getMessage(); + return browser; + } + case MRJ_2_1: + File systemFolder; + try { + systemFolder = (File) findFolder.invoke(null, new Object[] + { kSystemFolderType }); + } catch (IllegalArgumentException iare) { + browser = null; + errorMessage = iare.getMessage(); + return browser; + } catch (IllegalAccessException iae) { + browser = null; + errorMessage = iae.getMessage(); + return browser; + } catch (InvocationTargetException ite) { + browser = null; + errorMessage = ite.getTargetException().getClass() + ": " + + ite.getTargetException().getMessage(); + return browser; + } + String[] systemFolderFiles = systemFolder.list(); + // Avoid a FilenameFilter because that can't be stopped mid-list + for(int i = 0; i < systemFolderFiles.length; i++) { + try { + File file = new File(systemFolder, systemFolderFiles[i]); + if (!file.isFile()) { + continue; + } + // We're looking for a file with a creator code of + // 'MACS' and a type of 'FNDR'. Only requiring + // the type results in non-Finder applications + // being picked up on certain Mac OS 9 systems, + // especially German ones, and sending a GURL + // event to those applications results in a logout + // under Multiple Users. + Object fileType = getFileType.invoke + (null, new Object[] { file }); + if (FINDER_TYPE.equals(fileType.toString())) { + Object fileCreator = getFileCreator.invoke + (null, new Object[] { file }); + if (FINDER_CREATOR.equals(fileCreator.toString())) { + browser = file.toString(); // Actually the + // Finder, but that's OK + return browser; + } + } + } catch (IllegalArgumentException iare) { + //WTF? browser = browser; + errorMessage = iare.getMessage(); + return null; + } catch (IllegalAccessException iae) { + browser = null; + errorMessage = iae.getMessage(); + return browser; + } catch (InvocationTargetException ite) { + browser = null; + errorMessage = ite.getTargetException().getClass() + ": " + + ite.getTargetException().getMessage(); + return browser; + } + } + browser = null; + break; + case MRJ_3_0: + case MRJ_3_1: + browser = ""; // Return something non-null + break; + case WINDOWS_NT: + browser = "cmd.exe"; + break; + case WINDOWS_9x: + browser = "command.com"; + break; + case OTHER: + default: + browser = "netscape"; + break; + } + return browser; + } + + /** Attempts to open the default web browser to the given URL. + * @param url The URL to open + * @throws IOException If the web browser could not be located or + * does not run */ + public static void openURL(String url) throws IOException { + if (!loadedWithoutErrors) { + throw new IOException("Exception in finding browser: " + + errorMessage); + } + Object browser = locateBrowser(); + if (browser == null) { + throw new IOException("Unable to locate browser: " + errorMessage); + } + + switch (jvm) { + case MRJ_2_0: + Object aeDesc = null; + try { + aeDesc = aeDescConstructor.newInstance(new Object[] { url }); + putParameter.invoke(browser, new Object[] + { keyDirectObject, aeDesc }); + sendNoReply.invoke(browser, new Object[] { }); + } catch (InvocationTargetException ite) { + throw new IOException("InvocationTargetException while creating" + +" AEDesc: " + ite.getMessage()); + } catch (IllegalAccessException iae) { + throw new IOException("IllegalAccessException while building " + + "AppleEvent: " + iae.getMessage()); + } catch (InstantiationException ie) { + throw new IOException("InstantiationException while creating " + + "AEDesc: " + ie.getMessage()); + } finally { + aeDesc = null; // Encourage it to get disposed if it + // was created + browser = null; // Ditto + } + break; + case MRJ_2_1: + Runtime.getRuntime().exec(new String[] { (String) browser, url } ); + break; + case MRJ_3_0: + int[] instance = new int[1]; + int result = ICStart(instance, 0); + if (result == 0) { + int[] selectionStart = new int[] { 0 }; + byte[] urlBytes = url.getBytes(); + int[] selectionEnd = new int[] { urlBytes.length }; + result = ICLaunchURL(instance[0], new byte[] { 0 }, urlBytes, + urlBytes.length, selectionStart, + selectionEnd); + if (result == 0) { + // Ignore the return value; the URL was launched + // successfully regardless of what happens here. + ICStop(instance); + } else { + throw new IOException("Unable to launch URL: " + result); + } + } else { + throw new IOException("Unable to create an Internet Config " + + "instance: " + result); + } + break; + case MRJ_3_1: + try { + openURL.invoke(null, new Object[] { url }); + } catch (InvocationTargetException ite) { + throw new IOException("InvocationTargetException while calling " + + "openURL: " + ite.getMessage()); + } catch (IllegalAccessException iae) { + throw new IOException("IllegalAccessException while calling " + + "openURL: " + iae.getMessage()); + } + break; + case WINDOWS_NT: + // Add quotes around the URL to allow ampersands and other special + // characters to work. + Process process = Runtime.getRuntime().exec(new String[] + { (String) browser, FIRST_WINDOWS_PARAMETER, + SECOND_WINDOWS_PARAMETER, THIRD_WINDOWS_PARAMETER, + '"' + url + '"' }); + // This avoids a memory leak on some versions of Java on + // Windows. That's hinted at in + // <http://developer.java.sun.com/developer/qow/archive/68/>. + try { + process.waitFor(); + process.exitValue(); + } catch (InterruptedException ie) { + throw new IOException("InterruptedException while launching " + + "browser: " + ie.getMessage()); + } + break; + case WINDOWS_9x: + // Add quotes around the URL to allow ampersands and other special + // characters to work. + // Note: windows 98 doesn't expect the THIRD_WINDOWS_PARAMETER for + // its title. + process = Runtime.getRuntime().exec(new String[] + { (String) browser, FIRST_WINDOWS_PARAMETER, + SECOND_WINDOWS_PARAMETER, '"' + url + '"' }); + // This avoids a memory leak on some versions of Java on + // Windows. That's hinted at in + // <http://developer.java.sun.com/developer/qow/archive/68/>. + try { + process.waitFor(); + process.exitValue(); + } catch (InterruptedException ie) { + throw new IOException("InterruptedException while launching " + + "browser: " + ie.getMessage()); + } + break; + case OTHER: + // Assume that we're on Unix and that Netscape is installed + + // First, attempt to open the URL in a currently running + // session of Netscape + process = Runtime.getRuntime().exec(new String[] + {(String)browser, NETSCAPE_REMOTE_PARAMETER, + NETSCAPE_OPEN_PARAMETER_START + url + + NETSCAPE_OPEN_PARAMETER_END }); + try { + int exitCode = process.waitFor(); + if (exitCode != 0) { // if the command had an error + Runtime.getRuntime().exec(new String[] + { (String) browser, url } ); + } else if(process.getErrorStream() != null) { + // Netscape may not be open, so the command may not have an + // error, it just wouldn't have a process to attach to... + BufferedReader reader = new BufferedReader + (new InputStreamReader(process.getErrorStream())); + String errorStr = reader.readLine(); + + if ( errorStr != null ) { + // Command failed, start up the browser + process = Runtime.getRuntime().exec(new String[] { + (String) browser, url }); + } + } + } catch (InterruptedException ie) { + throw new IOException("InterruptedException while launching " + + "browser: " + ie.getMessage()); + } + break; + default: + // This should never occur, but if it does, we'll try the + // simplest thing possible + Runtime.getRuntime().exec(new String[] { (String) browser, url }); + break; + } + } + + /** Methods required for Mac OS X. The presence of native methods + * does not cause any problems on other platforms. */ + private native static int ICStart(int[] instance, int signature); + private native static int ICStop(int[] instance); + private native static int ICLaunchURL + (int instance, byte[] hint, byte[] data, int len, int[] selectionStart, + int[] selectionEnd); +} + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoder.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoder.java new file mode 100644 index 0000000..8da71fe --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoder.java @@ -0,0 +1,114 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +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.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.foundation.xml.XMLDecoder; + +import org.xml.sax.SAXException; + +/** +* An implementation of XMLDecoder that reads objects from +* XMLRPC format. +* This implementation is not thread-safe, so a new instances +* should be created to accomodate multiple threads. +*/ +public class XMLRPCDecoder implements XMLDecoder +{ + XMLRPCDecoderHelper helper = new XMLRPCDecoderHelper(); + + /** + * Decodes an object in XML-RPC format from the specified input stream. + * @param anInputStream The input stream from which to read. + * The stream will be read fully. + * @param aDescription A description to accompany error messages + * for the stream, typically a file name. + * @param aURL A URL against which relative references within the + * XML will be resolved. + * @return The object that was constructed from the XML content, + * or null if no object could be constructed. + */ + public Object decode( + InputStream anInputStream, String aDescription, URL aURL ) + { + Object result = null; + + try { + SAXParser sparser = SAXParserFactory.newInstance().newSAXParser(); + sparser.parse(anInputStream,helper,aURL.toExternalForm()); + result = helper.getResult(); + } catch (ParserConfigurationException e) { + throw new WotonomyException("Problem in parser configuration", e ); + } catch (IOException e) { + throw new WotonomyException("IOException thrown while parsing", e ); + } catch (SAXException e) { + throw new WotonomyException("SAXException thrown while parsing", e ); + } + helper.reset(); + return result; + } + + /** + * Decodes an XML-RPC message from the specified input stream. + * Stand-alone values not wrapped in "methodCall" or "param" + * tags will be treated as a response. + * @param anInputStream The input stream from which to read. + * The stream will be read fully. + * @param aReceiver an XMLRPCReceiver that will be invoked with + * the appropriate method: request, response, or fault. + */ + public void decode( + InputStream anInputStream, XMLRPCReceiver aReceiver ) + { + try + { + SAXParser sparser = SAXParserFactory.newInstance().newSAXParser(); + sparser.parse(anInputStream,helper); + + if ( helper.isRequest() ) + { + aReceiver.request( helper.getMethodName(), helper.getParameters() ); + } + else + if ( helper.isFault() ) + { + aReceiver.fault( helper.getFaultCode(), helper.getFaultString() ); + } + else // all else is considered a response + { + aReceiver.response( helper.getResult() ); + } + } catch (ParserConfigurationException e) { + throw new WotonomyException("Problem in parser configuration", e ); + } catch (IOException e) { + throw new WotonomyException("IOException thrown while parsing", e ); + } catch (SAXException e) { + throw new WotonomyException("SAXException thrown while parsing", e ); + } + helper.reset(); + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoderHelper.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoderHelper.java new file mode 100644 index 0000000..2368672 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoderHelper.java @@ -0,0 +1,534 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +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.xml; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +/** +* Used by XMLDecoder to implement the necessary interfaces +* required by the jclark xp parser. +* This class is not thread safe. +*/ +class XMLRPCDecoderHelper extends DefaultHandler +{ + protected final Object nilMarker = new Object(); + protected Stack valueStack; + protected String methodName; + protected int faultCode; + protected String faultString; + protected List parameters; + protected StringBuffer cdataBuffer; + + + + public XMLRPCDecoderHelper() + { + valueStack = new Stack(); + parameters = new LinkedList(); + cdataBuffer = new StringBuffer(); + reset(); + } + + public void reset() + { + valueStack.clear(); + parameters.clear(); + cdataBuffer.setLength( 0 ); + methodName = null; + faultCode = 0; + faultString = null; + } + + public boolean isRequest() + { + return ( methodName != null ); + } + + public boolean isResponse() + { + return ( methodName == null ); + } + + public boolean isFault() + { + // faults are responses + return ( isResponse() ) && ( faultString != null ); + } + + public int getFaultCode() + { + return faultCode; + } + + public String getFaultString() + { + return faultString; + } + + public String getMethodName() + { + return methodName; + } + + public Object[] getParameters() + { + return parameters.toArray(); + } + + public void endDocument() throws SAXException { + // TODO Auto-generated method stub + super.endDocument(); + } + + public void startDocument() throws SAXException { + // TODO Auto-generated method stub + super.startDocument(); + reset(); + } + + public Object getResult() + { + if ( valueStack.empty() ) return null; + Object result = valueStack.peek(); + if ( result == nilMarker ) result = null; + return result; + } + + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + super.startElement(uri, localName, qName, attributes); + if ( XMLRPCEncoder.VALUE.equals( localName ) ) + { + ValueMarker marker = new ValueMarker(); + String classname = attributes.getValue( uri, XMLRPCEncoder.CLASS ); + if ( classname != null ) + { + try + { + Class c = Class.forName( classname ); + if ( c != null ) + { + marker.setMarkerClass( c ); + } + } + catch ( Exception exc ) + { + System.out.println( "XMLRPCDecoderHelper.startElement: " + + "Can't find class: " + classname ); + } + } + valueStack.push( marker ); + } + } + + public void characters(char[] ch, int start, int length) throws SAXException { + super.characters(ch, start, length); + char[] someChars = new char[((start+length)<=ch.length) ? length : ch.length-start]; + for (int i = 0; i < someChars.length; i++ ) { + someChars[i] = ch[start+i]; + } + cdataBuffer.append(someChars); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + super.endElement(uri, localName, qName); + + // if any cdata is buffered or if string value + if ( ( XMLRPCEncoder.STRING.equals( localName ) ) + || ( cdataBuffer.length() > 0 ) ) + { + // push value on the stack + valueStack.push( cdataBuffer.toString() ); + cdataBuffer.setLength( 0 ); + } + + if ( XMLRPCEncoder.VALUE.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { +// ValueMarker marker = (ValueMarker) valueStack.pop(); + ValueMarker marker = null; + Object markerValue = valueStack.pop(); + if ( markerValue instanceof ValueMarker ) + { + marker = (ValueMarker) markerValue; + } + else + { + throw new WotonomyException( "Expected value marker, found" + + markerValue.getClass() + " : " + markerValue ); + } + +//System.out.println( "getMarkerClass: " + marker.getMarkerClass() + " : " + value ); +//System.out.println( valueStack ); + if ( marker.getMarkerClass() != null ) + { + // apply introspection + if ( value instanceof Map ) + { + Map map = (Map)value; + Map.Entry entry; + value = marker.getMarkerClass().newInstance(); + Iterator it = map.entrySet().iterator(); + Object entryValue; + while( it.hasNext() ) + { + entry = (Map.Entry) it.next(); + entryValue = entry.getValue(); + if ( entryValue == nilMarker ) entryValue = null; + Introspector.set( + value, entry.getKey().toString(), entryValue ); + } + } + if ( ! ( value.getClass().equals( marker.getMarkerClass() ) ) ) + { + Object converted = + ValueConverter.convertObjectToClass( + value, marker.getMarkerClass() ); + if ( converted != null ) + { + value = converted; + } + } + } + } + catch ( Exception exc ) + { + // fall back on unconverted value + } + + valueStack.push( value ); +// System.out.println( "convertedValue: " + value + "("+ value.getClass() +")" ); + } + else + if ( XMLRPCEncoder.MEMBER.equals( localName ) ) // Map.Entry + { + // leave key and value to be handled by struct + } + else + if ( XMLRPCEncoder.STRUCT.equals( localName ) ) // write Entries to map or object + { + // write values to array (reverse the order) + Object value; + Map map = new HashMap(); + while ( ( ! valueStack.empty() ) + && ( ! ( valueStack.peek() instanceof ValueMarker ) ) ) + { + value = valueStack.pop(); + map.put( valueStack.pop(), value ); + } + // push the list on the stack + valueStack.push( map ); + } + else + if ( XMLRPCEncoder.ARRAY.equals( localName ) ) + { +//System.out.println( "ended ARRAY: " + valueStack.size() ); + // write values to array (reverse the order) + Object value; + LinkedList list = new LinkedList(); + while ( ( ! valueStack.empty() ) + && ( ! ( valueStack.peek() instanceof ValueMarker ) ) ) + { + value = valueStack.pop(); + if ( value == nilMarker ) value = null; + list.addFirst( value ); + } + // push the list on the stack + valueStack.push( list ); + } + else + if ( XMLRPCEncoder.INT.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { + valueStack.push( + new Integer( value.toString() ) ); + } + catch ( NumberFormatException exc ) + { + throw new WotonomyException( + "Invalid double format: " + value.toString() ); + } + } + else + if ( XMLRPCEncoder.I4.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { + valueStack.push( + new Integer( value.toString() ) ); + } + catch ( NumberFormatException exc ) + { + throw new WotonomyException( + "Invalid double format: " + value.toString() ); + } + } + else + if ( XMLRPCEncoder.NIL.equals( localName ) ) + { + valueStack.push( nilMarker ); + } + else + if ( XMLRPCEncoder.DOUBLE.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { + valueStack.push( + new Double( value.toString() ) ); + } + catch ( NumberFormatException exc ) + { + throw new WotonomyException( + "Invalid double format: " + value.toString() ); + } + } + else + if ( XMLRPCEncoder.DATE.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { + valueStack.push( + XMLRPCEncoder.DATEFORMAT8601.parseObject( + value.toString() ) ); + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Invalid date format: " + value ); + } + } + else + if ( XMLRPCEncoder.BOOLEAN.equals( localName ) ) + { + Object value = valueStack.pop(); + if ( XMLRPCEncoder.TRUE.equals( value ) ) + { + valueStack.push( Boolean.TRUE ); + } + else + if ( XMLRPCEncoder.FALSE.equals( value ) ) + { + valueStack.push( Boolean.FALSE ); + } + else + { + throw new WotonomyException( + "Invalid boolean format: " + value ); + } + } + else + if ( XMLRPCEncoder.BASE64.equals( localName ) ) + { + throw new WotonomyException( "Not implemented yet." ); + } + else + if ( XMLRPCEncoder.FAULT.equals( localName ) ) + { + Map faultMap = (Map) valueStack.pop(); + try + { + faultCode = ((Integer) + faultMap.get( XMLRPCEncoder.FAULTCODE )).intValue(); + faultString = (String) faultMap.get( XMLRPCEncoder.FAULTSTRING ); + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Invalid fault format: " + faultMap ); + } + } + else + if ( XMLRPCEncoder.METHODNAME.equals( localName ) ) + { + methodName = (String) valueStack.pop(); + } + else + if ( XMLRPCEncoder.PARAM.equals( localName ) ) + { + //NOTE: this leaves the parameter on the stack + parameters.add( getResult() ); + } + } + + + + public void endPrefixMapping(String prefix) throws SAXException { + // TODO Auto-generated method stub + super.endPrefixMapping(prefix); + } + + public void error(SAXParseException e) throws SAXException { + // TODO Auto-generated method stub + super.error(e); + } + + public void fatalError(SAXParseException e) throws SAXException { + // TODO Auto-generated method stub + super.fatalError(e); + } + + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + // TODO Auto-generated method stub + super.ignorableWhitespace(ch, start, length); + } + + public void notationDecl(String name, String publicId, String systemId) throws SAXException { + // TODO Auto-generated method stub + super.notationDecl(name, publicId, systemId); + } + + public void processingInstruction(String target, String data) throws SAXException { + // TODO Auto-generated method stub + super.processingInstruction(target, data); + } + + public InputSource resolveEntity(String publicId, String systemId) throws SAXException { + // NOTE: Sun accepted an incompatible api difference. The (false) should be tossed by hotspot + try { + if (false) throw new IOException("Fake exception to make it compile in both 1.4 and 1.5"); + return super.resolveEntity(publicId, systemId); + } catch (IOException e) { + throw new SAXException(e.getClass().getName() + " thrown while resolving entity.",e); + } + } + + public void setDocumentLocator(Locator locator) { + // TODO Auto-generated method stub + super.setDocumentLocator(locator); + } + + public void skippedEntity(String name) throws SAXException { + // TODO Auto-generated method stub + super.skippedEntity(name); + } + + + + public void startPrefixMapping(String prefix, String uri) throws SAXException { + // TODO Auto-generated method stub + super.startPrefixMapping(prefix, uri); + } + + public void unparsedEntityDecl(String name, String publicId, String systemId, String notationName) throws SAXException { + // TODO Auto-generated method stub + super.unparsedEntityDecl(name, publicId, systemId, notationName); + } + + public void warning(SAXParseException e) throws SAXException { + // TODO Auto-generated method stub + super.warning(e); + } + + + // marker class + + private class ValueMarker + { + private Class theClass; + + public ValueMarker() + { + theClass = null; + } + + public void setMarkerClass( Class aClass ) + { + theClass = aClass; + } + + public Class getMarkerClass() + { + return theClass; + } + + public String toString() + { + return "[ValueMarker: " + theClass + "]"; + } + } + +} + +/* + * $Log$ + * Revision 1.1 2006/02/19 01:44:03 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.7 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.6 2001/03/03 15:16:35 mpowers + * Fixed bug in decoding empty strings: string handler did nothing and + * empty cdatas were ignored, so no value was placed on the stack. + * + * Revision 1.5 2001/02/17 16:52:06 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.4 2001/02/09 15:51:39 mpowers + * Fixed a pernicious bug: I was using the character event entirely + * incorrectly, but the problem only exhibited itself with large data + * and even then only randomly. Now using a string buffer. + * + * Revision 1.3 2001/02/07 19:24:28 mpowers + * Moved XML classes to separate package. + * + * Revision 1.2 2001/02/06 14:34:23 mpowers + * Forgot to rename the package declarations. + * + * Revision 1.1 2001/02/06 14:31:19 mpowers + * Moving XML utilities from util to xml package. + * + * Revision 1.1.1.1 2000/12/21 15:52:39 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:48 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCEncoder.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCEncoder.java new file mode 100644 index 0000000..3a63d45 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCEncoder.java @@ -0,0 +1,526 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +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.xml; + +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Array; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.foundation.xml.XMLEncoder; + +import org.dom4j.Element; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.XMLWriter; +import org.dom4j.util.NonLazyElement; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** +* An implementation of XMLEncoder that serializes objects +* into XMLRPC format, which is found at http://xmlrpc.com/spec. +* We extend that standard only in that we add a "class" +* attribute to the "value" tag, so that a java-based decoder +* can more closely reconstruct the original java data structure. +* The class attribute can be safely ignored by clients. +* This implementation is not thread-safe, so a new instances +* should be created to accomodate multiple threads. +*/ +public class XMLRPCEncoder implements XMLEncoder +{ + public static final String METHODCALL = "methodCall"; + public static final String METHODNAME = "methodName"; + public static final String METHODRESPONSE = "methodResponse"; + public static final String PARAMS = "params"; + public static final String PARAM = "param"; + public static final String FAULT = "fault"; + public static final String FAULTCODE = "faultCode"; + public static final String FAULTSTRING = "faultString"; + + public static final String VALUE = "value"; + public static final String CLASS = "class"; + + public static final String STRUCT = "struct"; + public static final String MEMBER = "member"; + public static final String NAME = "name"; + + public static final String ARRAY = "array"; + public static final String DATA = "data"; + + public static final String NIL = "nil"; + public static final String INT = "int"; + public static final String I4 = "i4"; + public static final String BOOLEAN = "boolean"; + public static final String STRING = "string"; + public static final String DOUBLE = "double"; + public static final String DATE = "dateTime.iso8601"; + public static final String BASE64 = "base64"; + + public static final String TRUE = "1"; + public static final String FALSE = "0"; + + public static final Format DATEFORMAT8601 = + new SimpleDateFormat( "yyyyMMdd'T'HHmmss" ); + + /** + * Encodes an object to the specified output stream as XML. + * @param anObject The object to be serialized to XML format. + * @param anOutputStream The output stream to which the object + * will be written. + */ + public void encode( Object anObject, OutputStream anOutputStream ) + { + try + { + + //XMLWriter writer = new UTF8XMLWriter( anOutputStream ); + RPCXMLWriter writer = new RPCXMLWriter(anOutputStream,OutputFormat.createCompactFormat()); + writeValueToXMLWriter( anObject, writer ); + writer.flush(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + } + + /** + * Encodes a method request in XML-RPC format in a "methodCall" tag, + * and writes the XML to the specified output stream. + * This method only writes XML: the caller is responsible for + * generating the appropriate header, if any, which should set + * the content type as "text/xml" and the content length as appropriate. + * The caller is also responsible for writing the xml version tag. + * @param aMethodName The method name to appear in the "methodName" tag. + * @param aParameterArray An array of objects, each of which will be + * encoded as values enclosed in a "param" tag, all of which will be + * enclosed in a "params" tag. + * @param anOutputStream The stream to which the XML will be written. + */ + public void encodeRequest( + String aMethodName, Object[] aParameterArray, + OutputStream anOutputStream ) + { + try + { + RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat()); + writer.processingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" ); + writer.startElement( METHODCALL ); + writer.startElement( METHODNAME ); + writer.write( aMethodName ); + writer.endElement( METHODNAME ); + writer.startElement( PARAMS ); + for ( int i = 0; i < aParameterArray.length; i++ ) + { + writer.startElement( PARAM ); + writeValueToXMLWriter( aParameterArray[i], writer ); + writer.endElement( PARAM ); + } + writer.endElement( PARAMS ); + writer.endElement( METHODCALL ); + writer.flush(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + + //TODO: should this return the content-length? + } + + /** + * Encodes a method response in XML-RPC format in a "methodResponse" tag, + * and writes the XML to the specified output stream. + * This method only writes XML: the caller is responsible for + * generating the appropriate header, if any, which should set + * the content type as "text/xml" and the content length as appropriate. + * The caller is also responsible for writing the xml version tag. + * @param aResult A object which will be + * encoded as values enclosed in a "param" tag, all of which will be + * enclosed in a "params" tag. + * @param anOutputStream The stream to which the XML will be written. + */ + public void encodeResponse( + Object aResult, OutputStream anOutputStream ) + { + try + { + RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat() ); + writer.processingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" ); + writer.startElement( METHODRESPONSE ); + writer.startElement( PARAMS ); + writer.startElement( PARAM ); + writeValueToXMLWriter( aResult, writer ); + writer.endElement( PARAM ); + writer.endElement( PARAMS ); + writer.endElement( METHODRESPONSE ); + writer.flush(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + + //TODO: should this return the content-length? + } + + /** + * Encodes a fault response in XML-RPC format in a "methodResponse" tag, + * and writes the XML to the specified output stream. + * This method only writes XML: the caller is responsible for first + * generating the appropriate header, if any, which should set + * the content type as "text/xml" and the content length as appropriate. + * The caller is also responsible for writing the xml version tag. + * @param aFaultCode An application-defined error code. + * @param aFaultString A human-readable error description. + * @param anOutputStream The stream to which the XML will be written. + */ + public void encodeFault( + int aFaultCode, String aFaultString, OutputStream anOutputStream ) + { + try + { + RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat() ); + writer.processingInstruction( "xml", "version=\"1.0\"" ); + writer.startElement( METHODRESPONSE ); + writer.startElement( FAULT ); + writer.startElement( VALUE ); + writer.startElement( STRUCT ); + + writer.startElement( MEMBER ); + writer.startElement( NAME ); + writer.write( FAULTCODE ); + writer.endElement( NAME ); + writer.startElement( VALUE ); + writer.startElement( INT ); + writer.write( new Integer( aFaultCode ).toString() ); + writer.endElement( INT ); + writer.endElement( VALUE ); + writer.endElement( MEMBER ); + + writer.startElement( MEMBER ); + writer.startElement( NAME ); + writer.write( FAULTSTRING ); + writer.endElement( NAME ); + writer.startElement( VALUE ); + writer.startElement( STRING ); + writer.write( aFaultString ); + writer.endElement( STRING ); + writer.endElement( VALUE ); + writer.endElement( MEMBER ); + + writer.endElement( STRUCT ); + writer.endElement( VALUE ); + writer.endElement( FAULT ); + writer.endElement( METHODRESPONSE ); + writer.flush(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + + //TODO: should this return the content-length? + } + + /** + * Performs the actual writing of the file to XML. + */ + private void writeValueToXMLWriter( + Object anObject, RPCXMLWriter writer ) + { + try + { + + + if ( anObject == null ) + { + writer.startElement( VALUE ); + // write nil for null + Element nill = new NonLazyElement(NIL); + writer.write(nill); + } + else + if ( anObject instanceof Collection ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + // write items in the order we get them from the iterator + + writer.startElement( ARRAY ); + writer.startElement( DATA ); + Iterator it = ((Collection)anObject).iterator(); + while ( it.hasNext() ) + { + writeValueToXMLWriter( it.next(), writer ); + } + writer.endElement( DATA ); + writer.endElement( ARRAY ); + + } + else + if ( anObject instanceof Map ) + { + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + // write items in the order we get them from the iterator + //FIXME: The method-based properties are being ignored! + + Map.Entry entry; + writer.startElement( STRUCT ); + writer.startElement( MEMBER ); + Iterator it = ((Map)anObject).entrySet().iterator(); + while ( it.hasNext() ) + { + entry = (Map.Entry) it.next(); + writer.startElement( NAME ); + writeValueToXMLWriter( entry.getKey(), writer ); + writer.endElement( NAME ); + writeValueToXMLWriter( entry.getValue(), writer ); + } + writer.endElement( MEMBER ); + writer.endElement( STRUCT ); + } + else // not a collection + { + // check for primitive types + if ( anObject instanceof String ) + { + writer.startElement( VALUE ); + + writer.startElement( STRING ); + writer.write( anObject.toString() ); + writer.endElement( STRING ); + } + else + if ( anObject instanceof StringBuffer ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + writer.startElement( STRING ); + writer.write( anObject.toString() ); + writer.endElement( STRING ); + } + else + if ( anObject instanceof Number ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + if ( ( anObject instanceof Double ) + || ( anObject instanceof Float ) ) + { + writer.startElement( DOUBLE ); + writer.write( anObject.toString() ); + writer.endElement( DOUBLE ); + } + else + { + writer.startElement( INT ); + writer.write( anObject.toString() ); + writer.endElement( INT ); + } + } + else + if ( anObject instanceof Date ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + writer.startElement( DATE ); + writer.write( DATEFORMAT8601.format( anObject ) ); + writer.endElement( DATE ); + } + else + if ( anObject instanceof Boolean ) + { + writer.startElement( BOOLEAN ); + if ( ((Boolean)anObject).booleanValue() ) + { + writer.write( "1" ); + } + else + { + writer.write( "0" ); + } + writer.endElement( BOOLEAN ); + } + else + if ( anObject.getClass().isArray() ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + writer.startElement( ARRAY ); + writer.startElement( DATA ); + + int length = Array.getLength( anObject ); + for ( int i = 0; i < length; i++ ) + { + writeValueToXMLWriter( Array.get( anObject, i ), writer ); + } + + writer.endElement( DATA ); + writer.endElement( ARRAY ); + } + else // not primitive or collection, treat as struct + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + List readProperties = new ArrayList(); + String[] read = Introspector.getReadPropertiesForObject( anObject ); + for ( int i = 0; i < read.length; i++ ) + { + readProperties.add( read[i] ); + } + + List properties = new ArrayList(); + String[] write = Introspector.getWritePropertiesForObject( anObject ); + for ( int i = 0; i < write.length; i++ ) + { + properties.add( write[i] ); + } + + // only use readable properties + properties.retainAll( readProperties ); + +// if ( properties.size() > 0 ) +// { + String key; + Object value; + Iterator it = properties.iterator(); + writer.startElement( STRUCT ); + while ( it.hasNext() ) + { + key = (String) it.next(); + value = Introspector.get( anObject, key ); + + writer.startElement( MEMBER ); + writer.startElement( NAME ); + writer.write( key ); + writer.endElement( NAME ); + writeValueToXMLWriter( value, writer ); + writer.endElement( MEMBER ); + } + writer.endElement( STRUCT ); +/* + } + else // no properties - write a converted string + { + writer.startElement( STRING ); + Object converted = + ValueConverter.convertObjectToClass( anObject, String.class ); + if ( converted != null ) + { + writer.write( converted.toString() ); + } + else + { + writer.write( anObject.toString() ); + } + writer.endElement( STRING ); + } +*/ + } + } + + writer.endElement( VALUE ); + } + catch ( Exception exc ) + { + System.err.println( "XMLFileSoup.writeValueToXMLWriter: " + exc ); + exc.printStackTrace(); + } + + } +/* + public static void main( String[] argv ) + { + System.out.println( "<test>" ); + XMLRPCEncoder encoder = new XMLRPCEncoder(); + encoder.encodeRequest( "systemObject.test", new Object[] { + new net.wotonomy.test.TestObject(), + new net.wotonomy.test.TestObject(), + new net.wotonomy.test.TestObject() }, System.out ); + System.out.println(); + System.out.println(); + encoder.encodeResponse( new net.wotonomy.test.TestObject(), System.out ); + System.out.println(); + System.out.println(); + encoder.encodeFault( -1, "This is a fault.", System.out ); + System.out.println(); + System.out.println(); + System.out.println( "</test>" ); + } +*/ + + private class RPCXMLWriter extends XMLWriter { + + public RPCXMLWriter(OutputStream arg0, OutputFormat arg1) throws UnsupportedEncodingException { + super(arg0, arg1); + } + + public void endElement(String localname) throws SAXException { + super.endElement(null, localname, null); + } + + public void startElement(String localname) throws SAXException { + this.startElement(localname, null); + } + public void startElement(String localname, Attributes attributes) throws SAXException { + this.startElement(null, localname, null, attributes); + } + + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCReceiver.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCReceiver.java new file mode 100644 index 0000000..48c9e41 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCReceiver.java @@ -0,0 +1,72 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +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.xml; + +/** +* A call-back interface that receives an XML-RPC transaction message. +* Used by XMLRPCDecoder to return values from a message. +*/ +public interface XMLRPCReceiver +{ + /** + * Receives an XML-RPC request. + * @param aMethodName The method name of the request. + * @param aParameterArray The objects contained in the request, in order. + */ + void request( String aMethodName, Object[] aParameterArray ); + + /** + * Receives an XML-RPC response. + * @param aResult The object contained in the response. + */ + void response( Object aResult ); + + /** + * Receives an XML-RPC fault response. + * @param aFaultCode The fault code contained in the response. + * @param aFaultString The fault string contained in the response. + */ + void fault( int aFaultCode, String aFaultString ); +} + +/* + * $Log$ + * Revision 1.1 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.2 2001/02/06 14:34:23 mpowers + * Forgot to rename the package declarations. + * + * Revision 1.1 2001/02/06 14:31:19 mpowers + * Moving XML utilities from util to xml package. + * + * Revision 1.1.1.1 2000/12/21 15:52:44 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCSelector.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCSelector.java new file mode 100644 index 0000000..7c1f104 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCSelector.java @@ -0,0 +1,238 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +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.xml; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLConnection; + +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* An NSSelector customized to invoke methods with XMLRPC +* when a URL is passed in as the object to the invoke() method. +* The method name and parameters will be marshalled and sent +* as an XMLRPC request to the host specified by the URL. <br><br> +* +* To use this class simply as an XMLRPC client, just call +* invoke() with a URL referencing the XMLRPC server and an +* optional array of parameters. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class XMLRPCSelector extends NSSelector +{ + private boolean copyStream = false; + + /** + * Constructor specifying a method name. + */ + public XMLRPCSelector (String aMethodName) + { + super( aMethodName, EMPTY_CLASS_ARRAY ); + } + + /** + * Constructor specifying a method name and an array of parameter types. + * When accessing XMLRPC servers, invoke() does require that the + * specified objects match the types in the parameter type array. + */ + public XMLRPCSelector (String aMethodName, Class[] aParameterTypeArray) + { + super( aMethodName, aParameterTypeArray ); + } + + /** + * Invokes this selector's method on the specified object + * using the specified parameters. + */ + public Object invoke (Object anObject, Object[] parameters) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + if ( anObject instanceof URL ) + { + Receiver receiver = new Receiver(); + byte[] copyOfResponse = null; + try + { + URLConnection cn = ((URL)anObject).openConnection(); + + // set properties + cn.setDoOutput(true); + cn.setDoInput(true); + cn.setRequestProperty( + "content-type","text/xml"); + + // send parameters + OutputStream out = cn.getOutputStream(); + new XMLRPCEncoder().encodeRequest( + name(), parameters, out ); + out.flush(); + out.close(); + + // get response: getInputStream initiates the request + InputStream input = + new BufferedInputStream( cn.getInputStream() ); + if ( copyStream ) + { + ByteArrayOutputStream byteArray = + new ByteArrayOutputStream(); + int b; + while ( ( b = input.read() ) != -1 ) + { + byteArray.write( b ); + } + copyOfResponse = byteArray.toByteArray(); + input = new ByteArrayInputStream( copyOfResponse ); + } + new XMLRPCDecoder().decode( input, receiver ); + } + catch ( FileNotFoundException exc ) + { + throw new WotonomyException( "Server did not return a response." ); + } + catch ( Exception exc ) + { + if ( copyOfResponse != null ) + { + System.out.println( new String( copyOfResponse ) ); + exc.printStackTrace(); + } + throw new InvocationTargetException( exc ); + } + + if ( receiver.faultString == null ) + { + return receiver.result; + } + else + { + throw new InvocationTargetException( + new WotonomyException( + receiver.faultCode + ": " + receiver.faultString ) ); + } + } + + // else: not a URL + return super.invoke( anObject, parameters ); + } + + public static Object invoke + (String methodName, Class[] parameterTypes, Object anObject, Object[] parameters) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return new XMLRPCSelector( methodName, parameterTypes ).invoke( anObject, parameters ); + } + + public static Object invoke + (String methodName, Object anObject) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return XMLRPCSelector.invoke( + methodName, EMPTY_CLASS_ARRAY, anObject, EMPTY_OBJECT_ARRAY ); + } + + public static Object invoke + (String methodName, Class[] parameterTypes, + Object anObject, Object aParameter) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return XMLRPCSelector.invoke( + methodName, parameterTypes, anObject, new Object[] { aParameter } ); + } + + public static Object invoke + (String methodName, Class[] parameterTypes, + Object anObject, Object p1, Object p2) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return XMLRPCSelector.invoke( + methodName, parameterTypes, anObject, new Object[] { p1, p2 } ); + } + + private class Receiver implements XMLRPCReceiver + { + public Object result; + public int faultCode; + public String faultString; + + public Receiver() + { + result = null; + faultCode = -1; + faultString = null; + } + + public void request( + String aMethodName, Object[] aParameterArray ) + { + throw new WotonomyException( + "Invalid response: Expected response but received request." ); + } + + public void response( + Object aResult ) + { + result = aResult; + faultCode = -1; + faultString = null; + } + + public void fault( + int aFaultCode, String aFaultString) + { + result = null; + faultCode = aFaultCode; + faultString = aFaultString; + } + } + + +} + +/* + * $Log$ + * Revision 1.1 2006/02/19 01:44:03 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.1 2001/02/07 19:24:28 mpowers + * Moved XML classes to separate package. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCServlet.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCServlet.java new file mode 100644 index 0000000..a9981a4 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCServlet.java @@ -0,0 +1,283 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +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.xml; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Hashtable; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* A servlet that can make any java object into an XML-RPC server. +* Simply pass in the object to the constructor and XML-RPC requests +* to this servlet will call the appropriate methods and convert +* the results to an XML-RPC response. <br><br> +* +* Depending on your servlet container, it may be necessary to +* create a simple subclass that creates your handler object +* in its constructor and then calls setHandler(). <br><br> +* +* Responses are in the specification's standard response format.<br><br> +* +* Faults are returned if any exception is thrown in the method, +* or if the specified method is not found on the object. +* The fault string is the toString value of the exception, +* and the fault code is the hasCode value of the exception. <br><br> +* +* Remember that this servlet only responds to POSTs, +* per the XML-RPC spec. +*/ +public class XMLRPCServlet extends HttpServlet +{ + protected Object handler; + protected boolean synchronizing; + private Hashtable selectorCache = new Hashtable(); // thread safe + private boolean copyStream = false; + + /** + * Default constructor initializes internal state. + */ + public XMLRPCServlet() + { + handler = null; + synchronizing = false; + } + + /** + * Constructor takes any java object and allows its methods + * to be invoked via XMLRPC requests to this servlet. + * Simply calls setHandler(). + */ + public XMLRPCServlet( Object aHandler ) + { + this(); + setHandler( aHandler ); + } + + /** + * Gets the object whose methods will be invoked to + * handle incoming requests. + */ + public Object getHandler() + { + return handler; + } + + /** + * Sets the object whose methods will be invoked to + * handle incoming requests. + */ + public void setHandler( Object aHandler ) + { + handler = aHandler; + } + + /** + * Gets whether the servlet should synchonize on the + * object before invoking methods on it. + * Defaults to false. + */ + public boolean isSynchronizing() + { + return synchronizing; + } + + /** + * Sets whether the servlet should synchonize on the + * object before invoking methods on it. + * Defaults to false. + */ + public void setSynchronizing( boolean willSynchronize ) + { + synchronizing = willSynchronize; + } + + /** + * Overridden to service the request. + */ + protected void doPost( + HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + if ( getHandler() != null ) + { + InputStream input = req.getInputStream(); + byte[] copyOfRequest = null; + + if ( copyStream ) + { + ByteArrayOutputStream byteArray = + new ByteArrayOutputStream(); + int b; + while ( ( b = input.read() ) != -1 ) + { + byteArray.write( b ); + } + copyOfRequest = byteArray.toByteArray(); + input = new ByteArrayInputStream( copyOfRequest ); + } + + try + { + new XMLRPCDecoder().decode( input, + new Receiver( this, resp ) ); + } + catch ( WotonomyException exc ) + { + if ( copyOfRequest != null ) + { + System.out.println( new String( copyOfRequest ) ); + exc.printStackTrace(); + } + // catches io exceptions thrown in handleRequest. + Throwable t = exc.getWrappedThrowable(); + if ( t instanceof IOException ) + { + throw (IOException)t; + } + throw exc; + } + } + } + + /** + * Called by doPost after parsing an incoming request, + * and is responsible for invoking the specified method + * with the specified parameters on the handler object. + * (This implementation calls getOutputStream() on the response.) + * Override to customize the handling of the request. + */ + protected void handleRequest( String aMethodName, + Object[] aParameterArray, HttpServletResponse aResponse ) + { + OutputStream output = null; + + try + { + output = aResponse.getOutputStream(); + aResponse.setStatus( HttpServletResponse.SC_OK ); // always 200 + aResponse.setContentType( "text/xml" ); + } + catch ( IOException exc ) + { + // caught in doPost + throw new WotonomyException( exc ); + } + + // get the array of types + XMLRPCEncoder encoder = new XMLRPCEncoder(); + Class[] types = new Class[ aParameterArray.length ]; + for ( int i = 0; i < aParameterArray.length; i++ ) + { + types[i] = aParameterArray[i].getClass(); + } + + //TODO: selectors should be cached if possible + + Object handler = getHandler(); + if ( isSynchronizing() ) + { + synchronized ( handler ) + { + execute( encoder, handler, output, + new NSSelector( aMethodName, types ), aParameterArray ); + } + } + else + { + execute( encoder, handler, output, + new NSSelector( aMethodName, types ), aParameterArray ); + } + } + + private void execute( XMLRPCEncoder anEncoder, Object aHandler, + OutputStream output, NSSelector aSelector, Object[] aParameterArray ) + { + try + { + Object result = + aSelector.invoke( aHandler, aParameterArray ); + anEncoder.encodeResponse( result, output ); + } + catch ( Exception exc ) + { + anEncoder.encodeFault( + exc.hashCode(), exc.toString(), output ); + } + } + + private class Receiver implements XMLRPCReceiver + { + XMLRPCServlet controller; + HttpServletResponse response; + + public Receiver( + XMLRPCServlet aController, + HttpServletResponse aResponse ) + { + controller = aController; + response = aResponse; + } + + public void request( + String aMethodName, Object[] aParameterArray ) + { + controller.handleRequest( + aMethodName, aParameterArray, response ); + } + + public void response( + Object aResult ) + { + // does nothing + } + + public void fault( + int aFaultCode, String aFaultString) + { + // does nothing + } + } +} + +/* + * $Log$ + * Revision 1.1 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.1 2001/02/07 19:24:28 mpowers + * Moved XML classes to separate package. + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/package.html b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/package.html new file mode 100644 index 0000000..4303e07 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/package.html @@ -0,0 +1,30 @@ +<body> +<p> +The XML object serialization framework, including +an XML-RPC serializer implementation and an easy-to-use +XML-RPC servlet and client. +</p> +<p> +The primary serialization interfaces are XMLEncoder and XMLDecoder. +</p> +<p> +The implementation of those interfaces is in XMLRPCEncoder and XMLRPCDecoder. +In addition to serializing java objects to an XML-RPC struct format, these +classes also define methods for generating and parsing XML-RPC requests, +responses, and faults. Other implementations (like SOAP) may follow. +</p> +<p> +The XMLRPCServlet utilizes the framework to allow you to turn any +java object into an XML-RPC server with one line of code. +</p> +<p> +XMLRPCSelector turns NSSelector into a XML-RPC client. If you +pass in a URL as the target object, the selector will invoke its +method on the specified XML-RPC server and return the result as +a java object. The selector otherwise works normally. +</p> +<p> +This package has dependencies on the foundation and util packages. +The servlet has a dependency on javax.servlet.HttpServlet. +</p> +</body> |
