From 09d1fdd4e3d30bb4f0b4be0166954350cdc85f2b Mon Sep 17 00:00:00 2001 From: hns Date: Fri, 29 Dec 2000 17:57:21 +0000 Subject: [PATCH] Check in current Hop snapshot --- .../ApplicationStoppedException.java | 21 + src/helma/framework/FrameworkException.java | 17 + src/helma/framework/IRemoteApp.java | 20 + src/helma/framework/RedirectException.java | 34 + src/helma/framework/RequestTrans.java | 52 + src/helma/framework/ResponseTrans.java | 207 +++ src/helma/framework/TimeoutException.java | 18 + src/helma/framework/extensions/Database.java | 1066 +++++++++++ src/helma/framework/extensions/ESMail.java | 214 +++ .../framework/extensions/FtpExtension.java | 419 +++++ .../framework/extensions/ImageExtension.java | 127 ++ .../framework/extensions/MailExtension.java | 255 +++ src/helma/image/ActivatedImageWrapper.java | 47 + src/helma/image/IRemoteGenerator.java | 24 + src/helma/image/IRemoteImage.java | 37 + src/helma/image/ImageGenerator.java | 133 ++ src/helma/image/ImageWrapper.java | 245 +++ src/helma/image/RemoteImage.java | 106 ++ src/helma/image/Server.java | 90 + src/helma/image/SunImageWrapper.java | 56 + src/helma/mime/LanguageTag.java | 161 ++ src/helma/mime/MimeHeaderHolder.java | 51 + src/helma/mime/MimeHeaders.java | 147 ++ src/helma/mime/MimeHeadersFactory.java | 25 + src/helma/mime/MimeParser.java | 228 +++ src/helma/mime/MimeParserException.java | 14 + src/helma/mime/MimeParserFactory.java | 24 + src/helma/mime/MimeType.java | 374 ++++ src/helma/mime/MimeTypeFormatException.java | 14 + src/helma/mime/MultipartInputStream.java | 214 +++ src/helma/mime/Utils.java | 143 ++ .../objectmodel/ConcurrencyException.java | 51 + src/helma/objectmodel/DbMapping.java | 457 +++++ src/helma/objectmodel/DbSource.java | 95 + src/helma/objectmodel/INode.java | 114 ++ src/helma/objectmodel/INodeListener.java | 15 + src/helma/objectmodel/IPathElement.java | 21 + src/helma/objectmodel/IProperty.java | 34 + src/helma/objectmodel/IServer.java | 79 + src/helma/objectmodel/Key.java | 140 ++ src/helma/objectmodel/Node.java | 765 ++++++++ src/helma/objectmodel/NodeDataSource.java | 46 + src/helma/objectmodel/NodeEvent.java | 59 + .../objectmodel/ObjectNotFoundException.java | 49 + src/helma/objectmodel/Property.java | 332 ++++ src/helma/objectmodel/Relation.java | 308 +++ src/helma/objectmodel/SystemProperties.java | 133 ++ .../objectmodel/db/ApplicationManager.java | 139 ++ src/helma/objectmodel/db/DbWrapper.java | 281 +++ .../objectmodel/db/ExternalizableVector.java | 39 + .../objectmodel/db/HopSocketFactory.java | 43 + src/helma/objectmodel/db/IDGenerator.java | 73 + src/helma/objectmodel/db/Node.java | 1643 +++++++++++++++++ src/helma/objectmodel/db/NodeManager.java | 959 ++++++++++ src/helma/objectmodel/db/Property.java | 561 ++++++ src/helma/objectmodel/db/Server.java | 355 ++++ src/helma/objectmodel/db/Transactor.java | 338 ++++ .../objectmodel/db/WrappedNodeManager.java | 186 ++ src/helma/servlet/AcmeFileServlet.java | 208 +++ src/helma/servlet/AcmeServletClient.java | 254 +++ src/helma/servlet/ServletClient.java | 312 ++++ src/helma/util/HtmlEncoder.java | 365 ++++ src/helma/util/InetAddressFilter.java | 56 + src/helma/util/Logger.java | 103 ++ src/helma/util/ParanoidServerSocket.java | 50 + src/helma/util/Timer.java | 70 + src/helma/util/Uploader.java | 110 ++ src/helma/util/UrlEncoder.java | 126 ++ src/helma/util/WebBroadcaster.java | 350 ++++ .../xmlrpc/AuthenticatedXmlRpcHandler.java | 20 + src/helma/xmlrpc/Base64.java | 130 ++ src/helma/xmlrpc/Benchmark.java | 104 ++ src/helma/xmlrpc/ServerInputStream.java | 61 + src/helma/xmlrpc/WebServer.java | 448 +++++ src/helma/xmlrpc/XmlRpc.java | 583 ++++++ src/helma/xmlrpc/XmlRpcClient.java | 226 +++ src/helma/xmlrpc/XmlRpcClientLite.java | 358 ++++ src/helma/xmlrpc/XmlRpcException.java | 24 + src/helma/xmlrpc/XmlRpcHandler.java | 23 + src/helma/xmlrpc/XmlRpcProxyServlet.java | 42 + src/helma/xmlrpc/XmlRpcServer.java | 284 +++ src/helma/xmlrpc/XmlRpcServlet.java | 46 + src/helma/xmlrpc/fesi/FesiRpcExtension.java | 170 ++ src/helma/xmlrpc/fesi/FesiRpcServer.java | 97 + src/helma/xmlrpc/fesi/FesiRpcUtil.java | 85 + 85 files changed, 16603 insertions(+) create mode 100644 src/helma/framework/ApplicationStoppedException.java create mode 100644 src/helma/framework/FrameworkException.java create mode 100644 src/helma/framework/IRemoteApp.java create mode 100644 src/helma/framework/RedirectException.java create mode 100644 src/helma/framework/RequestTrans.java create mode 100644 src/helma/framework/ResponseTrans.java create mode 100644 src/helma/framework/TimeoutException.java create mode 100644 src/helma/framework/extensions/Database.java create mode 100644 src/helma/framework/extensions/ESMail.java create mode 100644 src/helma/framework/extensions/FtpExtension.java create mode 100644 src/helma/framework/extensions/ImageExtension.java create mode 100644 src/helma/framework/extensions/MailExtension.java create mode 100644 src/helma/image/ActivatedImageWrapper.java create mode 100644 src/helma/image/IRemoteGenerator.java create mode 100644 src/helma/image/IRemoteImage.java create mode 100644 src/helma/image/ImageGenerator.java create mode 100644 src/helma/image/ImageWrapper.java create mode 100644 src/helma/image/RemoteImage.java create mode 100644 src/helma/image/Server.java create mode 100644 src/helma/image/SunImageWrapper.java create mode 100644 src/helma/mime/LanguageTag.java create mode 100644 src/helma/mime/MimeHeaderHolder.java create mode 100644 src/helma/mime/MimeHeaders.java create mode 100644 src/helma/mime/MimeHeadersFactory.java create mode 100644 src/helma/mime/MimeParser.java create mode 100644 src/helma/mime/MimeParserException.java create mode 100644 src/helma/mime/MimeParserFactory.java create mode 100644 src/helma/mime/MimeType.java create mode 100644 src/helma/mime/MimeTypeFormatException.java create mode 100644 src/helma/mime/MultipartInputStream.java create mode 100644 src/helma/mime/Utils.java create mode 100644 src/helma/objectmodel/ConcurrencyException.java create mode 100644 src/helma/objectmodel/DbMapping.java create mode 100644 src/helma/objectmodel/DbSource.java create mode 100644 src/helma/objectmodel/INode.java create mode 100644 src/helma/objectmodel/INodeListener.java create mode 100644 src/helma/objectmodel/IPathElement.java create mode 100644 src/helma/objectmodel/IProperty.java create mode 100644 src/helma/objectmodel/IServer.java create mode 100644 src/helma/objectmodel/Key.java create mode 100644 src/helma/objectmodel/Node.java create mode 100644 src/helma/objectmodel/NodeDataSource.java create mode 100644 src/helma/objectmodel/NodeEvent.java create mode 100644 src/helma/objectmodel/ObjectNotFoundException.java create mode 100644 src/helma/objectmodel/Property.java create mode 100644 src/helma/objectmodel/Relation.java create mode 100644 src/helma/objectmodel/SystemProperties.java create mode 100644 src/helma/objectmodel/db/ApplicationManager.java create mode 100644 src/helma/objectmodel/db/DbWrapper.java create mode 100644 src/helma/objectmodel/db/ExternalizableVector.java create mode 100644 src/helma/objectmodel/db/HopSocketFactory.java create mode 100644 src/helma/objectmodel/db/IDGenerator.java create mode 100644 src/helma/objectmodel/db/Node.java create mode 100644 src/helma/objectmodel/db/NodeManager.java create mode 100644 src/helma/objectmodel/db/Property.java create mode 100644 src/helma/objectmodel/db/Server.java create mode 100644 src/helma/objectmodel/db/Transactor.java create mode 100644 src/helma/objectmodel/db/WrappedNodeManager.java create mode 100644 src/helma/servlet/AcmeFileServlet.java create mode 100644 src/helma/servlet/AcmeServletClient.java create mode 100644 src/helma/servlet/ServletClient.java create mode 100644 src/helma/util/HtmlEncoder.java create mode 100644 src/helma/util/InetAddressFilter.java create mode 100644 src/helma/util/Logger.java create mode 100644 src/helma/util/ParanoidServerSocket.java create mode 100644 src/helma/util/Timer.java create mode 100644 src/helma/util/Uploader.java create mode 100644 src/helma/util/UrlEncoder.java create mode 100644 src/helma/util/WebBroadcaster.java create mode 100644 src/helma/xmlrpc/AuthenticatedXmlRpcHandler.java create mode 100644 src/helma/xmlrpc/Base64.java create mode 100644 src/helma/xmlrpc/Benchmark.java create mode 100644 src/helma/xmlrpc/ServerInputStream.java create mode 100644 src/helma/xmlrpc/WebServer.java create mode 100644 src/helma/xmlrpc/XmlRpc.java create mode 100644 src/helma/xmlrpc/XmlRpcClient.java create mode 100644 src/helma/xmlrpc/XmlRpcClientLite.java create mode 100644 src/helma/xmlrpc/XmlRpcException.java create mode 100644 src/helma/xmlrpc/XmlRpcHandler.java create mode 100644 src/helma/xmlrpc/XmlRpcProxyServlet.java create mode 100644 src/helma/xmlrpc/XmlRpcServer.java create mode 100644 src/helma/xmlrpc/XmlRpcServlet.java create mode 100644 src/helma/xmlrpc/fesi/FesiRpcExtension.java create mode 100644 src/helma/xmlrpc/fesi/FesiRpcServer.java create mode 100644 src/helma/xmlrpc/fesi/FesiRpcUtil.java diff --git a/src/helma/framework/ApplicationStoppedException.java b/src/helma/framework/ApplicationStoppedException.java new file mode 100644 index 00000000..1d359733 --- /dev/null +++ b/src/helma/framework/ApplicationStoppedException.java @@ -0,0 +1,21 @@ +// ApplicationStoppedException.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework; + +import FESI.Exceptions.EcmaScriptException; + +/** + * This is thrown when a request is made to a stopped + * application + */ + +public class ApplicationStoppedException extends RuntimeException { + + + public ApplicationStoppedException () { + super ("The application has been stopped"); + } + + +} diff --git a/src/helma/framework/FrameworkException.java b/src/helma/framework/FrameworkException.java new file mode 100644 index 00000000..847a497b --- /dev/null +++ b/src/helma/framework/FrameworkException.java @@ -0,0 +1,17 @@ +// FrameworkException.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework; + +/** + * The basic exception class used to tell when certain things go + * wrong in evaluation of requests. + */ + +public class FrameworkException extends RuntimeException { + + public FrameworkException (String msg) { + super (msg); + } + +} diff --git a/src/helma/framework/IRemoteApp.java b/src/helma/framework/IRemoteApp.java new file mode 100644 index 00000000..4970e536 --- /dev/null +++ b/src/helma/framework/IRemoteApp.java @@ -0,0 +1,20 @@ +// RemoteApp.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework; + +import java.rmi.*; + +/** + * RMI interface for an application. Currently only execute is used and supported. + */ + +public interface IRemoteApp extends Remote { + + public ResponseTrans execute (RequestTrans param) throws RemoteException; + + public ResponseTrans get (String path, String sessionID) throws RemoteException; + + public void ping () throws RemoteException; + + } diff --git a/src/helma/framework/RedirectException.java b/src/helma/framework/RedirectException.java new file mode 100644 index 00000000..21a54d00 --- /dev/null +++ b/src/helma/framework/RedirectException.java @@ -0,0 +1,34 @@ +// RedirectException.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework; + +import FESI.Exceptions.EcmaScriptException; + +/** + * RedirectException is thrown internally when a response is redirected to a + * new URL. + */ + +public class RedirectException extends EcmaScriptException { + + String url; + + public RedirectException (String url) { + super ("Redirection Request to "+url); + this.url = url; + } + + public String getMessage () { + return url; + } + + public void printStackTrace(java.io.PrintStream s) { + + } + + public void printStackTrace(java.io.PrintWriter w) { + + } + +} diff --git a/src/helma/framework/RequestTrans.java b/src/helma/framework/RequestTrans.java new file mode 100644 index 00000000..1828966d --- /dev/null +++ b/src/helma/framework/RequestTrans.java @@ -0,0 +1,52 @@ +// RequestTrans.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework; + +import java.io.*; +import java.util.*; +import helma.objectmodel.*; + +/** + * A Transmitter for a request from the servlet client. Objects of this + * class are directly exposed to JavaScript as global property req. + */ + +public class RequestTrans implements Serializable { + + public String path; + public String session; + private Hashtable values; + // this is used to hold the EcmaScript form data object + public transient Object data; + + public RequestTrans () { + super (); + values = new Hashtable (); + } + + public RequestTrans (Hashtable values) { + this.values = values; + } + + public void set (String name, Object value) { + values.put (name, value); + } + + public Enumeration keys () { + return values.keys (); + } + + public Object get (String name) { + try { + return values.get (name); + } catch (Exception x) { + return null; + } + } + + public Hashtable getReqData () { + return values; + } + +} diff --git a/src/helma/framework/ResponseTrans.java b/src/helma/framework/ResponseTrans.java new file mode 100644 index 00000000..64d7b108 --- /dev/null +++ b/src/helma/framework/ResponseTrans.java @@ -0,0 +1,207 @@ +// ResponseTrans.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework; + +import java.io.*; +import java.util.*; +import helma.objectmodel.*; +import helma.util.*; + +/** + * A Transmitter for a response to the servlet client. Objects of this + * class are directly exposed to JavaScript as global property res. + */ + +public class ResponseTrans implements Serializable { + + public String contentType = "text/html"; + // the page body + private char[] body = null; + // contains the redirect URL + public String redirect = null; + + // cookies + public String cookieKeys[]; + public String cookieValues[]; + public int cookieDays[]; + int nCookies = 0; + + // used to allow or disable client side caching + public boolean cache = true; + + // the buffer used to build the body + private transient StringBuffer buffer = null; + // these are used to implement the _as_string variants for Hop templates. + private transient Stack buffers; + + + public ResponseTrans () { + super (); + } + + public void reset () { + if (buffer != null) + buffer.setLength (0); + redirect = null; + } + + + /** + * This is called before a template is rendered as string (xxx_as_string) to redirect the output + * to a new string buffer. + */ + public void pushStringBuffer () { + if (buffers == null) + buffers = new Stack(); + if (buffer != null) + buffers.push (buffer); + buffer = new StringBuffer (128); + } + + /** + * Returns the content of the current string buffer and switches back to the previos one. + */ + public String popStringBuffer () { + StringBuffer b = buffer; + buffer = buffers.empty() ? null : (StringBuffer) buffers.pop (); + return b.toString (); + } + + /** + * Append a string to the response unchanged. + */ + public void write (Object what) { + if (what != null) { + if (buffer == null) + buffer = new StringBuffer (512); + buffer.append (what.toString ()); + } + } + + /** + * Replace special characters with entities, including <, > and ", thus allowing + * no HTML tags. + */ + public void encode (Object what) { + if (what != null) { + if (buffer == null) + buffer = new StringBuffer (512); + HtmlEncoder.encodeAll (what.toString (), buffer); + } + } + + /** + * Replace special characters with entities but leave <, > and ", allowing HTML tags + * in the response. + */ + public void format (Object what) { + if (what != null) { + if (buffer == null) + buffer = new StringBuffer (512); + HtmlEncoder.encode (what.toString (), buffer); + } + } + + + /** + * Replace special characters with entities, including <, > and ", thus allowing + * no HTML tags. + */ + public void encodeXml (Object what) { + if (what != null) { + if (buffer == null) + buffer = new StringBuffer (512); + HtmlEncoder.encodeXml (what.toString (), buffer); + } + } + + + public void append (String what) { + if (what != null) { + if (buffer == null) + buffer = new StringBuffer (512); + buffer.append (what); + } + } + + public void redirect (String url) throws RedirectException { + redirect = url; + throw new RedirectException (url); + } + + + /** + * This has to be called after writin to this response has finished and before it is shipped back to the + * web server. Transforms the string buffer into a char array to minimize size. + */ + public void close () { + if (buffer != null) + body = new String (buffer).toCharArray(); + } + + public String getContentString () { + return body == null ? "" : new String (body); + } + + public int getContentLength () { + if (body != null) + return body.length; + return 0; + } + + public String getContentType () { + return contentType; + } + + public synchronized void setCookie (String key, String value) { + setCookie (key, value, -1); + } + + public synchronized void setCookie (String key, String value, int days) { + if (nCookies == 0) { + cookieKeys = new String [3]; + cookieValues = new String [3]; + cookieDays = new int [3]; + } + if (nCookies == cookieKeys.length) { + String nk[] = new String [nCookies+3]; + System.arraycopy (cookieKeys, 0, nk, 0, nCookies); + String nv[] = new String [nCookies+3]; + System.arraycopy (cookieValues, 0, nv, 0, nCookies); + int nd[] = new int [nCookies+3]; + System.arraycopy (cookieDays, 0, nd, 0, nCookies); + cookieKeys = nk; + cookieValues = nv; + cookieDays = nd; + } + cookieKeys [nCookies] = key; + cookieValues [nCookies] = value; + cookieDays [nCookies] = days; + nCookies += 1; + } + + public void resetCookies () { + nCookies = 0; + } + + public int countCookies () { + return nCookies; + } + + public int getDaysAt (int i) { + return cookieDays[i]; + } + + public String getKeyAt (int i) { + return cookieKeys[i]; + } + + public String getValueAt (int i) { + return cookieValues[i]; + } + + +} + + diff --git a/src/helma/framework/TimeoutException.java b/src/helma/framework/TimeoutException.java new file mode 100644 index 00000000..fed5079d --- /dev/null +++ b/src/helma/framework/TimeoutException.java @@ -0,0 +1,18 @@ +// TimeoutException.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework; + +/** + * TimeoutException is thrown by the request evaluator when a request could + * not be serviced within the timeout period specified for an application. + */ + +public class TimeoutException extends RuntimeException { + public TimeoutException () { + super ("Request timed out"); + } +} + + + diff --git a/src/helma/framework/extensions/Database.java b/src/helma/framework/extensions/Database.java new file mode 100644 index 00000000..845a5046 --- /dev/null +++ b/src/helma/framework/extensions/Database.java @@ -0,0 +1,1066 @@ +// Database.java +// FESI Copyright (c) Jean-Marc Lugrin, 1999 +// +// This program 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 of the License, or (at your option) any later version. + +// This program 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, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +// Modified to use HOP database connections, Hannes Wallnöfer 2000 + +package helma.framework.extensions; + +import helma.objectmodel.*; +import FESI.Parser.*; +import FESI.AST.*; +import FESI.Interpreter.*; +import FESI.Exceptions.*; +import FESI.Extensions.*; +import FESI.Data.*; + +import java.util.Date; +import java.util.Enumeration; +import java.util.Vector; +import java.sql.*; + + +/** + * A Database object, representing a connection to a JDBC Driver + */ +class ESDatabase extends ESObject { + + private transient Connection connection = null; // Null if not connected + private transient DatabaseMetaData databaseMetaData = null; + private transient String driverName = null; + private transient ClassLoader driverLoader = null; + private transient Exception lastError = null; + private transient ESObject esRowSetPrototype = null; + private transient boolean driverOK = false; + + /** + * Create a new database object based on a hop data source. + * + * @param prototype The prototype object for all database object + * @param evaluator The current evaluator + * @param esRowSetPrototype The prototype to use to create row set + * @param dbsource The name of the DB source + */ + + ESDatabase(ESObject prototype, + Evaluator evaluator, + ESObject esRowSetPrototype, + String dbsource, int flag) { + super(prototype, evaluator); + this.esRowSetPrototype = esRowSetPrototype; // specific to an evaluator + try { + DbSource src = (DbSource) IServer.dbSources.get (dbsource.toLowerCase ()); + connection = src.getConnection (); + this.driverName = src.getDriverName (); + } catch (Exception e) { + // System.err.println("##Cannot find driver class: " + e); + // e.printStackTrace(); + lastError = e; + } + driverOK = true; + } + + /** + * Create a new database object based on a driver name, with driver on the classpath + * + * @param prototype The prototype object for all database object + * @param evaluator The current evaluator + * @param esRowSetPrototype The prototype to use to create row set + * @param driverName The class name of the JDBC driver + */ + + ESDatabase(ESObject prototype, + Evaluator evaluator, + ESObject esRowSetPrototype, + String driverName) { + super(prototype, evaluator); + this.driverName = driverName; + this.esRowSetPrototype = esRowSetPrototype; // specific to an evaluator + try { + Class driverClass = Class.forName(driverName); + if (!Driver.class.isAssignableFrom(driverClass)) { + + // System.err.println("##Bad class " + driverClass); + lastError = new EcmaScriptException("Class " + driverClass + " is not a JDBC driver"); + } + driverClass.newInstance(); // may be needed by some drivers, harmless for others + } catch (ClassNotFoundException e) { + // System.err.println("##Cannot find driver class: " + e); + // e.printStackTrace(); + lastError = e; + } catch (InstantiationException e) { + + // System.err.println("##Cannot instantiate driver class: " + e); + // e.printStackTrace(); + lastError = e; + } catch (IllegalAccessException e) { + // ignore as this may happen depending on driver, it may not be severe + // for example because the instance was created at class initialization time + } + driverOK = true; + } + + /** + * Create a new database object based on a driver name and its classpath + * + * @param prototype The prototype object for all database object + * @param evaluator The current evaluator + * @param esRowSetPrototype The prototype to use to create row set + * @param driverName The class name of the JDBC driver + * @param pathName The path to the classes of the JDBC driver + */ + ESDatabase(ESObject prototype, + Evaluator evaluator, + ESObject esRowSetPrototype, + String driverName, + String pathName) { + super(prototype, evaluator); + this.driverName = driverName; + this.esRowSetPrototype = esRowSetPrototype; + try { + this.driverLoader = LocalClassLoader.makeLocalClassLoader(pathName); + } catch (EcmaScriptException e) { + // System.err.println("##Cannot find driver class: " + e); + // e.printStackTrace(); + lastError = e; + return; + } + try { + Class driverClass = driverLoader.loadClass(driverName); + if (!Driver.class.isAssignableFrom(driverClass)) { + // System.err.println("##Bad class " + driverClass); + // e.printStackTrace(); + lastError = new EcmaScriptException("Class " + driverClass + " is not a JDBC driver"); + return; + } + driverClass.newInstance(); + } catch (ClassNotFoundException e) { + // System.err.println("##Cannot find driver class: " + e); + // e.printStackTrace(); + lastError = e; + } catch (InstantiationException e) { + // System.err.println("##Cannot instantiate driver class: " + e); + // e.printStackTrace(); + lastError = e; + } catch (IllegalAccessException e) { + // System.err.println("##Cannot access driver class: " + e); + // e.printStackTrace(); + lastError = e; + } + driverOK = true; + } + + /** + * Create the database prototype object which cannot be connected + * + * @param prototype The prototype object for the database prototype object + * @param evaluator The current evaluator + */ + + ESDatabase(ESObject prototype, + Evaluator evaluator) { + super(prototype, evaluator); + this.driverName = null; + this.esRowSetPrototype = null; + driverOK = false; // Avoid usage of this object + } + + public String getESClassName() { + return "Database"; + } + + public String toString() { + if (driverName==null) return "[database protoype]"; + return "[Database: '" + driverName + + (driverOK ? + (connection==null ? "' - disconnected] " : " - connected]") + : " - in error]"); + } + + public String toDetailString() { + return "ES:[Object: builtin " + this.getClass().getName() + ":" + + this.toString() + "]"; + } + + ESValue getLastError() throws EcmaScriptException { + if (lastError == null) { + return ESNull.theNull; + } else { + return ESLoader.normalizeValue(lastError, evaluator); + } + } + + + /** + * Connect to the database, using the specific url, optional user name and password + * + * @param arguments The argument list + * @return true if successful, false otherwise + */ + ESValue connect(ESValue arguments[]) throws EcmaScriptException { + if (!driverOK) { + throw new EcmaScriptException("Driver not initialized properly - cannot connect"); + } + lastError = null; + String url = (arguments.length>0) ? arguments[0].toString() : null; + String userName = (arguments.length>1) ? arguments[1].toString() : null; + String password = (arguments.length>2) ? arguments[2].toString() : null; + try { + if (userName == null) { + connection = DriverManager.getConnection(url); + } else { + connection = DriverManager.getConnection(url,userName,password); + } + } catch(Exception e) { + // System.err.println("##Cannot connect: " + e); + // e.printStackTrace(); + lastError = e; + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + + + /** + * Disconnect from the database, nop if not conected + * + * @return true if successful, false if error during idsconnect + */ + ESValue disconnect() throws EcmaScriptException { + if (!driverOK) { + throw new EcmaScriptException("Driver not initialized properly - cannot disconnect"); + } + lastError = null; + if (connection != null) { + try { + connection.close(); + connection = null; + lastError = null; + } catch (SQLException e) { + // System.err.println("##Cannot disonnect: " + e); + // e.printStackTrace(); + lastError = e; + return ESBoolean.makeBoolean(false); + } + } + return ESBoolean.makeBoolean(true); + } + + ESValue release() { + if (driverOK) { + try { + disconnect(); + } catch (EcmaScriptException e) { + // ignored + } + } + return ESUndefined.theUndefined; + } + + ESValue executeRetrieval(ESValue arguments[]) throws EcmaScriptException { + String sql = (arguments.length>0) ? arguments[0].toString() : null; + + if (connection==null) { + throw new EcmaScriptException("JDBC driver not connected"); + } + Statement statement = null; + ResultSet resultSet = null; + + try { + statement = connection.createStatement(); + resultSet = statement.executeQuery(sql); // will return true if first result is a result set + } catch(SQLException e) { + // System.err.println("##Cannot retrieve: " + e); + // e.printStackTrace(); + lastError = e; + try { + if (statement!=null) statement.close(); + } catch (Exception ignored) { + } + statement = null; + return ESBoolean.makeBoolean(false); + } + ESRowSet rowSet = new ESRowSet(esRowSetPrototype, evaluator, sql, this, statement, resultSet); + return rowSet; + //return ESBoolean.makeBoolean(true); + } + + ESValue executeCommand(ESValue arguments[]) throws EcmaScriptException { + String sql = (arguments.length>0) ? arguments[0].toString() : null; + int count = 0; + + if (connection==null) { + throw new EcmaScriptException("JDBC driver not connected"); + } + + Statement statement = null; + try { + + statement = connection.createStatement(); + count = statement.executeUpdate(sql); // will return true if first result is a result set + } catch(SQLException e) { + // System.err.println("##Cannot retrieve: " + e); + // e.printStackTrace(); + lastError = e; + try { + if (statement != null) statement.close(); + } catch (Exception ignored) { + } + statement = null; + return ESBoolean.makeBoolean(false); + } + if (statement!=null) try { + statement.close(); + } catch (SQLException e) { + // ignored + } + return new ESNumber(count); + //return ESBoolean.makeBoolean(true); + } + + public Object getMetaData() + { + if (databaseMetaData == null) + try { + databaseMetaData = connection.getMetaData(); + } catch (SQLException e) { + // ignored + } + return databaseMetaData; + } +} + + +/** + * A RowSet object + */ +class ESRowSet extends ESObject { + + private transient ESDatabase database = null; + private transient String sql = null; + private transient Statement statement = null; + private transient ResultSet resultSet = null; + private transient ResultSetMetaData resultSetMetaData = null; + private transient Vector colNames = null; + private transient boolean lastRowSeen = false; + private transient boolean firstRowSeen = false; + private transient Exception lastError = null; + + ESRowSet(ESObject prototype, + Evaluator evaluator, + String sql, + ESDatabase database, + Statement statement, + ResultSet resultSet) throws EcmaScriptException { + super(prototype, evaluator); + this.sql = sql; + this.database = database; + this.statement = statement; + this.resultSet = resultSet; + + if (sql==null) throw new NullPointerException(); + if (resultSet==null) throw new NullPointerException(); + if (statement==null) throw new NullPointerException(); + if (database==null) throw new NullPointerException(); + + try { + + this.resultSetMetaData = resultSet.getMetaData(); + int numcols = resultSetMetaData.getColumnCount(); + //IServer.getLogger().log("$$NEXT : " + numcols); + colNames = new Vector(numcols); + for (int i=0; i0 && idx <=colNames.size()) { + return (String) colNames.elementAt(idx-1); // to base 0 + } else { + throw new EcmaScriptException("Column index (base 1) " + idx + + " out of range, max: " +colNames.size()); + } + } + + + public int getColumnDatatypeNumber(int idx) throws EcmaScriptException { + if (resultSet == null) { + throw new EcmaScriptException("Attempt to access a released result set"); + } + if (idx>0 && idx <=colNames.size()) { + try { + return resultSetMetaData.getColumnType(idx); + } catch (SQLException e) { + lastError = e; + return -1; + } + } else { + throw new EcmaScriptException("Column index (base 1) " + idx + + " out of range, max: " +colNames.size()); + } + } + + + public String getColumnDatatypeName(int idx) throws EcmaScriptException { + if (resultSet == null) { + throw new EcmaScriptException("Attempt to access a released result set"); + } + if (idx>0 && idx <=colNames.size()) { + try { + return resultSetMetaData.getColumnTypeName(idx); + } catch (SQLException e) { + lastError = e; + return null; + } + } else { + throw new EcmaScriptException("Column index (base 1) " + idx + + " out of range, max: " +colNames.size()); + } + } + + + public ESValue getColumnItem(String propertyName) throws EcmaScriptException { + if (resultSet == null) { + throw new EcmaScriptException("Attempt to access a released result set"); + } + if (!firstRowSeen) { + throw new EcmaScriptException("Attempt to access data before the first row is read"); + } + int hash = propertyName.hashCode(); + try { + int index = -1; // indicates not a valid index value + try { + char c = propertyName.charAt(0); + if ('0' <= c && c <= '9') { + index = Integer.parseInt(propertyName); + } + } catch (NumberFormatException e) { + } catch (StringIndexOutOfBoundsException e) { // for charAt + } + if (index>=0) { + return getProperty(index); + } + Object o = resultSet.getObject(propertyName); + ESValue value = ESLoader.normalizeValue(o, evaluator); + // IServer.getLogger().log("&& @VALUE : " + value); + lastError = null; + return value; + } catch (SQLException e) { + //System.err.println("##Cannot get property '" + propertyName + "' " + e); + //e.printStackTrace(); + lastError = e; + } + return ESUndefined.theUndefined; + } + + public ESValue getProperty(String propertyName, int hash) + throws EcmaScriptException { + //System.err.println(" &&& Getting property '" + propertyName + "'"); + + // Length property is firsy checked + + // First return system or or prototype properties + if (propertyName.equals("length")) { + return new ESNumber((double) colNames.size()); + } else if (super.hasProperty(propertyName, hash)) { + return super.getProperty(propertyName, hash); + } else { + if (resultSet == null) { + throw new EcmaScriptException("Attempt to access a released result set"); + } + if (!firstRowSeen) { + throw new EcmaScriptException("Attempt to access data before the first row is read"); + } + try { + int index = -1; // indicates not a valid index value + try { + char c = propertyName.charAt(0); + if ('0' <= c && c <= '9') { + index = Integer.parseInt(propertyName); + } + } catch (NumberFormatException e) { + } catch (StringIndexOutOfBoundsException e) { // for charAt + } + if (index>=0) { + return getProperty(index); + } + Object o = resultSet.getObject(propertyName); + ESValue value = ESLoader.normalizeValue(o, evaluator); + // IServer.getLogger().log("&& @VALUE : " + value); + lastError = null; + return value; + } catch (SQLException e) { + // System.err.println("##Cannot get property '" + propertyName + "' " + e); + // e.printStackTrace(); + lastError = e; + } + } + return ESUndefined.theUndefined; + } + + public ESValue getProperty(int index) + throws EcmaScriptException { + if (!firstRowSeen) { + throw new EcmaScriptException("Attempt to access data before the first row is read"); + } + if (resultSet == null) { + throw new EcmaScriptException("Attempt to access a released result set"); + } + + try { + Object o = resultSet.getObject(index); + ESValue value = ESLoader.normalizeValue(o, evaluator); + lastError = null; + return value; + } catch (SQLException e) { + // System.err.println("##Cannot get property: " + e); + // e.printStackTrace(); + lastError = e; + } + return ESUndefined.theUndefined; + } + + /* + * Returns an enumerator for the key elements of this object. + * + * @return the enumerator - may have 0 length of coulmn names where not found + */ + public Enumeration getProperties() { + if (resultSet == null) { + return (new Vector()).elements(); + } + return colNames.elements(); + } + + /** + * Get all properties (including hidden ones), for the command + * @listall of the interpreter. Include the visible properties of the + * prototype (that is the one added by the user) but not the + * hidden ones of the prototype (otherwise this would list + * all functions for any object). + * + * @return An enumeration of all properties (visible and hidden). + */ + public Enumeration getAllProperties() { + return new Enumeration() { + String [] specialProperties = getSpecialPropertyNames(); + int specialEnumerator = 0; + Enumeration props = getProperties(); // all of object properties + String currentKey = null; + int currentHash = 0; + boolean inside = false; // true when examing prototypes properties + public boolean hasMoreElements() { + // OK if we already checked for a property and one exists + if (currentKey != null) return true; + // Loop on special properties first + if (specialEnumerator < specialProperties.length) { + currentKey = specialProperties[specialEnumerator]; + currentHash = currentKey.hashCode(); + specialEnumerator++; + return true; + } + // loop on standard or prototype properties + while (props.hasMoreElements()) { + currentKey = (String) props.nextElement(); + currentHash = currentKey.hashCode(); + if (inside) { + try { + if (hasProperty(currentKey, currentHash)) continue; + } catch (EcmaScriptException ignore) { + } + // SHOULD CHECK IF NOT IN SPECIAL + } + return true; + } + // If prototype properties have not yet been examined, look for them + if (!inside && getPrototype() != null) { + inside = true; + props = getPrototype().getProperties(); + while (props.hasMoreElements()) { + currentKey = (String) props.nextElement(); + currentHash = currentKey.hashCode(); + try { + if (hasProperty(currentKey, currentHash)) continue; + } catch (EcmaScriptException ignore) { + } + return true; + } + } + return false; + } + public Object nextElement() { + if (hasMoreElements()) { + String key = currentKey; + currentKey = null; + return key; + } else { + throw new java.util.NoSuchElementException(); + } + } + }; + } + + public String[] getSpecialPropertyNames() { + String [] ns = {"length"}; + return ns; + } + + + ESValue next() throws EcmaScriptException { + boolean status = false; + if (lastRowSeen) { + throw new EcmaScriptException("Attempt to access a next row after last row has been returned"); + } + if (resultSet == null) { + throw new EcmaScriptException("Attempt to access a released result set"); + } + try { + status = resultSet.next(); + lastError = null; + } catch (SQLException e) { + // System.err.println("##Cannot do next:" + e); + // e.printStackTrace(); + lastError = e; + } + if (status) firstRowSeen = true; + else lastRowSeen = true; + return ESBoolean.makeBoolean(status); + } + + public String toString() { + return "[RowSet: '"+sql+"'" + + (resultSet==null ? " - released]" : + (lastRowSeen ? " - at end]" : + (firstRowSeen ? "]" : " - at start]"))); + } + +} + + + + +public class Database extends Extension { + + + private transient Evaluator evaluator = null; + private ESObject esDatabasePrototype = null; + private ESObject esRowSetPrototype = null; + + public Database () { + super(); + } + + ////////////////// Added by Hannes Wallnoefer + class GlobalGetDBConnection extends BuiltinFunctionObject { + GlobalGetDBConnection(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) + throws EcmaScriptException { + if (arguments.length != 1) + throw new EcmaScriptException ("Wrong number of arguments in getDBConnection(dbsource)"); + ESDatabase db = new ESDatabase (esDatabasePrototype, this.evaluator, + esRowSetPrototype, arguments[0].toString(), 0); + return db; + } + } + + + class GlobalObjectDatabase extends BuiltinFunctionObject { + GlobalObjectDatabase(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + return doConstruct(thisObject, arguments); + } + + public ESObject doConstruct(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESDatabase database = null; + //IServer.getLogger().log("esDatabasePrototype="+esDatabasePrototype); + if (arguments.length==0) { + throw new EcmaScriptException("Database requires 1 or 2 arguments"); + } else if (arguments.length==1) { + database = new ESDatabase(esDatabasePrototype, + this.evaluator, + esRowSetPrototype, + arguments[0].toString()); + } else if (arguments.length>1) { + database = new ESDatabase(esDatabasePrototype, + this.evaluator, + esRowSetPrototype, + arguments[0].toString(), + arguments[1].toString()); + } + return database; + } + + + } + + + class DatabaseGetLastError extends BuiltinFunctionObject { + DatabaseGetLastError(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESDatabase database = (ESDatabase) thisObject; + return database.getLastError(); + } + } + + class DatabaseRelease extends BuiltinFunctionObject { + DatabaseRelease(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESDatabase database = (ESDatabase) thisObject; + return database.release(); + } + } + + + class DatabaseConnect extends BuiltinFunctionObject { + DatabaseConnect(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESDatabase database = (ESDatabase) thisObject; + return database.connect(arguments); + } + } + + + class DatabaseDisconnect extends BuiltinFunctionObject { + DatabaseDisconnect(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESDatabase database = (ESDatabase) thisObject; + return database.disconnect(); + } + } + + class DatabaseExecuteRetrieval extends BuiltinFunctionObject { + DatabaseExecuteRetrieval(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESDatabase database = (ESDatabase) thisObject; + return database.executeRetrieval(arguments); + } + } + + + class DatabaseExecuteCommand extends BuiltinFunctionObject { + DatabaseExecuteCommand(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESDatabase database = (ESDatabase) thisObject; + return database.executeCommand(arguments); + } + } + + + class DatabaseGetMetaData extends BuiltinFunctionObject { + DatabaseGetMetaData(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESDatabase database = (ESDatabase) thisObject; + return new ESWrapper(database.getMetaData(), this.evaluator); + } + } + + + + + + class RowSetRelease extends BuiltinFunctionObject { + RowSetRelease(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + return rowSet.release(); + } + } + + class RowSetNext extends BuiltinFunctionObject { + RowSetNext(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + return rowSet.next(); + } + } + + class RowSetGetColumnCount extends BuiltinFunctionObject { + RowSetGetColumnCount(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + return new ESNumber((double) rowSet.getColumnCount()); + } + } + + class RowSetGetColumnName extends BuiltinFunctionObject { + RowSetGetColumnName(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + if (arguments.length<1) { + throw new EcmaScriptException("Missing parameter in function " + this); + } + int idx = arguments[0].toUInt32(); + String name = rowSet.getColumnName(idx); // base 1 + if (name==null) { + return ESUndefined.theUndefined; + } else { + return new ESString(name); + } + } + } + + + class RowSetGetColumnItem extends BuiltinFunctionObject { + RowSetGetColumnItem(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + if (arguments.length<1) { + throw new EcmaScriptException("Missing parameter in function " + this); + } + String name = arguments[0].toString(); + return rowSet.getColumnItem(name); + } + } + + + + class RowSetGetColumnDatatypeNumber extends BuiltinFunctionObject { + RowSetGetColumnDatatypeNumber(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + if (arguments.length<1) { + throw new EcmaScriptException("Missing parameter in function " + this); + } + int idx = arguments[0].toUInt32(); + return new ESNumber((double)rowSet.getColumnDatatypeNumber(idx)); + } + } + + + class RowSetGetColumnDatatypeName extends BuiltinFunctionObject { + RowSetGetColumnDatatypeName(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + if (arguments.length<1) { + throw new EcmaScriptException("Missing parameter in function " + this); + } + int idx = arguments[0].toUInt32(); + String name = rowSet.getColumnDatatypeName(idx); + //IServer.getLogger().log("Datat type name for col " + idx + " is " +name); + if (name==null) { + return ESUndefined.theUndefined; + } else { + return new ESString(name); + } + } + } + + + class RowSetHasMoreRows extends BuiltinFunctionObject { + RowSetHasMoreRows(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + return ESBoolean.makeBoolean(rowSet.hasMoreRows()); + } + } + + + class RowSetGetMetaData extends BuiltinFunctionObject { + RowSetGetMetaData(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESRowSet rowSet = (ESRowSet) thisObject; + return new ESWrapper(rowSet.getMetaData(), this.evaluator); + } + } + + + public void initializeExtension(Evaluator evaluator) throws EcmaScriptException { + + this.evaluator = evaluator; + GlobalObject go = evaluator.getGlobalObject(); + ObjectPrototype op = (ObjectPrototype) evaluator.getObjectPrototype(); + FunctionPrototype fp = (FunctionPrototype) evaluator.getFunctionPrototype(); + + esRowSetPrototype = new ObjectPrototype(op, evaluator); + esDatabasePrototype = new ESDatabase(op, evaluator); // No driver + + ESObject globalDatabaseObject = new GlobalObjectDatabase("Database", evaluator, fp); + globalDatabaseObject.putHiddenProperty("prototype",esDatabasePrototype); + globalDatabaseObject.putHiddenProperty("length",new ESNumber(2)); + + + esDatabasePrototype.putHiddenProperty("constructor",globalDatabaseObject); + esDatabasePrototype.putHiddenProperty("getLastError", + new DatabaseGetLastError("getLastError", evaluator, fp)); + esDatabasePrototype.putHiddenProperty("release", + new DatabaseRelease("release", evaluator, fp)); + esDatabasePrototype.putHiddenProperty("connect", + new DatabaseConnect("connect", evaluator, fp)); + esDatabasePrototype.putHiddenProperty("disconnect", + new DatabaseDisconnect("disconnect", evaluator, fp)); + esDatabasePrototype.putHiddenProperty("executeRetrieval", + new DatabaseExecuteRetrieval("executeRetrieval", evaluator, fp)); + esDatabasePrototype.putHiddenProperty("executeCommand", + new DatabaseExecuteCommand("executeCommand", evaluator, fp)); + esDatabasePrototype.putHiddenProperty("getMetaData", + new DatabaseGetMetaData("getMetaData", evaluator, fp)); + + esRowSetPrototype.putHiddenProperty("release", + new RowSetRelease("release", evaluator, fp)); + esRowSetPrototype.putHiddenProperty("next", + new RowSetNext("next", evaluator, fp)); + esRowSetPrototype.putHiddenProperty("getColumnCount", + new RowSetGetColumnCount("getColumnCount", evaluator, fp)); + esRowSetPrototype.putHiddenProperty("getColumnName", + new RowSetGetColumnName("getColumnName", evaluator, fp)); + esRowSetPrototype.putHiddenProperty("getColumnItem", + new RowSetGetColumnItem("getColumnItem", evaluator, fp)); + esRowSetPrototype.putHiddenProperty("getColumnDatatypeNumber", + new RowSetGetColumnDatatypeNumber("getColumnDatatypeNumber", evaluator, fp)); + esRowSetPrototype.putHiddenProperty("getColumnDatatypeName", + new RowSetGetColumnDatatypeName("getColumnDatatypeName", evaluator, fp)); + esRowSetPrototype.putHiddenProperty("hasMoreRows", + new RowSetHasMoreRows("hasMoreRows", evaluator, fp)); + esRowSetPrototype.putHiddenProperty("getMetaData", + new RowSetGetMetaData("getMetaData", evaluator, fp)); + + go.putHiddenProperty("Database", globalDatabaseObject); + // added by Hannes Wallnoefer + go.putHiddenProperty ("getDBConnection", new GlobalGetDBConnection ("getDBConnection", evaluator, fp)); + } + } diff --git a/src/helma/framework/extensions/ESMail.java b/src/helma/framework/extensions/ESMail.java new file mode 100644 index 00000000..3db40051 --- /dev/null +++ b/src/helma/framework/extensions/ESMail.java @@ -0,0 +1,214 @@ +// ESMail.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + + +package helma.framework.extensions; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; +import java.io.*; +import java.util.*; +import helma.framework.core.*; +import helma.objectmodel.*; +import FESI.Data.*; +import FESI.Interpreter.*; +import FESI.Exceptions.*; + +/** + * A JavaScript wrapper around a JavaMail message class to send + * mail via SMTP from HOP + */ + +public class ESMail extends ESObject implements Serializable { + + INode node; + MailExtension mailx; + Properties mprops; + MimeMessage message; + Multipart multipart; + StringBuffer buffer; + + int status; + public static final int OK=0; + public static final int SUBJECT=10; + public static final int TEXT=11; + public static final int MIMEPART=12; + public static final int TO=20; + public static final int CC=21; + public static final int BCC=22; + public static final int FROM=23; + public static final int REPLYTO=24; + public static final int SEND=30; + + + public ESMail (MailExtension mailx) { + + super (mailx.esMailPrototype, mailx.evaluator); + this.status = OK; + this.mailx = mailx; + this.mprops = mailx.mprops; + + // create some properties and get the default Session + try { + Properties props = new Properties(); + props.put ("mail.smtp.host", mprops.getProperty ("smtp", "mail")); + + Session session = Session.getDefaultInstance(props, null); + message = new MimeMessage (session); + } catch (Throwable t) { + IServer.getLogger().log ("caught in mail constructor: "+t); + } + } + + public void setStatus (int status) { + // Only register the first error that occurrs + if (this.status == 0) + this.status = status; + } + + public int getStatus () { + return status; + } + + public ESValue getProperty(String propertyName, int hash) throws EcmaScriptException { + if ("status".equalsIgnoreCase (propertyName)) + return new ESNumber (status); + return super.getProperty (propertyName, hash); + } + + /** + * + */ + + public void setText (ESValue val) throws Exception { + if (buffer == null) + buffer = new StringBuffer (); + if (val != null) + buffer.append (val.toString ()); + } + + public void addPart (ESValue val[]) throws Exception { + if (val == null || val.length == 0) return; + if (multipart == null) { + multipart = new MimeMultipart (); + } + for (int i=0; i 1) + address = new InternetAddress (addstring, MimeUtility.encodeWord (add[1].toString (), "iso-8859-1", null)); + else + address = new InternetAddress (addstring); + message.setFrom (address); + } + + public void addTo (ESValue add[]) throws Exception { + String addstring = add[0].toString (); + if (addstring.indexOf ("@") < 0) + throw new AddressException (); + Address address = null; + if (add.length > 1) + address = new InternetAddress (addstring, MimeUtility.encodeWord (add[1].toString (), "iso-8859-1", null)); + else + address = new InternetAddress (addstring); + message.addRecipient (Message.RecipientType.TO, address); + } + + public void addCC (ESValue add[]) throws Exception { + String addstring = add[0].toString (); + if (addstring.indexOf ("@") < 0) + throw new AddressException (); + Address address = null; + if (add.length > 1) + address = new InternetAddress (addstring, MimeUtility.encodeWord (add[1].toString (), "iso-8859-1", null)); + else + address = new InternetAddress (addstring); + message.addRecipient (Message.RecipientType.CC, address); + } + + public void addBCC (ESValue add[]) throws Exception { + String addstring = add[0].toString (); + if (addstring.indexOf ("@") < 0) + throw new AddressException (); + Address address = null; + if (add.length > 1) + address = new InternetAddress (addstring, MimeUtility.encodeWord (add[1].toString (), "iso-8859-1", null)); + else + address = new InternetAddress (addstring); + message.addRecipient (Message.RecipientType.BCC, address); + } + + public void send () throws Exception { + if (buffer != null) + message.setText (buffer.toString ()); + else if (multipart != null) + message.setContent (multipart); + else + message.setText (""); + Transport.send (message); + } + + + private final INode getNode (Object obj) { + if (obj == null) + return null; + if (obj instanceof ESNode) + return ((ESNode) obj).getNode (); + if (obj instanceof ESWrapper) { + Object n = ((ESWrapper) obj).getJavaObject(); + if (n instanceof INode) + return (INode) n; + } + return null; + } + + +} + + + + + + + + + + + + diff --git a/src/helma/framework/extensions/FtpExtension.java b/src/helma/framework/extensions/FtpExtension.java new file mode 100644 index 00000000..136a9b33 --- /dev/null +++ b/src/helma/framework/extensions/FtpExtension.java @@ -0,0 +1,419 @@ +// FtpExtension.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework.extensions; + +import helma.objectmodel.*; +import FESI.Parser.*; +import FESI.AST.*; +import FESI.Interpreter.*; +import FESI.Exceptions.*; +import FESI.Extensions.*; +import FESI.Data.*; +import java.io.*; + +import com.oroinc.net.ftp.*; + + +/** + * A FTP-client object that allows to do some FTP from HOP applications. + * FTP support is far from complete but can easily be extended if more + * functionality is needed. + * This uses the NetComponent classes from savarese.org (ex oroinc.com). + */ + +class ESFtpClient extends ESObject { + + private FTPClient ftpclient; + private String server; + private Exception lastError = null; + private File localDir = null; + + + /** + * Create a new FTP Client + * + * @param prototype The prototype object for the FTP object + * @param evaluator The current evaluator + */ + ESFtpClient(ESObject prototype, Evaluator evaluator, ESValue srvstr) { + super(prototype, evaluator); + this.server = srvstr.toString (); + } + + ESFtpClient(ESObject prototype, Evaluator evaluator) { + super(prototype, evaluator); + } + + + public String getESClassName() { + return "FtpClient"; + } + + public String toString() { + return "[FtpClient]"; + } + + public String toDetailString() { + return "ES:[Object: builtin " + this.getClass().getName() + ":" + + this.toString() + "]"; + } + + ESValue getLastError() throws EcmaScriptException { + if (lastError == null) { + return ESNull.theNull; + } else { + return ESLoader.normalizeValue(lastError, evaluator); + } + } + + + /** + * Login to the FTP server + * + * @param arguments The argument list + * @return true if successful, false otherwise + */ + ESValue login(ESValue arguments[]) throws EcmaScriptException { + if (server == null) + return ESBoolean.makeBoolean(false); + try { + ftpclient = new FTPClient (); + ftpclient.connect (server); + ftpclient.login (arguments[0].toString(), arguments[1].toString()); + return ESBoolean.makeBoolean (true); + } catch (Exception x) { + return ESBoolean.makeBoolean (false); + } catch (NoClassDefFoundError x) { + return ESBoolean.makeBoolean (false); + } + } + + ESValue cd (ESValue arguments[]) throws EcmaScriptException { + if (ftpclient == null) + return ESBoolean.makeBoolean(false); + try { + ftpclient.changeWorkingDirectory (arguments[0].toString ()); + return ESBoolean.makeBoolean(true); + } catch (Exception wrong) {} + return ESBoolean.makeBoolean(false); + } + + + ESValue mkdir (ESValue arguments[]) throws EcmaScriptException { + if (ftpclient == null) + return ESBoolean.makeBoolean(false); + try { + return ESBoolean.makeBoolean(ftpclient.makeDirectory (arguments[0].toString ())); + } catch (Exception wrong) {} + return ESBoolean.makeBoolean(false); + } + + ESValue lcd (ESValue arguments[]) throws EcmaScriptException { + try { + localDir = new File (arguments[0].toString()); + if (!localDir.exists()) + localDir.mkdirs(); + return ESBoolean.makeBoolean(true); + } catch (Exception wrong) {} + return ESBoolean.makeBoolean(false); + } + + ESValue putFile(ESValue arguments[]) throws EcmaScriptException { + if (ftpclient == null) + return ESBoolean.makeBoolean(false); + try { + String fn = arguments[0].toString(); + File f = localDir == null ? new File (fn) : new File (localDir, fn); + InputStream fin = new BufferedInputStream (new FileInputStream (f)); + ftpclient.storeFile (arguments[1].toString (), fin); + fin.close (); + return ESBoolean.makeBoolean(true); + } catch (Exception wrong) {} + return ESBoolean.makeBoolean(false); + } + + ESValue putString(ESValue arguments[]) throws EcmaScriptException { + if (ftpclient == null) + return ESBoolean.makeBoolean(false); + try { + byte[] bytes = null; + // check if this already is a byte array + if (arguments[0] instanceof ESArrayWrapper) { + Object o = ((ESArrayWrapper) arguments[0]).toJavaObject (); + if (o instanceof byte[]) + bytes = (byte[]) o; + } + if (bytes == null) + bytes = arguments[0].toString().getBytes(); + ByteArrayInputStream bin = new ByteArrayInputStream (bytes); + ftpclient.storeFile (arguments[1].toString (), bin); + return ESBoolean.makeBoolean(true); + } catch (Exception wrong) {} + return ESBoolean.makeBoolean(false); + } + + + ESValue getFile(ESValue arguments[]) throws EcmaScriptException { + if (ftpclient == null ) + return ESBoolean.makeBoolean(false); + try { + String fn = arguments[0].toString(); + File f = localDir == null ? new File (fn) : new File (localDir, fn); + OutputStream out = new BufferedOutputStream (new FileOutputStream(f)); + ftpclient.retrieveFile (arguments[0].toString (), out); + out.close (); + return ESBoolean.makeBoolean(true); + } catch (Exception wrong) {} + return ESBoolean.makeBoolean(false); + } + + ESValue getString(ESValue arguments[]) throws EcmaScriptException { + if (ftpclient == null ) + return ESNull.theNull; + try { + ByteArrayOutputStream bout = new ByteArrayOutputStream (); + ftpclient.retrieveFile (arguments[0].toString (), bout); + return new ESString (bout.toString ()); + } catch (Exception wrong) {} + return ESNull.theNull; + } + + /** + * Disconnect from FTP server + * + * @param arguments The argument list + * @return true if successful, false otherwise + */ + ESValue logout (ESValue arguments[]) throws EcmaScriptException { + if (ftpclient != null) { + try { + ftpclient.logout (); + } catch (IOException ignore) {} + try { + ftpclient.disconnect (); + } catch (IOException ignore) {} + } + return ESBoolean.makeBoolean (true); + } + + ESValue binary (ESValue arguments[]) throws EcmaScriptException { + if (ftpclient != null) { + try { + ftpclient.setFileType (FTP.BINARY_FILE_TYPE); + return ESBoolean.makeBoolean (true); + } catch (IOException ignore) {} + } + return ESBoolean.makeBoolean (false); + } + + ESValue ascii (ESValue arguments[]) throws EcmaScriptException { + if (ftpclient != null) { + try { + ftpclient.setFileType (FTP.ASCII_FILE_TYPE); + return ESBoolean.makeBoolean (true); + } catch (IOException ignore) {} + } + return ESBoolean.makeBoolean (false); + } + + +} + + +public class FtpExtension extends Extension { + + private transient Evaluator evaluator = null; + private ESObject esFtpPrototype = null; + + public FtpExtension () { + super(); + } + + class GlobalObjectFtpClient extends BuiltinFunctionObject { + GlobalObjectFtpClient(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) + throws EcmaScriptException { + return doConstruct(thisObject, arguments); + } + + public ESObject doConstruct(ESObject thisObject, ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = null; + if (arguments.length != 1) + throw new EcmaScriptException("FtpClient requires 1 argument"); + ftp = new ESFtpClient (esFtpPrototype, + this.evaluator, + arguments[0]); + return ftp; + } + + } + + + class FtpClientLogin extends BuiltinFunctionObject { + FtpClientLogin(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.login (arguments); + } + } + + class FtpClientCD extends BuiltinFunctionObject { + FtpClientCD(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.cd (arguments); + } + } + + class FtpClientMKDIR extends BuiltinFunctionObject { + FtpClientMKDIR(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.mkdir (arguments); + } + } + + class FtpClientLCD extends BuiltinFunctionObject { + FtpClientLCD(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.lcd (arguments); + } + } + + class FtpClientPutFile extends BuiltinFunctionObject { + FtpClientPutFile (String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.putFile (arguments); + } + } + + class FtpClientPutString extends BuiltinFunctionObject { + FtpClientPutString (String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.putString (arguments); + } + } + + class FtpClientGetFile extends BuiltinFunctionObject { + FtpClientGetFile (String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.getFile (arguments); + } + } + + class FtpClientGetString extends BuiltinFunctionObject { + FtpClientGetString (String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.getString (arguments); + } + } + + class FtpClientLogout extends BuiltinFunctionObject { + FtpClientLogout(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.logout (arguments); + } + } + + class FtpClientBinary extends BuiltinFunctionObject { + FtpClientBinary(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.binary (arguments); + } + } + + class FtpClientAscii extends BuiltinFunctionObject { + FtpClientAscii(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, + ESValue[] arguments) + throws EcmaScriptException { + ESFtpClient ftp = (ESFtpClient) thisObject; + return ftp.ascii (arguments); + } + } + + + public void initializeExtension(Evaluator evaluator) throws EcmaScriptException { + + this.evaluator = evaluator; + GlobalObject go = evaluator.getGlobalObject(); + ObjectPrototype op = (ObjectPrototype) evaluator.getObjectPrototype(); + FunctionPrototype fp = (FunctionPrototype) evaluator.getFunctionPrototype(); + + esFtpPrototype = new ESFtpClient (op, evaluator); + + ESObject globalFtpObject = new GlobalObjectFtpClient("FtpClient", evaluator, fp); + globalFtpObject.putHiddenProperty("prototype",esFtpPrototype); + globalFtpObject.putHiddenProperty("length",new ESNumber(1)); + + + esFtpPrototype.putHiddenProperty("login", new FtpClientLogin("login", evaluator, fp)); + esFtpPrototype.putHiddenProperty("cd", new FtpClientCD("cd", evaluator, fp)); + esFtpPrototype.putHiddenProperty("mkdir", new FtpClientMKDIR("mkdir", evaluator, fp)); + esFtpPrototype.putHiddenProperty("lcd", new FtpClientLCD("lcd", evaluator, fp)); + esFtpPrototype.putHiddenProperty("putFile", new FtpClientPutFile("putFile", evaluator, fp)); + esFtpPrototype.putHiddenProperty("putString", new FtpClientPutString("putString", evaluator, fp)); + esFtpPrototype.putHiddenProperty("getFile", new FtpClientGetFile("getFile", evaluator, fp)); + esFtpPrototype.putHiddenProperty("getString", new FtpClientGetString("getString", evaluator, fp)); + esFtpPrototype.putHiddenProperty("logout", new FtpClientLogout("logout", evaluator, fp)); + esFtpPrototype.putHiddenProperty("binary", new FtpClientBinary("binary", evaluator, fp)); + esFtpPrototype.putHiddenProperty("ascii", new FtpClientAscii("ascii", evaluator, fp)); + + go.putHiddenProperty("FtpClient", globalFtpObject); + + } + } diff --git a/src/helma/framework/extensions/ImageExtension.java b/src/helma/framework/extensions/ImageExtension.java new file mode 100644 index 00000000..50d7bc77 --- /dev/null +++ b/src/helma/framework/extensions/ImageExtension.java @@ -0,0 +1,127 @@ +// ImageExtension.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework.extensions; + +import helma.objectmodel.*; +import helma.util.*; +import helma.image.*; + +import FESI.Interpreter.*; +import FESI.Exceptions.*; +import FESI.Extensions.*; +import FESI.Data.*; + +import java.io.*; +import java.util.*; +import java.rmi.Naming; + + +/** + * Extension to do Image manipulation from HOP. + */ + +public class ImageExtension extends Extension { + + protected Evaluator evaluator = null; + + static boolean remote = false; + + + public ImageExtension () { + super(); + } + + + class GlobalObjectImage extends BuiltinFunctionObject { + + ImageExtension imagex; + ImageGenerator imggen; + + GlobalObjectImage (String name, Evaluator evaluator, FunctionPrototype fp, ImageExtension imagex) { + super(fp, evaluator, name, 1); + this.imagex = imagex; + } + + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + return doConstruct(thisObject, arguments); + } + + public ESObject doConstruct(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + Object img = null; + IRemoteGenerator rgen = null; + + try { + if (imggen == null && !remote) { + try { + imggen = new ImageGenerator (); + } catch (UnsatisfiedLinkError noawt) { + remote = true; + } + } + + if (remote) + rgen = (IRemoteGenerator) Naming.lookup ("//localhost:3033/server"); + + if (arguments.length == 1) { + if (arguments[0] instanceof ESArrayWrapper) { + Object obj = ((ESArrayWrapper) arguments[0]).toJavaObject (); + if (obj instanceof byte[]) { + img = remote ? + (Object) rgen.createImage ((byte[]) obj) : + (Object) imggen.createImage ((byte[]) obj); + } + } else if (arguments[0] instanceof ESString) { + String imgurl = arguments[0].toString (); + img = remote ? + (Object) rgen.createPaintableImage (imgurl) : + (Object) imggen.createPaintableImage (imgurl); + } + } else if (arguments.length == 2) { + if (arguments[0].isNumberValue () && arguments[1].isNumberValue ()) { + img = remote ? + (Object) rgen.createPaintableImage (arguments[0].toInt32(), arguments[1].toInt32()) : + (Object) imggen.createPaintableImage (arguments[0].toInt32(), arguments[1].toInt32()); + } + } + } catch (Exception error) { + System.err.println ("Error creating Image: "+error); + } + + if (img == null) + throw new EcmaScriptException ("Error creating image: Bad parameters or setup problem."); + + return new ESWrapper (img, this.evaluator); + } + } + + + /** + * Called by the evaluator after the extension is loaded. + */ + public void initializeExtension(Evaluator evaluator) throws EcmaScriptException { + + this.evaluator = evaluator; + GlobalObject go = evaluator.getGlobalObject(); + FunctionPrototype fp = (FunctionPrototype) evaluator.getFunctionPrototype(); + + ESObject image = new GlobalObjectImage ("Image", evaluator, fp, this); // the Image constructor + + go.putHiddenProperty("Image", image); // register the constructor for a Image object. + + } + + + } + + + + + + + + + + + + diff --git a/src/helma/framework/extensions/MailExtension.java b/src/helma/framework/extensions/MailExtension.java new file mode 100644 index 00000000..55d91591 --- /dev/null +++ b/src/helma/framework/extensions/MailExtension.java @@ -0,0 +1,255 @@ +// MailExtension.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.framework.extensions; + +import helma.objectmodel.*; +import helma.util.*; + +import FESI.Interpreter.*; +import FESI.Exceptions.*; +import FESI.Extensions.*; +import FESI.Data.*; + +import java.io.*; +import java.util.*; + + +/** + * Extension to create and send mail messages via SMTP from HOP applications + */ + +public class MailExtension extends Extension { + + + protected Evaluator evaluator = null; + protected ObjectPrototype esMailPrototype = null; + protected Properties mprops; + + + public MailExtension () { + super(); + } + + public void setProperties (Properties props) { + this.mprops = props; + } + + /** + * Called by the evaluator after the extension is loaded. + */ + public void initializeExtension(Evaluator evaluator) throws EcmaScriptException { + + this.evaluator = evaluator; + GlobalObject go = evaluator.getGlobalObject(); + FunctionPrototype fp = (FunctionPrototype) evaluator.getFunctionPrototype(); + + ESObject op = evaluator.getObjectPrototype(); + esMailPrototype = new ObjectPrototype(op, evaluator); // the Node prototype + + ESObject mail = new GlobalObjectMail ("Mail", evaluator, fp, this); // the Mail constructor + + go.putHiddenProperty("Mail", mail); // register the constructor for a Mail object. + + + // methods for sending mail from JS... + ESObject p = new MailSetText ("setText", evaluator, fp); + esMailPrototype.putHiddenProperty("setText", p); + esMailPrototype.putHiddenProperty("addText", p); + + esMailPrototype.putHiddenProperty("addPart", new MailAddPart ("addPart", evaluator, fp)); + esMailPrototype.putHiddenProperty("setSubject", new MailSetSubject ("setSubject", evaluator, fp)); + esMailPrototype.putHiddenProperty("setReplyTo", new MailSetReplyTo ("setReplyTo", evaluator, fp)); + esMailPrototype.putHiddenProperty("setFrom", new MailSetFrom ("setFrom", evaluator, fp)); + + p = new MailAddTo ("addTo", evaluator, fp); + esMailPrototype.putHiddenProperty("addTo", p); + esMailPrototype.putHiddenProperty("setTo", p); + + p = new MailAddCC ("addCC", evaluator, fp); + esMailPrototype.putHiddenProperty("addCC", p); + esMailPrototype.putHiddenProperty("setCC", p); + + p = new MailAddBCC ("addBCC", evaluator, fp); + esMailPrototype.putHiddenProperty("addBCC", p); + esMailPrototype.putHiddenProperty("setBCC", p); + + esMailPrototype.putHiddenProperty("send", new MailSend ("send", evaluator, fp)); + + } + + + class GlobalObjectMail extends BuiltinFunctionObject { + + MailExtension mailx; + + GlobalObjectMail (String name, Evaluator evaluator, FunctionPrototype fp, MailExtension mailx) { + super(fp, evaluator, name, 1); + this.mailx = mailx; + } + + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + return doConstruct(thisObject, arguments); + } + + public ESObject doConstruct(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = null; + + if (arguments.length == 0) { + mail = new ESMail (mailx); + } else { + mail = new ESMail (mailx); + // should/could do something with extra arguments... + } + return mail; + } + } + + + class MailSetText extends BuiltinFunctionObject { + MailSetText (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + if (arguments.length == 1) try { + mail.setText (arguments[0]); + } catch (Exception x) { + mail.setStatus (ESMail.TEXT); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + + class MailAddPart extends BuiltinFunctionObject { + MailAddPart (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + try { + mail.addPart (arguments); + } catch (Exception x) { + mail.setStatus (ESMail.MIMEPART); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + + class MailSetSubject extends BuiltinFunctionObject { + MailSetSubject (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + if (arguments.length == 1) try { + mail.setSubject (arguments[0]); + } catch (Exception x) { + mail.setStatus (ESMail.SUBJECT); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + + class MailSetReplyTo extends BuiltinFunctionObject { + MailSetReplyTo (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + if (arguments.length == 1) try { + mail.setReplyTo (arguments[0]); + } catch (Exception x) { + mail.setStatus (ESMail.REPLYTO); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + + class MailSetFrom extends BuiltinFunctionObject { + MailSetFrom (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + try { + mail.setFrom (arguments); + } catch (Exception x) { + mail.setStatus (ESMail.FROM); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + + class MailAddTo extends BuiltinFunctionObject { + MailAddTo (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + try { + mail.addTo (arguments); + } catch (Exception x) { + mail.setStatus (ESMail.TO); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + + class MailAddCC extends BuiltinFunctionObject { + MailAddCC (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + try { + mail.addCC (arguments); + } catch (Exception x) { + mail.setStatus (ESMail.CC); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + + class MailAddBCC extends BuiltinFunctionObject { + MailAddBCC (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + try { + mail.addBCC (arguments); + } catch (Exception x) { + mail.setStatus (ESMail.BCC); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + + class MailSend extends BuiltinFunctionObject { + MailSend (String name, Evaluator evaluator, FunctionPrototype fp) { + super (fp, evaluator, name, 1); + } + public ESValue callFunction (ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESMail mail = (ESMail) thisObject; + try { + mail.send (); + } catch (Exception x) { + IServer.getLogger().log ("Error sending mail: "+x); + mail.setStatus (ESMail.SEND); + return ESBoolean.makeBoolean(false); + } + return ESBoolean.makeBoolean(true); + } + } + +} + diff --git a/src/helma/image/ActivatedImageWrapper.java b/src/helma/image/ActivatedImageWrapper.java new file mode 100644 index 00000000..14e42318 --- /dev/null +++ b/src/helma/image/ActivatedImageWrapper.java @@ -0,0 +1,47 @@ +// ActivatedImageWrapper.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.image; + +import java.awt.*; +import java.awt.image.*; +import com.activated.jimi.*; +import com.activated.jimi.util.*; + +/** + * A wrapper for an image that uses the Activated version of JIMI. + */ + +public class ActivatedImageWrapper extends ImageWrapper { + + public ActivatedImageWrapper (Image img, Graphics g, int width, int height, + ImageGenerator imggen) throws ClassNotFoundException { + super (img, g, width, height, imggen); + Class.forName ("com.activated.jimi.Jimi"); + } + + + public void reduceColors (int colors) { + try { + ColorReducer redux = new ColorReducer (colors, true); + img = redux.getColorReducedImage (img); + } catch (Exception x) { + throw new RuntimeException (x.getMessage ()); + } + } + + public void saveAs (String filename) { + try { + Jimi.putImage (img, filename); + } catch (JimiException x) { + throw new RuntimeException (x.getMessage ()); + } + } + + +} + + + + + diff --git a/src/helma/image/IRemoteGenerator.java b/src/helma/image/IRemoteGenerator.java new file mode 100644 index 00000000..4e81de65 --- /dev/null +++ b/src/helma/image/IRemoteGenerator.java @@ -0,0 +1,24 @@ +// IRemoteGenerator.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.image; + +import java.util.*; +import java.rmi.*; +import java.io.*; + +/** + * RMI interface for accessing remote image generators. + */ + +public interface IRemoteGenerator extends Remote { + + public IRemoteImage createPaintableImage (int w, int h) throws RemoteException; + + public IRemoteImage createPaintableImage (byte src[]) throws RemoteException; + + public IRemoteImage createPaintableImage (String urlstring) throws RemoteException; + + public IRemoteImage createImage (byte src[]) throws RemoteException; + +} diff --git a/src/helma/image/IRemoteImage.java b/src/helma/image/IRemoteImage.java new file mode 100644 index 00000000..1e57ae90 --- /dev/null +++ b/src/helma/image/IRemoteImage.java @@ -0,0 +1,37 @@ +// ActivatedImageWrapper.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.image; + +import java.util.*; +import java.rmi.*; +import java.io.*; + +/** + * RMI interface for accessing images on remote image servers. + */ + +public interface IRemoteImage extends Remote { + + public void setFont (String name, int style, int size) throws RemoteException; + public void setColor (int color) throws RemoteException; + public void setColor (int r, int g, int b) throws RemoteException; + public void reduceColors (int colors) throws RemoteException; + + public void drawString (String str, int x, int y) throws RemoteException; + public void drawRect (int x, int y, int w, int h) throws RemoteException; + public void drawLine (int x1, int y1, int x2, int y2) throws RemoteException; + public void fillRect (int x, int y, int w, int h) throws RemoteException; + + public int getWidth () throws RemoteException; + public int getHeight () throws RemoteException; + public void crop (int x, int y, int w, int h) throws RemoteException; + public void resize (int w, int h) throws RemoteException; + + public void saveAs (String filename) throws RemoteException; + public void readFrom (String filename) throws RemoteException; + + public byte[] getBytes (String type) throws RemoteException; + public void setBytes (byte[] bytes, String type) throws RemoteException; + +} \ No newline at end of file diff --git a/src/helma/image/ImageGenerator.java b/src/helma/image/ImageGenerator.java new file mode 100644 index 00000000..7457ddc1 --- /dev/null +++ b/src/helma/image/ImageGenerator.java @@ -0,0 +1,133 @@ +// ImageGenerator.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.image; + +import java.awt.*; +import java.net.URL; + +/** + * This creates an invisible frame in order to be able to create images + * from Java. (Java needs a window context in order to user the Image class). + */ + +public class ImageGenerator extends Window { + + public ImageGenerator () { + super (new Frame() { + public void setVisible (boolean b) { + // This frame can never be shown + } + public synchronized void dispose() { + try { + getToolkit().getSystemEventQueue(); + super.dispose(); + } catch (Exception e) { + // untrusted code not allowed to dispose + } + } + } + ); + this.setBounds (0, 0, 0, 0); + this.setVisible (true); + } + + public ImageWrapper createPaintableImage (int w, int h) { + Image img = createImage (w, h); + Graphics g = img.getGraphics (); + ImageWrapper rimg = null; + try { + try { + rimg = new ActivatedImageWrapper (img, g, w, h, this); + } catch (NoClassDefFoundError notfound) { + rimg = new SunImageWrapper (img, g, w, h, this); + } catch (ClassNotFoundException notfound) { + rimg = new SunImageWrapper (img, g, w, h, this); + } + } catch (Exception x) {} + return rimg; + } + + public ImageWrapper createPaintableImage (byte src[]) { + ImageWrapper rimg = null; + MediaTracker tracker = new MediaTracker (this); + try { + Image img1 = Toolkit.getDefaultToolkit ().createImage (src); + tracker.addImage (img1, 0); + tracker.waitForAll (); + int w = img1.getWidth (null); + int h = img1.getHeight (null); + Image img = createImage (w, h); + Graphics g = img.getGraphics (); + g.drawImage (img1, 0, 0, null); + try { + rimg = new ActivatedImageWrapper (img, g, w, h, this); + } catch (ClassNotFoundException notfound) { + rimg = new SunImageWrapper (img, g, w, h, this); + } catch (NoClassDefFoundError notfound) { + rimg = new SunImageWrapper (img, g, w, h, this); + } + } catch (Exception x) {} + return rimg; + } + + public ImageWrapper createImage (byte src[]) { + ImageWrapper rimg = null; + MediaTracker tracker = new MediaTracker (this); + try { + Image img = Toolkit.getDefaultToolkit ().createImage (src); + tracker.addImage (img, 0); + tracker.waitForAll (); + int w = img.getWidth (null); + int h = img.getHeight (null); + try { + rimg = new ActivatedImageWrapper (img, null, w, h, this); + } catch (ClassNotFoundException notfound) { + rimg = new SunImageWrapper (img, null, w, h, this); + } catch (NoClassDefFoundError notfound) { + rimg = new SunImageWrapper (img, null, w, h, this); + } + } catch (Exception x) {} + return rimg; + } + + + public ImageWrapper createPaintableImage (String urlstring) { + ImageWrapper rimg = null; + MediaTracker tracker = new MediaTracker (this); + try { + URL url = new URL (urlstring); + Image img1 = Toolkit.getDefaultToolkit ().getImage (url); + tracker.addImage (img1, 0); + tracker.waitForAll (); + int w = img1.getWidth (null); + int h = img1.getHeight (null); + Image img = createImage (w, h); + Graphics g = img.getGraphics (); + g.drawImage (img1, 0, 0, null); + try { + rimg = new ActivatedImageWrapper (img, g, w, h, this); + } catch (ClassNotFoundException notfound) { + rimg = new SunImageWrapper (img, g, w, h, this); + } catch (NoClassDefFoundError notfound) { + rimg = new SunImageWrapper (img, g, w, h, this); + } + } catch (Exception x) { + x.printStackTrace (); + } + return rimg; + } + + public Image createImage (String filename) { + Image img = null; + MediaTracker tracker = new MediaTracker (this); + try { + img = Toolkit.getDefaultToolkit ().getImage (filename); + tracker.addImage (img, 0); + tracker.waitForAll (); + } catch (Exception x) { + x.printStackTrace (); + } + return img; + } +} diff --git a/src/helma/image/ImageWrapper.java b/src/helma/image/ImageWrapper.java new file mode 100644 index 00000000..bd236a5f --- /dev/null +++ b/src/helma/image/ImageWrapper.java @@ -0,0 +1,245 @@ +// ImageWrapper.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.image; + +import java.awt.*; +import java.awt.image.*; +import java.util.*; +import java.rmi.*; +import java.rmi.server.*; +import java.io.*; + +/** + * Abstract base class for Image Wrappers. + */ + +public abstract class ImageWrapper { + + Image img; + Graphics g; + int width, height; + int fontstyle, fontsize; + String fontname; + ImageGenerator imggen; + + public ImageWrapper (Image img, Graphics g, int width, int height, ImageGenerator imggen) { + this.img = img; + this.g = g; + this.width = width; + this.height = height; + this.imggen = imggen; + if (g != null) { + Font f = g.getFont (); + fontname = f.getName (); + fontstyle = f.getStyle (); + fontsize = f.getSize (); + } + } + + /** + * image manipulation methods + */ + + public void setFont (String name, int style, int size) { + this.fontname = name; + this.fontstyle = style; + this.fontsize = size; + g.setFont (new Font (name, style, size)); + } + + public void setColor (int red, int green, int blue) { + g.setColor (new Color (red, green, blue)); + } + + public void setColor (int color) { + g.setColor (new Color (color)); + } + + public void drawString (String str, int x, int y) { + g.drawString (str, x, y); + } + + public void drawLine (int x1, int y1, int x2, int y2) { + g.drawLine (x1, y1, x2, y2); + } + + public void drawRect (int x, int y, int w, int h) { + g.drawRect (x, y, w, h); + } + + public void drawImage (String filename, int x, int y) { + try { + Image i = imggen.createImage (filename); + g.drawImage (i, x, y, null); + } catch (Exception ignore) {} + } + + public void fillRect (int x, int y, int w, int h) { + g.fillRect (x, y, w, h); + } + + public int getWidth () { + return width; + } + + public int getHeight () { + return height; + } + + public void crop (int x, int y, int w, int h) { + ImageFilter filter = new CropImageFilter (x, y, w, h); + img = Toolkit.getDefaultToolkit ().createImage(new FilteredImageSource(img.getSource(), filter)); + + } + + public void resize (int w, int h) { + ImageFilter filter = new AreaAveragingScaleFilter (w, h); + img = Toolkit.getDefaultToolkit ().createImage(new FilteredImageSource(img.getSource(), filter)); + } + + public abstract void reduceColors (int colors); + + public abstract void saveAs (String filename); + + public void readFrom (String filename) { + throw new RuntimeException ("Image.readFrom() is currently not implemented."); + } + + public byte[] getBytes (String type) { + throw new RuntimeException ("Image.getBytes() is currently not implemented."); + } + + public void setBytes (byte[] bytes, String type) { + throw new RuntimeException ("Image.setBytes() is currently not implemented."); + } + + public void fillString (String str) { + Filler filler = new Filler (0, 0, width, height); + filler.layout (str); + } + + public void fillString (String str, int x, int y, int w, int h) { + Filler filler = new Filler (x, y, w, h); + filler.layout (str); + } + + class Filler { + + int x, y, w, h; + int addedSpace = 0; + int xLeft, yLeft; + int realHeight; + transient Vector lines; + + public Filler (int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + public void layout (String str) { + int size = fontsize; + lines = new Vector (); + while (!splitMessage (str, size) && size > 10) { + lines.setSize (0); + size = Math.max (2, size-1); + } + Font oldfont = g.getFont (); + g.setFont (new Font (fontname, fontstyle, size)); + int l = lines.size(); + for (int i = 0; i < l; i++) { + ((Line) lines.elementAt (i)).paint (g, xLeft/2, yLeft/2 + y); + } + g.setFont (oldfont); + } + + private boolean splitMessage (String string, int size) { + + Font font = new Font (fontname, fontstyle, size); + FontMetrics metrics = Toolkit.getDefaultToolkit ().getFontMetrics (font); + int longestLine = 0; + int heightSoFar = 0; + int heightIncrement = (int) (0.84f * metrics.getHeight ()); + StringTokenizer tk = new StringTokenizer (string); + StringBuffer buffer = new StringBuffer(); + int spaceWidth = metrics.stringWidth(" "); + int currentLine = 0; + int currentWidth = 0; + int maxWidth = w - 2, maxHeight = h + addedSpace - 2; + while (tk.hasMoreTokens()) { + String nextToken = tk.nextToken(); + int nextWidth = metrics.stringWidth(nextToken); + if ((currentWidth + nextWidth >= maxWidth && currentWidth != 0)) { + Line line = new Line (buffer.toString(), x, heightSoFar, metrics); + lines.addElement (line); + if (line.textwidth > longestLine) + longestLine = line.textwidth; + buffer = new StringBuffer(); + + currentWidth = 0; + heightSoFar += heightIncrement; + } + buffer.append (nextToken); + buffer.append (" "); + currentWidth += (nextWidth + spaceWidth); + if (1.18*heightSoFar > maxHeight && fontsize > 10) + return false; + } + if (! "".equals (buffer.toString().trim())) { + Line line = new Line (buffer.toString(), x, heightSoFar, metrics); + lines.addElement (line); + + if (line.textwidth > longestLine) + longestLine = line.textwidth; + if (longestLine > maxWidth && fontsize > 10) + return false; + heightSoFar += heightIncrement; + } + xLeft = w - longestLine; + yLeft = addedSpace + h - heightSoFar; + realHeight = heightSoFar; + return (1.18*heightSoFar <= maxHeight); + } + + } + + + class Line implements Serializable { + + String text; + int xoff, yoff; + FontMetrics fm; + public int textwidth, len; + int ascent; + + + public Line (String text, int xoff, int yoff, FontMetrics fm) { + this.text = text.trim(); + len = text.length(); + this.xoff = xoff; + this.yoff = yoff; + this.fm = fm; + textwidth = (len == 0) ? 0 : fm.stringWidth(this.text); + ascent = (int) (0.9f * fm.getAscent()); + } + + public void paint (Graphics g, int xadd, int yadd) { + g.drawString (text, xoff+xadd, yoff+ascent+yadd); + } + + + public boolean contains (int y) { + return (y < yoff+fm.getHeight()) ? true : false; + } + + + } + +} + + + + + diff --git a/src/helma/image/RemoteImage.java b/src/helma/image/RemoteImage.java new file mode 100644 index 00000000..dff899e3 --- /dev/null +++ b/src/helma/image/RemoteImage.java @@ -0,0 +1,106 @@ +// RemoteImage.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.image; + +import java.awt.*; +import java.awt.image.*; +import java.util.*; +import java.rmi.*; +import java.rmi.server.*; + +/** + * Implementation of an image that is accessible via RMI. + */ + +public class RemoteImage extends UnicastRemoteObject implements IRemoteImage { + + ImageWrapper wrapped; + + public RemoteImage (ImageWrapper wrapped) throws RemoteException { + this.wrapped = wrapped; + } + + public void setFont (String name, int style, int size) { + wrapped.setFont (name, style, size); + } + + public void setColor (int red, int green, int blue) { + wrapped.setColor (red, green, blue); + } + + public void setColor (int color) { + wrapped.setColor (color); + } + + public void drawString (String str, int x, int y) { + wrapped.drawString (str, x, y); + } + + public void drawLine (int x1, int y1, int x2, int y2) { + wrapped.drawLine (x1, y1, x2, y2); + } + + public void drawRect (int x, int y, int w, int h) { + wrapped.drawRect (x, y, w, h); + } + + public void drawImage (String filename, int x, int y) { + wrapped.drawImage (filename, x, y); + } + + public void fillRect (int x, int y, int w, int h) { + wrapped.fillRect (x, y, w, h); + } + + public int getWidth () { + return wrapped.getWidth(); + } + + public int getHeight () { + return wrapped.getHeight(); + } + + public void crop (int x, int y, int w, int h) { + wrapped.crop (x, y, w, h); + } + + public void resize (int w, int h) { + wrapped.resize (w, h); + } + + public void reduceColors (int colors) { + wrapped.reduceColors (colors); + } + + public void saveAs (String filename) { + wrapped.saveAs (filename); + } + + public void readFrom (String filename) { + wrapped.readFrom (filename); + } + + public byte[] getBytes (String type) { + return wrapped.getBytes (type); + } + + public void setBytes (byte[] bytes, String type) { + wrapped.setBytes (bytes, type); + } + + public void fillString (String str) { + wrapped.fillString (str); + } + + public void fillString (String str, int x, int y, int w, int h) { + wrapped.fillString (str, x, y, w, h); + } + + +} + + + + + diff --git a/src/helma/image/Server.java b/src/helma/image/Server.java new file mode 100644 index 00000000..b0b92681 --- /dev/null +++ b/src/helma/image/Server.java @@ -0,0 +1,90 @@ +// Server.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.image; + +import java.awt.*; +import java.util.*; +import java.io.*; +import java.net.URL; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.rmi.*; +import java.rmi.server.*; +import java.rmi.registry.*; + +/** + * Implementation of RMI Image Generator. This accepts only connection from localhost. + */ + + public class Server extends UnicastRemoteObject implements IRemoteGenerator { + + static int port = 3033; + ImageGenerator imggen; + + public static void main (String args[]) throws Exception { + new Server (); + } + + + public Server () throws Exception { + + imggen = new ImageGenerator (); + + // the following seems not to be necessary after all ... + // System.setSecurityManager(new RMISecurityManager()); + + System.out.println ("Starting server on port "+port); + LocateRegistry.createRegistry (port); + try { + Naming.bind ("//:"+port+"/server", this); + } catch (Exception x) { + System.out.println ("error binding remote objects: " + x); + } + + } + + public IRemoteImage createPaintableImage (int x, int y) throws RemoteException { + try { + String client = RemoteServer.getClientHost (); + if (!InetAddress.getLocalHost ().equals (InetAddress.getByName (client))) + throw new RemoteException ("Access Denied"); + } catch (ServerNotActiveException ignore) { + } catch (UnknownHostException ignore) {} + return new RemoteImage (imggen.createPaintableImage (x, y)); + } + + public IRemoteImage createPaintableImage (byte[] bytes) throws RemoteException { + try { + String client = RemoteServer.getClientHost (); + if (!InetAddress.getLocalHost ().equals (InetAddress.getByName (client))) + throw new RemoteException ("Access Denied"); + } catch (ServerNotActiveException ignore) { + } catch (UnknownHostException ignore) {} + return new RemoteImage (imggen.createPaintableImage (bytes)); + } + + public IRemoteImage createPaintableImage (String url) throws RemoteException { + try { + String client = RemoteServer.getClientHost (); + if (!InetAddress.getLocalHost ().equals (InetAddress.getByName (client))) + throw new RemoteException ("Access Denied"); + } catch (ServerNotActiveException ignore) { + } catch (UnknownHostException ignore) {} + return new RemoteImage (imggen.createPaintableImage (url)); + } + + public IRemoteImage createImage (byte[] bytes) throws RemoteException { + try { + String client = RemoteServer.getClientHost (); + if (!InetAddress.getLocalHost ().equals (InetAddress.getByName (client))) + throw new RemoteException ("Access Denied"); + } catch (ServerNotActiveException ignore) { + } catch (UnknownHostException ignore) {} + return new RemoteImage (imggen.createImage (bytes)); + } + + + +} + diff --git a/src/helma/image/SunImageWrapper.java b/src/helma/image/SunImageWrapper.java new file mode 100644 index 00000000..4a7a60d9 --- /dev/null +++ b/src/helma/image/SunImageWrapper.java @@ -0,0 +1,56 @@ +// ActivatedImageWrapper.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.image; + +import java.awt.*; +import java.awt.image.*; +import com.sun.jimi.core.*; +import com.sun.jimi.core.util.*; +import Acme.JPM.Encoders.GifEncoder; +import java.io.IOException; +import java.io.FileOutputStream; + +/** + * A wrapper for an image that uses the Sun version of JIMI available at + * http://java.sun.com/products/jimi. + */ + +public class SunImageWrapper extends ImageWrapper { + + public SunImageWrapper (Image img, Graphics g, int width, int height, + ImageGenerator imggen) throws ClassNotFoundException { + super (img, g, width, height, imggen); + Class.forName ("com.sun.jimi.core.Jimi"); + } + + + public void reduceColors (int colors) { + try { + ColorReducer redux = new ColorReducer (colors, true); + img = redux.getColorReducedImage (img); + } catch (Exception x) { + throw new RuntimeException (x.getMessage ()); + } + } + + public void saveAs (String filename) { + try { + if (filename.toLowerCase().endsWith (".gif")) { + // sun's jimi package doesn't encode gifs, use Acme encoder + FileOutputStream fout = new FileOutputStream (filename); + GifEncoder enc = new GifEncoder (img, fout); + enc.encode (); + fout.close (); + } else { + Jimi.putImage (img, filename); + } + } catch (JimiException x) { + throw new RuntimeException (x.getMessage ()); + } catch (IOException iox) { + throw new RuntimeException (iox.getMessage ()); + } + } + + +} diff --git a/src/helma/mime/LanguageTag.java b/src/helma/mime/LanguageTag.java new file mode 100644 index 00000000..ea7b86e2 --- /dev/null +++ b/src/helma/mime/LanguageTag.java @@ -0,0 +1,161 @@ +// LanguageTag.java +// $Id$ +// (c) COPYRIGHT MIT, INRIA and Keio, 1999 +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +import java.util.*; +import java.io.*; + +/** + * This class is used to represent parsed Language tags, + * It creates a representation from a string based representation + * of the Language tag, as defined in RFC 1766 + * NOTE, we don't check that languages are defined according to ISO 639 + */ + +public class LanguageTag implements Serializable, Cloneable { + public static int NO_MATCH = -1; + public static int MATCH_LANGUAGE = 1; + public static int MATCH_SPECIFIC_LANGUAGE = 2; + // subtag is not dialect as subtype can be + // dialect or country identification or script variation, etc... + public static int MATCH_SUBTAG = 3; + public static int MATCH_SPECIFIC_SUBTAG = 4; + + /** + * String representation of the language + * + * @serial + */ + protected String language = null ; + /** + * String representation of subtag + * + * @serial + */ + protected String subtag = null ; + + /** + * external form of this language tag + * + * @serial + */ + protected String external = null ; + + /** + * How good the given LanguageTag matches the receiver of the method ? + * This method returns a matching level among: + *
+ *
NO_MATCH
Language not matching,
+ *
MATCH_LANGUAGE
Languages match roughly (with *),
+ *
MATCH_SPECIFIC_LANGUAGE
Languages match exactly,
+ *
MATCH_SUBTAG
Languages match, subtags matches roughly
+ *
MATCH_SPECIFIC_SUBAG
Languages match, subtag matches exactly
+ *
+ * The matches are ranked from worst match to best match, a simple + * Max ( match[i], matched) will give the best match. + * @param other The other LanguageTag to match against ourself. + */ + + public int match (LanguageTag other) { + int match = NO_MATCH; + // match types: + if ( language.equals("*") || other.language.equals("*") ) { + match = MATCH_LANGUAGE; + } else if ( ! language.equalsIgnoreCase(other.language) ) { + return NO_MATCH ; + } else { + match = MATCH_SPECIFIC_LANGUAGE; + } + // match subtypes: + if ((subtag == null) || (other.subtag == null)) + return match; + if ( subtag.equals("*") || other.subtag.equals("*") ) { + match = MATCH_SUBTAG ; + } else if ( ! subtag.equalsIgnoreCase(other.subtag) ) { + return NO_MATCH; + } else { + match = MATCH_SPECIFIC_SUBTAG; + } + return match; + } + + /** + * A printable representation of this LanguageTag. + * The printed representation is guaranteed to be parseable by the + * String constructor. + */ + + public String toString () { + if ( external == null ) { + if (subtag != null) { + external = language + "-" + subtag; + } else { + external = language; + } + } + return external ; + } + + /** + * Get the language + * @return The language, encoded as a String. + */ + + public String getLanguage() { + return language; + } + + /** + * Get the subtag + * @return The subtag, encoded as a string + */ + + public String getSubtag() { + return language; + } + + /** + * Construct a Language tag from a spec + * @parameter spec, A string representing a LangateTag + */ + public LanguageTag(String spec) { + int strl = spec.length() ; + int start = 0, look = -1 ; + // skip leading/trailing blanks: + while ((start < strl) && (spec.charAt (start)) <= ' ') + start++ ; + while ((strl > start) && (spec.charAt (strl-1) <= ' ')) + strl-- ; + // get the type: + StringBuffer sb = new StringBuffer () ; + while ((start < strl) && ((look = spec.charAt(start)) != '-') + && ((look = spec.charAt(start)) != ';')) { + sb.append ((char) look) ; + start++ ; + } + this.language = sb.toString() ; + if ( look == '-' ) { + start++ ; + sb.setLength(0) ; + while ((start < strl) + && ((look = spec.charAt(start)) > ' ') && (look != ';')) { + sb.append ((char) look) ; + start++ ; + } + this.subtag = sb.toString() ; + } + } + + /** + * construct directly a language tag + * it NEEDS both language and subtype parameters + */ + + public LanguageTag(String language, String subtag) { + this.language = language; + this.subtag = subtag; + } +} diff --git a/src/helma/mime/MimeHeaderHolder.java b/src/helma/mime/MimeHeaderHolder.java new file mode 100644 index 00000000..81c92f6d --- /dev/null +++ b/src/helma/mime/MimeHeaderHolder.java @@ -0,0 +1,51 @@ +// MimeHeaderHolder.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +import java.io.*; + +public interface MimeHeaderHolder { + + /** + * A new header has been parsed. + * @param name The name of the encountered header. + * @param buf The byte buffer containing the value. + * @param off Offset of the header value in the above buffer. + * @param len Length of the value in the above header. + * @exception MimeParserException if the parsing failed + */ + + public void notifyHeader(String name, byte buf[], int off, int len) + throws MimeParserException; + + /** + * The parsing is now about to start, take any appropriate action. + * This hook can return a true boolean value to enforce + * the MIME parser into transparent mode (eg the parser will not + * try to parse any headers. + *

This hack is primarily defined for HTTP/0.9 support, it might + * also be usefull for other hacks. + * @param parser The Mime parser. + * @return A boolean true if the MimeParser shouldn't + * continue the parsing, false otherwise. + * @exception MimeParserException if the parsing failed + * @exception IOException if an IO error occurs. + */ + + public boolean notifyBeginParsing(MimeParser parser) + throws MimeParserException, IOException; + + /** + * All the headers have been parsed, take any appropriate actions. + * @param parser The Mime parser. + * @exception MimeParserException if the parsing failed + * @exception IOException if an IO error occurs. + */ + + public void notifyEndParsing(MimeParser parser) + throws MimeParserException, IOException; + +} diff --git a/src/helma/mime/MimeHeaders.java b/src/helma/mime/MimeHeaders.java new file mode 100644 index 00000000..67438c7a --- /dev/null +++ b/src/helma/mime/MimeHeaders.java @@ -0,0 +1,147 @@ +// MimeHeaders.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +import java.io.*; +import java.util.*; + +/** + * The most stupid MIME header holder. + * This class uses a hashtable mapping header names (as String), to header + * values (as String). Header names are lowered before entering the hashtable. + */ + +public class MimeHeaders implements MimeHeaderHolder { + Hashtable headers = null; + MimeParser parser = null; + + /** + * A new header has been parsed. + * @param name The name of the encountered header. + * @param buf The byte buffer containing the value. + * @param off Offset of the header value in the above buffer. + * @param len Length of the value in the above header. + * @exception MimeParserException if the parsing failed + */ + + public void notifyHeader(String name, byte buf[], int off, int len) + throws MimeParserException + { + String lname = name.toLowerCase(); + String oldval = null; + if ( headers == null ) { + headers = new Hashtable(5); + } else { + oldval = (String) headers.get(lname); + } + String newval = ((oldval != null) + ? oldval + "," + new String(buf, 0, off, len) + : new String(buf, 0, off, len)); + headers.put(lname, newval); + } + + /** + * The parsing is now about to start, take any appropriate action. + * This hook can return a true boolean value to enforce + * the MIME parser into transparent mode (eg the parser will not + * try to parse any headers. + *

This hack is primarily defined for HTTP/0.9 support, it might + * also be usefull for other hacks. + * @param parser The Mime parser. + * @return A boolean true if the MimeParser shouldn't + * continue the parsing, false otherwise. + * @exception IOException if an IO error occurs. + */ + + public boolean notifyBeginParsing(MimeParser parser) + throws IOException + { + return false; + } + + /** + * All the headers have been parsed, take any appropriate actions. + * @param parser The Mime parser. + * @exception IOException if an IO error occurs. + */ + + public void notifyEndParsing(MimeParser parser) + throws IOException + { + return; + } + + /** + * Set a header value. + * @param name The header name. + * @param value The header value. + */ + + public void setValue(String name, String value) { + if ( headers == null ) + headers = new Hashtable(5); + headers.put(name.toLowerCase(), value); + } + + /** + * Retreive a header value. + * @param name The name of the header. + * @return The value for this header, or null if + * undefined. + */ + + public String getValue(String name) { + return ((headers != null) + ? (String) headers.get(name.toLowerCase()) + : null); + } + + /** + * Enumerate the headers defined by the holder. + * @return A enumeration of header names, or null if no + * header is defined. + */ + + public Enumeration enumerateHeaders() { + if ( headers == null ) + return null; + return headers.keys(); + } + + /** + * Get the entity stream attached to these headers, if any. + * @return An InputStream instance, or null if no + * entity available. + */ + + public InputStream getInputStream() { + return ((parser != null) ? parser.getInputStream() : null); + } + + /** + * Dump all headers to the given stream. + * @param out The stream to dump to. + */ + + public void dump(PrintStream out) { + Enumeration names = enumerateHeaders(); + if ( names != null ) { + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + out.println(name+": "+headers.get(name)); + } + } + } + + public MimeHeaders(MimeParser parser) { + this.parser = parser; + } + + public MimeHeaders() { + } + + +} diff --git a/src/helma/mime/MimeHeadersFactory.java b/src/helma/mime/MimeHeadersFactory.java new file mode 100644 index 00000000..4d161842 --- /dev/null +++ b/src/helma/mime/MimeHeadersFactory.java @@ -0,0 +1,25 @@ +// MimeHeadersFactory.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +/** + * A Mime header factory, that will build instances of the MimeHeaders class + * to hold MIME headers. + */ + +public class MimeHeadersFactory implements MimeParserFactory { + + /** + * Create a new header holder to hold the parser's result. + * @param parser The parser that has something to parse. + * @return A MimeParserHandler compliant object. + */ + + public MimeHeaderHolder createHeaderHolder(MimeParser parser) { + return new MimeHeaders(parser); + } + +} diff --git a/src/helma/mime/MimeParser.java b/src/helma/mime/MimeParser.java new file mode 100644 index 00000000..9166b5f0 --- /dev/null +++ b/src/helma/mime/MimeParser.java @@ -0,0 +1,228 @@ +// MimeParser.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +import java.util.*; +import java.io.* ; + +/** + * The MimeParser class parses an input MIME stream. + */ + +public class MimeParser { + protected int ch = -1 ; + protected InputStream input = null ; + protected byte buffer[] = new byte[128] ; + protected int bsize = 0 ; + + /** + * The factory used to create new MIME header holders. + */ + protected MimeParserFactory factory = null ; + + protected void expect (int car) + throws MimeParserException, IOException + { + if ( car != ch ) { + String sc = (new Character((char) car)).toString() ; + String se = (new Character((char) ch)).toString() ; + throw new MimeParserException ("expecting " + + sc + "("+car+")" + + " got " + + se + "("+ch+")\n" + + "context: " + + new String (buffer, 0, 0, bsize) + + "\n") ; + } + ch = input.read() ; + } + + protected void skipSpaces () + throws MimeParserException, IOException + { + while ( (ch == ' ') || (ch == '\t') ) + ch = input.read() ; + } + + protected final void append (int c) { + if ( bsize+1 >= buffer.length ) { + byte nb[] = new byte[buffer.length*2] ; + System.arraycopy (buffer, 0, nb, 0, buffer.length) ; + buffer = nb ; + } + buffer[bsize++] = (byte) c ; + } + + /* + * Get the header name: + */ + + protected String parse822HeaderName () + throws MimeParserException, IOException + { + bsize = 0 ; + while ( (ch >= 32) && (ch != ':') ) { + append ((char) ch) ; + ch = input.read() ; + } + expect (':') ; + if ( bsize <= 0 ) + throw new MimeParserException ("expected a header name.") ; + return new String (buffer, 0, 0, bsize) ; + } + + /* + * Get the header body, still trying to be 822 compliant *and* HTTP + * robust, which is unfortunatelly a contrdiction. + */ + + protected void parse822HeaderBody () + throws MimeParserException, IOException + { + bsize = 0 ; + skipSpaces () ; + loop: + while ( true ) { + switch (ch) { + case -1: + break loop ; + case '\r': + if ( (ch = input.read()) != '\n' ) { + append ('\r') ; + continue ; + } + // no break intentional + case '\n': + // do as if '\r' had been received. This defeats 822, but + // makes HTTP more "robust". I wish HTTP were a binary + // protocol. + switch (ch = input.read()) { + case ' ': case '\t': + do { + ch = input.read () ; + } while ((ch == ' ') || (ch == '\t')) ; + append(ch); + break ; + default: + break loop ; + } + break ; + default: + append ((char) ch) ; + break ; + } + ch = input.read() ; + } + return ; + } + + /* + * Parse the given input stream for an HTTP 1.1 token. + */ + + protected String parseToken (boolean lower) + throws MimeParserException, IOException + { + bsize = 0 ; + while ( true ) { + switch ( ch ) { + // CTLs + case -1: + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: + case 8: case 9: case 10: case 11: case 12: case 13: case 14: + case 15: case 16: case 17: case 18: case 19: case 20: case 21: + case 22: case 23: case 24: case 25: case 26: case 27: case 28: + case 29: case 30: case 31: + // tspecials + case '(': case ')': case '<': case '>': case '@': + case ',': case ';': case ':': case '\\': case '\"': + case '/': case '[': case ']': case '?': case '=': + case '{': case '}': case ' ': + return new String (buffer, 0, 0, bsize) ; + default: + append ((char) (lower + ? Character.toLowerCase((char) ch) + : ch)) ; + } + ch = input.read() ; + } + } + + protected void parse822Headers(MimeHeaderHolder msg) + throws MimeParserException, IOException + { + while ( true ) { + if ( ch == '\r' ) { + if ( (ch = input.read()) == '\n' ) + return ; + } else if ( ch == '\n' ) { + return ; + } + String name = parse822HeaderName () ; + skipSpaces() ; + parse822HeaderBody () ; + msg.notifyHeader(name, buffer, 0, bsize); + } + } + + public MimeHeaderHolder parse() + throws MimeParserException, IOException + { + MimeHeaderHolder msg = factory.createHeaderHolder(this); + ch = input.read() ; + cached = true ; + if ( ! msg.notifyBeginParsing(this) ) { + if ( ! cached ) + ch = input.read(); + parse822Headers (msg) ; + } + msg.notifyEndParsing(this); + return msg; + } + + boolean cached = false ; + + public int read() + throws IOException + { + if ( cached ) + cached = false; + else + ch = input.read(); + return ch; + } + + public void unread(int ch) { + if ( cached ) + throw new RuntimeException("cannot unread more then once !"); + this.ch = ch; + cached = true; + } + + /** + * Get the message body, as an input stream. + * @return The input stream used by the parser to get data, after + * a call to parse, this input stream contains exactly + * the body of the message. + */ + + public InputStream getInputStream () { + return input ; + } + + /** + * Create an instance of the MIMEParser class. + * @param in The input stream to be parsed as a MIME stream. + * @param factory The factory used to create MIME header holders. + */ + + public MimeParser (InputStream input, MimeParserFactory factory) { + this.input = input ; + this.factory = factory; + } + + +} diff --git a/src/helma/mime/MimeParserException.java b/src/helma/mime/MimeParserException.java new file mode 100644 index 00000000..778f849e --- /dev/null +++ b/src/helma/mime/MimeParserException.java @@ -0,0 +1,14 @@ +// MimeParserException.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +public class MimeParserException extends Exception { + + public MimeParserException(String msg) { + super(msg); + } + +} diff --git a/src/helma/mime/MimeParserFactory.java b/src/helma/mime/MimeParserFactory.java new file mode 100644 index 00000000..1ef3e6fa --- /dev/null +++ b/src/helma/mime/MimeParserFactory.java @@ -0,0 +1,24 @@ +// MimeParserFactory.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +/** + * This class is used by the MimeParser, to create new MIME message holders. + * Each MIME parse instances is custmozied wit hits own factory, which it + * will use to create MIME header holders. + */ + +public interface MimeParserFactory { + + /** + * Create a new header holder to hold the parser's result. + * @param parser The parser that has something to parse. + * @return A MimeParserHandler compliant object. + */ + + abstract public MimeHeaderHolder createHeaderHolder(MimeParser parser); + +} diff --git a/src/helma/mime/MimeType.java b/src/helma/mime/MimeType.java new file mode 100644 index 00000000..1c2ee5e0 --- /dev/null +++ b/src/helma/mime/MimeType.java @@ -0,0 +1,374 @@ +// MimeType.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +import java.util.*; +import java.io.*; + +/** + * This class is used to represent parsed MIME types. + * It creates this representation from a string based representation of + * the MIME type, as defined in the RFC 1345. + */ + +public class MimeType implements Serializable, Cloneable { + /** + * List of well known MIME types: + */ + public static MimeType TEXT_HTML = null ; + public static MimeType APPLICATION_POSTSCRIPT = null ; + public static MimeType TEXT_PLAIN = null ; + public static MimeType APPLICATION_X_WWW_FORM_URLENCODED = null ; + public static MimeType MULTIPART_FORM_DATA = null ; + public static MimeType APPLICATION_X_JAVA_AGENT = null ; + public static MimeType MESSAGE_HTTP = null ; + public static MimeType TEXT_CSS = null ; + public static MimeType TEXT = null ; + + static { + try { + TEXT_HTML + = new MimeType("text/html"); + APPLICATION_POSTSCRIPT + = new MimeType ("application/postscript") ; + TEXT_PLAIN + = new MimeType("text/plain") ; + APPLICATION_X_WWW_FORM_URLENCODED + = new MimeType("application/x-www-form-urlencoded") ; + MULTIPART_FORM_DATA + = new MimeType("multipart/form-data") ; + APPLICATION_X_JAVA_AGENT + = new MimeType("application/x-java-agent") ; + MESSAGE_HTTP + = new MimeType("message/http"); + TEXT_CSS + = new MimeType("text/css"); + TEXT + = new MimeType("text/*"); + } catch (MimeTypeFormatException e) { + System.out.println ("httpd.MimeType: invalid static init.") ; + System.exit (1) ; + } + } + + public final static int NO_MATCH = -1 ; + public final static int MATCH_TYPE = 1 ; + public final static int MATCH_SPECIFIC_TYPE = 2 ; + public final static int MATCH_SUBTYPE = 3 ; + public final static int MATCH_SPECIFIC_SUBTYPE = 4 ; + + /** + * String representation of type + * + * @serial + */ + protected String type = null ; + /** + * String representation of subtype + * + * @serial + */ + protected String subtype = null ; + /** + * parameter names + * + * @serial + */ + protected String pnames[] = null; + /** + * parameter values + * + * @serial + */ + protected String pvalues[] = null; + /** + * external form of this mime type + * + * @serial + */ + protected String external = null ; + + /** + * How good the given MimeType matches the receiver of the method ? + * This method returns a matching level among: + *

+ *
NO_MATCH
Types not matching,
+ *
MATCH_TYPE
Types match,
+ *
MATCH_SPECIFIC_TYPE
Types match exactly,
+ *
MATCH_SUBTYPE
Types match, subtypes matches too
+ *
MATCH_SPECIFIC_SUBTYPE
Types match, subtypes matches exactly
+ *
+ * The matches are ranked from worst match to best match, a simple + * Max ( match[i], matched) will give the best match. + * @param other The other MimeType to match against ourself. + */ + + public int match (MimeType other) { + int match = NO_MATCH; + // match types: + if ( type.equals("*") || other.type.equals("*") ) { + return MATCH_TYPE; + } else if ( ! type.equals (other.type) ) { + return NO_MATCH ; + } else { + match = MATCH_SPECIFIC_TYPE; + } + // match subtypes: + if ( subtype.equals("*") || other.subtype.equals("*") ) { + match = MATCH_SUBTYPE ; + } else if ( ! subtype.equals (other.subtype) ) { + return NO_MATCH; + } else { + match = MATCH_SPECIFIC_SUBTYPE; + } + return match; + } + + /** + * A printable representation of this MimeType. + * The printed representation is guaranteed to be parseable by the + * String constructor. + */ + + public String toString () { + if ( external == null ) { + StringBuffer sb = new StringBuffer (type) ; + sb.append((char) '/') ; + sb.append (subtype) ; + if ( pnames != null ) { + for (int i = 0 ; i < pnames.length; i++) { + sb.append(';'); + sb.append(pnames[i]); + if ( pvalues[i] != null ) { + sb.append('='); + sb.append(pvalues[i]); + } + } + } + external = sb.toString() ; + } + return external ; + } + + + /** + * Does this MIME type has some value for the given parameter ? + * @param name The parameter to check. + * @return True if this parameter has a value, false + * otherwise. + */ + public boolean hasParameter (String name) { + if ( pnames != null ) { + for (int i = 0 ; i < pnames.length ; i++) { + if ( pnames[i].equals(name) ) + return true ; + } + } + return false ; + } + + /** + * Get the major type of this mime type. + * @return The major type, encoded as a String. + */ + + public String getType() { + return type; + } + + /** + * Get the minor type (subtype) of this mime type. + * @return The minor or subtype encoded as a String. + */ + public String getSubtype() { + return subtype; + } + + + /** + * Get a mime type parameter value. + * @param name The parameter whose value is to be returned. + * @return The parameter value, or null if not found. + */ + public String getParameterValue (String name) { + if ( pnames != null ) { + for (int i = 0 ; i < pnames.length ; i++) { + if ( pnames[i].equals(name) ) + return pvalues[i]; + } + } + return null ; + } + + /** + * adds some parameters to a MimeType + * @param param a String array of parameter names + * @param values the corresponding String array of values + */ + public void addParameters(String[] param, String[] values) { + // sanity check + if ((param == null) || (values == null) || + (values.length != param.length)) + return; + if (pnames == null) { + pnames = param; + pvalues = values; + } else { + String[] nparam = new String[pnames.length+param.length]; + String[] nvalues = new String[pvalues.length+values.length]; + System.arraycopy(pnames, 0, nparam, 0, pnames.length); + System.arraycopy(param, 0, nparam, pnames.length, param.length); + System.arraycopy(pvalues, 0, nvalues, 0, pvalues.length); + System.arraycopy(values,0, nvalues, pvalues.length, values.length); + pnames = nparam; + pvalues = nvalues; + } + external = null; + } + + /** + * get a clone of this object + * @return another cloned instance of MimeType + */ + public MimeType getClone() { + try { + return (MimeType) clone(); + } catch (CloneNotSupportedException ex) { + // should never happen as we are Cloneable! + } + // never reached + return null; + } + + /** + * adds a parameterto a MimeType + * @param param the parameter name, as a String + * @param value the parameter value, as a Sting + */ + public void addParameter(String param, String value) { + String[] p = new String[1]; + String[] v = new String[1]; + p[0] = param; + v[0] = value; + addParameters(p, v); + } + + /** + * Construct MimeType object for the given string. + * The string should be the representation of the type. This methods + * tries to be compliant with HTTP1.1, p 15, although it is not + * (because of quoted-text not being accepted). + * FIXME + * @parameter spec A string representing a MimeType + * @return A MimeType object + * @exception MimeTypeFormatException if the string couldn't be parsed. + */ + public MimeType (String spec) + throws MimeTypeFormatException + { + int strl = spec.length() ; + int start = 0, look = -1 ; + // skip leading/trailing blanks: + while ((start < strl) && (spec.charAt (start)) <= ' ') + start++ ; + while ((strl > start) && (spec.charAt (strl-1) <= ' ')) + strl-- ; + // get the type: + StringBuffer sb = new StringBuffer () ; + while ((start < strl) && ((look = spec.charAt(start)) != '/')) { + sb.append ((char) look) ; + start++ ; + } + if ( look != '/' ) + throw new MimeTypeFormatException (spec) ; + this.type = sb.toString() ; + // get the subtype: + start++ ; + sb.setLength(0) ; + while ((start < strl) + && ((look = spec.charAt(start)) > ' ') && (look != ';')) { + sb.append ((char) look) ; + start++ ; + } + this.subtype = sb.toString() ; + // get parameters, if any: + while ((start < strl) && ((look = spec.charAt(start)) <= ' ')) + start++ ; + if ( start < strl ) { + if (spec.charAt(start) != ';') + throw new MimeTypeFormatException (spec) ; + start++ ; + Vector vp = new Vector(4) ; + Vector vv = new Vector(4) ; + while ( start < strl ) { + while ((start < strl) && (spec.charAt(start) <= ' ')) start++ ; + // get parameter name: + sb.setLength (0) ; + while ((start < strl) + && ((look=spec.charAt(start)) > ' ') && (look != '=')) { + sb.append (Character.toLowerCase((char) look)) ; + start++ ; + } + String name = sb.toString() ; + // get the value: + while ((start < strl) && (spec.charAt(start) <= ' ')) start++ ; + if (spec.charAt(start) != '=') + throw new MimeTypeFormatException (spec) ; + start++ ; + while ((start < strl) && + ((spec.charAt(start) == '"') || + (spec.charAt(start) <= ' '))) start++ ; + sb.setLength(0) ; + while ((start < strl) + && ((look=spec.charAt(start)) > ' ') + && (look != ';') + && (look != '"')) { + sb.append ((char) look) ; + start++ ; + } + while ((start < strl) && (spec.charAt(start) != ';')) start++ ; + start++ ; + String value = sb.toString() ; + vp.addElement(name); + vv.addElement(value); + } + this.pnames = new String[vp.size()]; + vp.copyInto(pnames); + this.pvalues = new String[vv.size()]; + vv.copyInto(pvalues); + } + } + + public MimeType (String type, String subtype + , String pnames[], String pvalues[]) { + this.type = type ; + this.subtype = subtype ; + this.pnames = pnames; + this.pvalues = pvalues; + } + + public MimeType (String type, String subtype) { + this.type = type; + this.subtype = subtype; + } + + public static void main (String args[]) { + if ( args.length == 1) { + MimeType type = null ; + try { + type = new MimeType (args[0]) ; + } catch (MimeTypeFormatException e) { + } + if ( type != null ) + System.out.println (type) ; + else + System.out.println ("Invalid mime type specification.") ; + } else { + System.out.println ("Usage: java MimeType ") ; + } + } + +} diff --git a/src/helma/mime/MimeTypeFormatException.java b/src/helma/mime/MimeTypeFormatException.java new file mode 100644 index 00000000..e8788ac8 --- /dev/null +++ b/src/helma/mime/MimeTypeFormatException.java @@ -0,0 +1,14 @@ +// MimeType.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +public class MimeTypeFormatException extends Exception { + + public MimeTypeFormatException(String msg) { + super(msg); + } + +} diff --git a/src/helma/mime/MultipartInputStream.java b/src/helma/mime/MultipartInputStream.java new file mode 100644 index 00000000..830156cf --- /dev/null +++ b/src/helma/mime/MultipartInputStream.java @@ -0,0 +1,214 @@ +// MultipartInputStream.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +import java.io.* ; + +/** + * A class to handle multipart MIME input streams. See RC 1521. + * This class handles multipart input streams, as defined by the RFC 1521. + * It prvides a sequential interface to all MIME parts, and for each part + * it delivers a suitable InputStream for getting its body. + */ + +public class MultipartInputStream extends InputStream { + InputStream in = null; + byte boundary[] = null ; + byte buffer[] = null ; + boolean partEnd = false ; + boolean fileEnd = false ; + + // Read boundary bytes of input in buffer + // Return true if enough bytes available, false otherwise. + + private final boolean readBoundaryBytes() + throws IOException + { + int pos = 0; + while ( pos < buffer.length ) { + int got = in.read(buffer, pos, buffer.length-pos); + if ( got < 0 ) + return false; + pos += got; + } + return true; + } + + // Skip to next input boundary, set stream at begining of content: + // Returns true if boundary was found, false otherwise. + + protected boolean skipToBoundary() + throws IOException + { + int ch = in.read() ; + skip: + while ( ch != -1 ) { + if ( ch != '-' ) { + ch = in.read() ; + continue ; + } + if ((ch = in.read()) != '-') + continue ; + in.mark(boundary.length) ; + if ( ! readBoundaryBytes() ) { + in.reset(); + ch = in.read(); + continue skip; + } + for (int i = 0 ; i < boundary.length ; i++) { + if ( buffer[i] != boundary[i] ) { + in.reset() ; + ch = in.read() ; + continue skip ; + } + } + // FIXME: should we check for a properly syntaxed part, which + // means that we should expect '\r\n'. For now, we just skip + // as much as we can. + if ( (ch = in.read()) == '\r' ) { + ch = in.read() ; + } + in.mark(3); + if( in.read() == '-' ) { // check fileEnd! + if( in.read() == '\r' && in.read() == '\n' ) { + fileEnd = true ; + return false ; + } + } + in.reset(); + return true ; + } + fileEnd = true ; + return false ; + } + + /** + * Read one byte of data from the current part. + * @return A byte of data, or -1 if end of file. + * @exception IOException If some IO error occured. + */ + + public int read() + throws IOException + { + int ch ; + if ( partEnd ) + return -1 ; + switch (ch = in.read()) { + case '\r': + // check for a boundary + in.mark(boundary.length+3) ; + int c1 = in.read() ; + int c2 = in.read() ; + int c3 = in.read() ; + if ((c1 == '\n') && (c2 == '-') && (c3 == '-')) { + if ( ! readBoundaryBytes() ) { + in.reset(); + return ch; + } + for (int i = 0 ; i < boundary.length ; i++) { + if ( buffer[i] != boundary[i] ) { + in.reset() ; + return ch ; + } + } + partEnd = true ; + if ( (ch = in.read()) == '\r' ) { + in.read() ; + } else if (ch == '-') { + // FIXME, check the second hyphen + if (in.read() == '-') + fileEnd = true ; + } else { + fileEnd = (ch == -1); + } + return -1 ; + } else { + in.reset () ; + return ch ; + } + // not reached + case -1: + fileEnd = true ; + return -1 ; + default: + return ch ; + } + } + + /** + * Read n bytes of data from the current part. + * @return the number of bytes data, read or -1 + * if end of file. + * @exception IOException If some IO error occured. + */ + public int read (byte b[], int off, int len) + throws IOException + { + int got = 0 ; + int ch ; + + while ( got < len ) { + if ((ch = read()) == -1) + return (got == 0) ? -1 : got ; + b[off+(got++)] = (byte) (ch & 0xFF) ; + } + return got ; + } + + public long skip (long n) + throws IOException + { + while ((--n >= 0) && (read() != -1)) + ; + return n ; + } + + public int available () + throws IOException + { + return in.available(); + } + + /** + * Switch to the next available part of data. + * One can interrupt the current part, and use this method to switch + * to next part before current part was totally read. + * @return A boolean true if there next partis ready, + * or false if this was the last part. + */ + + public boolean nextInputStream() + throws IOException + { + if ( fileEnd ) { + return false ; + } + if ( ! partEnd ) { + return skipToBoundary() ; + } else { + partEnd = false ; + return true ; + } + } + + /** + * Construct a new multipart input stream. + * @param in The initial (multipart) input stream. + * @param boundary The input stream MIME boundary. + */ + + public MultipartInputStream (InputStream in, byte boundary[]) { + this.in = (in.markSupported() + ? in + : new BufferedInputStream(in, boundary.length+4)); + this.boundary = boundary ; + this.buffer = new byte[boundary.length] ; + this.partEnd = false ; + this.fileEnd = false ; + } + +} diff --git a/src/helma/mime/Utils.java b/src/helma/mime/Utils.java new file mode 100644 index 00000000..99cf43b5 --- /dev/null +++ b/src/helma/mime/Utils.java @@ -0,0 +1,143 @@ +// Utils.java +// $Id$ +// (c) COPYRIGHT MIT and INRIA, 1998. +// Please first read the full copyright statement in file COPYRIGHT.html + +package helma.mime; + +import java.util.Hashtable; + +/** + * @version $Revision$ + * @author Benoît Mahé (bmahe@w3.org) + */ +public class Utils { + + private static Hashtable extension_map = new Hashtable(); + + private static void setSuffix(String ext, String ct) { + extension_map.put(ext, ct); + } + + static { + setSuffix("", "content/unknown"); + setSuffix(".uu", "application/octet-stream"); + setSuffix(".saveme", "application/octet-stream"); + setSuffix(".dump", "application/octet-stream"); + setSuffix(".hqx", "application/octet-stream"); + setSuffix(".arc", "application/octet-stream"); + setSuffix(".o", "application/octet-stream"); + setSuffix(".a", "application/octet-stream"); + setSuffix(".bin", "application/octet-stream"); + setSuffix(".exe", "application/octet-stream"); + setSuffix(".z", "application/octet-stream"); + setSuffix(".gz", "application/octet-stream"); + setSuffix(".oda", "application/oda"); + setSuffix(".pdf", "application/pdf"); + setSuffix(".eps", "application/postscript"); + setSuffix(".ai", "application/postscript"); + setSuffix(".ps", "application/postscript"); + setSuffix(".rtf", "application/rtf"); + setSuffix(".dvi", "application/x-dvi"); + setSuffix(".hdf", "application/x-hdf"); + setSuffix(".latex", "application/x-latex"); + setSuffix(".cdf", "application/x-netcdf"); + setSuffix(".nc", "application/x-netcdf"); + setSuffix(".tex", "application/x-tex"); + setSuffix(".texinfo", "application/x-texinfo"); + setSuffix(".texi", "application/x-texinfo"); + setSuffix(".t", "application/x-troff"); + setSuffix(".tr", "application/x-troff"); + setSuffix(".roff", "application/x-troff"); + setSuffix(".man", "application/x-troff-man"); + setSuffix(".me", "application/x-troff-me"); + setSuffix(".ms", "application/x-troff-ms"); + setSuffix(".src", "application/x-wais-source"); + setSuffix(".wsrc", "application/x-wais-source"); + setSuffix(".zip", "application/zip"); + setSuffix(".bcpio", "application/x-bcpio"); + setSuffix(".cpio", "application/x-cpio"); + setSuffix(".gtar", "application/x-gtar"); + setSuffix(".shar", "application/x-shar"); + setSuffix(".sh", "application/x-shar"); + setSuffix(".sv4cpio", "application/x-sv4cpio"); + setSuffix(".sv4crc", "application/x-sv4crc"); + setSuffix(".tar", "application/x-tar"); + setSuffix(".ustar", "application/x-ustar"); + setSuffix(".snd", "audio/basic"); + setSuffix(".au", "audio/basic"); + setSuffix(".aifc", "audio/x-aiff"); + setSuffix(".aif", "audio/x-aiff"); + setSuffix(".aiff", "audio/x-aiff"); + setSuffix(".wav", "audio/x-wav"); + setSuffix(".gif", "image/gif"); + setSuffix(".ief", "image/ief"); + setSuffix(".jfif", "image/jpeg"); + setSuffix(".jfif-tbnl", "image/jpeg"); + setSuffix(".jpe", "image/jpeg"); + setSuffix(".jpg", "image/jpeg"); + setSuffix(".jpeg", "image/jpeg"); + setSuffix(".tif", "image/tiff"); + setSuffix(".tiff", "image/tiff"); + setSuffix(".ras", "image/x-cmu-rast"); + setSuffix(".pnm", "image/x-portable-anymap"); + setSuffix(".pbm", "image/x-portable-bitmap"); + setSuffix(".pgm", "image/x-portable-graymap"); + setSuffix(".ppm", "image/x-portable-pixmap"); + setSuffix(".rgb", "image/x-rgb"); + setSuffix(".xbm", "image/x-xbitmap"); + setSuffix(".xpm", "image/x-xpixmap"); + setSuffix(".xwd", "image/x-xwindowdump"); + setSuffix(".htm", "text/html"); + setSuffix(".html", "text/html"); + setSuffix(".text", "text/plain"); + setSuffix(".c", "text/plain"); + setSuffix(".cc", "text/plain"); + setSuffix(".c++", "text/plain"); + setSuffix(".h", "text/plain"); + setSuffix(".pl", "text/plain"); + setSuffix(".txt", "text/plain"); + setSuffix(".java", "text/plain"); + setSuffix(".rtx", "application/rtf"); + setSuffix(".tsv", "texyt/tab-separated-values"); + setSuffix(".etx", "text/x-setext"); + setSuffix(".mpg", "video/mpeg"); + setSuffix(".mpe", "video/mpeg"); + setSuffix(".mpeg", "video/mpeg"); + setSuffix(".mov", "video/quicktime"); + setSuffix(".qt", "video/quicktime"); + setSuffix(".avi", "application/x-troff-msvideo"); + setSuffix(".movie", "video/x-sgi-movie"); + setSuffix(".mv", "video/x-sgi-movie"); + setSuffix(".mime", "message/rfc822"); + } + + /** + * A useful utility routine that tries to guess the content-type + * of an object based upon its extension. + */ + public static String guessContentTypeFromName(String fname) { + String ext = ""; + int i = fname.lastIndexOf('#'); + + if (i != -1) + fname = fname.substring(0, i - 1); + i = fname.lastIndexOf('.'); + i = Math.max(i, fname.lastIndexOf('/')); + i = Math.max(i, fname.lastIndexOf('?')); + + if (i != -1 && fname.charAt(i) == '.') { + ext = fname.substring(i).toLowerCase(); + } + return (String) extension_map.get(ext); + } + + public static MimeType getMimeType(String filename) { + try { + return new MimeType(guessContentTypeFromName(filename)); + } catch (MimeTypeFormatException ex) { + return null; + } + } + +} diff --git a/src/helma/objectmodel/ConcurrencyException.java b/src/helma/objectmodel/ConcurrencyException.java new file mode 100644 index 00000000..f6684b5d --- /dev/null +++ b/src/helma/objectmodel/ConcurrencyException.java @@ -0,0 +1,51 @@ +// ConcurrencyException.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel; + + +/** + * Thrown when more than one thrad tries to modify a Node. The evaluator + * will normally catch this and try again after a period of time. + */ + +public class ConcurrencyException extends RuntimeException { + + public ConcurrencyException (String msg) { + super (msg); + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/DbMapping.java b/src/helma/objectmodel/DbMapping.java new file mode 100644 index 00000000..b4c859d2 --- /dev/null +++ b/src/helma/objectmodel/DbMapping.java @@ -0,0 +1,457 @@ +// DbMapping.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel; + +import helma.framework.core.Application; +import helma.objectmodel.db.WrappedNodeManager; +import java.util.*; +import java.sql.*; +import com.workingdogs.village.*; + +/** + * A DbMapping describes how a certain type of Nodes is to mapped to a + * relational database table. Basically it consists of a set of JavaScript property-to- + * Database row bindings which are represented by instances of the Relation class. + */ + +public class DbMapping { + + Application app; + String typename; + + SystemProperties props; + + DbSource source; + String table; + + DbMapping parent; + DbMapping subnodes; + DbMapping properties; + private Relation parentRel; + private Relation subnodesRel; + private Relation propertiesRel; + + // Map of property names to Relations objects + public Hashtable prop2db; + // Map of db columns to Relations objects + public Hashtable db2prop; + + String idField; + String nameField; + private String idgen; + + Schema schema = null; + KeyDef keydef = null; + + private long lastTypeChange; + public long lastDataChange; + + public DbMapping () { + + prop2db = new Hashtable (); + db2prop = new Hashtable (); + + parent = null; + subnodes = null; + properties = null; + idField = "id"; + } + + public DbMapping (Application app, String typename, SystemProperties props) { + + this.app = app; + this.typename = typename; + + prop2db = new Hashtable (); + db2prop = new Hashtable (); + + parent = null; + subnodes = null; + properties = null; + idField = "id"; + + this.props = props; + read (); + + app.putDbMapping (typename, this); + } + + /** + * Read the mapping from the Properties. Return true if the properties were changed. + */ + public boolean read () { + + long lastmod = props.lastModified (); + if (lastmod == lastTypeChange) + return false; + + this.table = props.getProperty ("_tablename"); + this.idgen = props.getProperty ("_idgen"); + String sourceName = props.getProperty ("_datasource"); + if (sourceName != null) + source = (DbSource) IServer.dbSources.get (sourceName.toLowerCase ()); + lastTypeChange = lastmod; + // set the cached schema & keydef to null so it's rebuilt the next time around + schema = null; + keydef = null; + return true; + } + + public void rewire () { + + // if (table != null && source != null) { + // IServer.getLogger().log ("set data source for "+typename+" to "+source); + Hashtable p2d = new Hashtable (); + Hashtable d2p = new Hashtable (); + + for (Enumeration e=props.keys(); e.hasMoreElements(); ) { + String propName = (String) e.nextElement (); + + if (!propName.startsWith ("_") && propName.indexOf (".") < 0) { + String dbField = props.getProperty (propName); + Relation rel = new Relation (dbField, propName, this, props); + p2d.put (propName, rel); + if (rel.localField != null) + d2p.put (rel.localField, rel); + // IServer.getLogger().log ("Mapping "+propName+" -> "+dbField); + + } else if ("_id".equalsIgnoreCase (propName)) { + idField = props.getProperty (propName); + + } else if ("_name".equalsIgnoreCase (propName)) { + nameField = props.getProperty (propName); + } + } + prop2db = p2d; + db2prop = d2p; + + String parentMapping = props.getProperty ("_parent"); + if (parentMapping != null) { + parentRel = new Relation (parentMapping, "_parent", this, props); + if (parentRel.isReference ()) + parent = parentRel.other; + else + parent = (DbMapping) app.getDbMapping (parentMapping); + } + + String subnodeMapping = props.getProperty ("_subnodes"); + if (subnodeMapping != null) { + subnodesRel = new Relation (subnodeMapping, "_subnodes", this, props); + if (subnodesRel.isReference ()) + subnodes = subnodesRel.other; + else + subnodes = (DbMapping) app.getDbMapping (subnodeMapping); + } + + String propertiesMapping = props.getProperty ("_properties"); + if (propertiesMapping != null) { + propertiesRel = new Relation (propertiesMapping, "_properties", this, props); + if (propertiesRel.isReference ()) + properties = propertiesRel.other; + else + properties = (DbMapping) app.getDbMapping (propertiesMapping); + // take over groupby flag from subnodes, if properties are subnodes + if (propertiesRel.subnodesAreProperties && subnodesRel != null) + propertiesRel.groupby = subnodesRel.groupby; + } + + IServer.getLogger().log ("rewiring: "+parent+" -> "+this+" -> "+subnodes); + } + + + + public Connection getConnection () throws ClassNotFoundException, SQLException { + if (source == null) + throw new SQLException ("Tried to get Connection from non-relational embedded data source."); + return source.getConnection (); + } + + public DbSource getDbSource () { + return source; + } + + public String getSourceID () { + return source == null ? "" : source.url; + } + + public String getTableName () { + return table; + } + + public Application getApplication () { + return app; + } + + public String getAppName () { + return app.getName(); + } + + public String getTypeName () { + return typename; + } + + /** + * Get the primary key column name for objects using this mapping. + */ + public String getIDField () { + return idField; + } + + /** + * Get the column used for (internal) names of objects of this type. + */ + public String getNameField () { + return nameField; + } + + /** + * Translate a database column name to a JavaScript property name according to this mapping. + */ + public Relation columnNameToProperty (String columnName) { + return (Relation) db2prop.get (columnName); + } + + /** + * Translate a JavaScript property name to a database column name according to this mapping. + */ + public Relation propertyToColumnName (String propName) { + return (Relation) prop2db.get (propName); + } + + public DbMapping getParentMapping () { + return parent; + } + + public DbMapping getSubnodeMapping () { + return subnodes; + } + + public void setSubnodeMapping (DbMapping sm) { + subnodes = sm; + } + + public DbMapping getExactPropertyMapping (String propname) { + if (propname == null) + return null; + Relation rel = (Relation) prop2db.get (propname.toLowerCase()); + return rel != null ? rel.other : null; + } + + public DbMapping getPropertyMapping (String propname) { + if (propname == null) + return properties; + Relation rel = (Relation) prop2db.get (propname.toLowerCase()); + return rel != null && !rel.virtual ? rel.other : properties; + } + + public void setPropertyMapping (DbMapping pm) { + properties = pm; + } + + public void setSubnodeRelation (Relation rel) { + subnodesRel = rel; + } + + public void setPropertyRelation (Relation rel) { + propertiesRel = rel; + } + + public Relation getSubnodeRelation () { + return subnodesRel; + } + + public Relation getPropertyRelation () { + return propertiesRel; + } + + public Relation getPropertyRelation (String propname) { + if (propname == null) + return propertiesRel; + Relation rel = (Relation) prop2db.get (propname.toLowerCase()); + return rel != null ? rel : propertiesRel; + } + + public String getIDgen () { + return idgen; + } + + public WrappedNodeManager getWrappedNodeManager () { + if (app == null) + throw new RuntimeException ("Can't get node manager from internal db mapping"); + return app.getWrappedNodeManager (); + } + + public boolean isRelational () { + return source != null; + } + + /** + * Return a Village Schema object for this DbMapping. + */ + public Schema getSchema () throws ClassNotFoundException, SQLException, DataSetException { + if (!isRelational ()) + throw new SQLException ("Can't get Schema for non-relational data mapping"); + // Use local variable s to avoid synchronization (schema may be nulled elsewhere) + Schema s = schema; + if (s != null) + return s; + schema = new Schema ().schema (getConnection (), table, "*"); + return schema; + } + + /** + * Return a Village Schema object for this DbMapping. + */ + public KeyDef getKeyDef () { + if (!isRelational ()) + throw new RuntimeException ("Can't get KeyDef for non-relational data mapping"); + // Use local variable s to avoid synchronization (keydef may be nulled elsewhere) + KeyDef k = keydef; + if (k != null) + return k; + keydef = new KeyDef ().addAttrib (idField); + return keydef; + } + + public String toString () { + if (app == null) + return "[unspecified internal DbMapping]"; + else + return ("["+app.getName()+"."+typename+"]"); + } + + public long getLastTypeChange () { + return lastTypeChange; + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/DbSource.java b/src/helma/objectmodel/DbSource.java new file mode 100644 index 00000000..38a83dfb --- /dev/null +++ b/src/helma/objectmodel/DbSource.java @@ -0,0 +1,95 @@ +// DbSource.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.objectmodel; + +import java.sql.*; +import java.util.Hashtable; +import helma.objectmodel.db.Transactor; + +/** + * This class describes a releational data source (URL, driver, user and password). + */ + +public class DbSource { + + private String name; + protected String url; + private String driver; + protected String user; + private String password; + + public DbSource (String name) throws ClassNotFoundException { + this.name = name; + url = IServer.dbProps.getProperty (name+".url"); + driver = IServer.dbProps.getProperty (name+".driver"); + Class.forName (driver); + user = IServer.dbProps.getProperty (name+".user"); + password = IServer.dbProps.getProperty (name+".password"); + IServer.getLogger().log ("created db source ["+name+", "+url+", "+driver+", "+user+"]"); + IServer.dbSources.put (name.toLowerCase (), this); + } + + public Connection getConnection () throws ClassNotFoundException, SQLException { + Transactor tx = (Transactor) Thread.currentThread (); + Connection con = tx.getConnection (this); + if (con == null || con.isClosed ()) { + Class.forName (driver); + con = DriverManager.getConnection (url, user, password); + // If we wanted to use SQL transactions, we'd set autoCommit to + // false here and make commit/rollback invocations in Transactor methods; + IServer.getLogger().log ("Created new Connection to "+url); + tx.registerConnection (this, con); + } + return con; + } + + public String getDriverName () { + return driver; + } + + public String getName () { + return name; + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/INode.java b/src/helma/objectmodel/INode.java new file mode 100644 index 00000000..2a124be4 --- /dev/null +++ b/src/helma/objectmodel/INode.java @@ -0,0 +1,114 @@ +// INode.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel; + +import java.util.*; +import java.io.*; + +/** + * Interface that all Nodes implement. Currently, there are two implementations: + * Transient nodes which only exist in memory, and persistent Nodes, which are + * stored in a database (either the internal Object DB or an external relational DB). + */ + +public interface INode { + + public final static String webTypes = "image/jpeg, image/gif, image/png"; + + public final static int TRANSIENT = -3; + public final static int VIRTUAL = -2; + public final static int INVALID = -1; + public final static int CLEAN = 0; + public final static int NEW = 1; + public final static int MODIFIED = 2; + public final static int DELETED = 3; + + + + /** + * id-related methods + */ + + public String getID (); + public String getName (); + public String getNameOrID (); // get name or id depending if it's a named or an anonymous node. + public void setDbMapping (DbMapping dbmap); + public DbMapping getDbMapping (); + public int getState (); + public void setState (int s); + public String getFullName (); + public String getFullName (INode root); + public INode[] getPath (); + public void setName (String name); + public long lastModified (); + public long created (); + public boolean isAnonymous (); // is this a named node, or an anonymous node in a collection? + // public void setPrototype (String prototype); + // public String getPrototype (); + public INode getCacheNode (); + + /** + * node-related methods + */ + + public INode getParent (); + public void setSubnodeRelation (String rel); + public String getSubnodeRelation (); + public int numberOfNodes (); + public INode addNode (INode node); + public INode addNode (INode node, int where); + public INode createNode (String name); + public INode createNode (String name, int where); + public Enumeration getSubnodes (); + public INode getSubnode (String name); + public INode getSubnodeAt (int index); + public int contains (INode node); + public boolean remove (); + public void removeNode (INode node); + + /** + * property-related methods + */ + + public Enumeration properties (); + public IProperty get (String name, boolean inherit); + public String getString (String name, boolean inherit); + public String getString (String name, String defaultValue, boolean inherit); + public boolean getBoolean (String name, boolean inherit); + public Date getDate (String name, boolean inherit); + public long getInteger (String name, boolean inherit); + public double getFloat (String name, boolean inherit); + public INode getNode (String name, boolean inherit); + public Object getJavaObject (String name, boolean inherit); + + public void setString (String name, String value); + public void setBoolean (String name, boolean value); + public void setDate (String name, Date value); + public void setInteger (String name, long value); + public void setFloat (String name, double value); + public void setNode (String name, INode value); + public void setJavaObject (String name, Object value); + + public void unset (String name); + + /** + * content-related methods + */ + + public String getContentType (); + public void setContentType (String type); + public int getContentLength (); + public void setContent (byte content[], String type); + public void setContent (String content); + public byte[] getContent (); + public String getText (); + public String getUrl (INode root, INode userroot, String tmpname); + public String getHref (INode root, INode userroot, String tmpname, String prefix); + + +} + + + + diff --git a/src/helma/objectmodel/INodeListener.java b/src/helma/objectmodel/INodeListener.java new file mode 100644 index 00000000..ffb75c10 --- /dev/null +++ b/src/helma/objectmodel/INodeListener.java @@ -0,0 +1,15 @@ +// INodeListener.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel; + +/** + * An interface for objects that wish to be notified when certain nodes are + * modified. This is not used in the HOP as much as it used to be. + */ + +public interface INodeListener { + + public void nodeChanged (NodeEvent event); + +} \ No newline at end of file diff --git a/src/helma/objectmodel/IPathElement.java b/src/helma/objectmodel/IPathElement.java new file mode 100644 index 00000000..05b4bfa6 --- /dev/null +++ b/src/helma/objectmodel/IPathElement.java @@ -0,0 +1,21 @@ +// INode.java +// Copyright (c) Hannes Wallnöfer 2000 + +package helma.objectmodel; + + +/** + * Minimal Interface for Nodes that build a hierarchic tree + */ + +public interface IPathElement { + + public INode getSubnode (String name); + public INode getNode (String name, boolean inherit); + + +} + + + + diff --git a/src/helma/objectmodel/IProperty.java b/src/helma/objectmodel/IProperty.java new file mode 100644 index 00000000..3016fac5 --- /dev/null +++ b/src/helma/objectmodel/IProperty.java @@ -0,0 +1,34 @@ +// IProperty.java +// Copyright (c) Hannes Wallnöfer 1997-2000 + +package helma.objectmodel; + +import java.util.Date; + +/** + * Interface that is implemented by node properties. + */ + +public interface IProperty { + + public static final int STRING = 1; + public static final int BOOLEAN = 2; + public static final int DATE = 3; + public static final int INTEGER = 4; + public static final int FLOAT = 5; + public static final int NODE = 6; + public static final int JAVAOBJECT = 7; + + public String getName (); + public int getType (); + public Object getValue (); + + public INode getNodeValue (); + public String getStringValue (); + public boolean getBooleanValue (); + public long getIntegerValue (); + public double getFloatValue (); + public Date getDateValue (); + public Object getJavaObjectValue (); + + } \ No newline at end of file diff --git a/src/helma/objectmodel/IServer.java b/src/helma/objectmodel/IServer.java new file mode 100644 index 00000000..4e1c972a --- /dev/null +++ b/src/helma/objectmodel/IServer.java @@ -0,0 +1,79 @@ +// IServer.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel; + +import helma.util.*; +import helma.xmlrpc.WebServer; +import java.util.*; +import java.io.*; + +/** + * Abstract Server class. Defines the methods all servers have to implement. + */ + +public abstract class IServer { + + // public static final Object sync = new Object (); + public static SystemProperties sysProps, dbProps; + public static Hashtable dbSources; + + protected static String hopHome = null; + + private static Logger logger; + + protected static WebServer xmlrpc; + + + /* public abstract INode getAppRoot (String appID); + + public abstract INode getAppNode (String appID, Vector path, String name); + + public abstract INode getSubnode (String path); */ + + public static void throwNodeEvent (NodeEvent evt) { + // noop + } + + public static void addNodeListener (String id, INodeListener listener) { + // noop + } + + public static void removeNodeListener (String node, INodeListener listener) { + // noop + } + + public static Logger getLogger () { + if (logger == null) { + String logDir = sysProps.getProperty ("logdir"); + if (logDir == null) { + logger = new Logger (System.out); + } else { + try { + File helper = new File (logDir); + if (hopHome != null && !helper.isAbsolute ()) + helper = new File (hopHome, logDir); + logDir = helper.getAbsolutePath (); + logger = new Logger (logDir, "hop"); + } catch (IOException iox) { + System.err.println ("Could not create Logger for log/hop: "+iox); + // fallback to System.out + logger = new Logger (System.out); + } + } + } + return logger; + } + + public static String getHopHome () { + return hopHome; + } + + public static WebServer getXmlRpcServer() { + return xmlrpc; + } + +} + + + diff --git a/src/helma/objectmodel/Key.java b/src/helma/objectmodel/Key.java new file mode 100644 index 00000000..b04c2354 --- /dev/null +++ b/src/helma/objectmodel/Key.java @@ -0,0 +1,140 @@ +// Key.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel; + + +import Acme.LruHashtable; +import java.io.Serializable; + +/** + * This is the internal representation of a database key. It is constructed + * out of the database URL, the table name, the user name and the database + * key of the node and unique within each HOP application. Currently only + * single keys are supported. + */ +public class Key implements Serializable { + + protected String type; + protected String id; + private int hash; + private static LruHashtable keycache; + + + public synchronized static Key makeKey (DbMapping dbmap, String id) { + String _type = dbmap == null ? "" : dbmap.typename; + String _id = id.trim (); // removed .toLowerCase() - hw + return makeKey (_type, _id); + } + + private synchronized static Key makeKey (String _type, String _id) { + if (keycache == null) + keycache = new LruHashtable (1000, 0.9f); + Key k = (Key) keycache.get (_type+"#"+_id); + if (k == null) { + k = new Key (_type, _id); + keycache.put (_type+"#"+_id, k); + } + return k; + } + + private Key (String type, String id) { + this.type = type; + this.id = id; + hash = this.id.hashCode (); + } + + public boolean equals (Object what) { + if (what == this) + return true; + if (what == null || !(what instanceof Key)) + return false; + Key other = (Key) what; + if (type == null) + return (id.equals (other.id) && other.type == null); + else + return (id.equals (other.id) && type.equals (other.type)); + } + + public int hashCode () { + return hash; + } + + /** + * Get the Key for a virtual node contained by this node, that is, a node that does + * not represent a record in the database. The main objective here is to generate + * a key that can't be mistaken for a relational db key. + */ + public Key getVirtualKey (String sid) { + Key virtkey = makeKey ("", getVirtualID (type, id, sid)); + return virtkey; + } + + public static String getVirtualID (DbMapping pmap, String pid, String sid) { + String ptype = pmap == null ? "" : pmap.typename; + return ptype+"/"+pid + "*h~v*" + sid; + } + + public static String getVirtualID (String ptype, String pid, String sid) { + return ptype+"/"+pid + "*h~v*" + sid; + } + + public String toString () { + return type+"["+id+"]"; + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/Node.java b/src/helma/objectmodel/Node.java new file mode 100644 index 00000000..33721463 --- /dev/null +++ b/src/helma/objectmodel/Node.java @@ -0,0 +1,765 @@ +// Node.java +// Copyright (c) Hannes Wallnöfer 1997-2000 + +package helma.objectmodel; + + +import java.util.*; +import java.io.*; +import helma.util.*; + +/** + * A transient implementation of INode. If a transient node is stored in a + * database, it is automatically (along with all reachable subnodes) rebuilt + * as a persistent node. + */ + +public class Node implements INode, Serializable { + + + protected Hashtable propMap, nodeMap; + protected Vector nodes; + protected Node parent; + protected Vector links; // links to this node + protected Vector proplinks; // nodes using this node as property + + protected String contentType; + protected byte content[]; + + protected long created; + protected long lastmodified; + + protected String id, name; + // is the main identity a named property or an anonymous node in a collection? + protected boolean anonymous = false; + + transient DbMapping dbmap; + + transient boolean adoptName = true; // little helper to know if this node is being converted + + private static long idgen = 0; + + public String generateID () { + return "t"+idgen++; // make transient ids differ from persistent ones + } + + public Node () { + id = generateID (); + name = id; + created = lastmodified = System.currentTimeMillis (); + } + + /** + * Erstellt einen neuen Node. + */ + public Node (String n) { + id = generateID (); + name = n == null || "".equals (n) ? id : n; + created = lastmodified = System.currentTimeMillis (); + } + + /** + * Erstellt einen Clone eines Nodes in der "lokalen" Implementierung von INode. + */ + public Node (INode node, Hashtable ntable, boolean conversionRoot) { + this.id = generateID (); + this.name = node.getName (); + created = lastmodified = System.currentTimeMillis (); + setContent (node.getContent (), node.getContentType ()); + created = lastmodified = System.currentTimeMillis (); + ntable.put (node, this); + adoptName = !conversionRoot; // only take over name from property if this is not the transient root + for (Enumeration e = node.getSubnodes (); e.hasMoreElements (); ) { + INode next = (INode) e.nextElement (); + Node nextc = (Node) ntable.get (next); + if (nextc == null) + nextc = new Node (next, ntable, true); + addNode (nextc); + } + for (Enumeration e = node.properties (); e.hasMoreElements (); ) { + IProperty next = (IProperty) e.nextElement (); + int t = next.getType (); + if (t == IProperty.NODE) { + INode n = next.getNodeValue (); + Node nextc = (Node) ntable.get (n); + if (nextc == null) + nextc = new Node (n, ntable, true); + setNode (next.getName (), nextc); + } else if (t == IProperty.STRING) { + setString (next.getName (), next.getStringValue ()); + } else if (t == IProperty.INTEGER) { + setInteger (next.getName (), next.getIntegerValue ()); + } else if (t == IProperty.FLOAT) { + setFloat (next.getName (), next.getFloatValue ()); + } else if (t == IProperty.BOOLEAN) { + setBoolean (next.getName (), next.getBooleanValue ()); + } else if (t == IProperty.DATE) { + setDate (next.getName (), next.getDateValue ()); + } + } + adoptName = true; // switch back to normal name adoption behaviour + } + + + public void setDbMapping (DbMapping dbmap) { + this.dbmap = dbmap; + } + + public DbMapping getDbMapping () { + return dbmap; + } + + + /** + * navigation-related + */ + + public String getID () { + return id; + } + + public boolean isAnonymous () { + return anonymous; + } + + + public String getName () { + return name; + } + + public String getNameOrID () { + return anonymous ? id : name; + } + + public int getState () { + return TRANSIENT; + } + + public void setState (int s) { + // state always is TRANSIENT on this kind of node + } + + public String getFullName () { + return getFullName (null); + } + + public String getFullName (INode root) { + String fullname = ""; + String divider = null; + StringBuffer b = new StringBuffer (); + Node p = this; + while (p != null && p.parent != null && p != root) { + if (divider != null) + b.insert (0, divider); + else + divider = "/"; + b.insert (0, p.getNameOrID ()); + p = p.parent; + } + return b.toString (); + } + + public INode[] getPath () { + int pathSize = 1; + INode p = getParent (); + while (p != null) { + pathSize +=1; + p = p.getParent (); + } + INode path[] = new INode[pathSize]; + p = this; + for (int i = pathSize-1; i>=0; i--) { + path[i] = p; + p = p.getParent (); + } + return path; + } + + public void setName (String name) { + if (name.indexOf('/') > -1) + throw new RuntimeException ("The name of the node must not contain \"/\"."); + if (name == null || name.trim().length() == 0) + this.name = id; + else + this.name = name; + } + + + public INode getParent () { + return parent; + } + + + /** + * INode-related + */ + + public void setSubnodeRelation (String rel) { + throw new RuntimeException ("Can't set subnode relation for non-persistent Node."); + } + + public String getSubnodeRelation () { + return null; + } + + public int numberOfNodes () { + return nodes == null ? 0 : nodes.size (); + } + + public INode addNode (INode elem) { + return addNode (elem, numberOfNodes ()); + } + + public INode addNode (INode elem, int where) { + + if (where < 0 || where > numberOfNodes ()) + where = numberOfNodes (); + + String n = elem.getName(); + if (n.indexOf('/') > -1) + throw new RuntimeException ("The name of a node must not contain \"/\" (slash)."); + + // IServer.getLogger().log ("adding: "+node+" -- "+node.getContentLength ()); + if (nodeMap != null && nodeMap.get (elem.getID ()) != null) { + nodes.removeElement (elem); + where = Math.min (where, numberOfNodes ()); + nodes.insertElementAt (elem, where); + return elem; + } + + if (nodeMap == null) nodeMap = new Hashtable (); + if (nodes == null) nodes = new Vector (); + + nodeMap.put (elem.getID (), elem); + nodes.insertElementAt (elem, where); + + if (elem instanceof Node) { + Node node = (Node) elem; + if (node.parent == null) { + node.parent = this; + node.anonymous = true; + } + if (node.parent != null && (node.parent != this || !node.anonymous)) { + node.registerLink (this); + } + } + + lastmodified = System.currentTimeMillis (); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_ADDED, node)); + return elem; + } + + public INode createNode () { + return createNode (null, 0); // where is ignored since this is an anonymous node + } + + public INode createNode (int where) { + return createNode (null, where); + } + + public INode createNode (String nm) { + return createNode (nm, numberOfNodes ()); // where is usually ignored (if nm != null) + } + + public INode createNode (String nm, int where) { + boolean anon = false; + if (nm == null || "".equals (nm.trim ())) + anon = true; + INode n = new Node (nm); + if (anon) + addNode (n, where); + else + setNode (nm, n); + return n; + } + + + /** + * register a node that links to this node. + */ + protected void registerLink (Node from) { + if (links == null) + links = new Vector (); + if (!links.contains (from)) + links.addElement (from); + } + + public INode getSubnode (String name) { + return getSubnode (name, false); + } + + public INode getSubnode (String name, boolean inherit) { + StringTokenizer st = new StringTokenizer (name, "/"); + Node retval = this, runner; + while (st.hasMoreTokens () && retval != null) { + runner = retval; + String next = st.nextToken().trim().toLowerCase (); + if ("".equals (next)) + retval = this; + else + retval = runner.nodeMap == null ? null : (Node) runner.nodeMap.get (next); + if (retval == null) + retval = (Node) runner.getNode (next, inherit); + if (retval == null && inherit && runner == this && parent != null) + retval = (Node) parent.getSubnode (next, inherit); + } + return retval; + } + + + public INode getSubnodeAt (int index) { + return nodes == null ? null : (INode) nodes.elementAt (index); + } + + public int contains (INode n) { + if (n == null || nodes == null) + return -1; + return nodes.indexOf (n); + } + + public boolean remove () { + if (anonymous) + parent.unset (name); + else + parent.removeNode (this); + return true; + } + + + public void removeNode (INode node) { + // IServer.getLogger().log ("removing: "+ node); + releaseNode (node); + Node n = (Node) node; + if (n.getParent () == this && n.anonymous) { + int l = n.links == null ? 0 : n.links.size (); // notify nodes that link to n that n is going down. + for (int i = 0; i < l; i++) { + Node link = (Node) n.links.elementAt (i); + link.releaseNode (n); + } + if (n.proplinks != null) { + // clean up all nodes that use n as a property + for (Enumeration e1 = n.proplinks.elements (); e1.hasMoreElements (); ) try { + Property p = (Property) e1.nextElement (); + p.node.propMap.remove (p.propname.toLowerCase ()); + } catch (Exception ignore) {} + } + for (Enumeration e2 = n.properties (); e2.hasMoreElements (); ) { + // tell all nodes that are properties of n that they are no longer used as such + Property p = (Property) e2.nextElement (); + if (p != null && p.type == Property.NODE) + p.unregisterNode (); + } + // remove all subnodes, giving them a chance to destroy themselves. + Vector v = new Vector (); // removeElement modifies the Vector we are enumerating, so we are extra careful. + for (Enumeration e3 = n.getSubnodes (); e3.hasMoreElements (); ) { + v.addElement (e3.nextElement ()); + } + int m = v.size (); + for (int i=0; i -1 && nodes.elementAt (runner) != node) + runner = nodes.indexOf (node, Math.min (nodes.size()-1, runner+1)); + if (runner > -1) + nodes.removeElementAt (runner); + // nodes.remove (node); + Object what = nodeMap.remove (node.getName ().toLowerCase ()); + // Server.throwNodeEvent (new NodeEvent (node, NodeEvent.NODE_REMOVED)); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_REMOVED, node)); + lastmodified = System.currentTimeMillis (); + // IServer.getLogger().log ("released node "+node +" from "+this+" oldobj = "+what); + } + + public Enumeration getSubnodes () { + return nodes == null ? new Vector ().elements () : nodes.elements (); + } + + + /** + * property-related + */ + + public Enumeration properties () { + return propMap == null ? new Vector ().elements () : propMap.elements (); + } + + + private Property getProperty (String propname, boolean inherit) { + Property prop = propMap == null ? null : (Property) propMap.get (propname); + if (prop == null && inherit && parent != null) { + prop = parent.getProperty (propname, inherit); + } + // check if we have to create a virtual node + if (prop == null && dbmap != null) { + Relation rel = dbmap.getPropertyRelation (propname); + if (rel != null && rel.virtual) { + prop = makeVirtualNode (propname, rel); + } + } + return prop; + } + + private Property makeVirtualNode (String propname, Relation rel) { + INode node = new helma.objectmodel.db.Node (rel.propname, dbmap.getWrappedNodeManager()); + // node.setState (TRANSIENT); + // make a db mapping good enough that the virtual node finds its subnodes + DbMapping dbm = new DbMapping (); + dbm.setSubnodeMapping (rel.other); + dbm.setSubnodeRelation (rel); + dbm.setPropertyMapping (rel.other); + dbm.setPropertyRelation (rel); + node.setDbMapping (dbm); + setNode (propname, node); + return (Property) propMap.get (propname); + } + + + public IProperty get (String propname, boolean inherit) { + propname = propname.toLowerCase (); + return getProperty (propname, inherit); + } + + public String getString (String propname, String defaultValue, boolean inherit) { + String propValue = getString (propname, inherit); + return propValue == null ? defaultValue : propValue; + } + + public String getString (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getStringValue (); + } catch (Exception ignore) {} + return null; + } + + public long getInteger (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getIntegerValue (); + } catch (Exception ignore) {} + return 0; + } + + public double getFloat (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getFloatValue (); + } catch (Exception ignore) {} + return 0.0; + } + + public Date getDate (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getDateValue (); + } catch (Exception ignore) {} + return null; + } + + + public boolean getBoolean (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getBooleanValue (); + } catch (Exception ignore) {} + return false; + } + + public INode getNode (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getNodeValue (); + } catch (Exception ignore) {} + return null; + } + + public Object getJavaObject (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getJavaObjectValue (); + } catch (Exception ignore) {} + return null; + } + + // create a property if it doesn't exist for this name + private Property initProperty (String propname) { + if (propMap == null) + propMap = new Hashtable (); + propname = propname.trim (); + String p2 = propname.toLowerCase (); + Property prop = (Property) propMap.get (p2); + if (prop == null) { + prop = new Property (propname, this); + propMap.put (p2, prop); + } + return prop; + } + + public void setString (String propname, String value) { + // IServer.getLogger().log ("setting String prop"); + Property prop = initProperty (propname); + try { + prop.setStringValue (value); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + } catch (java.text.ParseException x) { + throw new RuntimeException ("Fehler beim Parsen des Datum-Strings"); + } + lastmodified = System.currentTimeMillis (); + } + + public void setInteger (String propname, long value) { + // IServer.getLogger().log ("setting bool prop"); + Property prop = initProperty (propname); + prop.setIntegerValue (value); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + } + + public void setFloat (String propname, double value) { + // IServer.getLogger().log ("setting bool prop"); + Property prop = initProperty (propname); + prop.setFloatValue (value); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + } + + public void setBoolean (String propname, boolean value) { + // IServer.getLogger().log ("setting bool prop"); + Property prop = initProperty (propname); + prop.setBooleanValue (value); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + } + + + public void setDate (String propname, Date value) { + // IServer.getLogger().log ("setting date prop"); + Property prop = initProperty (propname); + prop.setDateValue (value); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + } + + public void setJavaObject (String propname, Object value) { + // IServer.getLogger().log ("setting date prop"); + Property prop = initProperty (propname); + prop.setJavaObjectValue (value); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + } + + public void setNode (String propname, INode value) { + // IServer.getLogger().log ("setting date prop"); + Property prop = initProperty (propname); + prop.setNodeValue (value); + + // check if the main identity of this node is as a named property + // or as an anonymous node in a collection + if (value instanceof Node) { + Node n = (Node) value; + if (n.parent == null && n.adoptName) { + n.name = propname; + n.parent = this; + n.anonymous = false; + } + } + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_ADDED, n)); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + } + + public void unset (String propname) { + if (propMap == null) + return; + try { + Property p = (Property) propMap.remove (propname.toLowerCase ()); + if (p != null && p.type == Property.NODE) + p.unregisterNode (); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + } catch (Exception ignore) {} + } + + protected void registerPropLink (Property p) { + if (proplinks == null) + proplinks = new Vector (); + proplinks.addElement (p); + // IServer.getLogger().log ("registered proplink from "+p.node.getFullName ()); + // the NodeEvent is thrown later, since the node is not yet in the prop table + } + + protected void unregisterPropLink (Property p) { + if (proplinks != null) + proplinks.removeElement (p); + // IServer.getLogger().log ("unregistered proplink from "+p.node.getFullName ()); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.NODE_REMOVED)); + // Server.throwNodeEvent (new NodeEvent (p.node, NodeEvent.SUBNODE_REMOVED, this)); + } + + + /** + * content-related + */ + + public boolean isText () throws IOException { + return getContentType().indexOf ("text/") == 0; + } + + public boolean isBinary () throws IOException { + return getContentType().indexOf ("text/") != 0; + } + + public String getContentType () { + if (contentType == null) + return "text/plain"; + return contentType; + } + + public void setContentType (String type) { + contentType = type; + lastmodified = System.currentTimeMillis (); + } + + public int getContentLength () { + if (content == null) + return 0; + return content.length; + } + + public void setContent (byte cnt[], String type) { + if (type != null) + contentType = type; + content = cnt; + lastmodified = System.currentTimeMillis (); + } + + public void setContent (String cstr) { + content = cstr == null ? null : cstr.getBytes (); + lastmodified = System.currentTimeMillis (); + } + + public byte[] getContent () { + if (content == null || content.length == 0) + return "".getBytes (); + byte retval[] = new byte[content.length]; + System.arraycopy (content, 0, retval, 0, content.length); + return retval; + } + + public String writeToFile (String dir) { + return writeToFile (dir, null); + } + + public String writeToFile (String dir, String fname) { + try { + File base = new File (dir); + // make directories if they don't exist + if (!base.exists ()) + base.mkdirs (); + + String filename = name; + if (fname != null) { + if (fname.indexOf (".") < 0) { + // check if we can use extension from name + int ndot = name == null ? -1 : name.lastIndexOf ("."); + if (ndot > -1) + filename = fname + name.substring (ndot); + else + filename = fname; + } else { + filename = fname; + } + } + File file = new File (base, filename); + FileOutputStream fout = new FileOutputStream (file); + fout.write (getContent ()); + fout.close (); + return filename; + } catch (Exception x) { + return null; + } + } + + public String getText () { + if (content != null) { + if (getContentType ().indexOf ("text/") == 0) { + return new String (content); + } else { + return null; + } + } + return null; + } + + + public String getHref (INode root, INode userroot, String tmpname, String prefix) { + return prefix + getUrl (root, userroot, tmpname); + } + + public String getUrl (INode root, INode userroot, String tmpname) { + throw new RuntimeException ("HREFs on transient (non-db based) Nodes not supported"); + } + + + public long lastModified () { + return lastmodified; + } + + public long created () { + return created; + } + + public String toString () { + return "Node " + name; + } + + + protected Node convert (INode n) { + Hashtable ntable = new Hashtable (); + Node converted = new Node (n, ntable, false); + return converted; + } + + Node cacheNode; + /** + * Get the cache node for this node. This can be used to store transient cache data per node from Javascript. + */ + public synchronized INode getCacheNode () { + if (cacheNode == null) + cacheNode = new Node(); + return cacheNode; + } + +} + + + + diff --git a/src/helma/objectmodel/NodeDataSource.java b/src/helma/objectmodel/NodeDataSource.java new file mode 100644 index 00000000..47aca47e --- /dev/null +++ b/src/helma/objectmodel/NodeDataSource.java @@ -0,0 +1,46 @@ +// NodeDataSource.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.objectmodel; + +import javax.activation.*; +import java.io.*; + +/** + * Makes Nodes usable as Datasources in the Java Activation Framework (JAF) + */ + +public class NodeDataSource implements DataSource { + + private INode node; + private String name; + + public NodeDataSource (INode node) { + this.node = node; + this.name = node.getName (); + } + + public NodeDataSource (INode node, String name) { + this.node = node; + this.name = name; + } + + + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(node.getContent ()); + } + + public OutputStream getOutputStream () throws IOException { + throw new IOException ("Can't write to Node object."); + } + + public String getContentType() { + return node.getContentType (); + } + + public String getName() + { + return name; + } + +} diff --git a/src/helma/objectmodel/NodeEvent.java b/src/helma/objectmodel/NodeEvent.java new file mode 100644 index 00000000..b62bea67 --- /dev/null +++ b/src/helma/objectmodel/NodeEvent.java @@ -0,0 +1,59 @@ +// NodeEvent.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel; + +import java.io.*; + +/** + * This is passed to NodeListeners when a node is modified. + */ + +public class NodeEvent implements Serializable { + + public static final int CONTENT_CHANGED = 0; + public static final int PROPERTIES_CHANGED = 1; + public static final int NODE_REMOVED = 2; + public static final int NODE_RENAMED = 3; + public static final int SUBNODE_ADDED = 4; + public static final int SUBNODE_REMOVED = 5; + + public int type; + public String id; + public transient INode node; + public transient Object arg; + + public NodeEvent (INode node, int type) { + super (); + this.node = node; + this.id = node.getID (); + this.type = type; + } + + public NodeEvent (INode node, int type, Object arg) { + super (); + this.node = node; + this.id = node.getID (); + this.type = type; + this.arg = arg; + } + + public String toString () { + switch (type) { + case CONTENT_CHANGED: + return "NodeEvent: content changed"; + case PROPERTIES_CHANGED: + return "NodeEvent: properties changed"; + case NODE_REMOVED: + return "NodeEvent: node removed"; + case NODE_RENAMED: + return "NodeEvent: node moved"; + case SUBNODE_ADDED: + return "NodeEvent: subnode added"; + case SUBNODE_REMOVED: + return "NodeEvent: subnode removed"; + } + return "NodeEvent: invalid type"; + } + +} \ No newline at end of file diff --git a/src/helma/objectmodel/ObjectNotFoundException.java b/src/helma/objectmodel/ObjectNotFoundException.java new file mode 100644 index 00000000..e58d19bb --- /dev/null +++ b/src/helma/objectmodel/ObjectNotFoundException.java @@ -0,0 +1,49 @@ +// ObjectNotFoundException.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel; + + +/** + * Thrown when an object could not found in the database where + * it was expected. + */ + +public class ObjectNotFoundException extends Exception { + + public ObjectNotFoundException (String msg) { + super (msg); + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/Property.java b/src/helma/objectmodel/Property.java new file mode 100644 index 00000000..71a0f7c2 --- /dev/null +++ b/src/helma/objectmodel/Property.java @@ -0,0 +1,332 @@ +// Property.java +// Copyright (c) Hannes Wallnöfer 1997-2000 + +package helma.objectmodel; + +import helma.util.*; +import java.util.*; +import java.io.*; +import java.text.*; + +/** + * A property implementation for Nodes stored inside a database. + */ + +public final class Property implements IProperty, Serializable, Cloneable { + + + protected String propname; + protected Node node; + + public String svalue; + public boolean bvalue; + public long lvalue; + public double dvalue; + public INode nvalue; + public Object jvalue; + + public int type; + + + public Property (Node node) { + this.node = node; + } + + public Property (String propname, Node node) { + this.propname = propname; + this.node = node; + } + + public String getName () { + return propname; + } + + public Object getValue () { + switch (type) { + case STRING: + return svalue; + case BOOLEAN: + return new Boolean (bvalue); + case INTEGER: + return new Long (lvalue); + case FLOAT: + return new Double (dvalue); + case DATE: + return new Date (lvalue); + case NODE: + return nvalue; + case JAVAOBJECT: + return jvalue; + } + return null; + } + + public void setStringValue (String value) throws ParseException { + if (type == NODE) + unregisterNode (); + // IServer.getLogger().log ("setting string value of property "+propname + " to "+value); + if (type == DATE) { + SimpleDateFormat dateformat = new SimpleDateFormat (); + dateformat.setLenient (true); + Date date = dateformat.parse (value); + this.lvalue = date.getTime (); + return; + } + if (type == BOOLEAN) { + if ("true".equalsIgnoreCase (value)) + this.bvalue = true; + else if ("false".equalsIgnoreCase (value)) + this.bvalue = false; + return; + } + if (type == INTEGER) { + this.lvalue = Long.parseLong (value); + return; + } + if (type == FLOAT) { + this.dvalue = new Double (value).doubleValue (); + return; + } + if (type == JAVAOBJECT) + this.jvalue = null; + this.svalue = value; + type = STRING; + } + + public void setIntegerValue (long value) { + if (type == NODE) + unregisterNode (); + if (type == JAVAOBJECT) + this.jvalue = null; + type = INTEGER; + this.lvalue = value; + } + + public void setFloatValue (double value) { + if (type == NODE) + unregisterNode (); + if (type == JAVAOBJECT) + this.jvalue = null; + type = FLOAT; + this.dvalue = value; + } + + public void setDateValue (Date value) { + if (type == NODE) + unregisterNode (); + if (type == JAVAOBJECT) + this.jvalue = null; + type = DATE; + this.lvalue = value.getTime(); + } + + public void setBooleanValue (boolean value) { + if (type == NODE) + unregisterNode (); + if (type == JAVAOBJECT) + this.jvalue = null; + type = BOOLEAN; + this.bvalue = value; + } + + public void setNodeValue (INode value) { + if (type == JAVAOBJECT) + this.jvalue = null; + if (type == NODE && nvalue != value) { + unregisterNode (); + registerNode (value); + } else if (type != NODE) + registerNode (value); + type = NODE; + this.nvalue = value; + } + + public void setJavaObjectValue (Object value) { + if (type == NODE) + unregisterNode (); + type = JAVAOBJECT; + this.jvalue = value; + } + + + /** + * tell a the value node that it is no longer used as a property. + */ + protected void unregisterNode () { + if (nvalue != null && nvalue instanceof Node) { + Node n = (Node) nvalue; + if (!n.anonymous && propname.equals (n.getName()) && this.node == n.getParent()) { + // this is the "main" property of a named node, so handle this as a total delete. + IServer.getLogger().log ("deleting named property"); + if (n.proplinks != null) { + for (Enumeration e = n.proplinks.elements (); e.hasMoreElements (); ) { + Property p = (Property) e.nextElement (); + p.node.propMap.remove (p.propname.toLowerCase ()); + } + } + if (n.links != null) { + for (Enumeration e = n.links.elements (); e.hasMoreElements (); ) { + Node n2 = (Node) e.nextElement (); + n2.releaseNode (n); + } + } + + } else { + n.unregisterPropLink (this); + } + } + } + + /** + * tell the value node that it is being used as a property value. + */ + protected void registerNode (INode n) { + if (n != null && n instanceof Node) + ((Node) n).registerPropLink (this); + } + + public String getStringValue () { + switch (type) { + case STRING: + return svalue; + case BOOLEAN: + return "" + bvalue; + case DATE: + SimpleDateFormat format = new SimpleDateFormat ("dd.MM.yy hh:mm"); + return format.format (new Date (lvalue)); + case INTEGER: + return Long.toString (lvalue); + case FLOAT: + return Double.toString (dvalue); + case NODE: + return nvalue.getName (); + case JAVAOBJECT: + return jvalue.toString (); + } + return ""; + } + + public String toString () { + return getStringValue (); + } + + public long getIntegerValue () { + if (type == INTEGER) + return lvalue; + return 0; + } + + public double getFloatValue () { + if (type == FLOAT) + return dvalue; + return 0.0; + } + + + public Date getDateValue () { + if (type == DATE) + return new Date (lvalue); + return null; + } + + public boolean getBooleanValue () { + if (type == BOOLEAN) + return bvalue; + return false; + } + + public INode getNodeValue () { + if (type == NODE) + return nvalue; + return null; + } + + public Object getJavaObjectValue () { + if (type == JAVAOBJECT) + return jvalue; + return null; + } + + public String getEditor () { + switch (type) { + case STRING: + return "password".equalsIgnoreCase (propname) ? + "" : + "" ; + case BOOLEAN: + return ""; + case INTEGER: + return "" ; + case FLOAT: + return "" ; + case DATE: + SimpleDateFormat format = new SimpleDateFormat ("dd.MM.yy hh:mm"); + String date = format.format (new Date (lvalue)); + return ""; + case NODE: + return ""; + } + return ""; + } + + private String escape (String s) { + char c[] = new char[s.length()]; + s.getChars (0, c.length, c, 0); + StringBuffer b = new StringBuffer (); + int copyfrom = 0; + for (int i = 0; i < c.length; i++) { + switch (c[i]) { + case '\\': + case '"': + if (i-copyfrom > 0) + b.append (c, copyfrom, i-copyfrom); + b.append ('\\'); + b.append (c[i]); + copyfrom = i+1; + } + } + if (c.length-copyfrom > 0) + b.append (c, copyfrom, c.length-copyfrom); + return b.toString (); + } + + public int getType () { + return type; + } + + public String getTypeString () { + switch (type) { + case STRING: + return "string"; + case BOOLEAN: + return "boolean"; + case DATE: + return "date"; + case INTEGER: + return "integer"; + case FLOAT: + return "float"; + case NODE: + return "node"; + } + return ""; + } + + + public Object clone () { + try { + Property c = (Property) super.clone(); + c.propname = this.propname; + c.svalue = this.svalue; + c.bvalue = this.bvalue; + c.lvalue = this.lvalue; + c.dvalue = this.dvalue; + c.type = this.type; + return c; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError (); + } + } + +} diff --git a/src/helma/objectmodel/Relation.java b/src/helma/objectmodel/Relation.java new file mode 100644 index 00000000..cb4341a6 --- /dev/null +++ b/src/helma/objectmodel/Relation.java @@ -0,0 +1,308 @@ +// Relation.java +// Copyright (c) Hannes Wallnöfer 1997-2000 + +package helma.objectmodel; + +import helma.framework.core.Application; +import java.util.Properties; + +/** + * This describes how a property of a persistent Object is stored in a + * relational database table. This can be either a scalar property (string, date, number etc.) + * or a reference to one or more other objects. + */ +public class Relation { + + // TODO: explain hop mapping types + public final static int INVALID = -1; + public final static int PRIMITIVE = 0; + public final static int FORWARD = 1; + public final static int BACKWARD = 2; + public final static int DIRECT = 3; + + public DbMapping home; + public DbMapping other; + public String propname; + public String localField, remoteField; + public int direction; + + public boolean virtual; + public boolean readonly; + public boolean aggressiveLoading; + public boolean aggressiveCaching; + public boolean subnodesAreProperties; + public String order; + public String groupby; + public String prototype; + + Relation filter = null; // additional relation used to filter subnodes + + /** + * This constructor is used to directly construct a Relation, as opposed to reading it from a proerty file + */ + public Relation (DbMapping other, String localField, String remoteField, int direction, boolean subnodesAreProperties) { + this.other = other; + this.localField = localField; + this.remoteField = remoteField; + this.direction = direction; + this.subnodesAreProperties = subnodesAreProperties; + } + + /** + * Reads a relation entry from a line in a properties file. + */ + public Relation (String desc, String propname, DbMapping home, Properties props) { + + this.home = home; + this.propname = propname; + other = null; + Application app = home.getApplication (); + boolean mountpoint = false; + + if (desc == null || "".equals (desc.trim ())) { + if (propname != null) { + direction = PRIMITIVE; + localField = propname; + } else { + direction = INVALID; + localField = propname; + } + } else { + desc = desc.trim (); + String descLower = desc.toLowerCase (); + if (descLower.startsWith ("[virtual]")) { + desc = desc.substring (9).trim (); + virtual = true; + } else if (descLower.startsWith ("[collection]")) { + desc = desc.substring (12).trim (); + virtual = true; + } else if (descLower.startsWith ("[mountpoint]")) { + desc = desc.substring (12).trim (); + virtual = true; + mountpoint = true; + } else { + virtual = false; + } + if (descLower.startsWith ("[readonly]")) { + desc = desc.substring (10).trim (); + readonly = true; + } else { + readonly = false; + } + if (desc.indexOf ("<") > -1) { + direction = BACKWARD; + int lt = desc.indexOf ("<"); + int dot = desc.indexOf ("."); + String otherType = dot < 0 ? desc.substring (1).trim () : desc.substring (1, dot).trim (); + other = app.getDbMapping (otherType); + if (other == null) + throw new RuntimeException ("DbMapping for "+otherType+" not found from "+home.typename); + remoteField = dot < 0 ? null : desc.substring (dot+1).trim (); + localField = lt < 0 ? null : desc.substring (0, lt).trim (); + if (mountpoint) prototype = otherType; + } else if (desc.indexOf (">") > -1) { + direction = FORWARD; + int bt = desc.indexOf (">"); + int dot = desc.indexOf ("."); + String otherType = dot > -1 ? desc.substring (bt+1, dot).trim () : desc.substring (bt+1).trim (); + other = app.getDbMapping (otherType); + if (other == null) + throw new RuntimeException ("DbMapping for "+otherType+" not found from "+home.typename); + localField = desc.substring (0, bt).trim (); + remoteField = dot < 0 ? null : desc.substring (dot+1).trim (); + if (mountpoint) prototype = otherType; + } else if (desc.indexOf (".") > -1) { + direction = DIRECT; + int dot = desc.indexOf ("."); + String otherType = desc.substring (0, dot).trim (); + other = app.getDbMapping (otherType); + if (other == null) + throw new RuntimeException ("DbMapping for "+otherType+" not found from "+home.typename); + remoteField = desc.substring (dot+1).trim (); + localField = null; + if (mountpoint) prototype = otherType; + } else { + if (virtual) { + direction = DIRECT; + other = app.getDbMapping (desc); + if (other == null) + throw new RuntimeException ("DbMapping for "+desc+" not found from "+home.typename); + remoteField = localField = null; + if (mountpoint) prototype = desc; + } else { + direction = PRIMITIVE; + localField = desc.trim (); + } + } + } + String loading = props.getProperty (propname+".loadmode"); + aggressiveLoading = loading != null && "aggressive".equalsIgnoreCase (loading.trim()); + String caching = props.getProperty (propname+".cachemode"); + aggressiveCaching = caching != null && "aggressive".equalsIgnoreCase (caching.trim()); + // get order property + order = props.getProperty (propname+".order"); + if (order != null && order.trim().length() == 0) order = null; + // get group by property + groupby = props.getProperty (propname+".groupby"); + if (groupby != null && groupby.trim().length() == 0) groupby = null; + // check if subnode condition should be applied for property relations + if ("_properties".equalsIgnoreCase (propname) || virtual) { + String subnodes2props = props.getProperty (propname+".aresubnodes"); + subnodesAreProperties = "true".equalsIgnoreCase (subnodes2props); + if (virtual) { + String subnodefilter = props.getProperty (propname+".subnoderelation"); + if (subnodefilter != null) { + filter = new Relation (subnodefilter, propname+".subnoderelation", home, props); + filter.groupby = groupby; + } + } + } + } + + public boolean isReference () { + return direction > PRIMITIVE; + } + + public boolean usesPrimaryKey () { + if (remoteField == null || other == null) + return false; + return remoteField.equalsIgnoreCase (other.getIDField()); + } + + public Relation getFilter () { + return filter; + } + + /** + * Gets a key string to cache a node with a specific value for this relation. If the + * Relation uses the primary key return just the key value, otherwise include info on the + * used column or even the base node to avoid collisions. + */ + public String getKeyID (INode home, String kval) { + // if the column is not the primary key, we add the column name to the key + if ((direction == DIRECT || direction == FORWARD) && !usesPrimaryKey ()) { + // check if the subnode relation also has to be considered + if (subnodesAreProperties) + return "["+home.getID()+"]"+remoteField+"="+kval; // HACK + else + return remoteField+"="+kval; + } else { + return kval; + } + } + + /** + * Get the local column name for this relation. Uses the home node's id as fallback if local field is not specified. + */ + public String getLocalField () { + if (localField == null) + return home.getIDField (); + return localField; + } + + /** + * Get the "remote" column name for this relation. Uses the remote node's id as fallback if the remote field is not specified. + */ + public String getRemoteField () { + if (remoteField == null) + return other.getIDField (); + return remoteField; + } + + + /** + * Return a Relation that defines the subnodes of a virtual node. + */ + public Relation getVirtualSubnodeRelation () { + if (!virtual) + throw new RuntimeException ("getVirtualSubnodeRelation called on non-virtual relation"); + if (filter != null) + return filter; + return getVirtualPropertyRelation (); + } + + /** + * Return a Relation that defines the properties of a virtual node. + */ + public Relation getVirtualPropertyRelation () { + if (!virtual) + throw new RuntimeException ("getVirtualPropertyRelation called on non-virtual relation"); + Relation vr = new Relation (other, localField, remoteField, direction, subnodesAreProperties); + vr.groupby = groupby; + return vr; + } + + /** + * Return a Relation that defines the subnodes of a group-by node. + */ + public Relation getGroupbySubnodeRelation () { + if (groupby == null) + throw new RuntimeException ("getGroupbySubnodeRelation called on non-group-by relation"); + if (filter != null) + return filter; + return getGroupbyPropertyRelation (); + } + + /** + * Return a Relation that defines the properties of a group-by node. + */ + public Relation getGroupbyPropertyRelation () { + if (groupby == null) + throw new RuntimeException ("getGroupbyPropertyRelation called on non-group-by relation"); + return new Relation (other, localField, remoteField, direction, subnodesAreProperties); + } + + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/SystemProperties.java b/src/helma/objectmodel/SystemProperties.java new file mode 100644 index 00000000..ebc7f9d0 --- /dev/null +++ b/src/helma/objectmodel/SystemProperties.java @@ -0,0 +1,133 @@ +// Property.java +// Copyright (c) Hannes Wallnöfer 1997-2000 + +package helma.objectmodel; + +import helma.util.*; +import java.util.*; +import java.io.*; + +/** + * A property dictionary that is updated from a property file each time the + * file is modified. It is also case insensitive. + */ + +public final class SystemProperties extends Properties { + + private Properties props; // wrapped properties + private Properties newProps; // used while building up props + private Properties defaultProps; + private File file; + private long lastread, lastcheck; + + public SystemProperties (String filename) { + this (filename, null); + } + + public SystemProperties (Properties defaultProps) { + this (null, defaultProps); + } + + public SystemProperties (String filename, Properties defaultProps) { + // IServer.getLogger().log ("building sysprops with file "+filename+" and node "+node); + this.defaultProps = defaultProps; + props = defaultProps == null ? + new Properties () : new Properties (defaultProps); + + if (filename != null) { + file = new File (filename); + checkFile (); + } + } + + public boolean wasModified () { + return file != null && file.exists() && file.lastModified () > lastread; + } + + public long lastModified () { + return file == null || !file.exists () ? 0 : file.lastModified (); + } + + + private synchronized void checkFile () { + if (wasModified ()) { + // IServer.getLogger().log ("Reading properties from file "+file); + newProps = defaultProps == null ? + new Properties () : new Properties (defaultProps); + try { + FileInputStream bpin = new FileInputStream (file); + load (bpin); + bpin.close (); + } catch (Exception x) { + IServer.getLogger().log ("Error reading properties from file "+file+": "+x); + } + lastread = System.currentTimeMillis (); + props = newProps; + newProps = null; + } + lastcheck = System.currentTimeMillis (); + } + + /* + * This should not be used directly if properties are read from file, + * otherwise changes will be lost whe the file is next modified. + */ + public Object put (Object key, Object value) { + if (newProps == null) + return props.put (key.toString().toLowerCase(), value); + else + return newProps.put (key.toString().toLowerCase(), value); + } + + public Object get (Object key) { + return props.get (key); + } + + public Object remove (Object key) { + return props.remove (key); + } + + public void clear () { + props.clear (); + } + + public boolean contains (Object obj) { + return props.contains (obj); + } + + public boolean containsKey (Object key) { + return props.containsKey (key); + } + + public boolean isEmpty () { + return props.isEmpty (); + } + + public String getProperty (String name) { + if (System.currentTimeMillis () - lastcheck > 1500l) + checkFile (); + return props.getProperty (name.toLowerCase()); + } + + public String getProperty (String name, String defaultValue) { + if (System.currentTimeMillis () - lastcheck > 1500l) + checkFile (); + return props.getProperty (name.toLowerCase(), defaultValue); + } + + public Enumeration keys () { + if (System.currentTimeMillis () - lastcheck > 1500l) + checkFile (); + return props.keys(); + } + + public Enumeration elements () { + if (System.currentTimeMillis () - lastcheck > 1500l) + checkFile (); + return props.elements(); + } + +} + + + diff --git a/src/helma/objectmodel/db/ApplicationManager.java b/src/helma/objectmodel/db/ApplicationManager.java new file mode 100644 index 00000000..cf344544 --- /dev/null +++ b/src/helma/objectmodel/db/ApplicationManager.java @@ -0,0 +1,139 @@ +// ApplicationManager.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel.db; + +import java.util.*; +import java.io.*; +import java.lang.reflect.*; +import java.rmi.*; +import java.rmi.server.*; +import helma.framework.*; +import helma.framework.core.*; +import helma.objectmodel.*; +import helma.servlet.*; +import Acme.Serve.*; +import javax.servlet.Servlet; + + +/** + * This class is responsible for starting and stopping HOP applications. + */ + +public class ApplicationManager { + + private Hashtable applications; + private int port; + private File appHome, dbHome; + private SystemProperties props; + private Server server; + private long lastModified; + + public ApplicationManager (int port, File appHome, File dbHome, SystemProperties props, Server server) { + this.port = port; + this.appHome = appHome; + this.dbHome = dbHome; + this.props = props; + this.server = server; + applications = new Hashtable (); + lastModified = 0; + } + + + // regularely check applications property file to create and start new applications + protected void checkForChanges () { + if (props.lastModified () > lastModified) { + try { + for (Enumeration e = props.keys(); e.hasMoreElements (); ) { + String appName = (String) e.nextElement (); + if (applications.get (appName) == null) { + start (appName); + register (appName); + } + } + // then stop deleted ones + for (Enumeration e = applications.keys(); e.hasMoreElements (); ) { + String appName = (String) e.nextElement (); + if (!props.containsKey (appName)) { + stop (appName); + } + } + } catch (Exception mx) { + IServer.getLogger().log ("Error starting applications: "+mx); + } + + lastModified = System.currentTimeMillis (); + } + } + + + private void start (String appName) { + IServer.getLogger().log ("Building application "+appName); + try { + Application app = new Application (appName, dbHome, appHome); + applications.put (appName, app); + app.start (); + } catch (Exception x) { + IServer.getLogger().log ("Error creating application "+appName+": "+x); + } + } + + private void stop (String appName) { + IServer.getLogger().log ("Stopping application "+appName); + try { + Application app = (Application) applications.get (appName); + if (server.websrv == null) { + Naming.unbind ("//:"+port+"/"+appName); + } else { + server.websrv.removeServlet ("/"+appName+"/"); + server.websrv.removeServlet ("/"+appName+"/*"); + } + app.stop (); + IServer.getLogger().log ("Unregistered application "+appName); + } catch (Exception x) { + IServer.getLogger().log ("Couldn't unregister app: "+x); + } + applications.remove (appName); + } + + private void register (String appName) { + try { + IServer.getLogger().log ("Binding application "+appName); + Application app = (Application) applications.get (appName); + if (server.websrv == null) { + Naming.rebind ("//:"+port+"/"+appName, app); + } else { + AcmeServletClient servlet = new AcmeServletClient (app); + server.websrv.addServlet ("/"+appName+"/", servlet); + server.websrv.addServlet ("/"+appName+"/*", servlet); + } + } catch (Exception x) { + IServer.getLogger().log ("Couldn't register app: "+x); + } + } + + public void startAll () { + try { + for (Enumeration e = props.keys(); e.hasMoreElements (); ) { + String appName = (String) e.nextElement (); + start (appName); + } + for (Enumeration e = props.keys(); e.hasMoreElements (); ) { + String appName = (String) e.nextElement (); + register (appName); + } + if (server.websrv != null) { + File staticContent = new File (server.getHopHome(), "static"); + IServer.getLogger().log("Serving static content from "+staticContent.getAbsolutePath()); + AcmeFileServlet fsrv = new AcmeFileServlet (staticContent); + server.websrv.addServlet ("/static/", fsrv); + server.websrv.addServlet ("/static/*", fsrv); + } + lastModified = System.currentTimeMillis (); + } catch (Exception mx) { + IServer.getLogger().log ("Error starting applications: "+mx); + mx.printStackTrace (); + } + } + +} diff --git a/src/helma/objectmodel/db/DbWrapper.java b/src/helma/objectmodel/db/DbWrapper.java new file mode 100644 index 00000000..b6a1edcf --- /dev/null +++ b/src/helma/objectmodel/db/DbWrapper.java @@ -0,0 +1,281 @@ +// DbWrapper.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + + +package helma.objectmodel.db; + +import com.sleepycat.db.*; +import helma.objectmodel.ObjectNotFoundException; +import java.io.*; + +/** + * A wrapper around a Berkeley embedded database. Used to gracefully handle the case + * when the native library can not be loaded. + */ + +public class DbWrapper { + + private boolean loaded, useTransactions; + + private Db db; + DbEnv dbenv; + final int checkpointPause = 600000; // min. 10 minutes between checkpoints + volatile long lastCheckpoint = 0; + volatile long txncount=0; + + private File dbBaseDir; + + public DbWrapper (String dbHome, String dbFilename, boolean useTx) throws DbException { + try { + dbBaseDir = new File (dbHome); + if (!dbBaseDir.exists()) + dbBaseDir.mkdirs(); + + useTransactions = useTx; + + int dbInitFlags = Db.DB_CREATE | Db.DB_THREAD | Db.DB_INIT_MPOOL; + if (useTransactions) { + dbInitFlags = dbInitFlags | Db.DB_INIT_TXN; + } + + dbenv = new DbEnv (0); + try { + dbenv.open (dbHome, dbInitFlags, 0); // for berkeley 3.0, add second parameter (null) + } catch (FileNotFoundException fnf) { + // we just created the dirs, so this shouldn't happen + } + + try { + dbenv.set_error_stream(System.err); + dbenv.set_errpfx("HOP: "); + } catch (Exception e) { + System.err.println("Error in DbWrapper: "+e.toString()); + } + + db = new Db (dbenv, 0); + try { + db.upgrade (dbFilename, 0); + } catch (Exception ignore) { + // nothing to upgrade, db doesn't exist + } + + try { + db.open (dbFilename, null, Db.DB_BTREE, Db.DB_CREATE, 0644); + } catch (FileNotFoundException fnf) { + // we just created the dirs, so this shouldn't happen + } + loaded = true; + + } catch (NoClassDefFoundError noclass) { + Server.getLogger().log ("Warning: Using file based db as fallback. Reason: "+noclass); + loaded = false; + } catch (UnsatisfiedLinkError nolib) { + Server.getLogger().log ("Warning: Using file based db as fallback. Reason: "+nolib); + loaded = false; + } + + } + + + public void shutdown () throws DbException { + if (loaded) { + db.close (0); + dbenv.close (0); + } + } + + public DbTxn beginTransaction () throws DbException { + if (loaded && useTransactions) + return dbenv.txn_begin (null, 0); + else + return null; + } + + public void commitTransaction (DbTxn txn) throws DbException { + if (txn == null || !loaded || !useTransactions) + return; + txn.commit (0); + if (++txncount%100 == 0 && System.currentTimeMillis()-checkpointPause > lastCheckpoint) { + // checkpoint transaction logs in time interval specified by server.checkpointPause + // if there are more then 100 transactions to checkpoint. + checkpoint (); + } + } + + public void abortTransaction (DbTxn txn) throws DbException { + if (txn == null || !loaded || !useTransactions) + return; + txn.abort (); + } + + protected void checkpoint () throws DbException { + if (!loaded || !useTransactions || txncount == 0) + return; + long now = System.currentTimeMillis(); + if (now - lastCheckpoint < checkpointPause) + return; + dbenv.txn_checkpoint (0, 0, 0); // for berkeley 3.0, remove third 0 parameter + txncount = 0; + lastCheckpoint = now; + Server.getLogger().log ("Spent "+(System.currentTimeMillis()-now)+" in checkpoint"); + } + + public IDGenerator getIDGenerator (DbTxn txn, String kstr) throws Exception { + if (loaded) + return getIDGenFromDB (txn, kstr); + else + return getIDGenFromFile (kstr); + } + + public Node getNode (DbTxn txn, String kstr) throws Exception { + if (loaded) + return getNodeFromDB (txn, kstr); + else + return getNodeFromFile (kstr); + } + + public void save (DbTxn txn, String kstr, Object obj) throws Exception { + if (loaded) + saveToDB (txn, kstr, obj); + else + saveToFile (kstr, obj); + } + + public void delete (DbTxn txn, String kstr) throws Exception { + if (loaded) + deleteFromDB (txn, kstr); + else + deleteFromFile (kstr); + } + + + private IDGenerator getIDGenFromDB (DbTxn txn, String kstr) throws Exception { + long now = System.currentTimeMillis (); + byte[] kbuf = kstr.getBytes (); + Dbt key = new Dbt (kbuf); + key.set_size (kbuf.length); + Dbt data = new Dbt (); + data.set_flags (Db.DB_DBT_MALLOC); + + db.get (txn, key, data, 0); + + byte[] b = data.get_data (); + if (b == null) + throw new ObjectNotFoundException ("Object not found for key "+kstr+"."); + + IDGenerator idgen = null; + ByteArrayInputStream bin = new ByteArrayInputStream (b); + ObjectInputStream oin = new ObjectInputStream (bin); + idgen = (IDGenerator) oin.readObject (); + + oin.close (); + return idgen; + } + + + private Node getNodeFromDB (DbTxn txn, String kstr) throws Exception { + long now = System.currentTimeMillis (); + byte[] kbuf = kstr.getBytes (); + Dbt key = new Dbt (kbuf); + key.set_size (kbuf.length); + Dbt data = new Dbt (); + data.set_flags (Db.DB_DBT_MALLOC); + + db.get (txn, key, data, 0); + + byte[] b = data.get_data (); + if (b == null) + throw new ObjectNotFoundException ("Object not found for key "+kstr+"."); + + Node node = null; + ByteArrayInputStream bin = new ByteArrayInputStream (b); + ObjectInputStream oin = new ObjectInputStream (bin); + node = (Node) oin.readObject (); + oin.close (); + return node; + } + + + private void saveToDB (DbTxn txn, String kstr, Object obj) throws Exception { + long now = System.currentTimeMillis (); + byte kbuf[] = kstr.getBytes(); + ByteArrayOutputStream bout = new ByteArrayOutputStream (); + ObjectOutputStream oout = new ObjectOutputStream (bout); + oout.writeObject (obj); + oout.close (); + byte vbuf[] = bout.toByteArray (); + + Dbt key = new Dbt (kbuf); + key.set_size (kbuf.length); + Dbt value = new Dbt (vbuf); + value.set_size (vbuf.length); + + db.put (txn, key, value, 0); + // IServer.getLogger().log ("saved "+obj+", size = "+vbuf.length); + } + + private void deleteFromDB (DbTxn txn, String kstr) throws Exception { + + byte kbuf[] = kstr.getBytes(); + + Dbt key = new Dbt (kbuf); + key.set_size (kbuf.length); + + db.del (txn, key, 0); + } + + //////////////////////////////////////////////////////////////////////////////// + // File based fallback methods + /////////////////////////////////////////////////////////////////////////////// + + private IDGenerator getIDGenFromFile (String kstr) throws Exception { + + File f = new File (dbBaseDir, kstr); + + if ( ! f.exists() ) + throw new ObjectNotFoundException ("Object not found for key "+kstr+"."); + + IDGenerator idgen = null; + FileInputStream bin = new FileInputStream (f); + ObjectInputStream oin = new ObjectInputStream (bin); + idgen = (IDGenerator) oin.readObject (); + + oin.close (); + return idgen; + } + + + private Node getNodeFromFile (String kstr) throws Exception { + + File f = new File (dbBaseDir, kstr); + + if ( ! f.exists() ) + throw new ObjectNotFoundException ("Object not found for key "+kstr+"."); + + Node node = null; + FileInputStream bin = new FileInputStream (f); + ObjectInputStream oin = new ObjectInputStream (bin); + node = (Node) oin.readObject (); + oin.close (); + return node; + } + + + private void saveToFile (String kstr, Object obj) throws Exception { + + File f = new File (dbBaseDir, kstr); + + FileOutputStream bout = new FileOutputStream (f); + ObjectOutputStream oout = new ObjectOutputStream (bout); + oout.writeObject (obj); + oout.close (); + } + + private void deleteFromFile (String kstr) throws Exception { + + File f = new File (dbBaseDir, kstr); + f.delete(); + } + + +} \ No newline at end of file diff --git a/src/helma/objectmodel/db/ExternalizableVector.java b/src/helma/objectmodel/db/ExternalizableVector.java new file mode 100644 index 00000000..71f30a1d --- /dev/null +++ b/src/helma/objectmodel/db/ExternalizableVector.java @@ -0,0 +1,39 @@ +// ExternalizableVector.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.objectmodel.db; + +import java.io.*; +import java.util.*; + +/** + * A subclass of Vector that implements the Externalizable interface in order + * to be able to control how it is serialized and deserialized. + */ + +public class ExternalizableVector extends Vector implements Externalizable { + + static final long serialVersionUID = 2316243615310540423L; + + public synchronized void readExternal (ObjectInput in) throws IOException { + try { + int size = in.readInt (); + for (int i=0; i 0) + newprop.setFloatValue (val.asDouble()); + else + newprop.setIntegerValue (val.asLong()); + break; + + case Types.LONGVARBINARY: + case Types.VARBINARY: + case Types.BINARY: + newprop.setStringValue (val.asString()); + break; + + case Types.LONGVARCHAR: + case Types.CHAR: + case Types.VARCHAR: + case Types.OTHER: + newprop.setStringValue (val.asString()); + break; + + case Types.DATE: + case Types.TIME: + case Types.TIMESTAMP: + newprop.setDateValue (val.asTimestamp()); + break; + + case Types.NULL: + continue; + + default: + newprop.setStringValue (val.asString()); + break; + } + + if(propMap == null) + propMap = new Hashtable (); + propMap.put (rel.propname.toLowerCase(), newprop); + // mark property as clean, since it's fresh from the db + newprop.dirty = false; + + // if the property is a pointer to another node, change the property type to NODE + if (rel.direction == Relation.FORWARD) { + newprop.nvalueID = newprop.getStringValue (); + newprop.type = IProperty.NODE; + } + } + markAs (CLEAN); + } + + + /** + * Creates a new Node with the given name. + */ + public Node (String n, WrappedNodeManager nmgr) { + this.nmgr = nmgr; + id = nmgr.generateID (dbmap); + checkWriteLock (); + this.name = n == null || "".equals (n) ? id : n; + created = lastmodified = System.currentTimeMillis (); + adoptName = true; + markAs (TRANSIENT); + // nmgr.registerNode (this); + } + + /** + * Creates a new Node with the given name. Only used by NodeManager for "root nodes" and + * not in a Transaction context, which is why we can immediately mark it as CLEAN. + */ + protected Node (String name, String id, String prototype, WrappedNodeManager nmgr) { + this.nmgr = nmgr; + this.id = id; + this.name = name == null || "".equals (name) ? id : name; + if (prototype != null) { + propMap = new Hashtable (); + Property prop = new Property ("prototype", this); + prop.setStringValue (prototype); + propMap.put ("prototype", prop); + } + created = lastmodified = System.currentTimeMillis (); + markAs (CLEAN); + } + + + /** + * Creates a new instance of Node, transforming from another implementation of + * interface INode. This Constructor is used when a transient + * node is converted into a persistent-capable one, hence the status is set to NEW. + */ + private Node (INode node, Hashtable ntable, boolean conversionRoot, WrappedNodeManager nmgr) { + this.nmgr = nmgr; + this.dbmap = node.getDbMapping (); + this.id = nmgr.generateID (dbmap); + checkWriteLock (); + this.name = node.getName (); + created = lastmodified = System.currentTimeMillis (); + setContent (node.getContent (), node.getContentType ()); + created = lastmodified = System.currentTimeMillis (); + ntable.put (node, this); + // only take over name from property if this is not the root of the current node conversion + adoptName = !conversionRoot; + for (Enumeration e = node.getSubnodes (); e.hasMoreElements (); ) { + INode next = (INode) e.nextElement (); + Node nextc = (next instanceof Node) ? (Node) next : (Node) ntable.get (next); // is this a Node already? + if (nextc == null) + nextc = new Node (next, ntable, true, nmgr); + if (nextc.state == INVALID) + nextc = nmgr.getNode (nextc.getID (), nextc.getDbMapping ()); + addNode (nextc); + } + for (Enumeration e = node.properties (); e.hasMoreElements (); ) { + IProperty next = (IProperty) e.nextElement (); + int t = next.getType (); + if (t == IProperty.NODE) { + INode n = next.getNodeValue (); + Node nextc = (n instanceof Node) ? (Node) n : (Node) ntable.get (n); // is this a Node already? + if (nextc == null) + nextc = new Node (n, ntable, true, nmgr); + if (nextc.state == INVALID) + nextc = nmgr.getNode (nextc.getID (), nextc.getDbMapping ()); + setNode (next.getName (), nextc); + } else if (t == IProperty.STRING) { + setString (next.getName (), next.getStringValue ()); + } else if (t == IProperty.INTEGER) { + setInteger (next.getName (), next.getIntegerValue ()); + } else if (t == IProperty.FLOAT) { + setFloat (next.getName (), next.getFloatValue ()); + } else if (t == IProperty.BOOLEAN) { + setBoolean (next.getName (), next.getBooleanValue ()); + } else if (t == IProperty.DATE) { + setDate (next.getName (), next.getDateValue ()); + } else if (t == IProperty.JAVAOBJECT) { + setJavaObject (next.getName (), next.getJavaObjectValue ()); + } + } + adoptName = true; // switch back to normal name adoption behaviour + markAs (NEW); + // nmgr.registerNode (this); + } + + protected synchronized void checkWriteLock () { + // System.err.println ("registering writelock for "+this.getName ()+" ("+lock+") to "+Thread.currentThread ()); + if (state == TRANSIENT) + return; // no need to lock transient node + Transactor current = (Transactor) Thread.currentThread (); + if (!current.isActive ()) + throw new helma.framework.TimeoutException (); + if (state == INVALID) { + IServer.getLogger().log ("Got Invalid Node: "+this); + Thread.dumpStack (); + throw new ConcurrencyException ("Node "+this+" was invalidated by another thread."); + } + if (lock != null && lock != current && lock.isAlive () && lock.isActive ()) { + IServer.getLogger().log ("Concurrency conflict for "+this+", lock held by "+lock); + throw new ConcurrencyException ("Tried to modify "+this+" from two threads at the same time."); + } + current.visitNode (this); + lock = current; + } + + protected synchronized void clearWriteLock () { + lock = null; + // check if the subnodes are relational. + // If so, clear the subnode vector. + // DbMapping smap = dbmap == null ? null : dbmap.getSubnodeMapping (); + // if (smap != null && smap.isRelational ()) + // subnodes = null; + } + + protected void markAs (int s) { + if (state == INVALID || state == VIRTUAL || state == TRANSIENT) + return; + state = s; + if (Thread.currentThread () instanceof Transactor) { + Transactor tx = (Transactor) Thread.currentThread (); + if (s == CLEAN) { + clearWriteLock (); + tx.dropNode (this); + } else { + tx.visitNode (this); + if (s == NEW) { + clearWriteLock (); + tx.visitCleanNode (this); + } + } + } + } + + public int getState () { + return state; + } + + public void setState (int s) { + this.state = s; + } + + /* Mark node as invalid so it is re-fetched from the database */ + public void invalidate () { + checkWriteLock (); + nmgr.evictNode (this); + } + + + /** + * navigation-related + */ + + public String getID () { + return id; + } + + protected void setID (String id) { + this.id = id; + ((Transactor) Thread.currentThread()).visitCleanNode (this); + } + + public boolean isAnonymous () { + return anonymous; + } + + public String getName () { + return name; + } + + /** + * Get something to identify this node within a URL. This is the ID for anonymous nodes + * and a property value for named properties. + */ + public String getNameOrID () { + // if subnodes are also mounted as properties, try to get the "nice" prop value + // instead of the id by turning the anonymous flag off. + if (anonymous && parentmap != null) { + Relation prel = parentmap.getPropertyRelation(); + if (prel != null && prel.subnodesAreProperties) try { + Relation localrel = dbmap.columnNameToProperty (prel.remoteField); + String propvalue = getString (localrel.propname, false); + if (propvalue != null && propvalue.length() > 0) { + setName (propvalue); + anonymous = false; + // nameProp = localrel.propname; + } + } catch (Exception ignore) {} // just fall back to ID + } + return anonymous ? id : name; + } + + + public String getFullName () { + return getFullName (null); + } + + public String getFullName (INode root) { + String fullname = ""; + String divider = null; + StringBuffer b = new StringBuffer (); + INode p = this; + int loopWatch = 0; + + while (p != null && p.getParent () != null && p != root) { + if (divider != null) + b.insert (0, divider); + else + divider = "/"; + b.insert (0, p.getNameOrID ()); + p = p.getParent (); + + loopWatch++; + if (loopWatch > 10) { + b.insert (0, "..."); + break; + } + } + return b.toString (); + } + + public INode[] getPath () { + int pathSize = 1; + INode p = getParent (); + + while (p != null) { + pathSize +=1; + p = p.getParent (); + if (pathSize > 100) // sanity check + break; + } + INode path[] = new INode[pathSize]; + p = this; + for (int i = pathSize-1; i>=0; i--) { + path[i] = p; + p = p.getParent (); + } + return path; + } + + public void setDbMapping (DbMapping dbmap) { + if (this.dbmap != dbmap) { + this.dbmap = dbmap; + ((Transactor) Thread.currentThread()).visitCleanNode (this); + } + } + + public DbMapping getDbMapping () { + return dbmap; + } + + public Key getKey () { + return Key.makeKey (dbmap, id); + } + + public void setSubnodeRelation (String rel) { + if ((rel == null && this.subnodeRelation == null) + || (rel != null && rel.equalsIgnoreCase (this.subnodeRelation))) + return; + checkWriteLock (); + this.subnodeRelation = rel; + DbMapping smap = dbmap == null ? null : dbmap.getSubnodeMapping (); + if (smap != null && smap.isRelational ()) { + subnodes = null; + subnodeCount = -1; + } + } + + public String getSubnodeRelation () { + return subnodeRelation; + } + + public void setName (String name) { + // "/" is used as delimiter, so it's not a legal char + if (name.indexOf('/') > -1) + throw new RuntimeException ("The name of the node must not contain \"/\"."); + if (name == null || name.trim().length() == 0) + this.name = id; // use id as name + else + this.name = name; + } + + /** + * Register a node as parent of the present node. We can't refer to the node directly, so we use + * the ID + DB map combo. + */ + public void setParent (Node parent) { + this.parentID = parent == null ? null : parent.getID(); + this.parentmap = parent == null ? null : parent.getDbMapping(); + } + + /** + * This version of setParent additionally marks the node as anonymous or non-anonymous, + * depending on the string argument. + */ + public void setParent (Node parent, String propertyName) { + this.parentID = parent == null ? null : parent.getID(); + this.parentmap = parent == null ? null : parent.getDbMapping(); + if (propertyName == null) { + this.anonymous = true; + } else { + this.anonymous = false; + this.name = propertyName; + } + } + + + public Key getParentKey () { + if (parentID == null) + return null; + + DbMapping pm = parentmap; + if (pm == null && dbmap != null) + pm = dbmap.getParentMapping (); + return Key.makeKey (pm, parentID); + } + + public INode getParent () { + if (parentID == null) + return null; + + DbMapping pm = parentmap; + if (pm == null && dbmap != null) + pm = dbmap.getParentMapping (); + return nmgr.getNode (parentID, pm); + } + + + /** + * INode-related + */ + + public INode addNode (INode elem) { + return addNode (elem, numberOfNodes ()); + } + + public INode addNode (INode elem, int where) { + + Node node = null; + if (elem instanceof Node) + node = (Node) elem; + else + node = convert (elem); + // if the new node is marked as TRANSIENT and this node is not, mark new node as NEW + if (state != TRANSIENT && node.state == TRANSIENT) + node.makePersistentCapable (); + + String n = node.getName(); + if (n.indexOf('/') > -1) + throw new RuntimeException ("\"/\" found in Node name."); + + // only lock node if it has to be modified for a change in subnodes + if (!ignoreSubnodeChange ()) + checkWriteLock (); + node.checkWriteLock (); + + // only mark this node as modified if subnodes are not in relational db + // pointing to this node. + if (!ignoreSubnodeChange () && (state == CLEAN || state == DELETED)) + markAs (MODIFIED); + if (node.state == CLEAN || node.state == DELETED) + node.markAs (MODIFIED); + + loadNodes (); + + if (where < 0 || where > numberOfNodes ()) + where = numberOfNodes (); + if (node.getParent () != null) { + // this makes the job of addLink, which means that addLink and addNode + // are functionally equivalent now. + if (node.getParent () != this || !node.anonymous) { + node.registerLink (this); + } + } + + if (subnodes != null && subnodes.contains (node.getID ())) { + // Node is already subnode of this - just move to new position + subnodes.removeElement (node.getID ()); + where = Math.min (where, numberOfNodes ()); + subnodes.insertElementAt (node.getID (), where); + } else { + if (subnodes == null) subnodes = new ExternalizableVector (); + subnodes.insertElementAt (node.getID (), where); + + // check if properties are subnodes (_properties.aresubnodes=true) + if (dbmap != null && node.dbmap != null) { + Relation prel = dbmap.getPropertyRelation(); + if (prel != null && prel.subnodesAreProperties) { + Relation localrel = node.dbmap.columnNameToProperty (prel.remoteField); + // if no relation from db column to prop name is found, assume that both are equal + String propname = localrel == null ? prel.remoteField : localrel.propname; + String prop = node.getString (propname, false); + if (prop != null && prop.length() > 0) { + INode old = getNode (prop, false); + if (old != null && old != node) { + unset (prop); + removeNode (old); + } + // throw new RuntimeException ("Property "+prop+" is already defined for "+this); + setNode (prop, node); + } + } + } + + if (node.getParent () == null) { + node.setParent (this); + node.anonymous = true; + } + } + + // check if the subnode is in relational db and needs to link back to this + // in order to make it a subnode + if (dbmap != null) { + Relation srel = dbmap.getSubnodeRelation (); + if (srel != null && srel.direction == Relation.BACKWARD) { + Relation backlink = srel.other.columnNameToProperty (srel.remoteField); + if (backlink != null && backlink.propname != null) { + if (node.get (backlink.propname, false) == null) { + if (this.state == VIRTUAL) + node.setString (backlink.propname, getParent().getID()); + else + node.setString (backlink.propname, getID()); + } + } + } + } + + lastmodified = System.currentTimeMillis (); + lastSubnodeChange = lastmodified; + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_ADDED, node)); + return node; + } + + public INode createNode () { + return createNode (null, numberOfNodes ()); // create new node at end of subnode array + } + + public INode createNode (int where) { + return createNode (null, where); + } + + public INode createNode (String nm) { + // parameter where is ignored if nm != null so we try to avoid calling numberOfNodes() + return createNode (nm, nm == null ? numberOfNodes () : 0); + } + + public INode createNode (String nm, int where) { + checkWriteLock (); + boolean anon = false; + if (nm == null || "".equals (nm.trim ())) + anon = true; + Node n = new Node (nm, nmgr); + if (anon) + addNode (n, where); + else + setNode (nm, n); + return n; + } + + + /** + * register a node that links to this node. + */ + protected void registerLink (Node from) { + if (links == null) + links = new ExternalizableVector (); + Object fromID = from.getID (); + if (!links.contains (fromID)) + links.addElement (fromID); + } + + public INode getSubnode (String path) { + StringTokenizer st = new StringTokenizer (path, "/"); + Node retval = this, runner; + while (st.hasMoreTokens () && retval != null) { + runner = retval; + String next = st.nextToken().trim().toLowerCase (); + if ("".equals (next)) { + retval = this; + } else { + runner.loadNodes (); + boolean found = runner.subnodes == null ? false : runner.subnodes.contains (next); + DbMapping smap = runner.dbmap == null ? null : runner.dbmap.getSubnodeMapping (); + retval = found ? nmgr.getNode (next, smap) : null; + if (retval != null && retval.parentID == null) { + retval.setParent (runner); + retval.anonymous = true; + } + } + if (retval == null) { + retval = (Node) runner.getNode (next, false); + } + } + return retval; + } + + public INode getSubnodeAt (int index) { + loadNodes (); + + if (subnodes == null) + return null; + + Relation srel = null; + DbMapping smap = null; + if (dbmap != null) { + srel = dbmap.getSubnodeRelation (); + smap = dbmap.getSubnodeMapping (); + } + Node retval = null; + if (subnodes.size () > index) { + // check if there is a group-by relation + if (srel != null && srel.groupby != null) + retval = nmgr.getNode (this, (String) subnodes.elementAt (index), srel); + else + retval = nmgr.getNode ((String) subnodes.elementAt (index), smap); + if (retval != null && retval.parentID == null) { + retval.setParent (this); + retval.anonymous = true; + } + } + return retval; + } + + public boolean remove () { + checkWriteLock (); + if (anonymous) + getParent ().unset (name); + else + getParent ().removeNode (this); + return true; + } + + + public void removeNode (INode node) { + IServer.getLogger().log ("removing: "+ node); + Node n = (Node) node; + checkWriteLock (); + n.checkWriteLock (); + + releaseNode (n); + if (n.getParent () == this) { + n.deepRemoveNode (); + } else { + // removed just a link, not the main node. + if (n.links != null) { + n.links.removeElement (this.id); + if (n.state == CLEAN) n.markAs (MODIFIED); + } + } + } + + /** + * "Locally" remove a subnode from the subnodes table. + * The logical stuff necessary for keeping data consistent is done in removeNode(). + */ + protected void releaseNode (Node node) { + if (subnodes != null) + subnodes.removeElement (node.getID ()); + + lastSubnodeChange = System.currentTimeMillis (); + + // check if the subnode is in relational db and has a link back to this + // which needs to be unset + if (dbmap != null) { + Relation srel = dbmap.getSubnodeRelation (); + if (srel != null && srel.direction == Relation.BACKWARD) { + Relation backlink = srel.other.columnNameToProperty (srel.remoteField); + if (backlink != null && id.equals (node.getString (backlink.propname, false))) + node.unset (backlink.propname); + } + } + + // check if subnodes are handled as virtual fs + if (dbmap != null && node.dbmap != null) { + Relation prel = dbmap.getPropertyRelation(); + if (prel != null && prel.subnodesAreProperties) { + Relation localrel = node.dbmap.columnNameToProperty (prel.remoteField); + // if no relation from db column to prop name is found, assume that both are equal + String propname = localrel == null ? prel.remoteField : localrel.propname; + String prop = node.getString (propname, false); + if (prop != null && getNode (prop, false) == node) + unset (prop); + } + } + + // If subnodes are relational no need to mark this node as modified + if (ignoreSubnodeChange ()) + return; + + // Server.throwNodeEvent (new NodeEvent (node, NodeEvent.NODE_REMOVED)); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_REMOVED, node)); + lastmodified = System.currentTimeMillis (); + // IServer.getLogger().log ("released node "+node +" from "+this+" oldobj = "+what); + if (state == CLEAN) markAs (MODIFIED); + } + + /** + * Delete the node from the db. This mainly tries to notify all nodes referring to this that + * it's going away. For nodes from the embedded db it also does a cascading delete, since + * it can tell which nodes are actual children and which are just linked in. + */ + protected void deepRemoveNode () { + + // notify nodes that link to this node that it is being deleted. + int l = links == null ? 0 : links.size (); + for (int i = 0; i < l; i++) { + // TODO: solve dbmap problem + Node link = nmgr.getNode ((String) links.elementAt (i), null); + if (link != null) link.releaseNode (this); + } + + // clean up all nodes that use n as a property + if (proplinks != null) { + for (Enumeration e1 = proplinks.elements (); e1.hasMoreElements (); ) try { + String pid = (String) e1.nextElement (); + Node pnode = nmgr.getNode (pid, null); + if (pnode != null) { + IServer.getLogger().log("Warning: Can't unset node property of "+pnode.getFullName ()); + } + } catch (Exception ignore) {} + } + + // tell all nodes that are properties of n that they are no longer used as such + if (propMap != null) { + for (Enumeration e2 = propMap.elements (); e2.hasMoreElements (); ) { + Property p = (Property) e2.nextElement (); + if (p != null && p.type == Property.NODE) + p.unregisterNode (); + } + } + + // cascading delete of all subnodes. This is never done for relational subnodes, because + // the parent info is not 100% accurate for them. + if (subnodes != null) { + Vector v = new Vector (); + // removeElement modifies the Vector we are enumerating, so we are extra careful. + for (Enumeration e3 = getSubnodes (); e3.hasMoreElements(); ) { + v.addElement (e3.nextElement()); + } + int m = v.size (); + for (int i=0; i= lastSubnodeCount || subnodeCount < 0) { + // count nodes in db without fetching anything + subnodeCount = nmgr.countNodes (this, subRel); + lastSubnodeCount = System.currentTimeMillis (); + } + return subnodeCount; + } + } + loadNodes (); + return subnodes == null ? 0 : subnodes.size (); + } + + /** + * Make sure the subnode index is loaded for subnodes stored in a relational data source. + * Depending on the subnode.loadmode specified in the type.properties, we'll load just the + * ID index or the actual nodes. + */ + protected void loadNodes () { + DbMapping smap = dbmap == null ? null : dbmap.getSubnodeMapping (); + if (smap != null && smap.isRelational ()) { + // check if subnodes need to be reloaded + Relation subRel = dbmap.getSubnodeRelation (); + synchronized (this) { + long lastChange = subRel.aggressiveCaching ? lastSubnodeChange : smap.lastDataChange; + // also reload if the type mapping has changed. + lastChange = Math.max (lastChange, dbmap.getLastTypeChange ()); + if (lastChange >= lastSubnodeFetch || subnodes == null) { + if (subRel.aggressiveLoading) + subnodes = nmgr.getNodes (this, dbmap.getSubnodeRelation()); + else + subnodes = nmgr.getNodeIDs (this, dbmap.getSubnodeRelation()); + lastSubnodeFetch = System.currentTimeMillis (); + } + } + } + } + + public Enumeration getSubnodes () { + loadNodes (); + class Enum implements Enumeration { + int count = 0; + public boolean hasMoreElements () { + return count < numberOfNodes (); + } + public Object nextElement () { + return getSubnodeAt (count++); + } + } + return new Enum (); + } + + private boolean ignoreSubnodeChange () { + // return true if a change in subnodes can be ignored because it is + // stored in the subnodes themselves. + Relation rel = dbmap == null ? null : dbmap.getSubnodeRelation(); + return (rel != null && rel.direction == Relation.BACKWARD); + } + + /** + * Get all properties of this node. + */ + public Enumeration properties () { + + final Relation prel = dbmap == null ? null : dbmap.getPropertyRelation (); + final DbMapping pmap = prel == null ? null : prel.other; + if (pmap != null && pmap.isRelational ()) { + class Enum implements Enumeration { + // get relational property nodes from db + Vector propnodes = nmgr.getNodes (Node.this, prel.subnodesAreProperties ? + dbmap.getSubnodeRelation () : prel); + // add non-relational properties from propMap + Enumeration penum = propMap == null ? null : propMap.elements (); + int size = propnodes.size (); + int psize = propMap == null ? 0 : propMap.size(); + int count = 0; + public boolean hasMoreElements () { + return count < size+psize; + } + public Object nextElement () { + // first deliver local, non-relational properties .... + if (penum != null && penum.hasMoreElements ()) { + count++; + return penum.nextElement (); + } + // .... then the relational ones. + Node n = nmgr.getNode ((String) propnodes.elementAt (count - psize), pmap); + Property prop = new Property (n.getName (), Node.this, n); + count++; + return prop; + } + } + return new Enum (); + } + + return propMap == null ? new Vector ().elements () : propMap.elements (); + + // NOTE: we don't enumerate node properties here + // return propMap == null ? new Vector ().elements () : propMap.elements (); + } + + + + public IProperty get (String propname, boolean inherit) { + return getProperty (propname, inherit); + } + + public String getParentInfo () { + return "anonymous:"+anonymous+",parentID:"+parentID+",parentmap:"+parentmap+",parent:"+getParent(); + } + + protected Property getProperty (String propname, boolean inherit) { + // IServer.getLogger().log ("GETTING PROPERTY: "+propname); + if (propname == null) + return null; + Property prop = propMap == null ? null : (Property) propMap.get (propname.toLowerCase ()); + + // See if this could be a relationally linked node which still doesn't know + // (i.e, still thinks it's just the key as a string) + DbMapping pmap = dbmap == null ? null : dbmap.getExactPropertyMapping (propname); + if (pmap != null && prop != null && prop.type != IProperty.NODE) { + // this is a relational node stored by id + prop.nvalueID = prop.getStringValue (); + prop.type = IProperty.NODE; + } + + + if (prop == null && dbmap != null) { + Relation prel = dbmap.getPropertyRelation (propname); + if (prel == null) + prel = dbmap.getPropertyRelation (); + if (prel != null && (prel.direction == Relation.DIRECT || prel.virtual)) { + // this *may* be a relational node stored by property name + try { + Node pn = nmgr.getNode (this, propname, prel); + if (pn != null) { + if (pn.parentID == null) { + pn.setParent (this); + pn.name = propname; + pn.anonymous = false; + } + prop = new Property (propname, this, pn); + } + } catch (RuntimeException nonode) { + // wasn't a node after all + } + } + } + if (prop == null && inherit && getParent () != null) { + prop = ((Node) getParent ()).getProperty (propname, inherit); + } + return prop; + } + + public String getString (String propname, String defaultValue, boolean inherit) { + String propValue = getString (propname, inherit); + return propValue == null ? defaultValue : propValue; + } + + public String getString (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getStringValue (); + } catch (Exception ignore) {} + return null; + } + + public long getInteger (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getIntegerValue (); + } catch (Exception ignore) {} + return 0; + } + + public double getFloat (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getFloatValue (); + } catch (Exception ignore) {} + return 0.0; + } + + public Date getDate (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getDateValue (); + } catch (Exception ignore) {} + return null; + } + + + public boolean getBoolean (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getBooleanValue (); + } catch (Exception ignore) {} + return false; + } + + public INode getNode (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getNodeValue (); + } catch (Exception ignore) {} + return null; + } + + public Object getJavaObject (String propname, boolean inherit) { + propname = propname.toLowerCase (); + Property prop = getProperty (propname, inherit); + try { + return prop.getJavaObjectValue (); + } catch (Exception ignore) {} + return null; + } + + public void setString (String propname, String value) { + // IServer.getLogger().log ("setting String prop"); + checkWriteLock (); + + if (propMap == null) + propMap = new Hashtable (); + + propname = propname.trim (); + String p2 = propname.toLowerCase (); + + Property prop = (Property) propMap.get (p2); + String oldvalue = null; + + if (prop != null) { + oldvalue = prop.getStringValue (); + // check if the value has changed + if (value != null && value.equals (oldvalue)) + return; + prop.setStringValue (value); + } else { + prop = new Property (propname, this); + prop.setStringValue (value); + propMap.put (p2, prop); + } + + // check if this may have an effect on the node's URL when using subnodesAreProperties + INode parent = getParent (); + + if (parent != null && parent.getDbMapping() != null) { + // check if this node is already registered with the old name; if so, remove it. + // then set parent's property to this node for the new name value + parentmap = parent.getDbMapping (); + Relation prel = parentmap.getPropertyRelation (); + + if (prel != null && prel.subnodesAreProperties && propname.equals (prel.remoteField)) { + INode n = parent.getNode (value, false); + if (n != null && n != this) { + parent.unset (value); + parent.removeNode (n); + } + + if (oldvalue != null) { + n = parent.getNode (oldvalue, false); + if (n == this) { + parent.unset (oldvalue); + parent.addNode (this); + // let the node cache know this key's not for this node anymore. + nmgr.evictKey (Key.makeKey (prel.other, prel.getKeyID (parent, oldvalue))); + } + } + parent.setNode (value, this); + setName (value); + } + } + + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) markAs (MODIFIED); + + } + + public void setInteger (String propname, long value) { + // IServer.getLogger().log ("setting bool prop"); + checkWriteLock (); + + if (propMap == null) + propMap = new Hashtable (); + propname = propname.trim (); + String p2 = propname.toLowerCase (); + + Property prop = (Property) propMap.get (p2); + if (prop != null) { + prop.setIntegerValue (value); + } else { + prop = new Property (propname, this); + prop.setIntegerValue (value); + propMap.put (p2, prop); + } + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) markAs (MODIFIED); + } + + public void setFloat (String propname, double value) { + // IServer.getLogger().log ("setting bool prop"); + checkWriteLock (); + + if (propMap == null) + propMap = new Hashtable (); + propname = propname.trim (); + String p2 = propname.toLowerCase (); + + Property prop = (Property) propMap.get (p2); + if (prop != null) { + prop.setFloatValue (value); + } else { + prop = new Property (propname, this); + prop.setFloatValue (value); + propMap.put (p2, prop); + } + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) markAs (MODIFIED); + } + + public void setBoolean (String propname, boolean value) { + // IServer.getLogger().log ("setting bool prop"); + checkWriteLock (); + + if (propMap == null) + propMap = new Hashtable (); + propname = propname.trim (); + String p2 = propname.toLowerCase (); + + Property prop = (Property) propMap.get (p2); + if (prop != null) { + prop.setBooleanValue (value); + } else { + prop = new Property (propname, this); + prop.setBooleanValue (value); + propMap.put (p2, prop); + } + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) markAs (MODIFIED); + } + + + public void setDate (String propname, Date value) { + // IServer.getLogger().log ("setting date prop"); + checkWriteLock (); + + if (propMap == null) + propMap = new Hashtable (); + propname = propname.trim (); + String p2 = propname.toLowerCase (); + + Property prop = (Property) propMap.get (p2); + if (prop != null) { + prop.setDateValue (value); + } else { + prop = new Property (propname, this); + prop.setDateValue (value); + propMap.put (p2, prop); + } + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) markAs (MODIFIED); + } + + public void setJavaObject (String propname, Object value) { + // IServer.getLogger().log ("setting jobject prop"); + checkWriteLock (); + + if (propMap == null) + propMap = new Hashtable (); + propname = propname.trim (); + String p2 = propname.toLowerCase (); + + Property prop = (Property) propMap.get (p2); + if (prop != null) { + prop.setJavaObjectValue (value); + } else { + prop = new Property (propname, this); + prop.setJavaObjectValue (value); + propMap.put (p2, prop); + } + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) markAs (MODIFIED); + } + + public void setNode (String propname, INode value) { + // IServer.getLogger().log ("setting node prop"); + checkWriteLock (); + + Node n = null; + if (value instanceof Node) + n = (Node) value; + else + n = convert (value); + // if the new node is marked as TRANSIENT and this node is not, mark new node as NEW + if (state != TRANSIENT && n.state == TRANSIENT) + n.makePersistentCapable (); + + n.checkWriteLock (); + + // check if the main identity of this node is as a named property + // or as an anonymous node in a collection + if (n.getParent () == null && n.adoptName) { + n.setParent (this); + n.name = propname; + n.anonymous = false; + // IServer.getLogger().log ("adopted named node: "+n.getFullName ()); + } // else IServer.getLogger().log ("not adopted: "+n.getFullName ()); + + if (propMap == null) + propMap = new Hashtable (); + propname = propname.trim (); + String p2 = propname.toLowerCase (); + + DbMapping nmap = dbmap == null ? null : dbmap.getPropertyMapping (propname); + if (nmap != null && nmap != n.getDbMapping()) { + n.setDbMapping (nmap); + n.setString ("prototype", nmap.getTypeName ()); + } + + Property prop = (Property) propMap.get (p2); + if (prop != null) { + if (prop.type == IProperty.NODE && n.getID ().equals (prop.nvalueID)) { + // nothing to do, just clean up locks and return + if (state == CLEAN) clearWriteLock (); + if (n.state == CLEAN) n.clearWriteLock (); + return; + } + } else { + prop = new Property (propname, this); + } + + prop.setNodeValue (n); + Relation rel = dbmap == null ? null : dbmap.getPropertyRelation (propname); + if (rel == null || rel.direction == Relation.FORWARD || rel.virtual || rel.other == null || !rel.other.isRelational()) { + // the node must be stored as explicit property + propMap.put (p2, prop); + } + String nID = n.getID(); + ((Transactor) Thread.currentThread ()).visitCleanNode (Key.makeKey (nmap, nID), n); + // if the field is not the primary key of the property, also register it + if (rel != null && !rel.getKeyID(this, p2).equals (nID)) + ((Transactor) Thread.currentThread ()).visitCleanNode (Key.makeKey (rel.other, rel.getKeyID(this, p2)), n); + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_ADDED, n)); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) markAs (MODIFIED); + if (n.state == DELETED) n.markAs (MODIFIED); + } + + /** + * Remove a property. Note that this works only for explicitly set properties, not for those + * specified via property relation. + */ + public void unset (String propname) { + if (propMap == null) + return; + try { + Property p = (Property) propMap.remove (propname.toLowerCase ()); + if (p != null) { + checkWriteLock (); + if (p.type == Property.NODE) + p.unregisterNode (); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) + markAs (MODIFIED); + } + } catch (Exception ignore) {} + } + + protected void registerPropLink (INode n) { + if (proplinks == null) + proplinks = new ExternalizableVector (); + String plid = n.getID (); + if (!proplinks.contains (plid)) + proplinks.addElement (n.getID ()); + if (state == CLEAN || state == DELETED) + markAs (MODIFIED); + } + + protected void unregisterPropLink (INode n) { + if (proplinks != null) + proplinks.removeElement (n.getID ()); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.NODE_REMOVED)); + // Server.throwNodeEvent (new NodeEvent (n, NodeEvent.SUBNODE_REMOVED, this)); + if (state == CLEAN) + markAs (MODIFIED); + } + + + public void sanityCheck () { + checkSubnodes (); + checkProperties (); + checkLinks (); + checkPropLinks (); + if (getParent () == null && !"root".equals (name)) { + System.out.println ("*** parent: "+parentID+": "+this.getName ()); + } + } + + + private void checkLinks () { + Vector v = links == null ? null : (Vector) links.clone (); + int l = v == null ? 0 : v.size (); + for (int i = 0; i < l; i++) { + String k = (String) v.elementAt (i); + Node link = nmgr.getNode (k, null); + if (link == null) { + links.removeElement (k); + System.out.println ("**** link "+k+": "+this.getFullName ()); + markAs (MODIFIED); + } + } + } + + private void checkPropLinks () { + Vector v = proplinks == null ? null : (Vector) proplinks.clone (); + int l = v == null ? 0 : v.size (); + for (int i = 0; i < l; i++) { + String k = (String) v.elementAt (i); + Node link = nmgr.getNode (k, null); + if (link == null) { + proplinks.removeElement (k); + System.out.println ("**** proplink "+k+": "+this.getFullName ()); + markAs (MODIFIED); + } + } + } + + private void checkSubnodes () { + Vector v = subnodes == null ? null : (Vector) subnodes.clone (); + int l = v == null ? 0 : v.size (); + for (int i = 0; i < l; i++) { + String k = (String) v.elementAt (i); + Node link = nmgr.getNode (k, null); + if (link == null) { + subnodes.removeElement (k); + System.out.println ("**** subnode "+k+": "+this.getFullName ()); + markAs (MODIFIED); + } + } + } + + private void checkProperties () { + Hashtable ht = propMap == null ? new Hashtable () : (Hashtable) propMap.clone (); + for (Enumeration ps = ht.elements (); ps.hasMoreElements (); ) { + Property p = (Property) ps.nextElement (); + if (p.getType () == IProperty.NODE && p.getNodeValue () == null) { + System.out.println ("**** property "+p.propname+"->"+p.nvalueID+": "+this.getFullName ()); + // INode par = getParent (); + // if (par != null) + // par.removeNode (this); + // markAs (DELETED); + } + } + + } + + /** + * content-related + */ + + public boolean isText () throws IOException { + return getContentType().indexOf ("text/") == 0; + } + + public boolean isBinary () throws IOException { + return getContentType().indexOf ("text/") != 0; + } + + public String getContentType () { + if (contentType == null) + return "text/plain"; + return contentType; + } + + public void setContentType (String type) { + checkWriteLock (); + contentType = type; + lastmodified = System.currentTimeMillis (); + if (state == CLEAN) markAs (MODIFIED); + } + + public int getContentLength () { + if (content == null) + return 0; + return content.length; + } + + public void setContent (byte cnt[], String type) { + checkWriteLock (); + + if (type != null) + contentType = type; + content = cnt; + lastmodified = System.currentTimeMillis (); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.CONTENT_CHANGED)); + if (state == CLEAN) markAs (MODIFIED); + } + + + public void setContent (String cstr) { + checkWriteLock (); + + content = cstr.getBytes (); + lastmodified = System.currentTimeMillis (); + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.CONTENT_CHANGED)); + if (state == CLEAN) markAs (MODIFIED); + } + + public byte[] getContent () { + + if (content == null || content.length == 0) + return "".getBytes (); + + byte retval[] = new byte[content.length]; + System.arraycopy (content, 0, retval, 0, content.length); + // IServer.getLogger().log ("copied "+retval.length+ " bytes"); + return retval; + } + + public String getText () { + if (content != null) { + if (getContentType ().startsWith ("text/")) { + return new String (content); + } else { + return null; + } + } + return null; + } + + + public String getHref (INode root, INode userroot, String tmpname, String prefix) { + return prefix + getUrl (root, userroot, tmpname); + } + + + /** + * Get the path to eiter the general data-root or the user root, depending on + * where this node is located. + */ + public String getUrl (INode root, INode userroot, String tmpname) { + // String fullname = ""; + String divider = "/"; + StringBuffer b = new StringBuffer (); + INode p = this; + int loopWatch = 0; + while (p != null && p.getParent () != null && p != root && p != userroot) { + b.insert (0, divider); + b.insert (0, UrlEncoder.encode (p.getNameOrID ())); + + if (p.getParent () == userroot) + b.insert (0, "users"+divider); + p = p.getParent (); + + if (loopWatch++ > 10) + break; + } + return b.toString()+UrlEncoder.encode (tmpname); + } + + public long lastModified () { + return lastmodified; + } + + public long created () { + return created; + } + + public String toString () { + return "Node " + name; + } + + + /** + * Recursively convert other implementations of INode into helma.objectmodel.db.Node. + */ + protected Node convert (INode n) { + Hashtable ntable = new Hashtable (); + Node converted = new Node (n, ntable, false, nmgr); + return converted; + } + + /** + * Recursively turn node status from TRANSIENT to NEW so that the Transactor will + * know it has to insert this node. + */ + protected void makePersistentCapable () { + for (Enumeration e = getSubnodes (); e.hasMoreElements (); ) { + Node n = (Node) e.nextElement (); + if (n.state == TRANSIENT) + n.makePersistentCapable (); + } + for (Enumeration e = properties (); e.hasMoreElements (); ) { + IProperty next = (IProperty) e.nextElement (); + if (next.getType () == IProperty.NODE) { + Node n = (Node) next.getNodeValue (); + if (n != null && n.state == TRANSIENT) + n.makePersistentCapable (); + } + } + state = NEW; + } + + + /** + * Get the cache node for this node. This can be used to store transient cache data per node from Javascript. + */ + public synchronized INode getCacheNode () { + if (cacheNode == null) + cacheNode = new helma.objectmodel.Node(); + return cacheNode; + } + +} + diff --git a/src/helma/objectmodel/db/NodeManager.java b/src/helma/objectmodel/db/NodeManager.java new file mode 100644 index 00000000..3364e1f2 --- /dev/null +++ b/src/helma/objectmodel/db/NodeManager.java @@ -0,0 +1,959 @@ +// NodeManager.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel.db; + +import java.io.*; +import java.util.Vector; +import java.util.Properties; +import Acme.LruHashtable; +import helma.objectmodel.*; +import helma.framework.core.Application; +import com.sleepycat.db.*; +import java.sql.*; +import java.util.Vector; +import java.util.Enumeration; +import com.workingdogs.village.*; + +/** + * The NodeManager is responsible for fetching Nodes from the internal or + * external data sources, caching them in a least-recently-used Hashtable, + * and writing changes back to the databases. + */ + +public final class NodeManager { + + private Application app; + + private LruHashtable cache; + + protected DbWrapper db; + + protected IDGenerator idgen; + + private long idBaseValue = 1l; + + private boolean logSql; + + // a wrapper that catches some Exceptions while accessing this NM + public final WrappedNodeManager safe; + + public NodeManager (Application app, String dbHome, Properties props) throws DbException { + this.app = app; + int cacheSize = Integer.parseInt (props.getProperty ("cachesize", "1000")); + cache = new LruHashtable (cacheSize, 0.9f); + IServer.getLogger().log ("set up node cache ("+cacheSize+")"); + + safe = new WrappedNodeManager (this); + + // get the initial id generator value + String idb = props.getProperty ("idBaseValue"); + if (idb != null) try { + idBaseValue = Long.parseLong (idb); + idBaseValue = Math.max (1l, idBaseValue); // 0 and 1 are reserved for root nodes + } catch (NumberFormatException ignore) {} + + db = new DbWrapper (dbHome, Server.dbFilename, Server.useTransactions); + initDb (); + + logSql = "true".equalsIgnoreCase(props.getProperty ("logsql")); + } + + /** + * Method used to create the root node and id-generator, if they don't exist already. + */ + public void initDb () throws DbException { + + DbTxn txn = null; + try { + txn = db.beginTransaction (); + + try { + idgen = db.getIDGenerator (txn, "idgen"); + if (idgen.getValue() < idBaseValue) { + idgen.setValue (idBaseValue); + db.save (txn, "idgen", idgen); + } + } catch (ObjectNotFoundException notfound) { + // will start with idBaseValue+1 + idgen = new IDGenerator (idBaseValue); + db.save (txn, "idgen", idgen); + } + + // check if we need to set the id generator to a base value + + Node node = null; + try { + node = db.getNode (txn, "0"); + node.nmgr = safe; + } catch (ObjectNotFoundException notfound) { + node = new Node ("root", "0", "root", safe); + db.save (txn, node.getID (), node); + registerNode (node); // register node with nodemanager cache + } + + try { + node = db.getNode (txn, "1"); + node.nmgr = safe; + } catch (ObjectNotFoundException notfound) { + node = new Node ("users", "1", null, safe); + db.save (txn, node.getID (), node); + registerNode (node); // register node with nodemanager cache + } + + db.commitTransaction (txn); + } catch (Exception x) { + System.err.println (">> "+x); + x.printStackTrace (); + try { + db.abortTransaction (txn); + } catch (Exception ignore) {} + throw (new DbException ("Error initializing db")); + } + } + + public void shutdown () throws DbException { + db.shutdown (); + this.cache = null; + } + + public void deleteNode (Node node) throws Exception { + if (node != null) { + String id = node.getID (); + synchronized (this) { + Transactor tx = (Transactor) Thread.currentThread (); + node.setState (Node.INVALID); + deleteNode (db, tx.txn, node); + } + } + } + + + public Node getNode (String kstr, DbMapping dbmap) throws Exception { + + if (kstr == null) + return null; + + Key key = Key.makeKey (dbmap, kstr); + Transactor tx = (Transactor) Thread.currentThread (); + // tx.timer.beginEvent ("getNode "+kstr); + + // See if Transactor has already come across this node + Node node = tx.getVisitedNode (key); + + if (node != null && node.getState() != Node.INVALID) { + // tx.timer.endEvent ("getNode "+kstr); + return node; + } + + // try to get the node from the shared cache + node = (Node) cache.get (key); + if (node == null || node.getState() == Node.INVALID) { + + // The requested node isn't in the shared cache. Synchronize with key to make sure only one + // version is fetched from the database. + synchronized (key) { + + // check again because only in the synchronized section can we be sure that + // another thread hasn't fetched the node in the meantime. + node = (Node) cache.get (key); + + if (node == null || node.getState() == Node.INVALID) { + node = getNodeByKey (db, tx.txn, kstr, dbmap); + if (node != null) { + cache.put (node.getKey (), node); + } + } + } // synchronized + } + + if (node != null) + tx.visitCleanNode (node.getKey (), node); + + // tx.timer.endEvent ("getNode "+kstr); + return node; + } + + public Node getNode (Node home, String kstr, Relation rel) throws Exception { + + if (kstr == null) + return null; + + Key key; + // If what we want is a virtual node create a "synthetic" key + if (rel.virtual /*&& home.getState() != INode.VIRTUAL */ || rel.groupby != null) + key = home.getKey ().getVirtualKey (kstr); + // if a key for a node from within the DB + else + key = Key.makeKey (rel.other, rel.getKeyID (home, kstr)); + + Transactor tx = (Transactor) Thread.currentThread (); + // tx.timer.beginEvent ("getNode "+kstr); + + // See if Transactor has already come across this node + Node node = tx.getVisitedNode (key); + + if (node != null && node.getState() != Node.INVALID) { + // tx.timer.endEvent ("getNode "+kstr); + return node; + } + + // try to get the node from the shared cache + node = (Node) cache.get (key); + if (node == null || node.getState() == Node.INVALID) { + + // The requested node isn't in the shared cache. Synchronize with key to make sure only one + // version is fetched from the database. + synchronized (key) { + + // check again because only in the synchronized section can we be sure that + // another thread hasn't fetched the node in the meantime. + node = (Node) cache.get (key); + + if (node == null || node.getState() == Node.INVALID) { + node = getNodeByRelation (db, tx.txn, home, kstr, rel); + if (node != null) { + Key primKey = node.getKey (); + cache.put (primKey, node); + if (!key.equals (primKey)) { + // cache node with extra (non-primary but unique) key + cache.put (key, node); + } + } + } + } // synchronized + } + + if (node != null) + tx.visitCleanNode (node.getKey (), node); + + // tx.timer.endEvent ("getNode "+kstr); + return node; + } + + public void registerNode (Node node) { + cache.put (node.getKey (), node); + } + + + public void evictNode (Node node) { + node.setState (INode.INVALID); + cache.remove (node.getKey ()); + } + + /** + * Used when a key stops being valid for a node. + */ + public void evictKey (Key key) { + cache.remove (key); + } + + + //////////////////////////////////////////////////////////////////////// + // methods to do the actual db work + //////////////////////////////////////////////////////////////////////// + + + public void insertNode (DbWrapper db, DbTxn txn, Node node) throws Exception { + + Transactor tx = (Transactor) Thread.currentThread (); + // tx.timer.beginEvent ("insertNode "+node); + + DbMapping dbm = node.getDbMapping (); + + if (dbm == null || !dbm.isRelational ()) { + db.save (txn, node.getID (), node); + } else { + IServer.getLogger().log ("inserting relational node: "+node.getID ()); + TableDataSet tds = null; + try { + tds = new TableDataSet (dbm.getConnection (), dbm.getSchema (), dbm.getKeyDef ()); + Record rec = tds.addRecord (); + rec.setValue (dbm.getIDField (), node.getID ()); + + String nameField = dbm.getNameField (); + if (nameField != null) + rec.setValue (nameField, node.getName ()); + + for (Enumeration e=dbm.prop2db.keys(); e.hasMoreElements(); ) { + String propname = (String) e.nextElement (); + Property p = node.getProperty (propname, false); + Relation rel = dbm.propertyToColumnName (propname); + + if (p != null && rel != null) { + switch (p.getType ()) { + case IProperty.STRING: + rec.setValue (rel.localField, p.getStringValue ()); + break; + case IProperty.BOOLEAN: + rec.setValue (rel.localField, p.getBooleanValue ()); + break; + case IProperty.DATE: + Timestamp t = new Timestamp (p.getDateValue ().getTime ()); + rec.setValue (rel.localField, t); + break; + case IProperty.INTEGER: + rec.setValue (rel.localField, p.getIntegerValue ()); + break; + case IProperty.FLOAT: + rec.setValue (rel.localField, p.getFloatValue ()); + break; + case IProperty.NODE: + if (rel.direction == Relation.FORWARD) { + // INode n = p.getNodeValue (); + // String foreignID = n == null ? null : n.getID (); + rec.setValue (rel.localField, p.getStringValue ()); + } + break; + } + p.dirty = false; + } else if (rel != null && rel.localField != null) { + rec.setValueNull (rel.localField); + } + } + rec.markForInsert (); + tds.save (); + } finally { + if (tds != null) { + tds.close (); + } + } + dbm.lastDataChange = System.currentTimeMillis (); + } + // tx.timer.endEvent ("insertNode "+node); + } + + public void updateNode (DbWrapper db, DbTxn txn, Node node) throws Exception { + + Transactor tx = (Transactor) Thread.currentThread (); + // tx.timer.beginEvent ("updateNode "+node); + + DbMapping dbm = node.getDbMapping (); + + if (dbm == null || !dbm.isRelational ()) { + db.save (txn, node.getID (), node); + } else { + + TableDataSet tds = null; + try { + tds = new TableDataSet (dbm.getConnection (), dbm.getSchema (), dbm.getKeyDef ()); + Record rec = tds.addRecord (); + rec.setValue (dbm.getIDField (), node.getID ()); + + int updated = 0; + + for (Enumeration e=dbm.prop2db.keys(); e.hasMoreElements(); ) { + String propname = (String) e.nextElement (); + Property p = node.getProperty (propname, false); + Relation rel = dbm.propertyToColumnName (propname); + if (rel != null && rel.readonly) + continue; + + if (p != null && rel != null) { + + if (p.dirty) { + switch (p.getType ()) { + case IProperty.STRING: + rec.setValue (rel.localField, p.getStringValue ()); + break; + case IProperty.BOOLEAN: + rec.setValue (rel.localField, p.getBooleanValue ()); + break; + case IProperty.DATE: + Timestamp t = new Timestamp (p.getDateValue ().getTime ()); + rec.setValue (rel.localField, t); + break; + case IProperty.INTEGER: + rec.setValue (rel.localField, p.getIntegerValue ()); + break; + case IProperty.FLOAT: + rec.setValue (rel.localField, p.getFloatValue ()); + break; + case IProperty.NODE: + if (rel.direction == Relation.FORWARD) { + // INode n = p.getNodeValue (); + // String foreignID = n == null ? null : n.getID (); + rec.setValue (rel.localField, p.getStringValue ()); + } + break; + } + updated++; + p.dirty = false; + } + + } else if (rel != null && rel.localField != null) { + updated++; + rec.setValueNull (rel.localField); + } + } + if (updated > 0) { + // mark the key value as clean so no try is made to update it + rec.markValueClean (dbm.getIDField ()); + rec.markForUpdate (); + tds.save (); + } + } finally { + if (tds != null) { + tds.close (); + } + } + dbm.lastDataChange = System.currentTimeMillis (); + } + // update may cause changes in the node's parent subnode array + if (node.isAnonymous()) { + Node parent = (Node) node.getParent (); + if (parent != null) + parent.lastSubnodeChange = System.currentTimeMillis (); + } + // tx.timer.endEvent ("updateNode "+node); + } + + public void deleteNode (DbWrapper db, DbTxn txn, Node node) throws Exception { + + Transactor tx = (Transactor) Thread.currentThread (); + // tx.timer.beginEvent ("deleteNode "+node); + + DbMapping dbm = node.getDbMapping (); + + if (dbm == null || !dbm.isRelational ()) { + db.delete (txn, node.getID ()); + } else { + Statement st = null; + try { + Connection con = dbm.getConnection (); + st = con.createStatement (); + st.executeUpdate ("DELETE FROM "+dbm.getTableName ()+" WHERE "+dbm.getIDField ()+" = "+node.getID ()); + } finally { + if (st != null) + st.close (); + } + dbm.lastDataChange = System.currentTimeMillis (); + } + // node may still be cached via non-primary keys. mark as invalid + node.setState (Node.INVALID); + // tx.timer.endEvent ("deleteNode "+node); + } + + public String generateID (DbMapping map) throws Exception { + + Transactor tx = (Transactor) Thread.currentThread (); + // tx.timer.beginEvent ("generateID "+map); + + QueryDataSet qds = null; + String retval = null; + try { + Connection con = map.getConnection (); + String q = "SELECT "+map.getIDgen()+".nextval FROM dual"; + qds = new QueryDataSet (con, q); + qds.fetchRecords (); + retval = qds.getRecord (0).getValue (1).asString (); + } finally { + // tx.timer.endEvent ("generateID "+map); + if (qds != null) { + qds.close (); + } + } + return retval; + } + + + /** + * Loades subnodes via subnode relation. Only the ID index is loaded, the nodes are + * loaded later on demand. + */ + public Vector getNodeIDs (Node home, Relation rel) throws Exception { + + Transactor tx = (Transactor) Thread.currentThread (); + // tx.timer.beginEvent ("getNodeIDs "+home); + + if (rel == null || rel.other == null || !rel.other.isRelational ()) { + // this should never be called for embedded nodes + throw new RuntimeException ("NodeMgr.countNodes called for non-relational node "+home); + } else { + Vector retval = new Vector (); + // if we do a groupby query (creating an intermediate layer of groupby nodes), + // retrieve the value of that field instead of the primary key + String idfield = rel.groupby == null ? rel.other.getIDField () : rel.groupby; + Connection con = rel.other.getConnection (); + String table = rel.other.getTableName (); + + QueryDataSet qds = null; + try { + Relation subrel = rel; + if (subrel.getFilter () != null) + subrel = subrel.getFilter (); + + if (home.getSubnodeRelation() != null) { + // subnode relation was explicitly set + qds = new QueryDataSet (con, "SELECT "+idfield+" FROM "+table+" "+home.getSubnodeRelation()); + } else { + String q = "SELECT "+idfield+" FROM "+table; + if (subrel.direction == Relation.BACKWARD) { + String homeid = home.getState() == Node.VIRTUAL ? home.parentID : home.getID (); + q += " WHERE "+subrel.remoteField+" = '"+homeid+"'"; + } + // set order, if specified and if not using subnode's relation + if (rel.groupby != null) + q += " GROUP BY "+rel.groupby+" ORDER BY "+rel.groupby; + else if (rel.order != null) + q += " ORDER BY "+rel.order; + qds = new QueryDataSet (con, q); + } + + if (logSql) + IServer.getLogger().log ("### getNodeIDs: "+qds.getSelectString()); + + qds.fetchRecords (); + for (int i=0; i 1) + throw new RuntimeException ("More than one value returned by query."); + Record rec = tds.getRecord (0); + node = new Node (dbm, rec, safe); + + } finally { + if (tds != null) { + tds.close (); + } + } + } + return node; + } + + private Node getNodeByRelation (DbWrapper db, DbTxn txn, Node home, String kstr, Relation rel) throws Exception { + Node node = null; + if (rel != null && rel.virtual && home.getState() != INode.VIRTUAL) { + Key k = home.getKey ().getVirtualKey (kstr); + node = (Node) cache.get (k); + if (node != null && node.getState() != INode.INVALID) { + if (rel.prototype != null && !rel.prototype.equals (node.getString ("prototype", false))) + node.setString ("prototype", rel.prototype); + return node; + } + // if subnodes are stored in embedded db we have to actually assign it the virtual node, + // otherwise it and its subnodes will be lost across restarts. + if (rel.other == null || (!rel.other.isRelational() && !home.getDbMapping().isRelational())) { + node = (Node) home.createNode (rel.propname); + if (rel.prototype != null) + node.setString ("prototype", rel.prototype); + } else { + node = new Node (home, kstr, safe, rel.prototype); + } + if (rel.prototype != null) { + node.setDbMapping (app.getDbMapping (rel.prototype)); + } else { + // make a db mapping good enough that the virtual node finds its subnodes + DbMapping dbm = new DbMapping (); + dbm.setSubnodeMapping (rel.other); + dbm.setSubnodeRelation (rel.getVirtualSubnodeRelation()); + dbm.setPropertyMapping (rel.other); + dbm.setPropertyRelation (rel.getVirtualPropertyRelation()); + node.setDbMapping (dbm); + } + } else if (rel != null && rel.groupby != null) { + node = new Node (home, kstr, safe, rel.prototype); + DbMapping dbm = new DbMapping (); + dbm.setSubnodeMapping (rel.other); + dbm.setSubnodeRelation (rel.getGroupbySubnodeRelation()); + dbm.setPropertyMapping (rel.other); + dbm.setPropertyRelation (rel.getGroupbyPropertyRelation()); + node.setDbMapping (dbm); + } else if (rel == null || rel.other == null || !rel.other.isRelational ()) { + node = db.getNode (txn, kstr); + node.nmgr = safe; + node.setDbMapping (rel.other); + return node; + } else { + TableDataSet tds = null; + try { + DbMapping dbm = rel.other; + tds = new TableDataSet (dbm.getConnection (), dbm.getSchema (), dbm.getKeyDef ()); + StringBuffer where = new StringBuffer (); + + where.append (rel.getRemoteField ()); + where.append (" = '"); + where.append (escape(kstr)); + where.append ("'"); + + // Additionally filter properties through subnode relation? + if (rel.subnodesAreProperties) { + String homeid = home.getState() == Node.VIRTUAL ? home.parentID : home.getID (); + // first check for dynamic subrel from node + String nodesubrel = home.getSubnodeRelation(); + if (nodesubrel != null && nodesubrel.trim().length() > 5) { + where.append (" and "); + where.append (nodesubrel.trim().substring(5).trim()); + } else { + Relation subrel = home.getDbMapping().getSubnodeRelation (); + if (subrel != null) { + if (subrel.getFilter () != null) + subrel = subrel.getFilter (); + if (subrel != null && subrel.direction == Relation.BACKWARD) { + where.append (" and "); + where.append (subrel.remoteField); + where.append (" = '"); + where.append (homeid); + where.append ("'"); + } + } + } + } + + tds.where (where.toString ()); + + if (logSql) + IServer.getLogger().log ("### getNodeByRelation: "+tds.getSelectString()); + + tds.fetchRecords (); + + if (tds.size () == 0) + return null; + if (tds.size () > 1) + throw new RuntimeException ("More than one value returned by query."); + Record rec = tds.getRecord (0); + node = new Node (rel.other, rec, safe); + + // Check if node is already cached with primary Key. + if (!rel.usesPrimaryKey()) { + Key pk = node.getKey(); + Node existing = (Node) cache.get (pk); + if (existing != null && existing.getState() != Node.INVALID) { + node = existing; + } + } + + } finally { + if (tds != null) { + tds.close (); + } + } + } + return node; + } + + // a utility method to escape single quotes + private String escape (String str) { + if (str == null) + return null; + if (str.indexOf ("'") < 0) + return str; + int l = str.length(); + StringBuffer sbuf = new StringBuffer (l + 10); + for (int i=0; i" : + "" ; + case BOOLEAN: + return ""; + case INTEGER: + return "" ; + case FLOAT: + return "" ; + case DATE: + SimpleDateFormat format = new SimpleDateFormat ("dd.MM.yy hh:mm"); + String date = format.format (new Date (lvalue)); + return ""; + case NODE: + DbMapping nvmap = null; + if (node.dbmap != null) + nvmap = node.dbmap.getPropertyMapping (propname); + return ""; + } + return ""; + } + + private String escape (String s) { + char c[] = new char[s.length()]; + s.getChars (0, c.length, c, 0); + StringBuffer b = new StringBuffer (); + int copyfrom = 0; + for (int i = 0; i < c.length; i++) { + switch (c[i]) { + case '\\': + case '"': + if (i-copyfrom > 0) + b.append (c, copyfrom, i-copyfrom); + b.append ('\\'); + b.append (c[i]); + copyfrom = i+1; + } + } + if (c.length-copyfrom > 0) + b.append (c, copyfrom, c.length-copyfrom); + return b.toString (); + } + + public int getType () { + return type; + + } + + public String getTypeString () { + switch (type) { + case STRING: + return "string"; + case BOOLEAN: + return "boolean"; + case DATE: + return "date"; + case INTEGER: + return "integer"; + case FLOAT: + return "float"; + case NODE: + return "node"; + } + return ""; + } + + + public Object clone () { + try { + Property c = (Property) super.clone(); + c.propname = this.propname; + c.svalue = this.svalue; + c.bvalue = this.bvalue; + c.lvalue = this.lvalue; + c.dvalue = this.dvalue; + c.nvalueID = this.nvalueID; + c.type = this.type; + return c; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError (); + } + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/db/Server.java b/src/helma/objectmodel/db/Server.java new file mode 100644 index 00000000..da3d48a6 --- /dev/null +++ b/src/helma/objectmodel/db/Server.java @@ -0,0 +1,355 @@ +// Server.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel.db; + +import java.util.*; +import java.io.*; +import java.rmi.*; +import java.rmi.server.*; +import java.rmi.registry.*; +import java.net.*; +import helma.objectmodel.*; +import helma.framework.*; +import helma.framework.core.*; +import helma.xmlrpc.*; +import helma.util.*; +import com.sleepycat.db.*; + + +/** + * HOP main class. + */ + + public class Server extends IServer implements Runnable { + + + static boolean useTransactions, paranoid; + + private ApplicationManager appManager; + + private Thread mainThread; + + protected static ThreadGroup txgroup; + + static String dbFilename = "hop.db"; + static String propfile; + static String dbPropfile = "db.properties"; + static String appsPropfile = "apps.properties"; + static SystemProperties appsProps; + static String dbDir = null; + static int port = 5055; + static int webport = 0; + + Acme.Serve.Serve websrv; + + public static void main (String args[]) throws IOException { + + boolean usageError = false; + + useTransactions = true; + + for (int i=0; i 0) { + websrv = new Acme.Serve.Serve (webport, sysProps); + } + + String xmlparser = sysProps.getProperty ("xmlparser"); + if (xmlparser != null) + XmlRpc.setDriver (xmlparser); + // XmlRpc.setDebug (true); + xmlrpc = new WebServer (port+1); + if (paranoid) { + xmlrpc.setParanoid (true); + String xallow = sysProps.getProperty ("allowXmlRpc"); + if (xallow != null) { + StringTokenizer st = new StringTokenizer (xallow, " ,;"); + while (st.hasMoreTokens ()) + xmlrpc.acceptClient (st.nextToken ()); + } + } + getLogger().log ("Starting XML-RPC server on port "+(port+1)); + + // the following seems not to be necessary after all ... + // System.setSecurityManager(new RMISecurityManager()); + if (paranoid) { + HopSocketFactory factory = new HopSocketFactory (); + String rallow = sysProps.getProperty ("allowWeb"); + if (rallow != null) { + StringTokenizer st = new StringTokenizer (rallow, " ,;"); + while (st.hasMoreTokens ()) + factory.addAddress (st.nextToken ()); + } + RMISocketFactory.setSocketFactory (factory); + } + + if (websrv == null) { + getLogger().log ("Starting server on port "+port); + LocateRegistry.createRegistry (port); + } + + + // start application framework + String appDir = sysProps.getProperty ("apphome", "apps"); + File appHome = new File (appDir); + if (hopHome != null && !appHome.isAbsolute()) + appHome = new File (hopHome, appDir); + appsProps = new SystemProperties (appsPropfile); + File dbHome = new File (dbDir); + appManager = new ApplicationManager (port, appHome, dbHome, appsProps, this); + + + } catch (Exception gx) { + getLogger().log ("Error initializing embedded database: "+gx); + gx.printStackTrace (); + /* try { + transactor.abort (); + } catch (Exception ignore) {} */ + return; + } + + // start applications + appManager.startAll (); + + // start embedded web server + if (websrv != null) { + Thread webthread = new Thread (websrv, "WebServer"); + webthread.start (); + } + + int count = 0; + while (Thread.currentThread () == mainThread) { + try { + mainThread.sleep (3000l); + } catch (InterruptedException ie) { + return; + } + appManager.checkForChanges (); + // print some thread stats now and then + if (count++ > 20) { + printThreadStats (); + count = 0; + } + } + + } + + + private void checkRunning () throws Exception { + try { + java.net.Socket socket = new java.net.Socket ("localhost", port); + } catch (Exception x) { + return; + } + // if we got so far, another server is already running on this port and db + throw new Exception ("Error: Server already running on this port"); + } + + public void printThreadStats () { + getLogger().log ("Thread Stats: "+txgroup.activeCount()+" active"); + Runtime rt = Runtime.getRuntime (); + long free = rt.freeMemory (); + long total = rt.totalMemory (); + getLogger().log ("Free memory: "+(free/1024)+" kB"); + getLogger().log ("Total memory: "+(total/1024)+" kB"); + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/db/Transactor.java b/src/helma/objectmodel/db/Transactor.java new file mode 100644 index 00000000..f3da7280 --- /dev/null +++ b/src/helma/objectmodel/db/Transactor.java @@ -0,0 +1,338 @@ +// Transactor.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel.db; + +import java.io.*; +import java.util.*; +import java.sql.*; +import helma.objectmodel.*; +import helma.util.Timer; +import helma.framework.TimeoutException; +import com.sleepycat.db.*; + +/** + * A subclass of thread that keeps track of changed nodes and triggers + * changes in the database when a transaction is commited. + */ + +public class Transactor extends Thread { + + NodeManager nmgr; + + // List of nodes to be updated + private Hashtable nodes; + // List of visited clean nodes + private Hashtable cleannodes; + // Is a transaction in progress? + private volatile boolean active; + private volatile boolean killed; + + // Transaction for the embedded database + protected DbTxn txn; + // Transactions for SQL data sources + protected Hashtable sqlCon; + + public Timer timer; + // when did the current transaction start? + private long tstart; + // a name to log the transaction. For HTTP transactions this is the rerquest path + private String tname; + + + public Transactor (Runnable runnable, NodeManager nmgr) { + super (Server.txgroup, runnable, "Transactor"); + this.nmgr = nmgr; + nodes = new Hashtable (); + cleannodes = new Hashtable (); + sqlCon = new Hashtable (); + active = false; + killed = false; + timer = new Timer(); + } + + public void visitNode (Node node) { + if (node != null) { + Key key = node.getKey (); + if (!nodes.containsKey (key)) { + nodes.put (key, node); + } + } + } + + public void dropNode (Node node) { + if (node != null) { + Key key = node.getKey (); + nodes.remove (key); + } + } + + public void visitCleanNode (Node node) { + if (node != null) { + Key key = node.getKey (); + if (!cleannodes.containsKey (key)) { + cleannodes.put (key, node); + } + } + } + + public void visitCleanNode (Key key, Node node) { + if (node != null) { + if (!cleannodes.containsKey (key)) { + cleannodes.put (key, node); + } + } + } + + + public Node getVisitedNode (Object key) { + return key == null ? null : (Node) cleannodes.get (key); + } + + public boolean isActive () { + return active; + } + + public void registerConnection (DbSource src, Connection con) { + sqlCon.put (src, con); + } + + public Connection getConnection (DbSource src) { + return (Connection) sqlCon.get (src); + } + + public synchronized void begin (String tnm) throws Exception { + if (active) { + abort (); + } + nodes.clear (); + cleannodes.clear (); + txn = nmgr.db.beginTransaction (); + active = true; + tstart = System.currentTimeMillis (); + tname = tnm; + } + + public synchronized void commit () throws Exception { + + if (killed) { + abort (); + return; + } + + int ins = 0, upd = 0, dlt = 0; + int l = nodes.size (); + + for (Enumeration e=nodes.elements (); e.hasMoreElements (); ) { + Node node = (Node) e.nextElement (); + + // update nodes in db + int nstate = node.getState (); + if (nstate == Node.NEW) { + nmgr.registerNode (node); // register node with nodemanager cache + nmgr.insertNode (nmgr.db, txn, node); + node.setState (Node.CLEAN); + ins++; + // IServer.getLogger().log ("inserted: "+node.getFullName ()); + } else if (nstate == Node.MODIFIED) { + nmgr.updateNode (nmgr.db, txn, node); + node.setState (Node.CLEAN); + upd++; + IServer.getLogger().log ("updated: "+node.getFullName ()); + } else if (nstate == Node.DELETED) { + // IServer.getLogger().log ("deleted: "+node.getFullName ()+" ("+node.getName ()+")"); + nmgr.deleteNode (nmgr.db, txn, node); + nmgr.evictNode (node); + dlt++; + } else { + // IServer.getLogger().log ("noop: "+node.getFullName ()); + } + node.clearWriteLock (); + } + + nodes.clear (); + cleannodes.clear (); + // sqlCon.clear (); + + if (nmgr.idgen.dirty) { + nmgr.db.save (txn, "idgen", nmgr.idgen); + nmgr.idgen.dirty = false; + } + + if (active) { + active = false; + nmgr.db.commitTransaction (txn); + txn = null; + } + + IServer.getLogger().log (tname+" "+l+" marked, "+ins+" inserted, "+upd+" updated, "+dlt+" deleted in "+(System.currentTimeMillis()-tstart)+" millis"); + } + + public synchronized void abort () throws Exception { + + int l = nodes.size (); + for (Enumeration e=nodes.elements(); e.hasMoreElements(); ) { + Node node = (Node) e.nextElement (); + // Declare node as invalid, so it won't be used by other threads that want to + // write on it and remove it from cache + nmgr.evictNode (node); + node.clearWriteLock (); + } + nodes.clear (); + cleannodes.clear (); + for (Enumeration e=sqlCon.elements(); e.hasMoreElements(); ) { + try { + Connection con = (Connection) e.nextElement (); + con.close (); + } catch (Exception ignore) {} + } + sqlCon.clear (); + + if (active) { + active = false; + nmgr.db.abortTransaction (txn); + txn = null; + } + IServer.getLogger().log (tname+" aborted after "+(System.currentTimeMillis()-tstart)+" millis"); + } + + public synchronized void kill () { + killed = true; + // The thread is told to stop by setting the thread flag in the EcmaScript + // evaluator, so we can hope that it stops without doing anything else. + try { + join (1000); + } catch (InterruptedException ir) { + Thread.currentThread().interrupt(); + } + + // Interrupt the thread if it has not noticed the flag (e.g. because it is busy + // reading from a network socket). + if (isAlive()) { + interrupt (); + try { + join (3000); + } catch (InterruptedException ignore) { + Thread.currentThread().interrupt(); + } + if (isAlive()) + // Sorry to be so rude... + stop (new TimeoutException()); + } + } + + public void cleanup () { + if (sqlCon != null) { + for (Enumeration e=sqlCon.elements(); e.hasMoreElements(); ) { + try { + Connection con = (Connection) e.nextElement (); + con.close (); + } catch (Exception ignore) {} + } + sqlCon.clear (); + sqlCon = null; + } + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/objectmodel/db/WrappedNodeManager.java b/src/helma/objectmodel/db/WrappedNodeManager.java new file mode 100644 index 00000000..30741446 --- /dev/null +++ b/src/helma/objectmodel/db/WrappedNodeManager.java @@ -0,0 +1,186 @@ +// WrappedNodeManager.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.objectmodel.db; + +import helma.objectmodel.*; +import java.util.Vector; + + +/** + * A wrapper around NodeManager that catches most Exceptions, or rethrows them as RuntimeExceptions. + * The idea behind this is that we don't care a lot about Exception classes, since Hop programming is done + * in JavaScript which doesn't know about them (except for the exception message). + */ + + public class WrappedNodeManager { + + NodeManager nmgr; + + public WrappedNodeManager (NodeManager nmgr) { + this.nmgr = nmgr; + } + + public Node getNode (String id, DbMapping dbmap) { + try { + return nmgr.getNode (id, dbmap); + } catch (ObjectNotFoundException x) { + return null; + } catch (Exception x) { + Server.getLogger().log ("Error retrieving Node via DbMapping: "+x.getMessage ()); + if ("true".equalsIgnoreCase (Server.sysProps.getProperty("debug"))) + x.printStackTrace(); + throw new RuntimeException ("Error retrieving Node: "+x.getMessage ()); + } + } + + public Node getNode (Node home, String id, Relation rel) { + try { + return nmgr.getNode (home, id, rel); + } catch (ObjectNotFoundException x) { + return null; + } catch (Exception x) { + Server.getLogger().log ("Error retrieving Node \""+id+"\" from "+home+": "+x.getMessage ()); + if ("true".equalsIgnoreCase (Server.sysProps.getProperty("debug"))) + x.printStackTrace(); + throw new RuntimeException ("Error retrieving Node: "+x.getMessage ()); + } + } + + public Vector getNodes (Node home, Relation rel) { + try { + return nmgr.getNodes (home, rel); + } catch (Exception x) { + if ("true".equalsIgnoreCase (Server.sysProps.getProperty("debug"))) + x.printStackTrace(); + throw new RuntimeException ("Error retrieving Nodes: "+x.getMessage ()); + } + } + + public Vector getNodeIDs (Node home, Relation rel) { + try { + return nmgr.getNodeIDs (home, rel); + } catch (Exception x) { + if ("true".equalsIgnoreCase (Server.sysProps.getProperty("debug"))) + x.printStackTrace(); + throw new RuntimeException ("Error retrieving NodeIDs: "+x.getMessage ()); + } + } + + public int countNodes (Node home, Relation rel) { + try { + return nmgr.countNodes (home, rel); + } catch (Exception x) { + if ("true".equalsIgnoreCase (Server.sysProps.getProperty("debug"))) + x.printStackTrace(); + throw new RuntimeException ("Error counting Node: "+x.getMessage ()); + } + } + + public void deleteNode (Node node) { + try { + nmgr.deleteNode (node); + } catch (Exception x) { + if ("true".equalsIgnoreCase (Server.sysProps.getProperty("debug"))) + x.printStackTrace(); + throw new RuntimeException ("Error deleting Node: "+x.getMessage ()); + } + } + + public void registerNode (Node node) { + nmgr.registerNode (node); + } + + public void evictNode (Node node) { + nmgr.evictNode (node); + } + + public void evictKey (Key key) { + nmgr.evictKey (key); + } + + + public String generateID () { + return nmgr.idgen.newID (); + } + + public String generateID (DbMapping map) { + try { + if (map == null || map.getIDgen() == null) + return nmgr.idgen.newID (); + else + return nmgr.generateID (map); + } catch (Exception x) { + throw new RuntimeException (x.toString ()); + } + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/servlet/AcmeFileServlet.java b/src/helma/servlet/AcmeFileServlet.java new file mode 100644 index 00000000..d2f4f003 --- /dev/null +++ b/src/helma/servlet/AcmeFileServlet.java @@ -0,0 +1,208 @@ +// FileServlet - servlet similar to a standard httpd +// +// Copyright (C)1996,1998 by Jef Poskanzer . 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. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS 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 AUTHOR OR 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. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package helma.servlet; + +import java.io.*; +import java.util.*; +import java.text.*; +import Acme.Serve.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/// Servlet similar to a standard httpd. +//

+// Implements the "GET" and "HEAD" methods for files and directories. +// Handles index.html. +// Redirects directory URLs that lack a trailing /. +// Handles If-Modified-Since and Range. +//

+// Fetch the software.
+// Fetch the entire Acme package. +//

+// @see Acme.Serve.Serve + +public class AcmeFileServlet extends FileServlet + { + + private File root; + + + /// Constructor. + public AcmeFileServlet(File root) + { + super (); + this.root = root; + } + + /// Services a single request from the client. + // @param req the servlet request + // @param req the servlet response + // @exception ServletException when an exception has occurred + public void service( HttpServletRequest req, HttpServletResponse res ) throws ServletException, IOException + { + boolean headOnly; + if ( req.getMethod().equalsIgnoreCase( "get" ) ) + headOnly = false; + else if ( ! req.getMethod().equalsIgnoreCase( "head" ) ) + headOnly = true; + else + { + res.sendError( HttpServletResponse.SC_NOT_IMPLEMENTED ); + return; + } + + String path = req.getServletPath(); + if ( path == null || path.charAt( 0 ) != '/' ) + { + res.sendError( HttpServletResponse.SC_BAD_REQUEST ); + return; + } + if ( path.indexOf( "/../" ) != -1 || path.endsWith( "/.." ) ) + { + res.sendError( HttpServletResponse.SC_FORBIDDEN ); + return; + } + + // Make a version without the leading /. + String pathname = path.substring( 1 ); + if ( pathname.length() == 0 ) + pathname = "./"; + + dispatchPathname( req, res, headOnly, path, pathname ); + } + + + protected void dispatchPathname( HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path, String pathname ) throws IOException + { + String filename = pathname.replace( '/', File.separatorChar ); + if ( filename.charAt( filename.length() - 1 ) == File.separatorChar ) + filename = filename.substring( 0, filename.length() - 1 ); + if (filename.startsWith ("static")) + filename = filename.substring ( Math.min (7, filename.length()) ); + + File file = new File( root, filename ); + + if ( file.exists() ) + { + if ( ! file.isDirectory() ) + serveFile( req, res, headOnly, path, filename, file ); + else + { + if ( pathname.charAt( pathname.length() - 1 ) != '/' ) + redirectDirectory( req, res, path, file ); + else + { + String indexFilename = + filename + File.separatorChar + "index.html"; + File indexFile = new File( indexFilename ); + if ( indexFile.exists() ) + serveFile( + req, res, headOnly, path, indexFilename, + indexFile ); + else + serveDirectory( + req, res, headOnly, path, filename, file ); + } + } + } + else + { + if ( pathname.endsWith( "/index.html" ) ) + dispatchPathname( + req, res, headOnly, path, + pathname.substring( 0, pathname.length() - 10 ) ); + else if ( pathname.equals( "index.html" ) ) + dispatchPathname( req, res, headOnly, path, "./" ); + else + res.sendError( HttpServletResponse.SC_NOT_FOUND ); + } + } + + + + + private void serveDirectory( HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path, String filename, File file ) throws IOException + { + log( "indexing " + path ); + if ( ! file.canRead() ) + { + res.sendError( HttpServletResponse.SC_FORBIDDEN ); + return; + } + res.setStatus( HttpServletResponse.SC_OK ); + res.setContentType( "text/html" ); + OutputStream out = res.getOutputStream(); + if ( ! headOnly ) + { + PrintStream p = new PrintStream( new BufferedOutputStream( out ) ); + p.println( "" ); + p.println( "Index of " + path + "" ); + p.println( "" ); + p.println( "

Index of " + path + "

" ); + p.println( "
" );
+	    p.println( "mode     bytes  last-changed  name" );
+	    p.println( "
" ); + String[] names = file.list(); + Acme.Utils.sortStrings( names ); + for ( int i = 0; i < names.length; ++i ) + { + String aFilename = filename + File.separatorChar + names[i]; + File aFile = new File( aFilename ); + String aFileType; + if ( aFile.isDirectory() ) + aFileType = "d"; + else if ( aFile.isFile() ) + aFileType = "-"; + else + aFileType = "?"; + String aFileRead = ( aFile.canRead() ? "r" : "-" ); + String aFileWrite = ( aFile.canWrite() ? "w" : "-" ); + String aFileExe = "-"; + String aFileSize = Acme.Fmt.fmt( aFile.length(), 8 ); + String aFileDate = + Acme.Utils.lsDateStr( new Date( aFile.lastModified() ) ); + String aFileDirsuf = ( aFile.isDirectory() ? "/" : "" ); + String aFileSuf = ( aFile.isDirectory() ? "/" : "" ); + p.println( + aFileType + aFileRead + aFileWrite + aFileExe + + " " + aFileSize + " " + aFileDate + " " + + "" + + names[i] + aFileSuf + "" ); + } + p.println( "
" ); + p.println( "
" ); + ServeUtils.writeAddress( p ); + p.println( "" ); + p.flush(); + } + out.close(); + } + + + } diff --git a/src/helma/servlet/AcmeServletClient.java b/src/helma/servlet/AcmeServletClient.java new file mode 100644 index 00000000..8d2fa55d --- /dev/null +++ b/src/helma/servlet/AcmeServletClient.java @@ -0,0 +1,254 @@ +// ServletClient.java +// Copyright (c) Hannes Wallnoefer, Raphael Spannocchi 1998-2000 + +/* Portierung von helma.asp.AspClient auf Servlets */ +/* Author: Raphael Spannocchi Datum: 27.11.1998 */ + +package helma.servlet; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.*; +import java.util.*; +import helma.framework.*; +import helma.framework.core.Application; +import helma.objectmodel.Node; +import helma.util.Uploader; + +/** + * This is the HOP servlet adapter that uses the Acme servlet API clone and communicates + * directly with hop applications instead of using RMI. + */ + +public class AcmeServletClient extends HttpServlet{ + + private int uploadLimit; // limit to HTTP uploads in kB + private Hashtable apps; + private Application app; + private String cookieDomain; + private boolean caching; + private boolean debug; + + + public AcmeServletClient (Application app) { + this.app = app; + this.uploadLimit = 1024; // generous 1mb upload limit + } + + + public void service (HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + execute (request, response); + } + + public void doGet (HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + execute (request, response); + } + + public void doPost (HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + execute (request, response); + } + + + private void execute (HttpServletRequest request, HttpServletResponse response) { + String protocol = request.getProtocol (); + Cookie[] cookies = request.getCookies(); + try { + RequestTrans reqtrans = new RequestTrans (); + // HACK - sessions not fully supported in Acme.Serve + // Thats ok, we dont need the session object, just the id. + reqtrans.session = request.getRequestedSessionId(); + if (cookies != null) { + for (int i=0; i < cookies.length;i++) try { // get Cookies + String nextKey = cookies[i].getName (); + String nextPart = cookies[i].getValue (); + reqtrans.set (nextKey, nextPart); + } catch (Exception badCookie) {} + } + // get optional path info + String pathInfo = request.getServletPath (); + if (pathInfo != null) { + if (pathInfo.indexOf (app.getName()) == 1) + pathInfo = pathInfo.substring (app.getName().length()+1); + reqtrans.path = trim (pathInfo); + } else + reqtrans.path = ""; + + String host = request.getHeader ("Host"); + if (host != null) { + host = host.toLowerCase(); + reqtrans.set ("http_host", host); + } + + String referer = request.getHeader ("Referer"); + if (referer != null) + reqtrans.set ("http_referer", referer); + + String remotehost = request.getRemoteAddr (); + if (remotehost != null) + reqtrans.set ("http_remotehost", remotehost); + + String browser = request.getHeader ("User-Agent"); + if (browser != null) + reqtrans.set ("http_browser", browser); + + for (Enumeration e = request.getParameterNames(); e.hasMoreElements(); ) { + // Params parsen + String nextKey = (String)e.nextElement(); + String[] paramValues = request.getParameterValues(nextKey); + String nextValue = paramValues[0]; // Only take first value + reqtrans.set (nextKey, nextValue); // generic Header, Parameter + } + + String contentType = request.getContentType(); + if (contentType != null && contentType.indexOf("multipart/form-data")==0) { + // File Upload + Uploader up; + try { + if ((up = getUpload (uploadLimit, request)) != null) { + Hashtable upload = up.getParts (); + for (Enumeration e = upload.keys(); e.hasMoreElements(); ) { + String nextKey = (String) e.nextElement (); + Object nextPart = upload.get (nextKey); + reqtrans.set (nextKey, nextPart); + } + } + } catch (Exception upx) { + String uploadErr = upx.getMessage (); + if (uploadErr == null || uploadErr.length () == 0) + uploadErr = upx.toString (); + reqtrans.set ("uploadError", uploadErr); + } + } + + ResponseTrans restrans = null; + restrans = app.execute (reqtrans); + writeResponse (response, restrans, cookies, protocol); + + } catch (Exception x) { + x.printStackTrace (); + try { + response.setContentType ("text/html"); + Writer out = response.getWriter (); + if (debug) + out.write ("Error:
" +x); + else + out.write ("This server is temporarily unavailable. Please check back later."); + out.flush (); + } catch (Exception io_e) {} + } + } + + + private void writeResponse (HttpServletResponse res, ResponseTrans trans, Cookie[] cookies, String protocol) { + for (int i = 0; i < trans.countCookies(); i++) try { + Cookie c = new Cookie(trans.getKeyAt(i), trans.getValueAt(i)); + c.setPath ("/"); + if (cookieDomain != null) + c.setDomain (cookieDomain); + int expires = trans.getDaysAt(i); + if (expires > 0) + c.setMaxAge(expires * 60*60*24); // Cookie time to live, days -> seconds + res.addCookie(c); + } catch (Exception ign) {} + + if (trans.redirect != null) { + try { + res.sendRedirect(trans.redirect); + } catch(Exception io_e) {} + + } else { + if (!trans.cache || ! caching) { + // Disable caching of response. + if (protocol == null || !protocol.endsWith ("1.1")) + res.setHeader ("Pragma", "no-cache"); // for HTTP 1.0 + else + res.setHeader ("Cache-Control", "no-cache"); // for HTTP 1.1 + } + res.setStatus( HttpServletResponse.SC_OK ); + res.setContentLength (trans.getContentLength ()); + res.setContentType (trans.contentType); + try { + Writer writer = res.getWriter (); + writer.write (trans.getContentString ()); + writer.flush (); + } catch(Exception io_e) { System.out.println ("Error in writeResponse: "+io_e); } + } + } + + private void redirectResponse (HttpServletRequest request, HttpServletResponse res, ResponseTrans trans, String url) { + try { + res.sendRedirect(url); + } catch (Exception e) { + System.err.println ("Exception bei redirect: " + e + e.getMessage()); + } + } + + + public Uploader getUpload (HttpServletRequest request) throws Exception { + return getUpload (500, request); + } + + public Uploader getUpload (int maxKbytes, HttpServletRequest request) throws Exception { + int contentLength = request.getContentLength (); + BufferedInputStream in = new BufferedInputStream (request.getInputStream ()); + Uploader up = null; + if (contentLength > maxKbytes*1024) { + throw new RuntimeException ("Upload exceeds limit of "+maxKbytes+" kb."); + } + String contentType = request.getContentType (); + up = new Uploader(maxKbytes); + up.load (in, contentType, contentLength); + return up; + } + + + public Object getUploadPart(Uploader up, String name) { + return up.getParts().get(name); + } + + + public String getServletInfo(){ + return new String("Helma ServletClient"); + } + + + private String trim (String str) { + + if (str == null) + return null; + char[] val = str.toCharArray (); + int len = val.length; + int st = 0; + + while ((st < len) && (val[st] <= ' ' || val[st] == '/')) { + st++; + } + while ((st < len) && (val[len - 1] <= ' ' || val[len - 1] == '/')) { + len--; + } + return ((st > 0) || (len < val.length)) ? new String (val, st, len-st) : str; + } + +} + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/servlet/ServletClient.java b/src/helma/servlet/ServletClient.java new file mode 100644 index 00000000..ecb714b4 --- /dev/null +++ b/src/helma/servlet/ServletClient.java @@ -0,0 +1,312 @@ +// ServletClient.java +// Copyright (c) Hannes Wallnöfer, Raphael Spannocchi 1998-2000 + +/* Portierung von helma.asp.AspClient auf Servlets */ +/* Author: Raphael Spannocchi Datum: 27.11.1998 */ + +package helma.servlet; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.*; +import java.rmi.Naming; +import java.rmi.RemoteException; +import java.util.*; +import helma.framework.*; +import helma.objectmodel.Node; +import helma.util.*; + +/** + * This is the HOP servlet adapter. This class communicates with hop applications + * via RMI. + */ + +public class ServletClient extends HttpServlet{ + + private String host = null; + private int port = 0; + private int uploadLimit; // limit to HTTP uploads in kB + private Hashtable apps; + private String appName; + private String appUrl; + private String cookieDomain; + private boolean caching; + private boolean debug; + + + public void init (ServletConfig init) { + apps = new Hashtable(); + + appName = init.getInitParameter ("application"); + if (appName == null) appName = "base"; + + host = init.getInitParameter ("host"); + if (host == null) host = "localhost"; + + String portstr = init.getInitParameter ("port"); + port = portstr == null ? 5055 : Integer.parseInt (portstr); + + String upstr = init.getInitParameter ("uploadLimit"); + uploadLimit = upstr == null ? 500 : Integer.parseInt (upstr); + + cookieDomain = init.getInitParameter ("cookieDomain"); + + appUrl = "//" + host + ":" + port + "/"; + debug = ("true".equalsIgnoreCase (init.getInitParameter ("debug"))); + + caching = ! ("false".equalsIgnoreCase (init.getInitParameter ("caching"))); + } + + + private IRemoteApp getApp (String appID) throws Exception { + IRemoteApp retval = (IRemoteApp) apps.get (appID); + if (retval != null) { + return retval; + } + retval = (IRemoteApp) Naming.lookup (appUrl + appID); + apps.put (appID, retval); + return retval; + } + + public void doGet (HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + execute (appName, request, response); + } + + public void doPost (HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + execute (appName, request, response); + } + + // not used anymore + private void get(String appID, HttpServletRequest request, HttpServletResponse response) { + } + + private void execute (String appID, HttpServletRequest request, HttpServletResponse response) { + String protocol = request.getProtocol (); + Cookie[] cookies = request.getCookies(); + try { + RequestTrans reqtrans = new RequestTrans (); + if (cookies != null) { + for (int i=0; i < cookies.length;i++) try { // get Cookies + String nextKey = cookies[i].getName (); + String nextPart = cookies[i].getValue (); + if ("HopSession".equals (nextKey)) + reqtrans.session = nextPart; + else + reqtrans.set (nextKey, nextPart); + } catch (Exception badCookie) {} + } + + // check if we need to create a session id + if (reqtrans.session == null) { + reqtrans.session = Long.toString (Math.round (Math.random ()*Long.MAX_VALUE), 16); + reqtrans.session += "@"+Long.toString (System.currentTimeMillis (), 16); + Cookie c = new Cookie("HopSession", reqtrans.session); + c.setPath ("/"); + if (cookieDomain != null) + c.setDomain (cookieDomain); + response.addCookie(c); + } + + // get optional path info + String pathInfo = request.getPathInfo (); + if (pathInfo != null) + reqtrans.path = trim (pathInfo); + else + reqtrans.path = ""; + + String host = request.getHeader ("Host"); + if (host != null) { + host = host.toLowerCase(); + reqtrans.set ("http_host", host); + } + + String referer = request.getHeader ("Referer"); + if (referer != null) + reqtrans.set ("http_referer", referer); + + String remotehost = request.getRemoteAddr (); + if (remotehost != null) + reqtrans.set ("http_remotehost", remotehost); + + String browser = request.getHeader ("User-Agent"); + if (browser != null) + reqtrans.set ("http_browser", browser); + + for (Enumeration e = request.getParameterNames(); e.hasMoreElements(); ) { + // Params parsen + String nextKey = (String)e.nextElement(); + String[] paramValues = request.getParameterValues(nextKey); + String nextValue = paramValues[0]; // Only take first value + reqtrans.set (nextKey, nextValue); // generic Header, Parameter + } + + String contentType = request.getContentType(); + if (contentType != null && contentType.indexOf("multipart/form-data")==0) { + // File Upload + Uploader up; + try { + if ((up = getUpload (uploadLimit, request)) != null) { + Hashtable upload = up.getParts (); + for (Enumeration e = upload.keys(); e.hasMoreElements(); ) { + String nextKey = (String) e.nextElement (); + Object nextPart = upload.get (nextKey); + reqtrans.set (nextKey, nextPart); + } + } + } catch (Exception upx) { + String uploadErr = upx.getMessage (); + if (uploadErr == null || uploadErr.length () == 0) + uploadErr = upx.toString (); + reqtrans.set ("uploadError", uploadErr); + } + } + + // get RMI ref to application and execute request + IRemoteApp app = getApp (appID); + ResponseTrans restrans = null; + try { + restrans = app.execute (reqtrans); + } catch (RemoteException cnx) { + apps.remove (appID); + app = getApp (appID); + app.ping (); + restrans = app.execute (reqtrans); + } + writeResponse (response, restrans, cookies, protocol); + + } catch (Exception x) { + apps.remove (appID); + try { + response.setContentType ("text/html"); + Writer out = response.getWriter (); + if (debug) + out.write ("Error:
" +x); + else + out.write ("This server is temporarily unavailable. Please check back later."); + out.flush (); + } catch (Exception io_e) {} + } + } + + + private void writeResponse (HttpServletResponse res, ResponseTrans trans, Cookie[] cookies, String protocol) { + + for (int i = 0; i < trans.countCookies(); i++) try { + Cookie c = new Cookie(trans.getKeyAt(i), trans.getValueAt(i)); + c.setPath ("/"); + if (cookieDomain != null) + c.setDomain (cookieDomain); + int expires = trans.getDaysAt(i); + if (expires > 0) + c.setMaxAge(expires * 60*60*24); // Cookie time to live, days -> seconds + res.addCookie(c); + } catch (Exception ign) {} + + if (trans.redirect != null) { + try { + res.sendRedirect(trans.redirect); + } catch(Exception io_e) {} + + } else { + if (!trans.cache || ! caching) { + // Disable caching of response. + if (protocol == null || !protocol.endsWith ("1.1")) + res.setHeader ("Pragma", "no-cache"); // for HTTP 1.0 + else + res.setHeader ("Cache-Control", "no-cache"); // for HTTP 1.1 + } + res.setContentLength (trans.getContentLength ()); + res.setContentType (trans.contentType); + try { + Writer writer = res.getWriter (); + writer.write (trans.getContentString ()); + writer.flush (); + } catch(Exception io_e) {} + } + } + + private void redirectResponse (HttpServletRequest request, HttpServletResponse res, ResponseTrans trans, String url) { + try { + res.sendRedirect(url); + } catch (Exception e) { + System.err.println ("Exception at redirect: " + e + e.getMessage()); + } + } + + + public Uploader getUpload (HttpServletRequest request) throws Exception { + return getUpload (500, request); + } + + public Uploader getUpload (int maxKbytes, HttpServletRequest request) throws Exception { + int contentLength = request.getContentLength (); + BufferedInputStream in = new BufferedInputStream (request.getInputStream ()); + Uploader up = null; + try { + if (contentLength > maxKbytes*1024) { + // consume all input to make Apache happy + byte b[] = new byte[1024]; + int read = 0; + while (read > -1) + read = in.read (b, 0, 1024); + throw new RuntimeException ("Upload exceeds limit of "+maxKbytes+" kb."); + } + String contentType = request.getContentType (); + up = new Uploader(maxKbytes); + up.load (in, contentType, contentLength); + } finally { + try { in.close (); } catch (Exception ignore) {} + } + return up; + } + + + public Object getUploadPart(Uploader up, String name) { + return up.getParts().get(name); + } + + + public String getServletInfo(){ + return new String("Helma ServletClient"); + } + + + private String trim (String str) { + + if (str == null) + return null; + char[] val = str.toCharArray (); + int len = val.length; + int st = 0; + + while ((st < len) && (val[st] <= ' ' || val[st] == '/')) { + st++; + } + while ((st < len) && (val[len - 1] <= ' ' || val[len - 1] == '/')) { + len--; + } + return ((st > 0) || (len < val.length)) ? new String (val, st, len-st) : str; + } + +} + + + + + + + + + + + + + + + + + + + diff --git a/src/helma/util/HtmlEncoder.java b/src/helma/util/HtmlEncoder.java new file mode 100644 index 00000000..0ff38c06 --- /dev/null +++ b/src/helma/util/HtmlEncoder.java @@ -0,0 +1,365 @@ +// HtmlEncoder.java +// Copyright (c) Hannes Wallnöfer 1997-2000 + +package helma.util; + +import java.io.*; +import java.util.*; +import java.awt.*; +import java.awt.image.*; +import java.text.*; + + +/** + * This is a utility class to encode special characters and do formatting + * for HTML output. + */ + +public final class HtmlEncoder { + + + static final Hashtable convertor = new Hashtable (128); + + // conversion table + static { + convertor.put(new Integer(160), " "); + convertor.put(new Integer(161), "¡"); + convertor.put(new Integer(162), "¢"); + convertor.put(new Integer(163), "£"); + convertor.put(new Integer(164), "¤"); + convertor.put(new Integer(165), "¥"); + convertor.put(new Integer(166), "¦"); + convertor.put(new Integer(167), "§"); + convertor.put(new Integer(168), "¨"); + convertor.put(new Integer(169), "©"); + convertor.put(new Integer(170), "ª"); + convertor.put(new Integer(171), "«"); + convertor.put(new Integer(172), "¬"); + convertor.put(new Integer(173), "­"); + convertor.put(new Integer(174), "®"); + convertor.put(new Integer(175), "¯"); + convertor.put(new Integer(176), "°"); + convertor.put(new Integer(177), "±"); + convertor.put(new Integer(178), "²"); + convertor.put(new Integer(179), "³"); + convertor.put(new Integer(180), "´"); + convertor.put(new Integer(181), "µ"); + convertor.put(new Integer(182), "¶"); + convertor.put(new Integer(183), "·"); + convertor.put(new Integer(184), "¸"); + convertor.put(new Integer(185), "¹"); + convertor.put(new Integer(186), "º"); + convertor.put(new Integer(187), "»"); + convertor.put(new Integer(188), "¼"); + convertor.put(new Integer(189), "½"); + convertor.put(new Integer(190), "¾"); + convertor.put(new Integer(191), "¿"); + convertor.put(new Integer(192), "À"); + convertor.put(new Integer(193), "Á"); + convertor.put(new Integer(194), "Â"); + convertor.put(new Integer(195), "Ã"); + convertor.put(new Integer(196), "Ä"); + convertor.put(new Integer(197), "Å"); + convertor.put(new Integer(198), "Æ"); + convertor.put(new Integer(199), "Ç"); + convertor.put(new Integer(200), "È"); + convertor.put(new Integer(201), "É"); + convertor.put(new Integer(202), "Ê"); + convertor.put(new Integer(203), "Ë"); + convertor.put(new Integer(204), "Ì"); + convertor.put(new Integer(205), "Í"); + convertor.put(new Integer(206), "Î"); + convertor.put(new Integer(207), "Ï"); + convertor.put(new Integer(208), "Ð"); + convertor.put(new Integer(209), "Ñ"); + convertor.put(new Integer(210), "Ò"); + convertor.put(new Integer(211), "Ó"); + convertor.put(new Integer(212), "Ô"); + convertor.put(new Integer(213), "Õ"); + convertor.put(new Integer(214), "Ö"); + convertor.put(new Integer(215), "×"); + convertor.put(new Integer(216), "Ø"); + convertor.put(new Integer(217), "Ù"); + convertor.put(new Integer(218), "Ú"); + convertor.put(new Integer(219), "Û"); + convertor.put(new Integer(220), "Ü"); + convertor.put(new Integer(221), "Ý"); + convertor.put(new Integer(222), "Þ"); + convertor.put(new Integer(223), "ß"); + convertor.put(new Integer(224), "à"); + convertor.put(new Integer(225), "á"); + convertor.put(new Integer(226), "â"); + convertor.put(new Integer(227), "ã"); + convertor.put(new Integer(228), "ä"); + convertor.put(new Integer(229), "å"); + convertor.put(new Integer(230), "æ"); + convertor.put(new Integer(231), "ç"); + convertor.put(new Integer(232), "è"); + convertor.put(new Integer(233), "é"); + convertor.put(new Integer(234), "ê"); + convertor.put(new Integer(235), "ë"); + convertor.put(new Integer(236), "ì"); + convertor.put(new Integer(237), "í"); + convertor.put(new Integer(238), "î"); + convertor.put(new Integer(239), "ï"); + convertor.put(new Integer(240), "ð"); + convertor.put(new Integer(241), "ñ"); + convertor.put(new Integer(242), "ò"); + convertor.put(new Integer(243), "ó"); + convertor.put(new Integer(244), "ô"); + convertor.put(new Integer(245), "õ"); + convertor.put(new Integer(246), "ö"); + convertor.put(new Integer(247), "÷"); + convertor.put(new Integer(248), "ø"); + convertor.put(new Integer(249), "ù"); + convertor.put(new Integer(250), "ú"); + convertor.put(new Integer(251), "û"); + convertor.put(new Integer(252), "ü"); + convertor.put(new Integer(253), "ý"); + convertor.put(new Integer(254), "þ"); + convertor.put(new Integer(255), "ÿ"); + } + + /** + * + */ + public final static String encode (String what) { + // try to make stringbuffer large enough from the start + StringBuffer ret = new StringBuffer (Math.round (what.length()*1.4f)); + encode (what, ret); + return ret.toString(); + } + + /** + * + */ + public final static void encode (String what, StringBuffer ret) { + if (what == null || what.length() == 0) { + return; + } + + StringReader in = new StringReader (what); + int c; + boolean closeTag=false, readTag=false, tagOpen=false; + boolean ignoreNewline = false, swallow = false; + StringBuffer tag = new StringBuffer (); + try { + while ((c = in.read()) != -1) { + if (readTag) { + if (Character.isLetterOrDigit ((char) c)) + tag.append ((char) c); + else if ('/' == c) + closeTag = true; + else { + String t = tag.toString (); + // set ignoreNewline on some tags, depending on wheather they're + // being opened or closed. + if ("td".equalsIgnoreCase (t)) { + ignoreNewline = closeTag; + swallow = true; // for some reason, it's a good idea to swallow (ignore) newlines after some tags + } else if ("th".equalsIgnoreCase (t)) { + ignoreNewline = closeTag; + swallow = true; + } else if ("table".equalsIgnoreCase (t)) { + ignoreNewline = !closeTag; + swallow = true; + } else if ("ul".equalsIgnoreCase (t)) { + ignoreNewline = !closeTag; + swallow = true; + } else if ("ol".equalsIgnoreCase (t)) { + ignoreNewline = !closeTag; + swallow = true; + } else if ("li".equalsIgnoreCase (t)) { + swallow = true; + ignoreNewline = closeTag; + } else if ("p".equalsIgnoreCase (t)) { + swallow = true; + } + + readTag = false; + closeTag = false; + tag.setLength (0); + } + } // if (readTag) + + switch (c) { + // case '&': + // ret.append ("&"); + // break; + case '\n': + if (!ignoreNewline && !swallow) + ret.append ("
"); + ret.append ('\n'); + if (!tagOpen) + swallow = false; + break; + case '<': + closeTag = false; + readTag = true; + tagOpen = true; + ret.append ('<'); + break; + case '>': + tagOpen = false; + ret.append ('>'); + break; + default: + if (c < 160) + ret.append ((char) c); + else if (c >= 160 && c <= 255) + ret.append (convertor.get(new Integer(c))); + else { + ret.append ("&#"); + ret.append (c); + ret.append (";"); + } + if (!tagOpen && !Character.isWhitespace ((char)c)) + swallow = false; + } + } + } catch (IOException e) {} + } + + /** + * + */ + public final static String encodeFormValue (String what) { + StringBuffer ret = new StringBuffer (Math.round (what.length()*1.4f)); + encodeAll (what, ret, false); + return ret.toString(); + } + + + /** + * + */ + public final static String encodeAll (String what) { + StringBuffer ret = new StringBuffer (Math.round (what.length()*1.4f)); + encodeAll (what, ret, true); + return ret.toString(); + } + + /** + * + */ + public final static String encodeAll (String what, StringBuffer ret) { + encodeAll (what, ret, true); + return ret.toString(); + } + + + /** + * + */ + public final static void encodeAll (String what, StringBuffer ret, boolean encodeNewline) { + if (what == null || what.length() == 0) { + return; + } + + StringReader in = new StringReader (what); + int c; + try { + while ((c = in.read()) != -1) { + switch (c) { + case '<' : + ret.append ("<"); + break; + case '>': + ret.append (">"); + break; + case '&': + ret.append ("&"); + break; + case '"': + ret.append ("""); + break; + case '\n': + if (encodeNewline) { + ret.append ("
"); + break; + } + default: + if (c < 160) + ret.append ((char) c); + else if (c >= 160 && c <= 255) + ret.append (convertor.get(new Integer(c))); + else { + ret.append ("&#"); + ret.append (c); + ret.append (";"); + } + } + } + } catch (IOException e) {} + } + + public final static String encodeSoft (String what) { + StringBuffer ret = new StringBuffer (Math.round (what.length()*1.4f)); + encodeSoft (what, ret); + return ret.toString(); + } + + public final static void encodeSoft (String what, StringBuffer ret) { + if (what == null || what.length() == 0) { + return; + } + + StringReader in = new StringReader (what); + int c; + try { + while ((c = in.read()) != -1) { + switch (c) { + case 128: // Euro-Symbol. This is for missing Unicode support in TowerJ. + ret.append ("€"); + break; + default: + if (c < 160) + ret.append ((char) c); + else if (c >= 160 && c <= 255) + ret.append (convertor.get(new Integer(c))); + else { + ret.append ("&#"); + ret.append (c); + ret.append (";"); + } + } + } + } catch (IOException e) {} + } + + + public final static String encodeXml (String what) { + StringBuffer ret = new StringBuffer (Math.round (what.length()*1.4f)); + encodeXml (what, ret); + return ret.toString(); + } + + public final static void encodeXml (String what, StringBuffer ret) { + if (what == null || what.length() == 0) { + return; + } + + StringReader in = new StringReader (what); + int c; + try { + while ((c = in.read()) != -1) { + switch (c) { + case '<' : + ret.append ("<"); + break; + case '>': + ret.append (">"); + break; + case '&': + ret.append ("&"); + break; + default: + ret.append ((char) c); + } + } + } catch (IOException e) {} + } + + + +} diff --git a/src/helma/util/InetAddressFilter.java b/src/helma/util/InetAddressFilter.java new file mode 100644 index 00000000..0ceaeccd --- /dev/null +++ b/src/helma/util/InetAddressFilter.java @@ -0,0 +1,56 @@ +// InetAddressFilter.java +// Copyright (c) Hannes Wallnöfer 1998-2000 + +package helma.util; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A class for paranoid servers to filter IP addresses. + */ + +public class InetAddressFilter { + + Vector patterns; + + public InetAddressFilter () { + patterns = new Vector (); + } + + public void addAddress (String address) throws IOException { + int pattern[] = new int[4]; + StringTokenizer st = new StringTokenizer (address, "."); + if (st.countTokens () != 4) + throw new IOException ("\""+address+"\" does not represent a valid IP address"); + for (int i=0; i<4; i++) { + String next = st.nextToken (); + if ("*".equals (next)) + pattern[i] = 256; + else + pattern[i] = (byte) Integer.parseInt (next); + } + patterns.addElement (pattern); + } + + public boolean matches (InetAddress address) { + if (address == null) + return false; + byte add[] = address.getAddress (); + if (add == null) + return false; + int l = patterns.size(); + for (int k=0; k dateLastRendered) + renderDate (); + // log directly to printstream or to buffer? + if (out == null) + entries.addElement (dateCache + " " + msg); + else + out.println (dateCache + " " + msg); + } + + private void renderDate () { + dateCache = dformat.format (new Date()); + dateLastRendered = System.currentTimeMillis (); + } + + public void run () { + while (Thread.currentThread () == logger) { + try { + if (currentFile.length() > 10000000) { + // rotate log files each 10 megs + swapFile (); + } + int l = entries.size(); + for (int i=0; i -1 && contentLength > maxKbytes*1024) + throw new IOException ("Size of upload exceeds limit of " + maxKbytes + " kB."); + + byte b[] = new byte[contentLength]; + MultipartInputStream in = new MultipartInputStream (new BufferedInputStream (is), boundary.getBytes ()); + + while (in.nextInputStream ()) { + + MimeParser parser = new MimeParser (in, new MimeHeadersFactory ()); + MimeHeaders headers = (MimeHeaders) parser.parse (); + + InputStream bodystream = parser.getInputStream (); + int read, count = 0; + + while ((read = bodystream.read (b, count, 4096)) > -1) { + count += read; + if (count == b.length) { + byte newb[] = new byte[count+4096]; + System.arraycopy (b, 0, newb, 0, count); + b = newb; + } + } + + byte newb[] = new byte[count]; + System.arraycopy (b, 0, newb, 0, count); + + String type = headers.getValue("Content-Type"); + String disposition = headers.getValue ("Content-Disposition"); + String name = getSubHeader (disposition, "name"); + String filename = getSubHeader (disposition, "filename"); + if (filename != null) { + int sep = filename.lastIndexOf ("\\"); + if (sep > -1) + filename = filename.substring (sep+1); + sep = filename.lastIndexOf ("/"); + if (sep > -1) + filename = filename.substring (sep+1); + } + if (filename != null) { + Node node = new Node (filename); + node.setContent (newb, type); + parts.put (name, node); + } else { + parts.put (name, new String (newb)); + } + + } + + } + + + + private String getSubHeader (String header, String subHeaderName) { + if (header == null) + return null; + String retval = null; + StringTokenizer headerTokenizer = new StringTokenizer(header, ";"); + while (headerTokenizer.hasMoreTokens()) { + String token = headerTokenizer.nextToken().trim (); + int i = token.indexOf ("="); + if (i > 0) { + String hname = token.substring (0, i).trim (); + if (hname.equalsIgnoreCase (subHeaderName)) + retval = token.substring (i+1).replace ('"', ' ').trim (); + } + } + return retval; + } + + +} diff --git a/src/helma/util/UrlEncoder.java b/src/helma/util/UrlEncoder.java new file mode 100644 index 00000000..927f7671 --- /dev/null +++ b/src/helma/util/UrlEncoder.java @@ -0,0 +1,126 @@ +/* + * @(#)URLEncoder.java 1.12 98/07/01 + * + * Copyright 1995-1998 by Sun Microsystems, Inc., + * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A. + * All rights reserved. + * + * This software is the confidential and proprietary information + * of Sun Microsystems, Inc. ("Confidential Information"). You + * shall not disclose such Confidential Information and shall use + * it only in accordance with the terms of the license agreement + * you entered into with Sun. + */ + +// Repackaged by Hannes Wallnoefer because this is the only way to +// encode space as %20 (no, subclassing doesn't work here). + +package helma.util; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.IOException; +import java.util.BitSet; + +/** + * The class contains a utility method for converting a + * String into a MIME format called + * "x-www-form-urlencoded" format. + *

+ * To convert a String, each character is examined in turn: + *

    + *
  • The ASCII characters 'a' through 'z', + * 'A' through 'Z', and '0' + * through '9' remain the same. + *
  • All other characters are converted into the 3-character string + * "%xy", where xy is the two-digit + * hexadecimal representation of the lower 8-bits of the character. + *
+ * + * @author Herb Jellinek + * @version 1.12, 07/01/98 + * @since JDK1.0 + */ +public class UrlEncoder { + static BitSet dontNeedEncoding; + static final int caseDiff = ('a' - 'A'); + + /* The list of characters that are not encoded have been determined by + referencing O'Reilly's "HTML: The Definitive Guide" (page 164). */ + + static { + dontNeedEncoding = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + dontNeedEncoding.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + dontNeedEncoding.set(i); + } + for (i = '0'; i <= '9'; i++) { + dontNeedEncoding.set(i); + } + // dontNeedEncoding.set(' '); /* removed to encode space as %20 */ + dontNeedEncoding.set('-'); + dontNeedEncoding.set('_'); + dontNeedEncoding.set('.'); + dontNeedEncoding.set('*'); + } + + /** + * You can't call the constructor. + */ + // private URLEncoder() { } + + /** + * Translates a string into x-www-form-urlencoded format. + * + * @param s String to be translated. + * @return the translated String. + * @since JDK1.0 + */ + public static String encode(String s) { + int maxBytesPerChar = 10; + ByteArrayOutputStream out = new ByteArrayOutputStream(s.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + OutputStreamWriter writer = new OutputStreamWriter(buf); + + for (int i = 0; i < s.length(); i++) { + int c = (int)s.charAt(i); + if (dontNeedEncoding.get(c)) { + if (c == ' ') { + c = '+'; + } + out.write(c); + } else { + // convert to external encoding before hex conversion + try { + writer.write(c); + writer.flush(); + } catch(IOException e) { + buf.reset(); + continue; + } + byte[] ba = buf.toByteArray(); + for (int j = 0; j < ba.length; j++) { + out.write('%'); + char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16); + // converting to use uppercase letter as part of + // the hex value if ch is a letter. + if (Character.isLetter(ch)) { + ch -= caseDiff; + } + out.write(ch); + ch = Character.forDigit(ba[j] & 0xF, 16); + if (Character.isLetter(ch)) { + ch -= caseDiff; + } + out.write(ch); + } + buf.reset(); + } + } + + return out.toString(); + } +} diff --git a/src/helma/util/WebBroadcaster.java b/src/helma/util/WebBroadcaster.java new file mode 100644 index 00000000..48eb09ef --- /dev/null +++ b/src/helma/util/WebBroadcaster.java @@ -0,0 +1,350 @@ +// WebBroadcaster.java +// Copyright (c) Hannes Wallnöfer 1999-2000 + +package helma.util; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A utility hack to do "html web broadcasts". + */ + +public class WebBroadcaster implements Runnable { + + private Vector connections; + private ServerSocket serverSocket; + private Thread listener; + private boolean paranoid; + static String lastResult = ""; + private Vector accept, deny; + static String reloadJS = "\r\n\r\n"; + static String scrollJS = "\r\n\r\n\r\n"; + long time; + int last; + + /** + * + */ + public static void main (String args[]) { + System.out.println ("Usage: java helma.util.WebBroadcaster [port]"); + int p = 8080; + if (args.length > 0) try { + p = Integer.parseInt (args[0]); + } catch (NumberFormatException nfx) { + System.out.println ("Error parsing port number: "+args[0]); + } + + try { + WebBroadcaster server = new WebBroadcaster (p); + // webserver.setParanoid (false); + // webserver.acceptClient ("192.168.*.*"); + System.out.println ("started web broadcast server on port "+p); + } catch (IOException x) { + System.out.println ("Error creating web broadcast server: "+x); + } + + } + + + /** + * Creates a Web server at the specified port number. + */ + public WebBroadcaster (int port) throws IOException { + super(); + connections = new Vector (); + accept = new Vector (); + deny = new Vector (); + // make a new server socket with extra large queue size + this.serverSocket = new ServerSocket (port, 2000); + listener = new Thread (this); + listener.start (); + } + + + public void broadcast (String message) { + long start = System.currentTimeMillis (); + int l = connections.size (); + synchronized (this) { + if (l != last) { + System.out.println ("broadcasting to "+l+" clients in "+time+" millis."); + last = l; + } + } + for (int i=l-1; i>=0; i--) { + try { + Connection c = (Connection) connections.elementAt (i); + c.send (message); + } catch (Exception ignore) {} + } + time = System.currentTimeMillis () - start; + } + + + /** + * Switch client filtering on/off. + * @see acceptClient(java.lang.String) + * @see denyClient(java.lang.String) + */ + public void setParanoid (boolean p) { + paranoid = p; + } + + + /** + * Add an IP address to the list of accepted clients. The parameter can contain '*' as wildcard + * character, e.g. "192.168.*.*". You must call setParanoid(true) in order for this to have any + * effect. + * + * @see denyClient(java.lang.String) + * @see setParanoid(boolean) + */ + public void acceptClient (String address) throws IllegalArgumentException { + try { + AddressMatcher m = new AddressMatcher (address); + accept.addElement (m); + } catch (Exception x) { + throw new IllegalArgumentException ("\""+address+"\" does not represent a valid IP address"); + } + } + + /** + * Add an IP address to the list of denied clients. The parameter can contain '*' as wildcard + * character, e.g. "192.168.*.*". You must call setParanoid(true) in order for this to have any + * effect. + * + * @see acceptClient(java.lang.String) + * @see setParanoid(boolean) + */ + public void denyClient (String address) throws IllegalArgumentException { + try { + AddressMatcher m = new AddressMatcher (address); + deny.addElement (m); + } catch (Exception x) { + throw new IllegalArgumentException ("\""+address+"\" does not represent a valid IP address"); + } + } + + private boolean checkSocket (Socket s) { + int l = deny.size (); + byte address[] = s.getInetAddress ().getAddress (); + for (int i=0; i 0) System.out.println ("Reusing connection: "+cycle); + String line = reader.readLine(); + if (line == null) throw new IOException ("connection reset"); + int contentLength = 0; + StringTokenizer tokens = new StringTokenizer(line); + String method = tokens.nextToken(); + String uri = tokens.nextToken (); + String httpversion = tokens.nextToken (); + keepalive = "HTTP/1.1".equals (httpversion); + + do { + // System.out.println (line); + line = reader.readLine(); + if (line != null) { + line = line.toLowerCase (); + if (line.startsWith ("content-length:")) + contentLength = Integer.parseInt (line.substring (15).trim ()); + if (line.startsWith ("connection:")) + keepalive = line.indexOf ("keep-alive") > -1; + } + } while (line != null && ! line.equals("")); + // System.out.println (""); + + if ("GET".equals (method)) { + + output.write (httpversion+" 200 OK\r\n"); + output.write ("Server: helma.WebBroadcast\r\n"); + output.write ("Content-Type: text/html\r\n"); + newConnection = uri.startsWith ("/NEW"); + if (!newConnection) { + output.write ("Content-Length: 5\r\n"); + if (keepalive) + output.write ("Connection: keep-alive\r\n"); + output.write ("\r\n"); + output.write ("done."); + output.flush (); + cycle += 1; + if (uri.startsWith ("/MSG")) + broadcast (uri+"
\r\n"); + continue; + } + + output.write ("Connection: close\r\n"); + output.write ("\r\n"); + output.write (reloadJS); + output.write (scrollJS); + output.flush (); + + connections.addElement (this); + + } else { + output.write ("HTTP/1.0 400 Bad Request\r\n"); + output.write ("Server: helma.WebBroadcast\r\n\r\n"); + output.write ("Bad Request."); + // output.write (lastResult); + output.flush (); + keepalive = false; + } + } while (keepalive && !newConnection); + } catch (Exception x) { + System.out.print ("."); + } finally { + if (newConnection) // leave connection open + return; + try { + output.close(); + } catch (IOException ignore) {} + try { + input.close(); + } catch (IOException ignore) {} + try { + socket.close(); + } catch (IOException ignore) {} + } + } + + public void cleanup () { + + } + + public synchronized void send (String message) { + try { + output.write (message); + output.flush (); + } catch (Exception exception) { + try { + connections.removeElement (this); + } catch (Exception ignore) {} + try { + output.close(); + } catch (IOException ignore) {} + try { + input.close(); + } catch (IOException ignore) {} + try { + socket.close(); + } catch (IOException ignore) {} + } + } + + + public String toString () { + return socket.getInetAddress ().getHostName (); + } + +} + + +class AddressMatcher { + + int pattern[]; + + public AddressMatcher (String address) throws Exception { + pattern = new int[4]; + StringTokenizer st = new StringTokenizer (address, "."); + if (st.countTokens () != 4) + throw new Exception ("\""+address+"\" does not represent a valid IP address"); + for (int i=0; i<4; i++) { + String next = st.nextToken (); + if ("*".equals (next)) + pattern[i] = 256; + else + pattern[i] = (byte) Integer.parseInt (next); + } + } + + public boolean matches (byte address[]) { + for (int i=0; i<4; i++) { + if (pattern[i] > 255) // wildcard + continue; + if (pattern[i] != address[i]) + return false; + } + return true; + } +} + +} \ No newline at end of file diff --git a/src/helma/xmlrpc/AuthenticatedXmlRpcHandler.java b/src/helma/xmlrpc/AuthenticatedXmlRpcHandler.java new file mode 100644 index 00000000..c6e5d0f3 --- /dev/null +++ b/src/helma/xmlrpc/AuthenticatedXmlRpcHandler.java @@ -0,0 +1,20 @@ +/* + * Copyright 2000 Hannes Wallnoefer + */ + +package helma.xmlrpc; + +import java.util.Vector; + +/** + * An XML-RPC handler that also handles user authentication. + */ + +public interface AuthenticatedXmlRpcHandler { + + /** + * Return the result, or throw an Exception if something went wrong. + */ + public Object execute (String method, Vector params, String user, String password) throws Exception; + +} \ No newline at end of file diff --git a/src/helma/xmlrpc/Base64.java b/src/helma/xmlrpc/Base64.java new file mode 100644 index 00000000..6d6380ed --- /dev/null +++ b/src/helma/xmlrpc/Base64.java @@ -0,0 +1,130 @@ +package helma.xmlrpc; + +/** + * Provides encoding of raw bytes to base64-encoded characters, and + * decoding of base64 characters to raw bytes. + * + * @author Kevin Kelley (kelley@iguana.ruralnet.net) + * @version 1.0 + * @date 06 August 1998 + */ + +public class Base64 +{ + /** + * returns an array of base64-encoded characters to represent the + * passed data array. + * + * @param data the array of bytes to encode + * @return base64-coded character array. + */ + + static public char[] encode(byte[] data) { + + char[] out = new char[((data.length + 2) / 3) * 4]; + // + // 3 bytes encode to 4 chars. Output is always an even + // multiple of 4 characters. + // + + for (int i = 0, index = 0; i < data.length; i += 3, + index += 4) { + boolean quad = false; + boolean trip = false; + int val = (0xFF & (int) data[i]); + + val <<= 8; + if ((i + 1) < data.length) { + val |= (0xFF & (int) data[i + 1]); + trip = true; + } + val <<= 8; + if ((i + 2) < data.length) { + val |= (0xFF & (int) data[i + 2]); + quad = true; + } + out[index + 3] = alphabet[(quad ? (val & 0x3F) : 64)]; + val >>= 6; + out[index + 2] = alphabet[(trip ? (val & 0x3F) : 64)]; + val >>= 6; + out[index + 1] = alphabet[val & 0x3F]; + val >>= 6; + out[index + 0] = alphabet[val & 0x3F]; + } + return out; + } + + + + /** + * Returns an array of bytes which were encoded in the passed + * character array. + * + * @param data the array of base64-encoded characters + * @return decoded data array + */ + static public byte[] decode(byte[] data) { + int len = ((data.length + 3) / 4) * 3; + if (data.length > 0 && data[data.length - 1] == '=') + --len; + if (data.length > 1 && data[data.length - 2] == '=') + --len; + byte[] out = new byte[len]; + + int shift = 0; // # of excess bits stored in accum + int accum = 0; // excess bits + int index = 0; + + for (int ix = 0; ix < data.length; ix++) { + int value = codes[data[ix] & 0xFF]; // ignore high byte of char + if (value >= 0) { + // skip over non-code + accum <<= 6; // bits shift up by 6 each time thru + shift += 6; // loop, with new bits being put in + accum |= value; // at the bottom. + if (shift >= 8) { + // whenever there are 8 or more shifted in, + shift -= 8; // write them out (from the top, leaving any + // excess at the bottom for next iteration. + out[index++] = (byte)((accum >> shift) & 0xff); + } + } + } + + if (index != out.length) throw new RuntimeException("Error decoding BASE64 element: miscalculated data length!"); + + return out; + } + + // + // code characters for values 0..63 + // + + static private char[] alphabet = + + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" + .toCharArray(); + + // + // lookup table for converting base64 characters to value in range 0..63 + // + + static private byte[] codes = new byte[256]; + + static { + for (int i = 0; i < 256; i++) + codes[i] = -1; + + for (int i = 'A'; i <= 'Z'; i++) + codes[i] = (byte)(i - 'A'); + + for (int i = 'a'; i <= 'z'; i++) + codes[i] = (byte)(26 + i - 'a'); + + for (int i = '0'; i <= '9'; i++) + codes[i] = (byte)(52 + i - '0'); + + codes['+'] = 62; + codes['/'] = 63; + } +} diff --git a/src/helma/xmlrpc/Benchmark.java b/src/helma/xmlrpc/Benchmark.java new file mode 100644 index 00000000..7d6c75fd --- /dev/null +++ b/src/helma/xmlrpc/Benchmark.java @@ -0,0 +1,104 @@ +/** + * Copyright 1999 Hannes Wallnoefer + */ + +package helma.xmlrpc; + +import java.util.*; +import java.io.IOException; + +public class Benchmark implements Runnable { + + XmlRpcClient client; + static String url; + static int clients = 8; + int gCalls = 0, gErrors = 0; + + Date date; + + public Benchmark () throws Exception { + client = new XmlRpcClientLite (url); + + Vector args = new Vector (); + // Some JITs (Symantec, IBM) have problems with several Threads + // starting all at the same time. + // This initial XML-RPC call seems to pacify them. + args.addElement (new Integer (123)); + client.execute ("math.abs", args); + date = new Date (); + date = new Date ((date.getTime()/1000)*1000); + + for (int i=0; i 0 && args.length < 3) { + url = args[0]; + XmlRpc.setKeepAlive (true); + if (args.length == 2) + XmlRpc.setDriver (args[1]); + new Benchmark (); + } else { + System.err.println ("Usage: java helma.xmlrpc.Benchmark URL [SAXDriver]"); + } + } + +} \ No newline at end of file diff --git a/src/helma/xmlrpc/ServerInputStream.java b/src/helma/xmlrpc/ServerInputStream.java new file mode 100644 index 00000000..f303f38c --- /dev/null +++ b/src/helma/xmlrpc/ServerInputStream.java @@ -0,0 +1,61 @@ +package helma.xmlrpc; + +import java.io.InputStream; +import java.io.IOException; + +// This class is borrowed from Apache JServ +class ServerInputStream extends InputStream { + // bytes remaining to be read from the input stream. This is + // initialized from CONTENT_LENGTH (or getContentLength()). + // This is used in order to correctly return a -1 when all the + // data POSTed was read. If this is left to -1, content length is + // assumed as unknown and the standard InputStream methods will be used + long available = -1; + + private InputStream in; + + public ServerInputStream(InputStream in, int available) { + this.in = in; + this.available = available; + } + + public int read() throws IOException { + if (available > 0) { + available--; + return in.read(); + } else if (available == -1) + return in.read (); + return -1; + } + + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + public int read(byte b[], int off, int len) throws IOException { + if (available > 0) { + if (len > available) { + // shrink len + len = (int) available; + } + int read = in.read(b, off, len); + if (read != -1) { + available -= read; + } else { + available = -1; + } + return read; + } else if (available == -1) + return in.read (b, off, len); + return -1; + } + + public long skip(long n) throws IOException { + long skip = in.skip(n); + if (available > 0) + available -= skip; + return skip; + } + + +} diff --git a/src/helma/xmlrpc/WebServer.java b/src/helma/xmlrpc/WebServer.java new file mode 100644 index 00000000..b1a6ef3d --- /dev/null +++ b/src/helma/xmlrpc/WebServer.java @@ -0,0 +1,448 @@ +package helma.xmlrpc; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A minimal web server that exclusively handles XML-RPC requests. + */ +public class WebServer implements Runnable { + + XmlRpcServer xmlrpc; + private ServerSocket serverSocket; + private int port; + private Thread listener; + private boolean paranoid; + private Vector accept, deny; + private Stack threadpool; + private ThreadGroup runners; + + + static final byte[] ctype = "Content-Type: text/xml\r\n".getBytes(); + static final byte[] clength = "Content-Length: ".getBytes(); + static final byte[] newline = "\r\n".getBytes(); + static final byte[] doubleNewline = "\r\n\r\n".getBytes(); + static final byte[] conkeep = "Connection: Keep-Alive\r\n".getBytes(); + static final byte[] conclose = "Connection: close\r\n".getBytes(); + static final byte[] ok = " 200 OK\r\n".getBytes(); + static final byte[] server = "Server: Helma XML-RPC 1.0\r\n".getBytes(); + + + /** + * This can be called from command line, but you'll have to edit and recompile + * to change the server port or handler objects. By default, it sets up the following responders: + *
  • A java.lang.String object + *
  • The java.lang.Math class (making its static methods callable via XML-RPC) + *
  • An Echo handler that returns the argument array + *
+ */ + public static void main (String args[]) { + System.err.println ("Usage: java helma.xmlrpc.WebServer [port]"); + int p = 8080; + if (args.length > 0) try { + p = Integer.parseInt (args[0]); + } catch (NumberFormatException nfx) { + System.err.println ("Error parsing port number: "+args[0]); + } + // XmlRpc.setDebug (true); + XmlRpc.setKeepAlive (true); + // XmlRpc.setEncoding ("UTF-8"); + try { + WebServer webserver = new WebServer (p); + // webserver.setParanoid (true); + // webserver.acceptClient ("192.168.*.*"); + webserver.addHandler ("string", "Welcome to XML-RPC!"); + webserver.addHandler ("math", Math.class); + webserver.addHandler ("auth", new AuthDemo()); + webserver.addHandler ("$default", new Echo()); + // XmlRpcClients can be used as Proxies in XmlRpcServers which is a cool feature for applets. + webserver.addHandler ("mttf", new XmlRpcClient ("http://www.mailtothefuture.com:80/RPC2")); + System.err.println ("started web server on port "+p); + } catch (IOException x) { + System.err.println ("Error creating web server: "+x); + } + + } + + + /** + * Creates a Web server at the specified port number. + */ + public WebServer (int port) throws IOException { + this (port, null); + } + + + /** + * Creates a Web server at the specified port number and IP address. + */ + public WebServer (int port, InetAddress add) throws IOException { + this.port = port; + xmlrpc = new XmlRpcServer (); + accept = new Vector (); + deny = new Vector (); + threadpool = new Stack (); + runners = new ThreadGroup ("XML-RPC Runner"); + this.serverSocket = new ServerSocket (port, 50, add); + listener = new Thread (this, "XML-RPC Weblistener"); + listener.start(); + } + + + /** + * Register a handler object with this name. Methods of this objects will be + * callable over XML-RPC as "name.method". + */ + public void addHandler (String name, Object target) { + xmlrpc.addHandler (name, target); + } + + /** + * Remove a handler object that was previously registered with this server. + */ + public void removeHandler (String name) { + xmlrpc.removeHandler (name); + } + + /** + * Switch client filtering on/off. + * @see acceptClient(java.lang.String) + * @see denyClient(java.lang.String) + */ + public void setParanoid (boolean p) { + paranoid = p; + } + + + /** + * Add an IP address to the list of accepted clients. The parameter can contain '*' as wildcard + * character, e.g. "192.168.*.*". You must call setParanoid(true) in order for this to have any + * effect. + * + * @see denyClient(java.lang.String) + * @see setParanoid(boolean) + */ + public void acceptClient (String address) throws IllegalArgumentException { + try { + AddressMatcher m = new AddressMatcher (address); + accept.addElement (m); + } catch (Exception x) { + throw new IllegalArgumentException ("\""+address+"\" does not represent a valid IP address"); + } + } + + /** + * Add an IP address to the list of denied clients. The parameter can contain '*' as wildcard + * character, e.g. "192.168.*.*". You must call setParanoid(true) in order for this to have any + * effect. + * + * @see acceptClient(java.lang.String) + * @see setParanoid(boolean) + */ + public void denyClient (String address) throws IllegalArgumentException { + try { + AddressMatcher m = new AddressMatcher (address); + deny.addElement (m); + } catch (Exception x) { + throw new IllegalArgumentException ("\""+address+"\" does not represent a valid IP address"); + } + } + + private boolean checkSocket (Socket s) { + int l = deny.size (); + byte address[] = s.getInetAddress ().getAddress (); + for (int i=0; i 255) + throw new RuntimeException ("System overload"); + return new Runner (); + } + } + + void releaseRunner (Runner runner) { + threadpool.push (runner); + } + + class Runner implements Runnable { + + Thread thread; + Connection con; + int count; + + public void handle (Socket socket) throws IOException { + con = new Connection (socket); + count = 0; + if (thread == null || !thread.isAlive()) { + thread = new Thread (runners, this); + thread.start (); + } else { + synchronized (this) { + notify (); + } + } + } + + public void run () { + while (Thread.currentThread () == thread) { + con.run (); + count++; + con = null; + + if (count > 200 || threadpool.size() > 20) + return; + + synchronized (this) { + releaseRunner (this); + try { + wait (); + } catch (InterruptedException ir) { + Thread.currentThread().interrupt(); + } + } + } + } + +} // end class Runner + + +class Connection implements Runnable { + + private Socket socket; + private BufferedInputStream input; + private BufferedOutputStream output; + // private Thread responder; + private long lastRequest; + private String user, password; + + public Connection (Socket socket) throws IOException { + // set read timeout to 30 seconds + socket.setSoTimeout (30000); + + this.socket = socket; + input = new BufferedInputStream (socket.getInputStream()); + output = new BufferedOutputStream (socket.getOutputStream()); + // responder = new Thread (this, "xmlrpc-worker"); + // responder.start(); + } + + + public void run () { + try { + boolean keepalive = false; + + do { + // reset user authentication + user = password = null; + String line = readLine (); + // Netscape sends an extra \n\r after bodypart, swallow it + if ("".equals (line)) + line = readLine(); + if (XmlRpc.debug) + System.err.println (line); + // get time of last request + lastRequest = System.currentTimeMillis (); + int contentLength = -1; + + // tokenize first line of HTTP request + StringTokenizer tokens = new StringTokenizer(line); + String method = tokens.nextToken(); + String uri = tokens.nextToken (); + String httpversion = tokens.nextToken (); + keepalive = XmlRpc.getKeepAlive() && "HTTP/1.1".equals (httpversion); + do { + line = readLine(); + if (line != null) { + if (XmlRpc.debug) + System.err.println (line); + String lineLower = line.toLowerCase (); + if (lineLower.startsWith ("content-length:")) + contentLength = Integer.parseInt (line.substring (15).trim ()); + if (lineLower.startsWith ("connection:")) + keepalive = XmlRpc.getKeepAlive() && lineLower.indexOf ("keep-alive") > -1; + if (lineLower.startsWith ("authorization: basic ")) + parseAuth (line); + } + } while (line != null && ! line.equals("")); + + if ("POST".equalsIgnoreCase (method)) { + ServerInputStream sin = new ServerInputStream (input, contentLength); + byte result[] = xmlrpc.execute (sin, user, password); + output.write (httpversion.getBytes()); + output.write (ok); + output.write (server); + if (keepalive) + output.write (conkeep); + else + output.write (conclose); + output.write (ctype); + output.write (clength); + output.write (Integer.toString (result.length).getBytes()); + output.write (doubleNewline); + output.write (result); + output.flush (); + } else { + output.write (httpversion.getBytes()); + output.write (" 400 Bad Request\r\n".getBytes()); + output.write ("Server: helma.XML-RPC\r\n\r\n".getBytes()); + output.write (("Method "+method+" not implemented (try POST)").getBytes()); + output.flush (); + keepalive = false; + } + } while (keepalive); + } catch (Exception exception) { + if (XmlRpc.debug) { + System.err.println (exception); + exception.printStackTrace (); + } + } finally { + try { + socket.close(); + } catch (IOException ignore) {} + } + } + + byte[] buffer; + private String readLine () throws IOException { + if (buffer == null) { + buffer = new byte[512]; + } + int next; + int count = 0; + for (;;) { + next = input.read(); + if (next < 0 || next == '\n') + break; + if (next != '\r') { + buffer[count++] = (byte) next; + } + if (count >= 512) + throw new IOException ("HTTP Header too long"); + } + return new String (buffer, 0, count); + } + + private void parseAuth (String line) { + try { + byte[] c = Base64.decode (line.substring (21).getBytes()); + String str = new String (c); + int col = str.indexOf (":"); + user = str.substring (0, col); + password = str.substring (col+1); + } catch (Throwable ignore) {} + } + +} + + +class AddressMatcher { + + int pattern[]; + + public AddressMatcher (String address) throws Exception { + pattern = new int[4]; + StringTokenizer st = new StringTokenizer (address, "."); + if (st.countTokens () != 4) + throw new Exception ("\""+address+"\" does not represent a valid IP address"); + for (int i=0; i<4; i++) { + String next = st.nextToken (); + if ("*".equals (next)) + pattern[i] = 256; + else + pattern[i] = (byte) Integer.parseInt (next); + } + } + + public boolean matches (byte address[]) { + for (int i=0; i<4; i++) { + if (pattern[i] > 255) // wildcard + continue; + if (pattern[i] != address[i]) + return false; + } + return true; + } +} + +} + +// An echo handler for debugging purposes +class Echo implements XmlRpcHandler { + public Object execute (String method, Vector v) throws Exception { + return (v); + } +} + +// An simple class that implements authentication +class AuthDemo implements AuthenticatedXmlRpcHandler { + public Object execute (String method, Vector v, String user, String password) throws Exception { + // our simplistic authentication guidelines never fail ;) + if (user == null || user.startsWith ("bad")) + throw new XmlRpcException (5, "Sorry, you're not allowed in here!"); + return ("Hello "+user); + } +} + diff --git a/src/helma/xmlrpc/XmlRpc.java b/src/helma/xmlrpc/XmlRpc.java new file mode 100644 index 00000000..40fa4803 --- /dev/null +++ b/src/helma/xmlrpc/XmlRpc.java @@ -0,0 +1,583 @@ +/** + * Copyright 1999 Hannes Wallnoefer + * XML-RPC base class. See http://www.xmlrpc.com/ + */ + +package helma.xmlrpc; + +import java.io.*; +import java.util.*; +import java.text.*; +import org.xml.sax.*; + + +/** + * This abstract base class provides basic capabilities for XML-RPC, like parsing of parameters + * or encoding Java objects into XML-RPC format. Any XML parser with a + * SAX interface can be used.

+ * XmlRpcServer and XmlRpcClient are the classes that actually implement an XML-RCP server and client. + * @see XmlRpcServer + * @see XmlRpcClient + */ + +public abstract class XmlRpc extends HandlerBase { + + public static final String version = "helma XML-RPC 1.0"; + + String methodName; + + // class name of SAX parser to use + private static Class parserClass; + private static Hashtable saxDrivers = new Hashtable (); + static { + saxDrivers.put ("xp", "com.jclark.xml.sax.Driver"); + saxDrivers.put ("ibm1", "com.ibm.xml.parser.SAXDriver"); + saxDrivers.put ("ibm2", "com.ibm.xml.parsers.SAXParser"); + saxDrivers.put ("aelfred", "com.microstar.xml.SAXDriver"); + saxDrivers.put ("oracle1", "oracle.xml.parser.XMLParser"); + saxDrivers.put ("oracle2", "oracle.xml.parser.v2.SAXParser"); + saxDrivers.put ("openxml", "org.openxml.parser.XMLSAXParser"); + } + + + // the stack we're parsing our values into. + Stack values; + Value currentValue; + + // formats for parsing and generating dateTime values + + // DateFormat datetime; + // now comes wapped into a synchronized class because dateFormat is not threadsafe + static Formatter dateformat = new Formatter (); + + // used to collect character data of parameter values + StringBuffer cdata; + boolean readCdata; + + // XML RPC parameter types used for dataMode + static final int STRING = 0; + static final int INTEGER = 1; + static final int BOOLEAN = 2; + static final int DOUBLE = 3; + static final int DATE = 4; + static final int BASE64 = 5; + static final int STRUCT = 6; + static final int ARRAY = 7; + static final int NIL = 8; + + // Error level + message + int errorLevel; + String errorMsg; + + static final int NONE = 0; + static final int RECOVERABLE = 1; + static final int FATAL = 2; + + // use HTTP keepalive? + static boolean keepalive = false; + + // for debugging output + public static boolean debug = false; + final static String types[] = {"String", "Integer", "Boolean", "Double", "Date", "Base64", "Struct", "Array", "Nil"}; + + // mapping between java encoding names and "real" names used in XML prolog. + // if you use an encoding not listed here send feedback to xmlrpc@helma.org + + static String encoding = "ISO8859_1"; + static Properties encodings = new Properties (); + static { + encodings.put ("UTF8", "UTF-8"); + encodings.put ("ISO8859_1", "ISO-8859-1"); + } + + + /** + * Set the SAX Parser to be used. The argument can either be the full class name or + * a user friendly shortcut if the parser is known to this class. The parsers that can + * currently be set by shortcut are listed in the main documentation page. If you are using + * another parser please send me the name of the SAX driver and I'll include it in a future release. + * If setDriver() is never called then the System property "sax.driver" is consulted. If that is not defined + * the driver defaults to OpenXML. + */ + public static void setDriver (String driver) throws ClassNotFoundException { + String parserClassName = null; + try { + parserClassName = (String) saxDrivers.get (driver); + if (parserClassName == null) + parserClassName = driver; + parserClass = Class.forName (parserClassName); + } catch (ClassNotFoundException x) { + throw new ClassNotFoundException ("SAX driver not found: "+parserClassName); + } + } + + /** + * Set the SAX Parser to be used by directly passing the Class object. + */ + public static void setDriver (Class driver) { + parserClass = driver; + } + + /** + * Set the encoding of the XML. This should be the name of a Java encoding + * contained in the encodings Hashtable. + */ + public static void setEncoding (String enc) { + encoding = enc; + } + + public String getEncoding () { + return encodings.getProperty (encoding, encoding); + } + + + /** + * Switch debugging output on/off. + */ + public static void setDebug (boolean val) { + debug = val; + } + + /** + * Switch HTTP keepalive on/off. + */ + public static void setKeepAlive (boolean val) { + keepalive = val; + } + + /** + * get current HTTP keepalive mode. + */ + public static boolean getKeepAlive () { + return keepalive; + } + + + /** + * Parse the input stream. For each root level object, method objectParsed + * is called. + */ + synchronized void parse (InputStream is) throws Exception { + + // reset values (XmlRpc objects are reusable) + errorLevel = NONE; + errorMsg = null; + values = new Stack (); + if (cdata == null) + cdata = new StringBuffer (128); + else + cdata.setLength (0); + readCdata = false; + currentValue = null; + + long now = System.currentTimeMillis (); + if (parserClass == null) { + // try to get the name of the SAX driver from the System properties + setDriver (System.getProperty ("sax.driver", "org.openxml.parser.XMLSAXParser")); + } + + Parser parser = null; + try { + parser = (Parser) parserClass.newInstance (); + } catch (NoSuchMethodError nsm) { + // This is thrown if no constructor exists for the parser class + // and is transformed into a regular exception. + throw new Exception ("Can't create Parser: "+parserClass); + } + + parser.setDocumentHandler (this); + parser.setErrorHandler (this); + + parser.parse (new InputSource (is)); + if (debug) + System.err.println ("Spent "+(System.currentTimeMillis () - now)+" millis parsing"); + } + + /** + * Writes the XML representation of a supported Java object to the XML writer. + */ + void writeObject (Object what, XmlWriter writer) { + writer.startElement ("value"); + if (what == null) { + // try sending experimental element + writer.emptyElement ("nil"); + } else if (what instanceof String) { + writer.chardata (what.toString ()); + } else if (what instanceof Integer) { + writer.startElement ("int"); + writer.write (what.toString ()); + writer.endElement ("int"); + } else if (what instanceof Boolean) { + writer.startElement ("boolean"); + writer.write (((Boolean) what).booleanValue () ? "1" : "0"); + writer.endElement ("boolean"); + } else if (what instanceof Double || what instanceof Float) { + writer.startElement ("double"); + writer.write (what.toString ()); + writer.endElement ("double"); + } else if (what instanceof Date) { + writer.startElement ("dateTime.iso8601"); + Date d = (Date) what; + writer.write (dateformat.format (d)); + writer.endElement ("dateTime.iso8601"); + } else if (what instanceof byte[]) { + writer.startElement ("base64"); + writer.write (Base64.encode ((byte[]) what)); + writer.endElement ("base64"); + } else if (what instanceof Vector) { + writer.startElement ("array"); + writer.startElement ("data"); + Vector v = (Vector) what; + int l2 = v.size (); + for (int i2=0; i2 (see code below). + if (depth == 0 || values.peek ().hashCode () != STRUCT) { + Value v = currentValue; + if (depth == 0) { + // This is a top-level object + objectParsed (v.value); + currentValue = null; + } else { + // add object to sub-array; if current container is a struct, add later (at ) + currentValue = (Value) values.pop (); + currentValue.endElement (v); + } + } + } + + // Handle objects contained in structs. + if ("member".equals (name)) { + Value v = currentValue; + currentValue = (Value) values.pop (); + currentValue.endElement (v); + } + + else if ("methodName".equals (name)) { + methodName = cdata.toString (); + cdata.setLength (0); + readCdata = false; + } + } + + + /** + * Method called by SAX driver. + */ + public void startElement (String name, AttributeList atts) throws SAXException { + + if (debug) + System.err.println ("startElement: "+name); + + if ("value".equals (name)) { + // System.err.println ("starting value"); + if (currentValue != null) + values.push (currentValue); + currentValue = new Value (); + // cdata object is reused + cdata.setLength(0); + readCdata = true; + } + + else if ("methodName".equals (name)) { + cdata.setLength(0); + readCdata = true; + } + + else if ("name".equals (name)) { + cdata.setLength(0); + readCdata = true; + } + + else if ("string".equals (name)) { + cdata.setLength(0); + readCdata = true; + } else if ("i4".equals (name) || "int".equals (name)) { + currentValue.setType (INTEGER); + cdata.setLength(0); + readCdata = true; + } else if ("boolean".equals (name)) { + currentValue.setType (BOOLEAN); + cdata.setLength(0); + readCdata = true; + } else if ("double".equals (name)) { + currentValue.setType (DOUBLE); + cdata.setLength(0); + readCdata = true; + } else if ("dateTime.iso8601".equals (name)) { + currentValue.setType (DATE); + cdata.setLength(0); + readCdata = true; + } else if ("base64".equals (name)) { + currentValue.setType (BASE64); + cdata.setLength(0); + readCdata = true; + } else if ("struct".equals (name)) + currentValue.setType (STRUCT); + else if ("array".equals (name)) + currentValue.setType (ARRAY); + else if ("nil".equals (name)) + currentValue.setType (NIL); + } + + + public void error (SAXParseException e) throws SAXException { + System.err.println ("Error parsing XML: "+e); + errorLevel = RECOVERABLE; + errorMsg = e.toString (); + } + + public void fatalError(SAXParseException e) throws SAXException { + System.err.println ("Fatal error parsing XML: "+e); + errorLevel = FATAL; + errorMsg = e.toString (); + } + + /** + * This represents an XML-RPC Value while the request is being parsed. + */ + class Value { + + int type; + Object value; + // the name to use for the next member of struct values + String nextMemberName; + + Hashtable struct; + Vector array; + + /** + * Constructor. + */ + public Value () { + this.type = STRING; + } + + /** + * Notification that a new child element has been parsed. + */ + public void endElement (Value child) { + if (type == ARRAY) + array.addElement (child.value); + else if (type == STRUCT) + struct.put (nextMemberName, child.value); + } + + /** + * Set the type of this value. If it's a container, create the corresponding java container. + */ + public void setType (int type) { + // System.err.println ("setting type to "+types[type]); + this.type = type; + if (type == ARRAY) + value = array = new Vector (); + if (type == STRUCT) + value = struct = new Hashtable (); + } + + /** + * Set the character data for the element and interpret it according to the + * element type + */ + public void characterData (String cdata) { + switch (type) { + case INTEGER: + value = new Integer (cdata.trim ()); + break; + case BOOLEAN: + value = "1".equals (cdata.trim ()) ? Boolean.TRUE : Boolean.FALSE; + break; + case DOUBLE: + value = new Double (cdata.trim ()); + break; + case DATE: + try { + value = dateformat.parse (cdata.trim ()); + } catch (ParseException p) { + // System.err.println ("Exception while parsing date: "+p); + throw new RuntimeException (p.getMessage ()); + } + break; + case BASE64: + value = Base64.decode (cdata.getBytes()); + break; + case STRING: + value = cdata; + break; + case STRUCT: + // this is the name to use for the next member of this struct + nextMemberName = cdata; + break; + } + } + + // This is a performance hack to get the type of a value without casting the Object. + // It breaks the contract of method hashCode, but it doesn't matter since + // Value objects are never used as keys in Hashtables. + public int hashCode () { + return type; + } + + public String toString () { + return (types[type]+" element "+value); + } + } + + + // A quick and dirty XML writer. + class XmlWriter { + + StringBuffer buf; + String enc; + + public XmlWriter (StringBuffer buf) { + // The encoding used for XML-RPC is ISO-8859-1 for pragmatical reasons (Frontier/Win). + this (buf, encoding); + } + + public XmlWriter (StringBuffer buf, String enc) { + this.buf = buf; + this.enc = enc; + // get name of encoding for XML prolog + String encName = encodings.getProperty (enc, enc); + buf.append (""); + } + + public void startElement (String elem) { + buf.append ("<"); + buf.append (elem); + buf.append (">"); + } + + public void endElement (String elem) { + buf.append (""); + } + + public void emptyElement (String elem) { + buf.append ("<"); + buf.append (elem); + buf.append ("/>"); + } + + + public void chardata (String text) { + int l = text.length (); + for (int i=0; iXmlRpcClientLite + * may work better for you. + */ +public class XmlRpcClient implements XmlRpcHandler { + + URL url; + String auth; + + /** + * Construct a XML-RPC client with this URL. + */ + public XmlRpcClient (URL url) { + this.url = url; + } + + /** + * Construct a XML-RPC client for the URL represented by this String. + */ + public XmlRpcClient (String url) throws MalformedURLException { + this.url = new URL (url); + } + + /** + * Construct a XML-RPC client for the specified hostname and port. + */ + public XmlRpcClient (String hostname, int port) throws MalformedURLException { + this.url = new URL ("http://"+hostname+":"+port+"/RPC2"); + } + + + /** + * Sets Authentication for this client. This will be sent as Basic Authentication header + * to the server as described in http://www.ietf.org/rfc/rfc2617.txt. + */ + public void setBasicAuthentication (String user, String password) { + if (user == null || password == null) + auth = null; + else { + char[] basicAuth = Base64.encode ((user+":"+password).getBytes()); + auth = new String (basicAuth).trim(); + } + } + + /** + * Generate an XML-RPC request and send it to the server. Parse the result and + * return the corresponding Java object. + * + * @exception XmlRpcException: If the remote host returned a fault message. + * @exception IOException: If the call could not be made because of lower level problems. + */ + public Object execute (String method, Vector params) throws XmlRpcException, IOException { + Worker worker = getWorker (); + try { + Object retval = worker.execute (method, params); + return retval; + } finally { + if (workers < 50 && !worker.fault) + pool.push (worker); + else + workers -= 1; + } + } + + Stack pool = new Stack (); + int workers = 0; + + private final Worker getWorker () throws IOException { + try { + return (Worker) pool.pop (); + } catch (EmptyStackException x) { + if (workers < 100) { + workers += 1; + return new Worker (); + } + throw new IOException ("XML-RPC System overload"); + } + } + + + class Worker extends XmlRpc { + + boolean fault; + Object result = null; + StringBuffer strbuf; + + public Worker () throws IOException { + super (); + } + + + public Object execute (String method, Vector params) throws XmlRpcException, IOException { + fault = false; + long now = System.currentTimeMillis (); + try { + ByteArrayOutputStream bout = new ByteArrayOutputStream (); + + if (strbuf == null) + strbuf = new StringBuffer (); + else + strbuf.setLength (0); + XmlWriter writer = new XmlWriter (strbuf); + writeRequest (writer, method, params); + byte[] request = writer.getBytes(); + + URLConnection con = url.openConnection (); + con.setDoInput (true); + con.setDoOutput (true); + con.setUseCaches (false); + con.setAllowUserInteraction(false); + con.setRequestProperty ("Content-Length", Integer.toString (request.length)); + con.setRequestProperty ("Content-Type", "text/xml"); + if (auth != null) + con.setRequestProperty ("Authorization", "Basic "+auth); + // con.connect (); + OutputStream out = con.getOutputStream (); + out.write (request); + out.flush (); + InputStream in = con.getInputStream (); + parse (in); + } catch (Exception x) { + x.printStackTrace (); + throw new IOException (x.getMessage ()); + } + if (fault) { // generate an XmlRpcException + XmlRpcException exception = null; + try { + Hashtable f = (Hashtable) result; + String faultString = (String) f.get ("faultString"); + int faultCode = Integer.parseInt (f.get ("faultCode").toString ()); + exception = new XmlRpcException (faultCode, faultString.trim ()); + } catch (Exception x) { + throw new XmlRpcException (0, "Invalid fault response"); + } + throw exception; + } + if (debug) + System.err.println ("Spent "+(System.currentTimeMillis () - now)+" in request"); + return result; + } + + + /** + * Called when the return value has been parsed. + */ + void objectParsed (Object what) { + result = what; + } + + + /** + * Generate an XML-RPC request from a method name and a parameter vector. + */ + void writeRequest (XmlWriter writer, String method, Vector params) throws IOException { + writer.startElement ("methodCall"); + + writer.startElement ("methodName"); + writer.write (method); + writer.endElement ("methodName"); + + writer.startElement ("params"); + int l = params.size (); + for (int i=0; i ...."); + System.err.println ("Arguments are sent as integers or strings."); + } + } +} + + diff --git a/src/helma/xmlrpc/XmlRpcClientLite.java b/src/helma/xmlrpc/XmlRpcClientLite.java new file mode 100644 index 00000000..39ce0eab --- /dev/null +++ b/src/helma/xmlrpc/XmlRpcClientLite.java @@ -0,0 +1,358 @@ +/** + * Copyright 1999 Hannes Wallnoefer + * Implements a XML-RPC client. See http://www.xmlrpc.com/ + */ + +package helma.xmlrpc; + +import java.net.*; +import java.io.*; +import java.util.*; +import org.xml.sax.*; + +/** + * A multithreaded, reusable XML-RPC client object. This version uses a homegrown + * HTTP client which can be quite a bit faster than java.net.URLConnection, especially + * when used with XmlRpc.setKeepAlive(true). + */ +public class XmlRpcClientLite extends XmlRpcClient { + + /** + * Construct a XML-RPC client with this URL. + */ + public XmlRpcClientLite (URL url) { + super (url); + } + + /** + * Construct a XML-RPC client for the URL represented by this String. + */ + public XmlRpcClientLite (String url) throws MalformedURLException { + super (url); + } + + /** + * Construct a XML-RPC client for the specified hostname and port. + */ + public XmlRpcClientLite (String hostname, int port) throws MalformedURLException { + super (hostname, port); + } + + + /** + * Generate an XML-RPC request and send it to the server. Parse the result and + * return the corresponding Java object. + * + * @exception XmlRpcException: If the remote host returned a fault message. + * @exception IOException: If the call could not be made because of lower level problems. + */ + public Object execute (String method, Vector params) throws XmlRpcException, IOException { + Worker worker = getWorker (); + try { + Object retval = worker.execute (method, params); + return retval; + } finally { + if (workers < 50 && !worker.fault) + pool.push (worker); + else + workers -= 1; + } + } + + Stack pool = new Stack (); + int workers = 0; + + private final Worker getWorker () throws IOException { + try { + return (Worker) pool.pop (); + } catch (EmptyStackException x) { + if (workers < 100) { + workers += 1; + return new Worker (); + } + throw new IOException ("XML-RPC System overload"); + } + } + + + class Worker extends XmlRpc { + + boolean fault; + Object result = null; + HttpClient client = null; + StringBuffer strbuf; + + public Worker () { + super (); + } + + + public Object execute (String method, Vector params) throws XmlRpcException, IOException { + long now = System.currentTimeMillis (); + fault = false; + try { + if (strbuf == null) + strbuf = new StringBuffer (); + else + strbuf.setLength (0); + XmlWriter writer = new XmlWriter (strbuf); + writeRequest (writer, method, params); + byte[] request = writer.getBytes(); + + // and send it to the server + if (client == null) + client = new HttpClient (url); + + client.write (request); + + InputStream in = client.getInputStream (); + + // parse the response + parse (in); + + // client keepalive is always false if XmlRpc.keepalive is false + if (!client.keepalive) + client.closeConnection (); + + if (debug) + System.err.println ("result = "+result); + + // check for errors from the XML parser + if (errorLevel == FATAL) + throw new Exception (errorMsg); + } catch (IOException iox) { + // this is a lower level problem, client could not talk to server for some reason. + + throw iox; + + } catch (Exception x) { + // same as above, but exception has to be converted to IOException. + if (XmlRpc.debug) + x.printStackTrace (); + + String msg = x.getMessage (); + if (msg == null || msg.length () == 0) + msg = x.toString (); + throw new IOException (msg); + } + + if (fault) { + // this is an XML-RPC-level problem, i.e. the server reported an error. + // throw an XmlRpcException. + + XmlRpcException exception = null; + try { + Hashtable f = (Hashtable) result; + String faultString = (String) f.get ("faultString"); + int faultCode = Integer.parseInt (f.get ("faultCode").toString ()); + exception = new XmlRpcException (faultCode, faultString.trim ()); + } catch (Exception x) { + throw new XmlRpcException (0, "Server returned an invalid fault response."); + } + throw exception; + } + if (debug) + System.err.println ("Spent "+(System.currentTimeMillis () - now)+" millis in request"); + return result; + } + + + /** + * Called when the return value has been parsed. + */ + void objectParsed (Object what) { + result = what; + } + + + /** + * Generate an XML-RPC request from a method name and a parameter vector. + */ + void writeRequest (XmlWriter writer, String method, Vector params) throws IOException { + writer.startElement ("methodCall"); + + writer.startElement ("methodName"); + writer.write (method); + writer.endElement ("methodName"); + + writer.startElement ("params"); + int l = params.size (); + for (int i=0; i -1; + } + } while (line != null && ! line.equals("")); + return new ServerInputStream (input, contentLength); + } + + + byte[] buffer; + private String readLine () throws IOException { + if (buffer == null) + buffer = new byte[512]; + int next; + int count = 0; + while (true) { + next = input.read(); + if (next < 0 || next == '\n') + break; + if (next != '\r') + buffer[count++] = (byte) next; + if (count >= 512) + throw new IOException ("HTTP Header too long"); + } + return new String (buffer, 0, count); + } + + + protected void finalize () throws Throwable { + closeConnection (); + } + + } + + /** + * Just for testing. + */ + public static void main (String args[]) throws Exception { + // XmlRpc.setDebug (true); + try { + String url = args[0]; + String method = args[1]; + Vector v = new Vector (); + for (int i=2; i ...."); + System.err.println ("Arguments are sent as integers or strings."); + } + } + + +} + + diff --git a/src/helma/xmlrpc/XmlRpcException.java b/src/helma/xmlrpc/XmlRpcException.java new file mode 100644 index 00000000..8bba5448 --- /dev/null +++ b/src/helma/xmlrpc/XmlRpcException.java @@ -0,0 +1,24 @@ +/** + * Copyright 1999 Hannes Wallnoefer + */ + +package helma.xmlrpc; + +/** + * This is thrown by the XmlRpcClient if the remote server reported an error. If something + * went wrong at a lower level (e.g. no http connection) an IOException will be thrown instead. + */ +public class XmlRpcException extends Exception { + + /** + * The fault code of the exception. For servers based on this library, this will always be 0. + * (If there are predefined error codes, they should be in the XML-RPC spec.) + */ + public final int code; + + public XmlRpcException (int code, String message) { + super (message); + this.code = code; + } + +} diff --git a/src/helma/xmlrpc/XmlRpcHandler.java b/src/helma/xmlrpc/XmlRpcHandler.java new file mode 100644 index 00000000..7b004a0e --- /dev/null +++ b/src/helma/xmlrpc/XmlRpcHandler.java @@ -0,0 +1,23 @@ +/* + * Copyright 1999 Hannes Wallnoefer + */ + +package helma.xmlrpc; + +import java.util.Vector; + +/** + * The XML-RPC server uses this interface to call a method of an RPC handler. This should + * be implemented by any class that wants to directly take control when it is called over RPC. Classes + * not implementing this interface will be wrapped into an Invoker + * object that tries to find the matching method for an XML-RPC request. + */ + +public interface XmlRpcHandler { + + /** + * Return the result, or throw an Exception if something went wrong. + */ + public Object execute (String method, Vector params) throws Exception; + +} \ No newline at end of file diff --git a/src/helma/xmlrpc/XmlRpcProxyServlet.java b/src/helma/xmlrpc/XmlRpcProxyServlet.java new file mode 100644 index 00000000..9ae072ac --- /dev/null +++ b/src/helma/xmlrpc/XmlRpcProxyServlet.java @@ -0,0 +1,42 @@ +// Copyright 2000 Hannes Wallnöfer + +package helma.xmlrpc; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.*; +import java.util.Vector; + +/** + * A Servlet that acts as a XML-RPC Proxy .

+ * + * The URL of the server to connect to is taken from the init parameter url. + */ + +public class XmlRpcProxyServlet extends HttpServlet { + + private XmlRpcServer xmlrpc; + + public void init (ServletConfig config) throws ServletException { + if ("true".equalsIgnoreCase (config.getInitParameter ("debug"))) + XmlRpc.setDebug (true); + String url = config.getInitParameter ("url"); + xmlrpc = new XmlRpcServer (); + try { + xmlrpc.addHandler ("$default", new XmlRpcClientLite (url)); + } catch (Exception x) { + throw new ServletException ("Invalid URL: "+url+" ("+x.toString ()+")"); + } + } + + public void doPost(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException { + byte[] result = xmlrpc.execute (req.getInputStream ()); + res.setContentType("text/xml"); + res.setContentLength (result.length); + OutputStream output = res.getOutputStream(); + output.write (result); + output.flush (); + } + +} diff --git a/src/helma/xmlrpc/XmlRpcServer.java b/src/helma/xmlrpc/XmlRpcServer.java new file mode 100644 index 00000000..b857eb1e --- /dev/null +++ b/src/helma/xmlrpc/XmlRpcServer.java @@ -0,0 +1,284 @@ +/** + * Copyright 1999 Hannes Wallnoefer + * Implements an XML-RPC server. See http://www.xmlrpc.com/ + */ + +package helma.xmlrpc; + +import java.io.*; +import java.util.*; +import java.lang.reflect.*; + +/** + * A multithreaded, reusable XML-RPC server object. The name may be misleading because this does not open any + * server sockets. Instead it is fed by passing an XML-RPC input stream to the execute method. + * If you want to open a HTTP listener, use the WebServer class instead. + */ +public class XmlRpcServer { + + /** + * + */ + + Hashtable handlers; + + /** + * Construct a new XML-RPC server. You have to register handlers to make it + * do something useful. + */ + public XmlRpcServer () { + handlers = new Hashtable (); + } + + /** + * Register a handler object with this name. Methods of this objects will be + * callable over XML-RPC as "handlername.methodname". For more information + * about XML-RPC handlers see the main documentation page. + */ + public void addHandler (String handlername, Object handler) { + if (handler instanceof XmlRpcHandler || handler instanceof AuthenticatedXmlRpcHandler) + handlers.put (handlername, handler); + else if (handler != null) + handlers.put (handlername, new Invoker (handler)); + } + + /** + * Remove a handler object that was previously registered with this server. + */ + public void removeHandler (String handlername) { + handlers.remove (handlername); + } + + /** + * Parse the request and execute the handler method, if one is found. Returns the result as XML. + * The calling Java code doesn't need to know whether the call was successful or not since this is all + * packed into the response. + */ + public byte[] execute (InputStream is) { + return execute (is, null, null); + } + + /** + * Parse the request and execute the handler method, if one is found. If the invoked handler is + * AuthenticatedXmlRpcHandler, use the credentials to authenticate the user. + */ + public byte[] execute (InputStream is, String user, String password) { + Worker worker = getWorker (); + byte[] retval = worker.execute (is, user, password); + pool.push (worker); + return retval; + } + + Stack pool = new Stack (); + int workers = 0; + + private final Worker getWorker () { + try { + return (Worker) pool.pop (); + } catch (EmptyStackException x) { + if (workers < 100) { + workers += 1; + return new Worker (); + } + throw new RuntimeException ("System overload"); + } + } + + + class Worker extends XmlRpc { + + Vector inParams; + Object outParam; + byte[] result; + StringBuffer strbuf; + + public byte[] execute (InputStream is, String user, String password) { + inParams = new Vector (); + if (strbuf == null) + strbuf = new StringBuffer (); + else + strbuf.setLength (0); + long now = System.currentTimeMillis (); + + try { + parse (is); + if (debug) { + System.err.println ("method name: "+methodName); + System.err.println ("inparams: "+inParams); + } + // check for errors from the XML parser + if (errorLevel > NONE) + throw new Exception (errorMsg); + + Object handler = null; + + String handlerName = null; + int dot = methodName.indexOf ("."); + if (dot > -1) { + handlerName = methodName.substring (0, dot); + handler = handlers.get (handlerName); + if (handler != null) + methodName = methodName.substring (dot+1); + } + + if (handler == null) { + handler = handlers.get ("$default"); + } + + if (handler == null) { + if (dot > -1) + throw new Exception ("RPC handler object \""+handlerName+"\" not found and no default handler registered."); + else + throw new Exception ("RPC handler object not found for \""+methodName+"\": no default handler registered."); + } + + if (handler instanceof AuthenticatedXmlRpcHandler) + outParam = ((AuthenticatedXmlRpcHandler) handler).execute (methodName, inParams, user, password); + else + outParam = ((XmlRpcHandler) handler).execute (methodName, inParams); + if (debug) + System.err.println ("outparam = "+outParam); + + XmlWriter writer = new XmlWriter (strbuf); + writeResponse (outParam, writer); + result = writer.getBytes (); + + } catch (Exception x) { + if (debug) + x.printStackTrace (); + XmlWriter writer = new XmlWriter (strbuf); + String message = x.toString (); + // check if XmlRpcException was thrown so we can get an error code + int code = x instanceof XmlRpcException ? ((XmlRpcException) x).code : 0; + writeError (code, message, writer); + try { + result = writer.getBytes (); + } catch (UnsupportedEncodingException encx) { + System.err.println ("XmlRpcServer.execute: "+encx); + result = writer.toString().getBytes(); + } + } + if (debug) + System.err.println ("Spent "+(System.currentTimeMillis () - now)+" millis in request"); + return result; + } + + /** + * Called when an object to be added to the argument list has been parsed. + */ + void objectParsed (Object what) { + inParams.addElement (what); + } + + /** + * Writes an XML-RPC response to the XML writer. + */ + void writeResponse (Object param, XmlWriter writer) { + writer.startElement ("methodResponse"); + // if (param == null) param = ""; // workaround for Frontier bug + writer.startElement ("params"); + writer.startElement ("param"); + writeObject (param, writer); + writer.endElement ("param"); + writer.endElement ("params"); + writer.endElement ("methodResponse"); + } + + /** + * Writes an XML-RPC error response to the XML writer. + */ + void writeError (int code, String message, XmlWriter writer) { + // System.err.println ("error: "+message); + Hashtable h = new Hashtable (); + h.put ("faultCode", new Integer (code)); + h.put ("faultString", message); + writer.startElement ("methodResponse"); + writer.startElement ("fault"); + writeObject (h, writer); + writer.endElement ("fault"); + writer.endElement ("methodResponse"); + } + + } // end of inner class Worker + +} // XmlRpcServer + +// This class uses Java Reflection to call methods matching an XML-RPC call +class Invoker implements XmlRpcHandler { + + private Object invokeTarget; + private Class targetClass; + + public Invoker(Object target) { + invokeTarget = target; + targetClass = invokeTarget instanceof Class ? + (Class) invokeTarget : invokeTarget.getClass(); + if (XmlRpc.debug) + System.err.println("Target object is " + targetClass); + } + + + // main method, sucht methode in object, wenn gefunden dann aufrufen. + public Object execute (String methodName, Vector params) throws Exception { + + + // Array mit Classtype bilden, ObjectAry mit Values bilden + Class[] argClasses = null; + Object[] argValues = null; + if(params != null){ + argClasses = new Class[params.size()]; + argValues = new Object[params.size()]; + for(int i = 0; i < params.size(); i++){ + argValues[i] = params.elementAt(i); + if (argValues[i] instanceof Integer) + argClasses[i] = Integer.TYPE; + else if (argValues[i] instanceof Double) + argClasses[i] = Double.TYPE; + else if (argValues[i] instanceof Boolean) + argClasses[i] = Boolean.TYPE; + else + argClasses[i] = argValues[i].getClass(); + } + } + + // Methode da ? + Method method = null; + + if (XmlRpc.debug) { + System.err.println("Searching for method: " + methodName); + for(int i = 0; i < argClasses.length; i++) + System.err.println("Parameter " + i + ": " + argClasses[i] + " = " + argValues[i]); + } + + try { + method = targetClass.getMethod(methodName, argClasses); + } + // Wenn nicht da dann entsprechende Exception returnen + catch(NoSuchMethodException nsm_e){ + throw nsm_e; + } + catch (SecurityException s_e){ + throw s_e; + } + + // invoke + Object returnValue = null; + try { + returnValue = method.invoke (invokeTarget, argValues); + } + catch (IllegalAccessException iacc_e){ + throw iacc_e; + } + catch (IllegalArgumentException iarg_e){ + throw iarg_e; + } + catch (InvocationTargetException it_e) { + if (XmlRpc.debug) + it_e.getTargetException ().printStackTrace (); + throw new Exception (it_e.getTargetException ().toString ()); + } + + return returnValue; + } + +} diff --git a/src/helma/xmlrpc/XmlRpcServlet.java b/src/helma/xmlrpc/XmlRpcServlet.java new file mode 100644 index 00000000..a420684a --- /dev/null +++ b/src/helma/xmlrpc/XmlRpcServlet.java @@ -0,0 +1,46 @@ +// Copyright 1999 Hannes Wallnöfer, Raphael Spannocchi + +package helma.xmlrpc; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.*; +import java.util.Vector; + +/** + * A prototype servlet to run XML-RPC.

+ * + * Note that some clients like the one in Frontier 5 and the first version of XmlRpcApplet + * had XML-RPC requests hard-coded to URI /RPC2. To work with these clients, you have + * to configure your servlet environment to respond to /RPC2. This has been fixed in the + * new version of the XmlRpcApplet. + * + */ + +public class XmlRpcServlet extends HttpServlet implements XmlRpcHandler { + + public XmlRpcServer xmlrpc; + + public void init(ServletConfig config) throws ServletException { + xmlrpc = new XmlRpcServer (); + xmlrpc.addHandler ("example", this); + } + + public void doPost(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException { + byte[] result = xmlrpc.execute (req.getInputStream ()); + res.setContentType("text/xml"); + res.setContentLength (result.length); + OutputStream output = res.getOutputStream(); + output.write (result); + output.flush (); + } + + /** + * Callback method for XML-RPC server + */ + public Object execute (String methodname, Vector params) { + return params; + } + +} diff --git a/src/helma/xmlrpc/fesi/FesiRpcExtension.java b/src/helma/xmlrpc/fesi/FesiRpcExtension.java new file mode 100644 index 00000000..b7b069d3 --- /dev/null +++ b/src/helma/xmlrpc/fesi/FesiRpcExtension.java @@ -0,0 +1,170 @@ +// RpcXtension.java +// Copyright (c) Hannes Wallnöfer, 1999 - All rights reserved + +package helma.xmlrpc.fesi; + +import helma.xmlrpc.*; + +import FESI.Interpreter.*; +import FESI.Exceptions.*; +import FESI.Extensions.*; +import FESI.Data.*; + +import java.io.*; +import java.util.*; +import java.net.*; + + +/** + * An extension to transparently call and serve XML-RPC from the + * FESI EcmaScript interpreter. + * The extension adds constructors for XML-RPC clients and servers to the Global Object. + * For more information on how to use this please look at the files server.es and + * client.es in the src/fesi directory of the distribution. + * + * All argument conversion is done automatically. Currently the following argument and return + * types are supported: + *

    + *
  • plain objects (with all properties returned by ESObject.getProperties ()) + *
  • arrays + *
  • strings + *
  • date objects + *
  • booleans + *
  • integer and float numbers (long values are not supported!) + *
+ * + */ +public class FesiRpcExtension extends Extension { + + Evaluator evaluator; + ESObject op; + + public void initializeExtension (Evaluator evaluator) throws EcmaScriptException { + // XmlRpc.setDebug (true); + this.evaluator = evaluator; + GlobalObject go = evaluator.getGlobalObject(); + FunctionPrototype fp = (FunctionPrototype) evaluator.getFunctionPrototype(); + + op = evaluator.getObjectPrototype(); + + go.putHiddenProperty ("Remote", new GlobalObjectRemote ("Remote", evaluator, fp)); // the Remote constructor + go.putHiddenProperty ("RemoteServer", new GlobalObjectRemoteServer ("RemoteServer", evaluator, fp)); // the RemoteServer constructor + + } + + + class GlobalObjectRemote extends BuiltinFunctionObject { + + GlobalObjectRemote (String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + return doConstruct(thisObject, arguments); + } + + public ESObject doConstruct(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESObject remote = null; + String url = null; + String robj = null; + if (arguments.length >= 1) + url = arguments[0].toString (); + if (arguments.length >= 2) + robj = arguments[1].toString (); + try { + remote = new ESRemote (op, this.evaluator, url, robj); + } catch (MalformedURLException x) { + throw new EcmaScriptException (x.toString ()); + } + return remote; + } + } + + class GlobalObjectRemoteServer extends BuiltinFunctionObject { + + + + GlobalObjectRemoteServer (String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + return doConstruct(thisObject, arguments); + } + + public ESObject doConstruct(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + ESObject remotesrv = null; + String globalname = null; + if (arguments.length < 1 || arguments.length > 2) + throw new EcmaScriptException ("Wrong number of arguments for constructor RemoteServer"); + int port = arguments[0].toInt32 (); + if (arguments.length == 2) + globalname = arguments[1].toString (); + try { + remotesrv = new FesiRpcServer (port, op, this.evaluator); + if (globalname != null) + this.evaluator.getGlobalObject ().putProperty (globalname, remotesrv, globalname.hashCode ()); + } catch (IOException x) { + throw new EcmaScriptException (x.toString ()); + } + return remotesrv; + } + } + + class ESRemote extends ObjectPrototype { + + URL url; + String remoteObject; + + public ESRemote (ESObject prototype, Evaluator evaluator, String urlstring, String robj) throws MalformedURLException { + super (prototype, evaluator); + this.url = new URL (urlstring); + remoteObject = robj; + } + + public ESRemote (ESObject prototype, Evaluator evaluator, URL url, String robj) { + super (prototype, evaluator); + this.url = url; + remoteObject = robj; + } + + public ESValue doIndirectCall(Evaluator evaluator, ESObject target, String functionName, ESValue arguments[]) + throws EcmaScriptException, NoSuchMethodException { + // System.out.println ("doIndirectCall called with "+functionName); + XmlRpcClient client = new XmlRpcClient (url); + long now = System.currentTimeMillis (); + Object retval = null; + int l = arguments.length; + Vector v = new Vector (); + for (int i=0; i + * Server.someObject = new SomeObject (); + * + * + */ + +public class FesiRpcServer extends ObjectPrototype { + + // This is public (for now) to be able to set access restrictions from the outside. + public WebServer srv; + Evaluator evaluator; + + /** + * Create an XML-RPC server with an already existing WebServer. + */ + public FesiRpcServer (WebServer srv, ESObject op, Evaluator eval) throws IOException, EcmaScriptException { + super (op, eval); + this.evaluator = eval; + this.srv = srv; + } + + /** + * Create an XML-RPC server listening on a specific port. + */ + public FesiRpcServer (int port, ESObject op, Evaluator eval) throws IOException, EcmaScriptException { + super (op, eval); + this.evaluator = eval; + srv = new WebServer (port); + } + + public void putProperty(String propertyName, ESValue propertyValue, int hash) throws EcmaScriptException { + if (propertyValue instanceof ESObject) + srv.addHandler (propertyName, new FesiInvoker ((ESObject) propertyValue)); + super.putProperty (propertyName, propertyValue, hash); + } + + public boolean deleteProperty (String propertyName, int hash) throws EcmaScriptException { + srv.removeHandler (propertyName); + super.deleteProperty (propertyName, hash); + return true; + } + + + class FesiInvoker implements XmlRpcHandler { + + ESObject target; + + public FesiInvoker (ESObject target) { + this.target = target; + } + + public Object execute (String method, Vector argvec) throws Exception { + // convert arguments + int l = argvec.size (); + + ESObject callTarget = target; + if (method.indexOf (".") > -1) { + StringTokenizer st = new StringTokenizer (method, "."); + int cnt = st.countTokens (); + for (int i=1; i