From 676b70519d6070c030d03b99304eb2671c7455ae Mon Sep 17 00:00:00 2001 From: hns Date: Tue, 10 Jun 2003 13:20:45 +0000 Subject: [PATCH] Merging changes from 1.2.4 to 1.2.5 --- build/antclick/apps.properties | 9 +- build/build.xml | 70 +-- build/main/apps.properties | 5 +- build/main/db/hopblog/0.xml | 8 - build/main/db/hopblog/1.xml | 8 - build/main/db/hopblog/2.xml | 11 - build/main/db/hopblog/3.xml | 24 - build/main/db/hopblog/idgen.xml | 6 - build/main/lib/ext/mysql.jar | Bin 0 -> 125901 bytes src/FESI/Data/ESLoader.java | 15 +- src/helma/framework/core/Application.java | 117 ++-- src/helma/framework/core/Prototype.java | 2 +- .../framework/core/RequestEvaluator.java | 5 + src/helma/framework/core/TypeManager.java | 7 +- src/helma/framework/core/ZippedAppFile.java | 12 +- src/helma/main/ApplicationManager.java | 513 ++++++++++-------- src/helma/main/Server.java | 2 +- src/helma/objectmodel/db/DbColumn.java | 43 +- src/helma/objectmodel/db/DbKey.java | 14 +- src/helma/objectmodel/db/DbMapping.java | 135 ++++- src/helma/objectmodel/db/MultiKey.java | 124 +++++ src/helma/objectmodel/db/Node.java | 273 +++------- src/helma/objectmodel/db/NodeManager.java | 277 +++++++++- src/helma/objectmodel/db/Property.java | 18 +- src/helma/objectmodel/db/Relation.java | 303 +++++++++-- src/helma/scripting/fesi/ESNode.java | 19 +- src/helma/scripting/fesi/HopExtension.java | 14 - src/helma/servlet/AbstractServletClient.java | 2 + src/helma/servlet/EmbeddedServletClient.java | 9 - src/helma/util/CronJob.java | 226 ++++---- src/helma/util/HtmlEncoder.java | 41 +- src/helma/util/StringUtils.java | 51 ++ 32 files changed, 1498 insertions(+), 865 deletions(-) delete mode 100644 build/main/db/hopblog/0.xml delete mode 100644 build/main/db/hopblog/1.xml delete mode 100644 build/main/db/hopblog/2.xml delete mode 100644 build/main/db/hopblog/3.xml delete mode 100644 build/main/db/hopblog/idgen.xml create mode 100644 build/main/lib/ext/mysql.jar create mode 100644 src/helma/objectmodel/db/MultiKey.java create mode 100644 src/helma/util/StringUtils.java diff --git a/build/antclick/apps.properties b/build/antclick/apps.properties index 8e93c8f9..94dea431 100644 --- a/build/antclick/apps.properties +++ b/build/antclick/apps.properties @@ -1,10 +1,13 @@ # List of apps to start. +# mount antville as root application +antville +antville.mountpoint = / +antville.static = static +antville.staticMountpoint = /static + # mount antville as /managehop to avoid # conflict with antville's manage.hac action manage manage.mountpoint = /manage/hop -# mount antville as root application -antville -antville.mountpoint = / diff --git a/build/build.xml b/build/build.xml index ee9717ba..26f76b7c 100644 --- a/build/build.xml +++ b/build/build.xml @@ -8,7 +8,7 @@ - + @@ -28,8 +28,7 @@ - - + @@ -68,8 +67,6 @@ - - @@ -210,8 +207,8 @@ - - + + @@ -251,17 +248,6 @@ - - -# list of applications to be started by helma -base -base.mountpoint = / -bloggerapi -himp -gong -lillebror -manage - @@ -360,14 +346,16 @@ manage - + + + + + - - - - - - + + + + @@ -411,36 +399,4 @@ manage - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/main/apps.properties b/build/main/apps.properties index c50bcb23..85768f87 100644 --- a/build/main/apps.properties +++ b/build/main/apps.properties @@ -1,10 +1,13 @@ -# List of apps to start. +# List of applications to start. base base.mountpoint = / +base.static = static +base.staticMountpoint = /static manage +gong himp bloggerapi lillebror diff --git a/build/main/db/hopblog/0.xml b/build/main/db/hopblog/0.xml deleted file mode 100644 index 7230be1b..00000000 --- a/build/main/db/hopblog/0.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/build/main/db/hopblog/1.xml b/build/main/db/hopblog/1.xml deleted file mode 100644 index 45babda3..00000000 --- a/build/main/db/hopblog/1.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/build/main/db/hopblog/2.xml b/build/main/db/hopblog/2.xml deleted file mode 100644 index 9cbf1ee2..00000000 --- a/build/main/db/hopblog/2.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - admin@somedomain.at - helma - helma - - diff --git a/build/main/db/hopblog/3.xml b/build/main/db/hopblog/3.xml deleted file mode 100644 index a8bbdf5c..00000000 --- a/build/main/db/hopblog/3.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - 31.05.2002 16:36:44 CEST - 2002.05.31 - - Congratulations! You successfully created your Helma HopBlog! - -As a first step you can login to your HopBlog using "helma" as user name and password (certainly without the quotes) and create or edit stories. - -Or you set-up HopBlog administration for yourself. You can do this by opening the file app.properties in the apps/hopblog directory of your Helma installation and editing the settings for siteAdmin, adminEmail and smtp. - -After that you should register the new administrator as HopBlog user. Simply enter the data you chose for siteAdmin and adminEmail in the appropriate fields of the registration form, fill in the other form fields and submit your data. If you have set a valid e-mail address and smtp server, you should get a message confirming the registration. - -Now you can login to your HopBlog using name and password of the newly created user. - -Let the fun begin... -:) - 31.05.2002 16:36:44 CEST - - diff --git a/build/main/db/hopblog/idgen.xml b/build/main/db/hopblog/idgen.xml deleted file mode 100644 index 0e1d4e21..00000000 --- a/build/main/db/hopblog/idgen.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - 3 - diff --git a/build/main/lib/ext/mysql.jar b/build/main/lib/ext/mysql.jar new file mode 100644 index 0000000000000000000000000000000000000000..32f0410490683d4d054396baf7a77ef978f12c62 GIT binary patch literal 125901 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 aoI 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 U-Gl#CDwRrGanesZjg=|=2jd3V-2eap literal 0 HcmV?d00001 diff --git a/src/FESI/Data/ESLoader.java b/src/FESI/Data/ESLoader.java index a990b18b..35293538 100644 --- a/src/FESI/Data/ESLoader.java +++ b/src/FESI/Data/ESLoader.java @@ -410,15 +410,21 @@ public abstract class ESLoader extends ESObject { // The simplest case is direct object compatibility sourceClass = params[i].getClass(); accepted = targetClass.isAssignableFrom(sourceClass); + if (targetClass != sourceClass) { + if (targetClass == Object.class) + distance += 2; + else + distance += 1; + } debugInfo = " accepted (subclassing)"; - + if (!accepted) { // If we do not have direct object compatibility, we check various // allowed conversions. - + // Handle number and number widening if ((isPrimitiveNumberClass(sourceClass) || - sourceClass == Character.class) + sourceClass == Character.class) && isPrimitiveNumberClass(targetClass)) { // Can be widened ? int targetSize = getNumberSize(targetClass); @@ -431,7 +437,7 @@ public abstract class ESLoader extends ESObject { } else { debugInfo = " rejected (not widening numbers)"; } - // Handle String of length 1 as a Char, which can be converted to a number + // Handle String of length 1 as a Char, which can be converted to a number } else if (targetClass == Character.class && params[i] instanceof String) { if (((String) params[i]).length()==1) { @@ -440,6 +446,7 @@ public abstract class ESLoader extends ESObject { convertToChar = new boolean[n]; } convertToChar[i] = true; + distance += 1; debugInfo = " accepted (String(1) as Character)"; } else { debugInfo = " rejected (String not of length 1)"; diff --git a/src/helma/framework/core/Application.java b/src/helma/framework/core/Application.java index 4172653b..a8d6f28d 100644 --- a/src/helma/framework/core/Application.java +++ b/src/helma/framework/core/Application.java @@ -77,12 +77,6 @@ public final class Application implements IPathElement, Runnable { */ protected SkinManager skinmgr; - /** - * Each application has one internal request evaluator for calling - * the scheduler and other internal functions. - */ - RequestEvaluator eval; - /** * Collections for evaluator thread pooling */ @@ -283,24 +277,25 @@ public final class Application implements IPathElement, Runnable { loadSessionData(null); } + // create and init type mananger typemgr = new TypeManager(this); typemgr.createPrototypes(); - // logEvent ("Started type manager for "+name); - // eval = new RequestEvaluator (this); - logEvent("Starting evaluators for " + name); + // create and init evaluator/thread lists freeThreads = new Stack(); allThreads = new Vector(); - // allThreads.addElement (eval); // preallocate minThreads request evaluators int minThreads = 0; try { minThreads = Integer.parseInt(props.getProperty("minThreads")); } catch (Exception ignore) { + // not parsable as number, keep 0 } + logEvent("Starting "+minThreads+" evaluator(s) for " + name); + for (int i = 0; i < minThreads; i++) { RequestEvaluator ev = new RequestEvaluator(this); @@ -339,7 +334,7 @@ public final class Application implements IPathElement, Runnable { public void start() { starttime = System.currentTimeMillis(); worker = new Thread(this, "Worker-" + name); - worker.setPriority(Thread.NORM_PRIORITY + 2); + // worker.setPriority(Thread.NORM_PRIORITY + 2); worker.start(); // logEvent ("session cleanup and scheduler thread started"); @@ -740,6 +735,13 @@ public final class Application implements IPathElement, Runnable { return p; } + /** + * Return the prototype with the given name, if it exists + */ + public Prototype getPrototypeByName(String name) { + return (Prototype) typemgr.prototypes.get(name); + } + /** * Return a collection containing all prototypes defined for this application */ @@ -1101,17 +1103,7 @@ public final class Application implements IPathElement, Runnable { * by an active RequestEvaluator thread. */ private Object invokeFunction(Object obj, String func, Object[] args) { - Thread thread = Thread.currentThread(); - RequestEvaluator reval = null; - int l = allThreads.size(); - - for (int i = 0; i < l; i++) { - RequestEvaluator r = (RequestEvaluator) allThreads.get(i); - - if ((r != null) && (r.rtx == thread)) { - reval = r; - } - } + RequestEvaluator reval = getCurrentRequestEvaluator(); if (reval != null) { try { @@ -1225,8 +1217,8 @@ public final class Application implements IPathElement, Runnable { // we use the classes from helma.doc-pacakge for introspection. // the first time an url like /appname/api/ is parsed, the application is read again // parsed for comments and exposed as an IPathElement - if (name.equals("api")) { - return eval.scriptingEngine.getIntrospector(); + if (name.equals("api") && allThreads.size() > 0) { + return ((RequestEvaluator) allThreads.get(0)).scriptingEngine.getIntrospector(); } return null; @@ -1306,44 +1298,47 @@ public final class Application implements IPathElement, Runnable { * kicking out expired user sessions. */ public void run() { - long cleanupSleep = 60000; // thread sleep interval (fixed) - long scheduleSleep = 60000; // interval for scheduler invocation - long lastScheduler = 0; // run scheduler immediately - long lastCleanup = System.currentTimeMillis(); + // interval between session cleanups + long sessionCleanupInterval = 60000; + long lastSessionCleanup = System.currentTimeMillis(); // logEvent ("Starting scheduler for "+name); - // as first thing, invoke function onStart in the root object - eval = new RequestEvaluator(this); - allThreads.addElement(eval); // read in standard prototypes to make first request go faster typemgr.updatePrototype("root"); typemgr.updatePrototype("global"); + // as first thing, invoke function onStart in the root object + RequestEvaluator eval = getEvaluator(); try { eval.invokeFunction((INode) null, "onStart", new Object[0]); } catch (Exception ignore) { logEvent("Error in " + name + "/onStart(): " + ignore); + } finally { + if (!stopped) { + releaseEvaluator(eval); + } } while (Thread.currentThread() == worker) { - // get session timeout - int sessionTimeout = 30; - - try { - sessionTimeout = Math.max(0, - Integer.parseInt(props.getProperty("sessionTimeout", - "30"))); - } catch (Exception ignore) { - System.out.println(ignore.toString()); - } long now = System.currentTimeMillis(); // check if we should clean up user sessions - if ((now - lastCleanup) > cleanupSleep) { + if ((now - lastSessionCleanup) > sessionCleanupInterval) { + + lastSessionCleanup = now; + + // get session timeout + int sessionTimeout = 30; + + try { + sessionTimeout = Math.max(0, + Integer.parseInt(props.getProperty("sessionTimeout", + "30"))); + } catch (Exception ignore) {} + try { - lastCleanup = now; Hashtable cloned = (Hashtable) sessions.clone(); @@ -1398,7 +1393,7 @@ public final class Application implements IPathElement, Runnable { try { thisEvaluator = getEvaluator(); } catch (RuntimeException rt) { - if (stopped == false) { + if (!stopped) { logEvent("couldn't execute " + j + ", maximum thread count reached"); @@ -1414,8 +1409,8 @@ public final class Application implements IPathElement, Runnable { (CronJob.millisToNextFullMinute() < 30000)) { CronRunner r = new CronRunner(thisEvaluator, j); - r.start(); activeCronJobs.put(j.getName(), r); + r.start(); } else { try { thisEvaluator.invokeFunction((INode) null, j.getFunction(), @@ -1423,19 +1418,23 @@ public final class Application implements IPathElement, Runnable { } catch (Exception ex) { logEvent("error running " + j + ": " + ex.toString()); } finally { - if (stopped == false) { + if (!stopped) { releaseEvaluator(thisEvaluator); } } } - - thisEvaluator = null; } } + + long sleepInterval = CronJob.millisToNextFullMinute(); + try { + sleepInterval = Integer.parseInt(props.getProperty("schedulerInterval"))*1000; + } catch (Exception ignore) {} + // sleep until the next full minute try { - worker.sleep(CronJob.millisToNextFullMinute()); + worker.sleep(sleepInterval); } catch (InterruptedException x) { logEvent("Scheduler for " + name + " interrupted"); worker = null; @@ -1645,14 +1644,14 @@ public final class Application implements IPathElement, Runnable { * */ public int countThreads() { - return threadgroup.activeCount() - 1; + return threadgroup.activeCount(); } /** * */ public int countEvaluators() { - return allThreads.size() - 1; + return allThreads.size(); } /** @@ -1666,7 +1665,7 @@ public final class Application implements IPathElement, Runnable { * */ public int countActiveEvaluators() { - return allThreads.size() - freeThreads.size() - 1; + return allThreads.size() - freeThreads.size(); } /** @@ -1838,14 +1837,14 @@ public final class Application implements IPathElement, Runnable { thisEvaluator.invokeFunction((INode) null, job.getFunction(), new Object[0], job.getTimeout()); } catch (Exception ex) { + // gets logged in RequestEvaluator + } finally { + if (!stopped) { + releaseEvaluator(thisEvaluator); + } + thisEvaluator = null; + activeCronJobs.remove(job.getName()); } - - if (stopped == false) { - releaseEvaluator(thisEvaluator); - } - - thisEvaluator = null; - activeCronJobs.remove(job.getName()); } } } diff --git a/src/helma/framework/core/Prototype.java b/src/helma/framework/core/Prototype.java index 234726d3..8a5b051c 100644 --- a/src/helma/framework/core/Prototype.java +++ b/src/helma/framework/core/Prototype.java @@ -121,7 +121,7 @@ public final class Prototype { public long getChecksum() { // long start = System.currentTimeMillis(); File[] f = getFiles(); - long c = 0; + long c = directory.lastModified(); for (int i = 0; i < f.length; i++) c += f[i].lastModified(); diff --git a/src/helma/framework/core/RequestEvaluator.java b/src/helma/framework/core/RequestEvaluator.java index 029d599d..3f4c870b 100644 --- a/src/helma/framework/core/RequestEvaluator.java +++ b/src/helma/framework/core/RequestEvaluator.java @@ -392,6 +392,11 @@ public final class RequestEvaluator implements Runnable { app.logEvent("Exception in " + Thread.currentThread() + ": " + x); + // Dump the profiling data to System.err + if (app.debug && !(x instanceof ScriptingException)) { + x.printStackTrace (); + } + // set done to false so that the error will be processed done = false; error = x.getMessage(); diff --git a/src/helma/framework/core/TypeManager.java b/src/helma/framework/core/TypeManager.java index 9f84c062..b4a54f94 100644 --- a/src/helma/framework/core/TypeManager.java +++ b/src/helma/framework/core/TypeManager.java @@ -166,7 +166,7 @@ public final class TypeManager { if (zipped == null) { File f = new File(appDir, list[i]); - if (!f.isDirectory()) { + if (!f.isDirectory() && f.exists()) { zipped = new ZippedAppFile(f, app); zipfiles.put(list[i], zipped); } @@ -237,7 +237,8 @@ public final class TypeManager { if ((dbmap != null) && dbmap.needsUpdate()) { dbmap.update(); - if ((proto != hopobjectProto) && (proto != globalProto)) { + // this is now done in dbmap.update()!!! + /*if ((proto != hopobjectProto) && (proto != globalProto)) { // set parent prototype, in case it has changed. String parentName = dbmap.getExtends(); @@ -246,7 +247,7 @@ public final class TypeManager { } else if (!app.isJavaPrototype(proto.getName())) { proto.setParentPrototype(hopobjectProto); } - } + } */ } } diff --git a/src/helma/framework/core/ZippedAppFile.java b/src/helma/framework/core/ZippedAppFile.java index 85d18374..a312970e 100644 --- a/src/helma/framework/core/ZippedAppFile.java +++ b/src/helma/framework/core/ZippedAppFile.java @@ -80,7 +80,17 @@ public class ZippedAppFile implements Updatable { String ename = entry.getName(); StringTokenizer st = new StringTokenizer(ename, "/"); - if (st.countTokens() == 2) { + int tokens = st.countTokens(); + if (tokens == 1) { + String fname = st.nextToken(); + + if ("app.properties".equalsIgnoreCase(fname)) { + app.props.addProps(file.getName(), zip.getInputStream(entry)); + } else if ("db.properties".equalsIgnoreCase(fname)) { + app.dbProps.addProps(file.getName(), zip.getInputStream(entry)); + } + + } else if (tokens == 2) { String dir = st.nextToken(); String fname = st.nextToken(); diff --git a/src/helma/main/ApplicationManager.java b/src/helma/main/ApplicationManager.java index af14b15e..aaf94a47 100644 --- a/src/helma/main/ApplicationManager.java +++ b/src/helma/main/ApplicationManager.java @@ -21,6 +21,7 @@ import helma.framework.core.*; import helma.objectmodel.*; import helma.servlet.*; import helma.util.SystemProperties; +import helma.util.StringUtils; import org.apache.xmlrpc.XmlRpcHandler; import org.mortbay.http.*; import org.mortbay.http.handler.*; @@ -28,7 +29,6 @@ import org.mortbay.jetty.servlet.*; import org.mortbay.util.*; import java.io.*; import java.lang.reflect.*; -import java.net.URLEncoder; import java.rmi.*; import java.rmi.server.*; import java.util.*; @@ -38,9 +38,9 @@ import javax.servlet.Servlet; * This class is responsible for starting and stopping Helma applications. */ public class ApplicationManager implements XmlRpcHandler { + private Hashtable descriptors; private Hashtable applications; private Hashtable xmlrpcHandlers; - private Properties mountpoints; private int port; private File hopHome; private SystemProperties props; @@ -50,10 +50,10 @@ public class ApplicationManager implements XmlRpcHandler { /** * Creates a new ApplicationManager object. * - * @param port ... - * @param hopHome ... - * @param props ... - * @param server ... + * @param port The RMI port we're binding to + * @param hopHome The Helma home directory + * @param props the properties defining the running apps + * @param server the server instance */ public ApplicationManager(int port, File hopHome, SystemProperties props, Server server) { @@ -61,13 +61,16 @@ public class ApplicationManager implements XmlRpcHandler { this.hopHome = hopHome; this.props = props; this.server = server; + descriptors = new Hashtable(); applications = new Hashtable(); xmlrpcHandlers = new Hashtable(); - mountpoints = new Properties(); lastModified = 0; } - // regularely check applications property file to create and start new applications + /** + * Called regularely check applications property file + * to create and start new applications. + */ protected void checkForChanges() { if (props.lastModified() > lastModified) { try { @@ -76,79 +79,28 @@ public class ApplicationManager implements XmlRpcHandler { if ((appName.indexOf(".") == -1) && (applications.get(appName) == null)) { - start(appName); - register(appName); + AppDescriptor appDesc = new AppDescriptor(appName); + appDesc.start(); + appDesc.bind(); } } // then stop deleted ones - for (Enumeration e = applications.keys(); e.hasMoreElements();) { - String appName = (String) e.nextElement(); + for (Enumeration e = descriptors.elements(); e.hasMoreElements();) { + AppDescriptor appDesc = (AppDescriptor) e.nextElement(); // check if application has been removed and should be stopped - if (!props.containsKey(appName)) { - stop(appName); + if (!props.containsKey(appDesc.appName)) { + appDesc.stop(); } else if (server.http != null) { - // check if application should be remounted at a - // different location on embedded web server - String oldMountpoint = mountpoints.getProperty(appName); - String mountpoint = getMountpoint(appName); - String pattern = getPathPattern(mountpoint); + // If application continues to run, remount + // as the mounting options may have changed. + appDesc.unbind(); + AppDescriptor ndesc = new AppDescriptor(appDesc.appName); + ndesc.app = appDesc.app; + ndesc.bind(); + descriptors.put(ndesc.appName, ndesc); - if (!pattern.equals(oldMountpoint)) { - Server.getLogger().log("Moving application " + appName + - " from " + oldMountpoint + " to " + - pattern); - - HttpContext oldContext = server.http.getContext(null, - oldMountpoint); - - if (oldContext != null) { - // oldContext.setContextPath(pattern); - oldContext.stop(); - oldContext.destroy(); - } - - Application app = (Application) applications.get(appName); - - if (!app.hasExplicitBaseURI()) { - app.setBaseURI(mountpoint); - } - - ServletHttpContext context = new ServletHttpContext(); - - context.setContextPath(pattern); - server.http.addContext(context); - - ServletHolder holder = context.addServlet(appName, "/*", - "helma.servlet.EmbeddedServletClient"); - - holder.setInitParameter("application", appName); - holder.setInitParameter("mountpoint", mountpoint); - - if ("true".equalsIgnoreCase(props.getProperty(appName + - ".responseEncoding"))) { - context.addHandler(new ContentEncodingHandler()); - } - - String cookieDomain = props.getProperty(appName + - ".cookieDomain"); - - if (cookieDomain != null) { - holder.setInitParameter("cookieDomain", cookieDomain); - } - - String uploadLimit = props.getProperty(appName + - ".uploadLimit"); - - if (uploadLimit != null) { - holder.setInitParameter("uploadLimit", uploadLimit); - } - - // holder.start (); - context.start(); - mountpoints.setProperty(appName, pattern); - } } } } catch (Exception mx) { @@ -159,134 +111,38 @@ public class ApplicationManager implements XmlRpcHandler { } } - void start(String appName) { - Server.getLogger().log("Building application " + appName); - try { - // check if application and db dirs are set, otherwise go with - // the defaults, passing null dirs to the constructor. - String appDirName = props.getProperty(appName + ".appdir"); - File appDir = (appDirName == null) ? null : new File(appDirName); - String dbDirName = props.getProperty(appName + ".dbdir"); - File dbDir = (dbDirName == null) ? null : new File(dbDirName); - - // create the application instance - Application app = new Application(appName, server, appDir, dbDir); - - applications.put(appName, app); - - // the application is started later in the register method, when it's bound - app.init(); - } catch (Exception x) { - Server.getLogger().log("Error creating application " + appName + ": " + x); - x.printStackTrace(); - } + /** + * Start an application by name + */ + public void start(String appName) { + AppDescriptor desc = new AppDescriptor(appName); + desc.start(); } - void stop(String appName) { - Server.getLogger().log("Stopping application " + appName); - - try { - Application app = (Application) applications.get(appName); - - // unbind from RMI server - if (port > 0) { - Naming.unbind("//:" + port + "/" + appName); - } - - // unbind from Jetty HTTP server - if (server.http != null) { - String mountpoint = mountpoints.getProperty(appName); - HttpContext context = server.http.getContext(null, mountpoint); - - if (context != null) { - context.stop(); - context.destroy(); - } - } - - // unregister as XML-RPC handler - xmlrpcHandlers.remove(app.getXmlRpcHandlerName()); - app.stop(); - Server.getLogger().log("Unregistered application " + appName); - } catch (Exception x) { - Server.getLogger().log("Couldn't unregister app: " + x); - } - - applications.remove(appName); - } - - void register(String appName) { - try { - Server.getLogger().log("Binding application " + appName); - - Application app = (Application) applications.get(appName); - - // bind to RMI server - if (port > 0) { - Naming.rebind("//:" + port + "/" + appName, new RemoteApplication(app)); - } - - // bind to Jetty HTTP server - if (server.http != null) { - String mountpoint = getMountpoint(appName); - - // if using embedded webserver (not AJP) set application URL prefix - if (!app.hasExplicitBaseURI()) { - app.setBaseURI(mountpoint); - } - - String pattern = getPathPattern(mountpoint); - ServletHttpContext context = new ServletHttpContext(); - - context.setContextPath(pattern); - server.http.addContext(context); - - ServletHolder holder = context.addServlet(appName, "/*", - "helma.servlet.EmbeddedServletClient"); - - holder.setInitParameter("application", appName); - holder.setInitParameter("mountpoint", mountpoint); - - if ("true".equalsIgnoreCase(props.getProperty(appName + - ".responseEncoding"))) { - context.addHandler(new ContentEncodingHandler()); - } - - String cookieDomain = props.getProperty(appName + ".cookieDomain"); - - if (cookieDomain != null) { - holder.setInitParameter("cookieDomain", cookieDomain); - } - - String uploadLimit = props.getProperty(appName + ".uploadLimit"); - - if (uploadLimit != null) { - holder.setInitParameter("uploadLimit", uploadLimit); - } - - String debug = props.getProperty(appName + ".debug"); - - if (debug != null) { - holder.setInitParameter("debug", debug); - } - - // holder.start (); - context.start(); - mountpoints.setProperty(appName, pattern); - } - - // register as XML-RPC handler - xmlrpcHandlers.put(app.getXmlRpcHandlerName(), app); - app.start(); - } catch (Exception x) { - Server.getLogger().log("Couldn't register and start app: " + x); - x.printStackTrace(); + /** + * Bind an application by name + */ + public void register(String appName) { + AppDescriptor desc = (AppDescriptor) descriptors.get(appName); + if (desc != null) { + desc.bind(); } } /** - * + * Stop an application by name + */ + public void stop(String appName) { + AppDescriptor desc = (AppDescriptor) descriptors.get(appName); + if (desc != null) { + desc.stop(); + } + } + + + /** + * Start all applications listed in the properties */ public void startAll() { try { @@ -294,33 +150,14 @@ public class ApplicationManager implements XmlRpcHandler { String appName = (String) e.nextElement(); if (appName.indexOf(".") == -1) { - start(appName); + AppDescriptor desc = new AppDescriptor(appName); + desc.start(); } } - for (Enumeration e = props.keys(); e.hasMoreElements();) { - String appName = (String) e.nextElement(); - - if (appName.indexOf(".") == -1) { - register(appName); - } - } - - if (server.http != null) { - // add handler for static files. - File staticContent = new File(server.getHopHome(), "static"); - - Server.getLogger().log("Serving static content from " + - staticContent.getAbsolutePath()); - - HttpContext context = server.http.addContext("/static/*"); - - context.setResourceBase(staticContent.getAbsolutePath()); - - ResourceHandler handler = new ResourceHandler(); - - context.addHandler(handler); - context.start(); + for (Enumeration e = descriptors.elements(); e.hasMoreElements();) { + AppDescriptor appDesc = (AppDescriptor) e.nextElement(); + appDesc.bind(); } lastModified = System.currentTimeMillis(); @@ -331,13 +168,13 @@ public class ApplicationManager implements XmlRpcHandler { } /** - * + * Stop all running applications. */ public void stopAll() { - for (Enumeration en = applications.keys(); en.hasMoreElements();) { - String appName = (String) en.nextElement(); + for (Enumeration en = descriptors.elements(); en.hasMoreElements();) { + AppDescriptor appDesc = (AppDescriptor) en.nextElement(); - stop(appName); + appDesc.stop(); } } @@ -375,6 +212,12 @@ public class ApplicationManager implements XmlRpcHandler { String method2 = method.substring(dot + 1); Application app = (Application) xmlrpcHandlers.get(handler); + if (app == null) { + app = (Application) xmlrpcHandlers.get("*"); + // use the original method name, the handler is resolved within the app. + method2 = method; + } + if (app == null) { throw new Exception("Handler \"" + handler + "\" not found for " + method); } @@ -382,13 +225,7 @@ public class ApplicationManager implements XmlRpcHandler { return app.executeXmlRpc(method2, params); } - private String getMountpoint(String appName) { - String mountpoint = props.getProperty(appName + ".mountpoint"); - - if (mountpoint == null) { - return "/" + URLEncoder.encode(appName); - } - + private String getMountpoint(String mountpoint) { mountpoint = mountpoint.trim(); if ("".equals(mountpoint)) { @@ -400,15 +237,227 @@ public class ApplicationManager implements XmlRpcHandler { return mountpoint; } + private String joinMountpoint(String prefix, String suffix) { + if (prefix.endsWith("/") || suffix.startsWith("/")) { + return prefix+suffix; + } else { + return prefix+"/"+suffix; + } + } + private String getPathPattern(String mountpoint) { + if (!mountpoint.startsWith("/")) { + mountpoint = "/"+mountpoint; + } + if ("/".equals(mountpoint)) { return "/"; } - if (!mountpoint.endsWith("/")) { - return mountpoint + "/*"; + if (mountpoint.endsWith("/")) { + return mountpoint + "*"; + } + + return mountpoint + "/*"; + } + + /** + * Inner class that describes an application and its start settings. + */ + class AppDescriptor { + + Application app; + + String appName; + File appDir; + File dbDir; + String mountpoint; + String pathPattern; + String staticDir; + String staticMountpoint; + String xmlrpcHandlerName; + String cookieDomain; + String uploadLimit; + String debug; + String charset; + boolean encode; + + /** + * Creates an AppDescriptor from the properties. + */ + AppDescriptor(String name) { + appName = name; + mountpoint = getMountpoint(props.getProperty(name+".mountpoint", + appName)); + pathPattern = getPathPattern(mountpoint); + staticDir = props.getProperty(name+".static"); + staticMountpoint = getPathPattern(props.getProperty(name+".staticMountpoint", + joinMountpoint(mountpoint, "static"))); + cookieDomain = props.getProperty(name+".cookieDomain"); + uploadLimit = props.getProperty(name+".uploadLimit"); + debug = props.getProperty(name+".debug"); + encode = "true".equalsIgnoreCase(props.getProperty(name + + ".responseEncoding")); + String appDirName = props.getProperty(name + ".appdir"); + appDir = (appDirName == null) ? null : new File(appDirName); + String dbDirName = props.getProperty(name + ".dbdir"); + dbDir = (dbDirName == null) ? null : new File(dbDirName); + } + + + void start() { + Server.getLogger().log("Building application " + appName); + + try { + // create the application instance + app = new Application(appName, server, appDir, dbDir); + + // register ourselves + descriptors.put(appName, this); + applications.put(appName, app); + + // the application is started later in the register method, when it's bound + app.init(); + app.start(); + } catch (Exception x) { + Server.getLogger().log("Error creating application " + appName + ": " + x); + x.printStackTrace(); + } + } + + void stop() { + Server.getLogger().log("Stopping application " + appName); + + // unbind application + unbind(); + + // stop application + try { + app.stop(); + Server.getLogger().log("Stopped application " + appName); + } catch (Exception x) { + Server.getLogger().log("Couldn't stop app: " + x); + } + + descriptors.remove(appName); + applications.remove(appName); + } + + void bind() { + try { + Server.getLogger().log("Binding application " + appName); + + // bind to RMI server + if (port > 0) { + Naming.rebind("//:" + port + "/" + appName, new RemoteApplication(app)); + } + + // bind to Jetty HTTP server + if (server.http != null) { + // if using embedded webserver (not AJP) set application URL prefix + if (!app.hasExplicitBaseURI()) { + app.setBaseURI(mountpoint); + } + + ServletHttpContext context = new ServletHttpContext(); + + context.setContextPath(pathPattern); + server.http.addContext(context); + + if (encode) { + context.addHandler(new ContentEncodingHandler()); + } + + ServletHolder holder = context.addServlet(appName, "/*", + "helma.servlet.EmbeddedServletClient"); + + holder.setInitParameter("application", appName); + // holder.setInitParameter("mountpoint", mountpoint); + + if (cookieDomain != null) { + holder.setInitParameter("cookieDomain", cookieDomain); + } + + if (uploadLimit != null) { + holder.setInitParameter("uploadLimit", uploadLimit); + } + + if (debug != null) { + holder.setInitParameter("debug", debug); + } + + holder.setInitParameter("charset", app.getCharset()); + + context.start(); + + if (staticDir != null) { + + File staticContent = new File(staticDir); + if (!staticContent.isAbsolute()) { + staticContent = new File(server.getHopHome(), staticDir); + } + + Server.getLogger().log("Serving static from " + + staticContent.getAbsolutePath()); + Server.getLogger().log("Mounting static at " + + staticMountpoint); + + HttpContext cx = server.http.addContext(staticMountpoint); + + cx.setResourceBase(staticContent.getAbsolutePath()); + + ResourceHandler handler = new ResourceHandler(); + + cx.addHandler(handler); + cx.start(); + } + } + + // register as XML-RPC handler + xmlrpcHandlerName = app.getXmlRpcHandlerName(); + xmlrpcHandlers.put(xmlrpcHandlerName, app); + // app.start(); + } catch (Exception x) { + Server.getLogger().log("Couldn't bind app: " + x); + x.printStackTrace(); + } + } + + void unbind() { + Server.getLogger().log("Unbinding application " + appName); + + try { + // unbind from RMI server + if (port > 0) { + Naming.unbind("//:" + port + "/" + appName); + } + + // unbind from Jetty HTTP server + if (server.http != null) { + HttpContext context = server.http.getContext(null, pathPattern); + + if (context != null) { + context.stop(); + context.destroy(); + } + + if (staticDir != null) { + context = server.http.getContext(null, staticMountpoint); + + if (context != null) { + context.stop(); + context.destroy(); + } + } + } + + // unregister as XML-RPC handler + xmlrpcHandlers.remove(xmlrpcHandlerName); + } catch (Exception x) { + Server.getLogger().log("Couldn't unbind app: " + x); + } + } - return mountpoint + "*"; } } diff --git a/src/helma/main/Server.java b/src/helma/main/Server.java index 7cc2622f..29d39bc3 100644 --- a/src/helma/main/Server.java +++ b/src/helma/main/Server.java @@ -36,7 +36,7 @@ import java.util.*; * Helma server main class. */ public class Server implements IPathElement, Runnable { - public static final String version = "1.2.4 (2003/04/16)"; + public static final String version = "1.2.5 (2003/06/06)"; // server-wide properties static SystemProperties appsProps; diff --git a/src/helma/objectmodel/db/DbColumn.java b/src/helma/objectmodel/db/DbColumn.java index 51c15b16..f2ce4dfc 100644 --- a/src/helma/objectmodel/db/DbColumn.java +++ b/src/helma/objectmodel/db/DbColumn.java @@ -26,10 +26,16 @@ public final class DbColumn { private final int type; private final Relation relation; + private final boolean isId; + private final boolean isPrototype; + private final boolean isName; + + private final boolean isMapped; + /** * Constructor */ - public DbColumn(String name, int type, Relation rel) { + public DbColumn(String name, int type, Relation rel, DbMapping dbmap) { this.name = name; this.type = type; this.relation = rel; @@ -37,6 +43,12 @@ public final class DbColumn { if (relation != null) { relation.setColumnType(type); } + + isId = name.equalsIgnoreCase(dbmap.getIDField()); + isPrototype = name.equalsIgnoreCase(dbmap.getPrototypeField()); + isName = name.equalsIgnoreCase(dbmap.getNameField()); + + isMapped = relation != null || isId || isPrototype || isName; } /** @@ -59,4 +71,33 @@ public final class DbColumn { public Relation getRelation() { return relation; } + + /** + * Returns true if this column serves as ID field for the prototype. + */ + public boolean isIdField() { + return isId; + } + + /** + * Returns true if this column serves as prototype field for the prototype. + */ + public boolean isPrototypeField() { + return isPrototype; + } + + /** + * Returns true if this column serves as name field for the prototype. + */ + public boolean isNameField() { + return isName; + } + + /** + * Returns true if this field is mapped by the prototype's db mapping. + */ + public boolean isMapped() { + return isMapped; + } + } diff --git a/src/helma/objectmodel/db/DbKey.java b/src/helma/objectmodel/db/DbKey.java index 8f947bbb..bf7bb5a3 100644 --- a/src/helma/objectmodel/db/DbKey.java +++ b/src/helma/objectmodel/db/DbKey.java @@ -46,9 +46,9 @@ public final class DbKey implements Key, Serializable { /** * * - * @param what ... + * @param what the other key to be compared with this one * - * @return ... + * @return true if both keys are identical */ public boolean equals(Object what) { if (what == this) { @@ -69,7 +69,7 @@ public final class DbKey implements Key, Serializable { /** * * - * @return ... + * @return this key's hash code */ public int hashCode() { if (hashcode == 0) { @@ -84,7 +84,7 @@ public final class DbKey implements Key, Serializable { /** * * - * @return ... + * @return the key of this key's object's parent object */ public Key getParentKey() { return null; @@ -93,7 +93,7 @@ public final class DbKey implements Key, Serializable { /** * * - * @return ... + * @return the unique storage name for this key's object */ public String getStorageName() { return storageName; @@ -102,7 +102,7 @@ public final class DbKey implements Key, Serializable { /** * * - * @return ... + * @return this key's object's id */ public String getID() { return id; @@ -111,7 +111,7 @@ public final class DbKey implements Key, Serializable { /** * * - * @return ... + * @return a string representation for this key */ public String toString() { return (storageName == null) ? ("[" + id + "]") : (storageName + "[" + id + "]"); diff --git a/src/helma/objectmodel/db/DbMapping.java b/src/helma/objectmodel/db/DbMapping.java index a4487f8e..38ca5824 100644 --- a/src/helma/objectmodel/db/DbMapping.java +++ b/src/helma/objectmodel/db/DbMapping.java @@ -17,12 +17,14 @@ package helma.objectmodel.db; import helma.framework.core.Application; +import helma.framework.core.Prototype; import helma.util.SystemProperties; import helma.util.Updatable; import java.sql.*; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; +import java.util.ArrayList; import java.util.Iterator; import java.util.StringTokenizer; @@ -75,6 +77,9 @@ public final class DbMapping implements Updatable { // Map of db columns by name HashMap columnMap; + // Array of aggressively loaded references + Relation[] joins; + // pre-rendered select statement String selectString = null; String insertString = null; @@ -159,6 +164,7 @@ public final class DbMapping implements Updatable { return props.lastModified() != lastTypeChange; } + /** * Read the mapping from the Properties. Return true if the properties were changed. * The read is split in two, this method and the rewire method. The reason is that in order @@ -173,9 +179,6 @@ public final class DbMapping implements Updatable { // can be stored in this table prototypeField = props.getProperty("_prototypefield"); - // see if this prototype extends (inherits from) any other prototype - extendsProto = props.getProperty("_extends"); - dbSourceName = props.getProperty("_db"); if (dbSourceName != null) { @@ -220,19 +223,43 @@ public final class DbMapping implements Updatable { lastTypeChange = props.lastModified(); + // see if this prototype extends (inherits from) any other prototype + extendsProto = props.getProperty("_extends"); + + if (extendsProto != null) { + parentMapping = app.getDbMapping(extendsProto); + if (parentMapping != null && parentMapping.needsUpdate()) { + parentMapping.update(); + } + } else { + parentMapping = null; + } + + // set the parent prototype in the corresponding Prototype object! + // this was previously done by TypeManager, but we need to do it + // ourself because DbMapping.update() may be called by other code than + // the TypeManager. + if (typename != null && + !"global".equalsIgnoreCase(typename) && + !"hopobject".equalsIgnoreCase(typename)) { + Prototype proto = app.getPrototypeByName(typename); + if (proto != null) { + if (extendsProto != null) { + proto.setParentPrototype(app.getPrototypeByName(extendsProto)); + } else if (!app.isJavaPrototype(typename)) { + proto.setParentPrototype(app.getPrototypeByName("hopobject")); + } + } + } + // null the cached columns and select string columns = null; columnMap.clear(); selectString = insertString = updateString = null; - if (extendsProto != null) { - parentMapping = app.getDbMapping(extendsProto); - } - - // if (tableName != null && dbSource != null) { - // app.logEvent ("set data dbSource for "+typename+" to "+dbSource); HashMap p2d = new HashMap(); HashMap d2p = new HashMap(); + ArrayList joinList = new ArrayList(); for (Enumeration e = props.keys(); e.hasMoreElements();) { String propName = (String) e.nextElement(); @@ -259,7 +286,22 @@ public final class DbMapping implements Updatable { if ((rel.columnName != null) && ((rel.reftype == Relation.PRIMITIVE) || (rel.reftype == Relation.REFERENCE))) { - d2p.put(rel.columnName.toUpperCase(), rel); + Relation old = (Relation) d2p.put(rel.columnName.toUpperCase(), rel); + // check if we're overwriting another relation + // if so, primitive relations get precendence to references + if (old != null) { + app.logEvent("*** Duplicate mapping for "+typename+"."+rel.columnName); + if (old.reftype == Relation.PRIMITIVE) { + d2p.put(old.columnName.toUpperCase(), old); + } + } + } + + // check if a reference is aggressively fetched + if ((rel.reftype == Relation.REFERENCE || + rel.reftype == Relation.COMPLEX_REFERENCE) && + rel.aggressiveLoading) { + joinList.add(rel); } // app.logEvent ("Mapping "+propName+" -> "+dbField); @@ -272,6 +314,9 @@ public final class DbMapping implements Updatable { prop2db = p2d; db2prop = d2p; + joins = new Relation[joinList.size()]; + joins = (Relation[]) joinList.toArray(joins); + String subnodeMapping = props.getProperty("_children"); if (subnodeMapping != null) { @@ -806,20 +851,31 @@ public final class DbMapping implements Updatable { // ok, we have the meta data, now loop through mapping... int ncols = meta.getColumnCount(); - - columns = new DbColumn[ncols]; + ArrayList list = new ArrayList(ncols); for (int i = 0; i < ncols; i++) { String colName = meta.getColumnName(i + 1); Relation rel = columnNameToRelation(colName); - columns[i] = new DbColumn(colName, meta.getColumnType(i + 1), rel); + DbColumn col = new DbColumn(colName, meta.getColumnType(i + 1), rel, this); + // if (col.isMapped()) { + list.add(col); + // } } + columns = new DbColumn[list.size()]; + columns = (DbColumn[]) list.toArray(columns); } return columns; } + /** + * Return the array of relations that are fetched with objects of this type. + */ + public Relation[] getJoins() { + return joins; + } + /** * * @@ -832,6 +888,7 @@ public final class DbMapping implements Updatable { */ public DbColumn getColumn(String columnName) throws ClassNotFoundException, SQLException { + DbColumn col = (DbColumn) columnMap.get(columnName); if (col == null) { @@ -849,10 +906,6 @@ public final class DbMapping implements Updatable { } } - if (col == null) { - throw new SQLException("Column " + columnName + " not found in " + this); - } - columnMap.put(columnName, col); } @@ -860,12 +913,13 @@ public final class DbMapping implements Updatable { } /** + * Get a StringBuffer initialized to the first part of the select statement + * for objects defined by this DbMapping * + * @return the StringBuffer containing the first part of the select query * - * @return ... - * - * @throws SQLException ... - * @throws ClassNotFoundException ... + * @throws SQLException if the table meta data could not be retrieved + * @throws ClassNotFoundException if the JDBC driver class was not found */ public StringBuffer getSelect() throws SQLException, ClassNotFoundException { String sel = selectString; @@ -874,11 +928,43 @@ public final class DbMapping implements Updatable { return new StringBuffer(sel); } - StringBuffer s = new StringBuffer("SELECT * FROM "); + StringBuffer s = new StringBuffer("SELECT "); + + /* DbColumn[] cols = columns; + + if (cols == null) { + cols = getColumns(); + } + + for (int i = 0; i < cols.length; i++) { + s.append(cols[i].getName()); + if (i < cols.length-1) { + s.append(','); + } + } + + for (int i = 0; i < joins.length; i++) { + } */ + + s.append ("*"); + + s.append(" FROM "); s.append(getTableName()); s.append(" "); + for (int i = 0; i < joins.length; i++) { + if (!joins[i].otherType.isRelational()) { + continue; + } + s.append("LEFT JOIN "); + s.append(joins[i].otherType.getTableName()); + s.append(" AS _HLM_"); + s.append(joins[i].propName); + s.append(" ON "); + joins[i].renderJoinConstraints(s); + } + // cache rendered string for later calls. selectString = s.toString(); @@ -944,6 +1030,11 @@ public final class DbMapping implements Updatable { try { DbColumn col = getColumn(columnName); + // This is not a mapped column. In case of doubt, add quotes. + if (col == null) { + return true; + } + switch (col.getType()) { case Types.CHAR: case Types.VARCHAR: diff --git a/src/helma/objectmodel/db/MultiKey.java b/src/helma/objectmodel/db/MultiKey.java new file mode 100644 index 00000000..5e72f03e --- /dev/null +++ b/src/helma/objectmodel/db/MultiKey.java @@ -0,0 +1,124 @@ +/* + * Helma License Notice + * + * The contents of this file are subject to the Helma License + * Version 2.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://adele.helma.org/download/helma/license.txt + * + * Copyright 1998-2003 Helma Software. All Rights Reserved. + * + * $RCSfile$ + * $Author$ + * $Revision$ + * $Date$ + */ + +package helma.objectmodel.db; + +import java.io.Serializable; +import java.util.Map; + +/** + * This is the internal representation of a database key with multiple + * columns. It is constructed from the logical table (type) name and the + * column name/column value pairs that identify the key's object + * + * NOTE: This class doesn't fully support the Key interface - getID always + * returns null since there is no unique key (at least we don't know about it). + */ +public final class MultiKey implements Key, Serializable { + // the name of the prototype which defines the storage of this object. + // this is the name of the object's prototype, or one of its ancestors. + // If null, the object is stored in the embedded db. + private final String storageName; + + // the id that defines this key's object within the above storage space + private final Map parts; + + // lazily initialized hashcode + private transient int hashcode = 0; + + /** + * make a key for a persistent Object, describing its datasource and key parts. + */ + public MultiKey(DbMapping dbmap, Map parts) { + this.parts = parts; + this.storageName = (dbmap == null) ? null : dbmap.getStorageTypeName(); + } + + /** + * + * + * @param what the other key to be compared with this one + * + * @return true if both keys are identical + */ + public boolean equals(Object what) { + if (what == this) { + return true; + } + + if (!(what instanceof MultiKey)) { + return false; + } + + MultiKey k = (MultiKey) what; + + // storageName is an interned string (by DbMapping, from where we got it) + // so we can compare by using == instead of the equals method. + return (storageName == k.storageName) && + ((parts == k.parts) || parts.equals(k.parts)); + } + + /** + * + * + * @return this key's hash code + */ + public int hashCode() { + if (hashcode == 0) { + hashcode = (storageName == null) ? (17 + (37 * parts.hashCode())) + : (17 + (37 * storageName.hashCode()) + + (+37 * parts.hashCode())); + } + + return hashcode; + } + + /** + * + * + * @return the key of this key's object's parent object + */ + public Key getParentKey() { + return null; + } + + /** + * + * + * @return the unique storage name for this key's object + */ + public String getStorageName() { + return storageName; + } + + /** + * + * + * @return this key's object's id + */ + public String getID() { + return null; + } + + /** + * + * + * @return a string representation for this key + */ + public String toString() { + return (storageName == null) ? ("[" + parts + "]") : (storageName + "[" + parts + "]"); + } +} diff --git a/src/helma/objectmodel/db/Node.java b/src/helma/objectmodel/db/Node.java index c9e85186..4b43428f 100644 --- a/src/helma/objectmodel/db/Node.java +++ b/src/helma/objectmodel/db/Node.java @@ -76,7 +76,7 @@ public final class Node implements INode, Serializable { transient private int state; /** - * This constructor is only used for instances of the NullNode subclass. Do not use for ordinary Nodes. + * This constructor is only used for NullNode instance. Do not use for ordinary Nodes. */ Node() { created = lastmodified = System.currentTimeMillis(); @@ -145,183 +145,28 @@ public final class Node implements INode, Serializable { } /** - * Constructor used for nodes being stored in a relational database table. + * Initializer used for nodes being stored in a relational database table. */ - public Node(DbMapping dbm, ResultSet rs, DbColumn[] columns, WrappedNodeManager nmgr) - throws SQLException, IOException { + public void init(DbMapping dbm, String id, String name, String protoName, + Hashtable propMap, WrappedNodeManager nmgr) { this.nmgr = nmgr; // see what prototype/DbMapping this object should use - dbmap = dbm; + this.dbmap = dbm; + // set the prototype name + this.prototype = protoName; - String protoField = dbmap.getPrototypeField(); - - if (protoField != null) { - String protoName = rs.getString(protoField); - - if (protoName != null) { - dbmap = nmgr.getDbMapping(protoName); - - if (dbmap == null) { - // invalid prototype name! - System.err.println("Warning: Invalid prototype name: " + protoName + - " - using default"); - dbmap = dbm; - } - } - } - - setPrototype(dbmap.getTypeName()); - - id = rs.getString(dbmap.getIDField()); - - // checkWriteLock (); - String nameField = dbmap.getNameField(); - - name = (nameField == null) ? id : rs.getString(nameField); + this.id = id; + this.name = name; + // If name was not set from resultset, create a synthetical name now. if ((name == null) || (name.length() == 0)) { - name = dbmap.getTypeName() + " " + id; + this.name = dbmap.getTypeName() + " " + id; } - created = lastmodified = System.currentTimeMillis(); + this.propMap = propMap; - for (int i = 0; i < columns.length; i++) { - Relation rel = columns[i].getRelation(); - - if ((rel == null) || - ((rel.reftype != Relation.PRIMITIVE) && - (rel.reftype != Relation.REFERENCE))) { - continue; - } - - Property newprop = new Property(rel.propName, this); - - switch (columns[i].getType()) { - case Types.BIT: - newprop.setBooleanValue(rs.getBoolean(columns[i].getName())); - - break; - - case Types.TINYINT: - case Types.BIGINT: - case Types.SMALLINT: - case Types.INTEGER: - newprop.setIntegerValue(rs.getLong(columns[i].getName())); - - break; - - case Types.REAL: - case Types.FLOAT: - case Types.DOUBLE: - newprop.setFloatValue(rs.getDouble(columns[i].getName())); - - break; - - case Types.DECIMAL: - case Types.NUMERIC: - - BigDecimal num = rs.getBigDecimal(columns[i].getName()); - - if (num == null) { - break; - } - - if (num.scale() > 0) { - newprop.setFloatValue(num.doubleValue()); - } else { - newprop.setIntegerValue(num.longValue()); - } - - break; - - case Types.VARBINARY: - case Types.BINARY: - newprop.setStringValue(rs.getString(columns[i].getName())); - - break; - - case Types.LONGVARBINARY: - case Types.LONGVARCHAR: - - try { - newprop.setStringValue(rs.getString(columns[i].getName())); - } catch (SQLException x) { - Reader in = rs.getCharacterStream(columns[i].getName()); - char[] buffer = new char[2048]; - int read = 0; - int r = 0; - - while ((r = in.read(buffer, read, buffer.length - read)) > -1) { - read += r; - - if (read == buffer.length) { - // grow input buffer - char[] newBuffer = new char[buffer.length * 2]; - - System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); - buffer = newBuffer; - } - } - - newprop.setStringValue(new String(buffer, 0, read)); - } - - break; - - case Types.CHAR: - case Types.VARCHAR: - case Types.OTHER: - newprop.setStringValue(rs.getString(columns[i].getName())); - - break; - - case Types.DATE: - case Types.TIME: - case Types.TIMESTAMP: - newprop.setDateValue(rs.getTimestamp(columns[i].getName())); - - break; - - case Types.NULL: - newprop.setStringValue(null); - - break; - - // continue; - default: - newprop.setStringValue(rs.getString(columns[i].getName())); - - break; - } - - if (rs.wasNull()) { - newprop.setStringValue(null); - } - - if (propMap == null) { - propMap = new Hashtable(); - } - - propMap.put(rel.propName.toLowerCase(), newprop); - - // if the property is a pointer to another node, change the property type to NODE - if ((rel.reftype == Relation.REFERENCE) && rel.usesPrimaryKey()) { - // FIXME: References to anything other than the primary key are not supported - newprop.convertToNodeReference(rel.otherType); - - // newprop.nhandle = new NodeHandle (new DbKey (rel.otherType, newprop.getStringValue ())); - // newprop.type = IProperty.NODE; - } - - // mark property as clean, since it's fresh from the db - newprop.dirty = false; - } - - // again set created and lastmodified. This is because - // lastmodified has been been updated, and we want both values to - // be identical to show that the node hasn't been changed since - // it was first created. + // set lastmodified and created timestamps and mark as clean created = lastmodified = System.currentTimeMillis(); markAs(CLEAN); } @@ -609,7 +454,7 @@ public final class Node implements INode, Serializable { } catch (Exception ignore) { // just fall back to default method } - + lastNameCheck = System.currentTimeMillis(); } @@ -914,7 +759,7 @@ public final class Node implements INode, Serializable { if (pn != null) { setParent((Node) pn); - anonymous = !pinfo.named; + // anonymous = !pinfo.named; lastParentSet = System.currentTimeMillis(); return pn; @@ -1577,7 +1422,7 @@ public final class Node implements INode, Serializable { // do not fetch subnodes for nodes that haven't been persisted yet or are in // the process of being persistified - except if "manual" subnoderelation is set. - if (subRel.aggressiveLoading && + if (subRel.aggressiveLoading && subRel.getGroup() == null && (((state != TRANSIENT) && (state != NEW)) || (subnodeRelation != null))) { // we don't want to load *all* nodes if we just want to count them @@ -1813,23 +1658,28 @@ public final class Node implements INode, Serializable { } // so if we have a property relation and it does in fact link to another object... - if ((propRel != null) && propRel.isCollection()) { + if ((propRel != null) && (propRel.isCollection() || propRel.isComplexReference())) { // in some cases we just want to create and set a generic node without consulting // the NodeManager if it exists: When we get a collection (aka virtual node) // from a transient node for the first time, or when we get a collection whose // content objects are stored in the embedded XML data storage. if ((state == TRANSIENT) && propRel.virtual) { - INode node = new Node(propname, propRel.getPrototype(), nmgr); + Node pn = new Node(propname, propRel.getPrototype(), nmgr); - node.setDbMapping(propRel.getVirtualMapping()); - setNode(propname, node); - prop = (Property) propMap.get(propname); + pn.setDbMapping(propRel.getVirtualMapping()); + pn.setParent(this); + if (propRel.needsPersistence()) { + setNode(propname, pn); + prop = (Property) propMap.get(propname); + } else { + prop = new Property(propname, this, pn); + } } // if this is from relational database only fetch if this node // is itself persistent. - else if ((state != TRANSIENT) && propRel.createPropertyOnDemand()) { + else if ((state != TRANSIENT) && propRel.createOnDemand()) { // this may be a relational node stored by property name - try { + // try { Node pn = nmgr.getNode(this, propname, propRel); if (pn != null) { @@ -1842,9 +1692,9 @@ public final class Node implements INode, Serializable { prop = new Property(propname, this, pn); } - } catch (RuntimeException nonode) { + // } catch (RuntimeException nonode) { // wasn't a node after all - } + // } } } } @@ -1985,6 +1835,41 @@ public final class Node implements INode, Serializable { return null; } + /** + * Directly set a property on this node + * + * @param propname ... + * @param value ... + */ + protected void set(String propname, Object value, int type) { + checkWriteLock(); + + if (propMap == null) { + propMap = new Hashtable(); + } + + propname = propname.trim(); + + String p2 = propname.toLowerCase(); + + Property prop = (Property) propMap.get(p2); + + if (prop != null) { + prop.setValue(value, type); + } else { + prop = new Property(propname, this); + prop.setValue(value, type); + propMap.put(p2, prop); + } + + // Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED)); + lastmodified = System.currentTimeMillis(); + + if (state == CLEAN) { + markAs(MODIFIED); + } + } + /** * * @@ -2324,6 +2209,17 @@ public final class Node implements INode, Serializable { String p2 = propname.toLowerCase(); + Relation rel = (dbmap == null) ? null : dbmap.getPropertyRelation(propname); + + if (rel != null && (rel.countConstraints() > 1 || rel.isComplexReference())) { + rel.setConstraints(this, n); + if (rel.isComplexReference()) { + Key key = new MultiKey(n.getDbMapping(), rel.getKeyParts(this)); + nmgr.nmgr.registerNode(n, key); + return; + } + } + Property prop = (propMap == null) ? null : (Property) propMap.get(p2); if (prop != null) { @@ -2346,8 +2242,6 @@ public final class Node implements INode, Serializable { prop.setNodeValue(n); - Relation rel = (dbmap == null) ? null : dbmap.getPropertyRelation(propname); - if ((rel == null) || (rel.reftype == Relation.REFERENCE) || rel.virtual || (rel.otherType == null) || !rel.otherType.isRelational()) { // the node must be stored as explicit property @@ -2435,6 +2329,15 @@ public final class Node implements INode, Serializable { if (state == CLEAN) { markAs(MODIFIED); } + } else if (dbmap != null) { + // check if this is a complex constraint and we have to + // unset constraints. + Relation rel = dbmap.getExactPropertyRelation(propname); + + if (rel != null && (rel.isComplexReference())) { + p = getProperty(propname); + rel.unsetConstraints(this, p.getNodeValue()); + } } } catch (Exception ignore) { } @@ -2538,8 +2441,8 @@ public final class Node implements INode, Serializable { * This method walks down node path to the first non-virtual node and return it. * limit max depth to 3, since there shouldn't be more then 2 layers of virtual nodes. */ - public INode getNonVirtualParent() { - INode node = this; + public Node getNonVirtualParent() { + Node node = this; for (int i = 0; i < 5; i++) { if (node == null) { @@ -2550,7 +2453,7 @@ public final class Node implements INode, Serializable { return node; } - node = node.getParent(); + node = (Node) node.getParent(); } return null; diff --git a/src/helma/objectmodel/db/NodeManager.java b/src/helma/objectmodel/db/NodeManager.java index d40c7d7a..a658a22f 100644 --- a/src/helma/objectmodel/db/NodeManager.java +++ b/src/helma/objectmodel/db/NodeManager.java @@ -19,6 +19,7 @@ package helma.objectmodel.db; import helma.framework.core.Application; import helma.objectmodel.*; import helma.util.CacheMap; +import java.math.BigDecimal; import java.io.*; import java.sql.*; import java.util.*; @@ -270,7 +271,10 @@ public final class NodeManager { Key key = null; // check what kind of object we're looking for and make an apropriate key - if (rel.virtual || (rel.groupby != null) || !rel.usesPrimaryKey()) { + if (rel.isComplexReference()) { + // a key for a complex reference + key = new MultiKey(rel.otherType, rel.getKeyParts(home)); + } else if (rel.virtual || (rel.groupby != null) || !rel.usesPrimaryKey()) { // a key for a virtually defined object that's never actually stored in the db // or a key for an object that represents subobjects grouped by some property, generated on the fly key = new SyntheticKey(home.getKey(), kstr); @@ -395,6 +399,14 @@ public final class NodeManager { cache.put(node.getKey(), node); } + + /** + * Register a node in the node cache using the key argument. + */ + protected void registerNode(Node node, Key key) { + cache.put(key, node); + } + /** * Remove a node from the node cache. If at a later time it is accessed again, * it will be refetched from the database. @@ -515,6 +527,7 @@ public final class NodeManager { try { int stmtNumber = 1; + // first column of insert statement is always the primary key stmt.setString(stmtNumber, node.getID()); Hashtable propMap = node.getPropMap(); @@ -523,7 +536,7 @@ public final class NodeManager { Relation rel = columns[i].getRelation(); Property p = null; - if ((rel != null) && (rel.isPrimitive() || rel.isReference())) { + if (rel != null && propMap != null && (rel.isPrimitive() || rel.isReference())) { p = (Property) propMap.get(rel.getPropName()); } @@ -961,6 +974,7 @@ public final class NodeManager { public List getNodeIDs(Node home, Relation rel) throws Exception { // Transactor tx = (Transactor) Thread.currentThread (); // tx.timer.beginEvent ("getNodeIDs "+home); + if ((rel == null) || (rel.otherType == null) || !rel.otherType.isRelational()) { // this should never be called for embedded nodes throw new RuntimeException("NodeMgr.getNodeIDs called for non-relational node " + @@ -982,7 +996,8 @@ public final class NodeManager { if (home.getSubnodeRelation() != null) { // subnode relation was explicitly set - q = new StringBuffer("SELECT ").append(idfield).append(" FROM ") + q = new StringBuffer("SELECT ").append(table).append('.') + .append(idfield).append(" FROM ") .append(table).append(" ") .append(home.getSubnodeRelation()) .toString(); @@ -1075,6 +1090,7 @@ public final class NodeManager { Connection con = dbm.getConnection(); Statement stmt = con.createStatement(); DbColumn[] columns = dbm.getColumns(); + Relation[] joins = dbm.getJoins(); StringBuffer q = dbm.getSelect(); try { @@ -1102,7 +1118,10 @@ public final class NodeManager { while (rs.next()) { // create new Nodes. - Node node = new Node(rel.otherType, rs, columns, safe); + Node node = createNode(rel.otherType, rs, columns, 0); + if (node == null) { + continue; + } Key primKey = node.getKey(); retval.add(new NodeHandle(primKey)); @@ -1115,7 +1134,10 @@ public final class NodeManager { cache.put(primKey, oldnode); } } + + fetchJoinedNodes(rs, joins, columns.length); } + } finally { // tx.timer.endEvent ("getNodes "+home); if (stmt != null) { @@ -1147,6 +1169,7 @@ public final class NodeManager { Connection con = dbm.getConnection(); Statement stmt = con.createStatement(); DbColumn[] columns = dbm.getColumns(); + Relation[] joins = dbm.getJoins(); StringBuffer q = dbm.getSelect(); try { @@ -1154,6 +1177,8 @@ public final class NodeManager { boolean needsQuotes = dbm.needsQuotes(idfield); q.append("WHERE "); + q.append(dbm.getTableName()); + q.append("."); q.append(idfield); q.append(" IN ("); @@ -1212,7 +1237,10 @@ public final class NodeManager { while (rs.next()) { // create new Nodes. - Node node = new Node(dbm, rs, columns, safe); + Node node = createNode(dbm, rs, columns, 0); + if (node == null) { + continue; + } Key primKey = node.getKey(); // for grouped nodes, collect subnode lists for the intermediary @@ -1260,6 +1288,8 @@ public final class NodeManager { } } } + + fetchJoinedNodes(rs, joins, columns.length); } // If these are grouped nodes, build the intermediary group nodes @@ -1280,6 +1310,8 @@ public final class NodeManager { groupnode.lastSubnodeFetch = System.currentTimeMillis(); } } + } catch (Exception x) { + System.err.println ("ERROR IN PREFETCHNODES: "+x); } finally { if (stmt != null) { try { @@ -1440,9 +1472,12 @@ public final class NodeManager { stmt = con.createStatement(); DbColumn[] columns = dbm.getColumns(); + Relation[] joins = dbm.getJoins(); StringBuffer q = dbm.getSelect(); q.append("WHERE "); + q.append(dbm.getTableName()); + q.append("."); q.append(idfield); q.append(" = "); @@ -1464,7 +1499,9 @@ public final class NodeManager { return null; } - node = new Node(dbm, rs, columns, safe); + node = createNode(dbm, rs, columns, 0); + + fetchJoinedNodes(rs, joins, columns.length); if (rs.next()) { throw new RuntimeException("More than one value returned by query."); @@ -1521,17 +1558,21 @@ public final class NodeManager { Connection con = dbm.getConnection(); DbColumn[] columns = dbm.getColumns(); + Relation[] joins = dbm.getJoins(); StringBuffer q = dbm.getSelect(); - if (home.getSubnodeRelation() != null) { + if (home.getSubnodeRelation() != null && !rel.isComplexReference()) { // combine our key with the constraints in the manually set subnode relation q.append("WHERE "); + q.append(dbm.getTableName()); + q.append("."); q.append(rel.accessName); q.append(" = '"); q.append(escape(kstr)); q.append("'"); - q.append(" AND "); + q.append(" AND ("); q.append(home.getSubnodeRelation().trim().substring(5)); + q.append(")"); } else { q.append(rel.buildQuery(home, home.getNonVirtualParent(), kstr, "WHERE ", false)); @@ -1549,7 +1590,9 @@ public final class NodeManager { return null; } - node = new Node(rel.otherType, rs, columns, safe); + node = createNode(rel.otherType, rs, columns, 0); + + fetchJoinedNodes(rs, joins, columns.length); if (rs.next()) { throw new RuntimeException("More than one value returned by query."); @@ -1564,6 +1607,7 @@ public final class NodeManager { node = existing; } } + } finally { if (stmt != null) { try { @@ -1577,6 +1621,221 @@ public final class NodeManager { return node; } + /** + * Create a new Node from a ResultSet. + */ + public Node createNode(DbMapping dbm, ResultSet rs, DbColumn[] columns, int offset) + throws SQLException, IOException { + Hashtable propMap = new Hashtable(); + String id = null; + String name = null; + String protoName = dbm.getTypeName(); + DbMapping dbmap = dbm; + + Node node = new Node(); + + for (int i = 0; i < columns.length; i++) { + + // set prototype? + if (columns[i].isPrototypeField()) { + protoName = rs.getString(i+1+offset); + + if (protoName != null) { + dbmap = getDbMapping(protoName); + + if (dbmap == null) { + // invalid prototype name! + System.err.println("Warning: Invalid prototype name: " + protoName + + " - using default"); + dbmap = dbm; + } + } + } + + // set id? + if (columns[i].isIdField()) { + id = rs.getString(i+1+offset); + // if id == null, the object doesn't actually exist - return null + if (id == null) { + return null; + } + } + + // set name? + if (columns[i].isNameField()) { + name = rs.getString(i+1+offset); + } + + Relation rel = columns[i].getRelation(); + + if ((rel == null) || + ((rel.reftype != Relation.PRIMITIVE) && + (rel.reftype != Relation.REFERENCE))) { + continue; + } + + Property newprop = new Property(rel.propName, node); + + switch (columns[i].getType()) { + case Types.BIT: + newprop.setBooleanValue(rs.getBoolean(i+1+offset)); + + break; + + case Types.TINYINT: + case Types.BIGINT: + case Types.SMALLINT: + case Types.INTEGER: + newprop.setIntegerValue(rs.getLong(i+1+offset)); + + break; + + case Types.REAL: + case Types.FLOAT: + case Types.DOUBLE: + newprop.setFloatValue(rs.getDouble(i+1+offset)); + + break; + + case Types.DECIMAL: + case Types.NUMERIC: + + BigDecimal num = rs.getBigDecimal(i+1+offset); + + if (num == null) { + break; + } + + if (num.scale() > 0) { + newprop.setFloatValue(num.doubleValue()); + } else { + newprop.setIntegerValue(num.longValue()); + } + + break; + + case Types.VARBINARY: + case Types.BINARY: + newprop.setStringValue(rs.getString(i+1+offset)); + + break; + + case Types.LONGVARBINARY: + case Types.LONGVARCHAR: + + try { + newprop.setStringValue(rs.getString(i+1+offset)); + } catch (SQLException x) { + Reader in = rs.getCharacterStream(i+1+offset); + char[] buffer = new char[2048]; + int read = 0; + int r = 0; + + while ((r = in.read(buffer, read, buffer.length - read)) > -1) { + read += r; + + if (read == buffer.length) { + // grow input buffer + char[] newBuffer = new char[buffer.length * 2]; + + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + buffer = newBuffer; + } + } + + newprop.setStringValue(new String(buffer, 0, read)); + } + + break; + + case Types.CHAR: + case Types.VARCHAR: + case Types.OTHER: + newprop.setStringValue(rs.getString(i+1+offset)); + + break; + + case Types.DATE: + newprop.setDateValue(rs.getDate(i+1+offset)); + + break; + + case Types.TIME: + newprop.setDateValue(rs.getTime(i+1+offset)); + + break; + + case Types.TIMESTAMP: + newprop.setDateValue(rs.getTimestamp(i+1+offset)); + + break; + + case Types.NULL: + newprop.setStringValue(null); + + break; + + // continue; + default: + newprop.setStringValue(rs.getString(i+1+offset)); + + break; + } + + if (rs.wasNull()) { + newprop.setStringValue(null); + } + + propMap.put(rel.propName.toLowerCase(), newprop); + + // if the property is a pointer to another node, change the property type to NODE + if ((rel.reftype == Relation.REFERENCE) && rel.usesPrimaryKey()) { + // FIXME: References to anything other than the primary key are not supported + newprop.convertToNodeReference(rel.otherType); + } + + // mark property as clean, since it's fresh from the db + newprop.dirty = false; + } + + if (id == null) { + return null; + } + + node.init(dbmap, id, name, protoName, propMap, safe); + + return node; + } + + /** + * Fetch nodes that are fetched additionally to another node via join. + */ + private void fetchJoinedNodes(ResultSet rs, Relation[] joins, int offset) + throws ClassNotFoundException, SQLException, IOException { + int resultSetOffset = offset; + // create joined objects + for (int i = 0; i < joins.length; i++) { + DbMapping jdbm = joins[i].otherType; + Node node = createNode(jdbm, rs, jdbm.getColumns(), resultSetOffset); + if (node != null) { + Key primKey = node.getKey(); + // register new nodes with the cache. If an up-to-date copy + // existed in the cache, use that. + synchronized (cache) { + Node oldnode = (Node) cache.put(primKey, node); + + if ((oldnode != null) && + (oldnode.getState() != INode.INVALID)) { + // found an ok version in the cache, use it. + cache.put(primKey, oldnode); + } + } + } + resultSetOffset += jdbm.getColumns().length; + } + } + + /** * Get a DbMapping for a given prototype name. This is just a proxy * method to the app's getDbMapping() method. diff --git a/src/helma/objectmodel/db/Property.java b/src/helma/objectmodel/db/Property.java index bd513008..8e4f1ea3 100644 --- a/src/helma/objectmodel/db/Property.java +++ b/src/helma/objectmodel/db/Property.java @@ -190,7 +190,7 @@ public final class Property implements IProperty, Serializable, Cloneable { /** * * - * @return ... + * @return the property's value in its native class */ public Object getValue() { return value; @@ -199,12 +199,24 @@ public final class Property implements IProperty, Serializable, Cloneable { /** * * - * @return ... + * @return the property's type as defined in helma.objectmodel.IProperty.java */ public int getType() { return type; } + /** + * Directly set the value of this property. + */ + protected void setValue(Object value, int type) { + if (type == NODE) { + unregisterNode(); + } + this.value = value; + this.type = type; + dirty = true; + } + /** * * @@ -419,7 +431,7 @@ public final class Property implements IProperty, Serializable, Cloneable { case DATE: - SimpleDateFormat format = new SimpleDateFormat("dd.MM.yy hh:mm:ss"); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); return format.format((Date) value); diff --git a/src/helma/objectmodel/db/Relation.java b/src/helma/objectmodel/db/Relation.java index 1a93f497..eda70d14 100644 --- a/src/helma/objectmodel/db/Relation.java +++ b/src/helma/objectmodel/db/Relation.java @@ -21,6 +21,8 @@ import helma.objectmodel.*; import java.sql.SQLException; import java.util.Properties; import java.util.Vector; +import java.util.Map; +import java.util.HashMap; /** * This describes how a property of a persistent Object is stored in a @@ -41,6 +43,10 @@ public final class Relation { // a 1-to-many relation, a field in another table points to objects of this type public final static int COLLECTION = 2; + // a 1-to-1 reference with multiple or otherwise not-trivial constraints + // this is managed differently than REFERENCE, hence the separate type. + public final static int COMPLEX_REFERENCE = 3; + // direct mapping is a very powerful feature: objects of some types can be directly accessed // by one of their properties/db fields. // public final static int DIRECT = 3; @@ -63,7 +69,8 @@ public final class Relation { boolean readonly; boolean aggressiveLoading; boolean aggressiveCaching; - boolean isPrivate; + boolean isPrivate = false; + boolean referencesPrimaryKey = false; String accessName; // db column used to access objects through this relation String order; String groupbyOrder; @@ -102,6 +109,7 @@ public final class Relation { //////////////////////////////////////////////////////////////////////////////////////////// public void update(String desc, Properties props) { Application app = ownType.getApplication(); + boolean notPrimitive = false; if ((desc == null) || "".equals(desc.trim())) { if (propName != null) { @@ -130,7 +138,9 @@ public final class Relation { prototype = proto; } else if ("object".equalsIgnoreCase(ref)) { virtual = false; - reftype = REFERENCE; + if (reftype != COMPLEX_REFERENCE) { + reftype = REFERENCE; + } } else { throw new RuntimeException("Invalid property Mapping: " + desc); } @@ -141,6 +151,12 @@ public final class Relation { throw new RuntimeException("DbMapping for " + proto + " not found from " + ownType.typename); } + + // make sure the type we're referring to is up to date! + if (otherType != null && otherType.needsUpdate()) { + otherType.update(); + } + } else { virtual = false; columnName = desc; @@ -148,13 +164,7 @@ public final class Relation { } } - String rdonly = props.getProperty(propName + ".readonly"); - - if ((rdonly != null) && "true".equalsIgnoreCase(rdonly)) { - readonly = true; - } else { - readonly = false; - } + readonly = "true".equalsIgnoreCase(props.getProperty(propName + ".readonly")); isPrivate = "true".equalsIgnoreCase(props.getProperty(propName + ".private")); @@ -167,6 +177,33 @@ public final class Relation { constraints = new Constraint[newConstraints.size()]; newConstraints.copyInto(constraints); + + if (reftype == REFERENCE || reftype == COMPLEX_REFERENCE) { + if (constraints.length == 0) { + referencesPrimaryKey = true; + } else { + boolean rprim = false; + for (int i=0; i 0 && !usesPrimaryKey()) { + reftype = COMPLEX_REFERENCE; + } else { + reftype = REFERENCE; + } + } + + if (reftype == COLLECTION) { + referencesPrimaryKey = (accessName == null) || + accessName.equalsIgnoreCase(otherType.getIDField()); + } + // if DbMapping for virtual nodes has already been created, // update its subnode relation. // FIXME: needs to be synchronized? @@ -175,6 +212,8 @@ public final class Relation { virtualMapping.subRelation = getVirtualSubnodeRelation(); virtualMapping.propRelation = getVirtualPropertyRelation(); } + } else { + referencesPrimaryKey = false; } } @@ -199,8 +238,13 @@ public final class Relation { // get additional filter property filter = props.getProperty(propName + ".filter"); - if ((filter != null) && (filter.trim().length() == 0)) { - filter = null; + if (filter != null) { + if (filter.trim().length() == 0) { + filter = null; + } else { + // parenthesise filter + filter = "("+filter+")"; + } } // get max size of collection @@ -237,7 +281,7 @@ public final class Relation { } // aggressive loading and caching is not supported for groupby-nodes - aggressiveLoading = aggressiveCaching = false; + // aggressiveLoading = aggressiveCaching = false; } // check if subnode condition should be applied for property relations @@ -248,9 +292,19 @@ public final class Relation { String foreign = props.getProperty(propName + ".foreign"); if ((local != null) && (foreign != null)) { - cnst.addElement(new Constraint(local, otherType.getTableName(), foreign, false)); + cnst.addElement(new Constraint(local, foreign, false)); columnName = local; } + + // parse additional contstraints from *.1 to *.9 + for (int i=1; i<10; i++) { + local = props.getProperty(propName + ".local."+i); + foreign = props.getProperty(propName + ".foreign."+i); + + if ((local != null) && (foreign != null)) { + cnst.addElement(new Constraint(local, foreign, false)); + } + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -283,6 +337,13 @@ public final class Relation { return reftype == COLLECTION; } + /** + * Returns true if this Relation describes a complex object reference property + */ + public boolean isComplexReference() { + return reftype == COMPLEX_REFERENCE; + } + /** * Tell wether the property described by this relation is to be handled as private, i.e. * a change on it should not result in any changed object/collection relations. @@ -291,14 +352,30 @@ public final class Relation { return isPrivate; } + /** + * Check whether aggressive loading is set for this relation + */ + public boolean loadAggressively() { + return aggressiveLoading; + } + + /** + * Returns the number of constraints for this relation. + */ + public int countConstraints() { + if (constraints == null) + return 0; + return constraints.length; + } + /** * Returns true if the object represented by this Relation has to be * created dynamically by the Helma objectmodel runtime as a virtual * node. Virtual nodes are objects which are only generated on demand * and never stored to a persistent storage. */ - public boolean createPropertyOnDemand() { - return virtual || (accessName != null) || (groupby != null); + public boolean createOnDemand() { + return virtual || (accessName != null) || (groupby != null) || isComplexReference(); } /** @@ -355,6 +432,15 @@ public final class Relation { return columnType; } + /** + * Get the group for a collection relation, if defined. + * + * @return the name of the column used to group child objects, if any. + */ + public String getGroup() { + return groupby; + } + /** * Add a constraint to the current list of constraints */ @@ -374,21 +460,11 @@ public final class Relation { /** * * - * @return ... + * @return true if the foreign key used for this relation is the + * other object's primary key. */ public boolean usesPrimaryKey() { - if (otherType != null) { - if (reftype == REFERENCE) { - return (constraints.length == 1) && constraints[0].foreignKeyIsPrimary(); - } - - if (reftype == COLLECTION) { - return (accessName == null) || - accessName.equalsIgnoreCase(otherType.getIDField()); - } - } - - return false; + return referencesPrimaryKey; } /** @@ -435,13 +511,13 @@ public final class Relation { return null; } - // if the collection node is prototyped, return the app's DbMapping + // if the collection node is prototyped, return the app's DbMapping // for that prototype if (prototype != null) { return otherType; } - // create a synthetic DbMapping that describes how to fetch the + // create a synthetic DbMapping that describes how to fetch the // collection's child objects. if (virtualMapping == null) { virtualMapping = new DbMapping(ownType.app); @@ -510,7 +586,7 @@ public final class Relation { vr.prototype = groupbyPrototype; vr.filter = filter; vr.constraints = constraints; - vr.addConstraint(new Constraint(null, null, groupby, true)); + vr.addConstraint(new Constraint(null, groupby, true)); vr.aggressiveLoading = aggressiveLoading; vr.aggressiveCaching = aggressiveCaching; @@ -531,7 +607,7 @@ public final class Relation { vr.prototype = groupbyPrototype; vr.filter = filter; vr.constraints = constraints; - vr.addConstraint(new Constraint(null, null, groupby, true)); + vr.addConstraint(new Constraint(null, groupby, true)); return vr; } @@ -545,11 +621,13 @@ public final class Relation { StringBuffer q = new StringBuffer(); String prefix = pre; - if (kstr != null) { + if (kstr != null && !isComplexReference()) { q.append(prefix); String accessColumn = (accessName == null) ? otherType.getIDField() : accessName; + q.append(otherType.getTableName()); + q.append("."); q.append(accessColumn); q.append(" = "); @@ -602,21 +680,39 @@ public final class Relation { public String renderConstraints(INode home, INode nonvirtual) throws SQLException { StringBuffer q = new StringBuffer(); - String suffix = " AND "; + String prefix = " AND "; for (int i = 0; i < constraints.length; i++) { + q.append(prefix); constraints[i].addToQuery(q, home, nonvirtual); - q.append(suffix); } if (filter != null) { + q.append(prefix); q.append(filter); - q.append(suffix); } return q.toString(); } + public void renderJoinConstraints(StringBuffer select) { + for (int i = 0; i < constraints.length; i++) { + select.append(ownType.getTableName()); + select.append("."); + select.append(constraints[i].localName); + select.append(" = _HLM_"); + select.append(propName); + select.append("."); + select.append(constraints[i].foreignName); + if (i == constraints.length-1) { + select.append(" "); + } else { + select.append(" AND "); + } + } + + } + /** * Get the order section to use for this relation */ @@ -656,16 +752,14 @@ public final class Relation { if (propname != null) { INode home = constraints[i].isGroupby ? parent : parent.getNonVirtualParent(); - String localName = constraints[i].localName; String value = null; - if ((localName == null) || - localName.equalsIgnoreCase(ownType.getIDField())) { + if (constraints[i].localKeyIsPrimary(home.getDbMapping())) { value = home.getID(); } else if (ownType.isRelational()) { - value = home.getString(ownType.columnNameToProperty(localName)); + value = home.getString(constraints[i].localProperty()); } else { - value = home.getString(localName); + value = home.getString(constraints[i].localName); } if ((value != null) && !value.equals(child.getString(propname))) { @@ -682,8 +776,7 @@ public final class Relation { * appropriate properties */ public void setConstraints(Node parent, Node child) { - INode home = parent.getNonVirtualParent(); - + Node home = parent.getNonVirtualParent(); for (int i = 0; i < constraints.length; i++) { // don't set groupby constraints since we don't know if the // parent node is the base node or a group node @@ -691,14 +784,26 @@ public final class Relation { continue; } - Relation crel = otherType.columnNameToRelation(constraints[i].foreignName); + // check if we update the local or the other object, depending on + // whether the primary key of either side is used. + if (constraints[i].foreignKeyIsPrimary()) { + String localProp = constraints[i].localProperty(); + if (localProp == null) { + System.err.println ("Error: column "+constraints[i].localName+ + " must be mapped in order to be used as constraint in "+ + Relation.this); + } else { + home.setString(localProp, child.getID()); + } + continue; + } + + Relation crel = otherType.columnNameToRelation(constraints[i].foreignName); if (crel != null) { // INode home = constraints[i].isGroupby ? parent : nonVirtual; - String localName = constraints[i].localName; - if ((localName == null) || - localName.equalsIgnoreCase(ownType.getIDField())) { + if (constraints[i].localKeyIsPrimary(home.getDbMapping())) { // only set node if property in child object is defined as reference. if (crel.reftype == REFERENCE) { INode currentValue = child.getNode(crel.propName); @@ -717,22 +822,92 @@ public final class Relation { child.setString(crel.propName, home.getID()); } } else if (crel.reftype == PRIMITIVE) { - String value = null; + Property prop = null; if (ownType.isRelational()) { - value = home.getString(ownType.columnNameToProperty(localName)); + prop = home.getProperty(constraints[i].localProperty()); } else { - value = home.getString(localName); + prop = home.getProperty(constraints[i].localName); } - if (value != null) { - child.setString(crel.propName, value); + if (prop != null) { + child.set(crel.propName, prop.getValue(), prop.getType()); } } } } } + /** + * Unset the constraints that link two objects together. + */ + public void unsetConstraints(Node parent, INode child) { + Node home = parent.getNonVirtualParent(); + for (int i = 0; i < constraints.length; i++) { + // don't set groupby constraints since we don't know if the + // parent node is the base node or a group node + if (constraints[i].isGroupby) { + continue; + } + + // check if we update the local or the other object, depending on + // whether the primary key of either side is used. + + if (constraints[i].foreignKeyIsPrimary()) { + String localProp = constraints[i].localProperty(); + if (localProp != null) { + home.setString(localProp, null); + } + continue; + } + + Relation crel = otherType.columnNameToRelation(constraints[i].foreignName); + if (crel != null) { + // INode home = constraints[i].isGroupby ? parent : nonVirtual; + + if (constraints[i].localKeyIsPrimary(home.getDbMapping())) { + // only set node if property in child object is defined as reference. + if (crel.reftype == REFERENCE) { + INode currentValue = child.getNode(crel.propName); + + if ((currentValue == home)) { + child.setString(crel.propName, null); + } + } else if (crel.reftype == PRIMITIVE) { + child.setString(crel.propName, null); + } + } else if (crel.reftype == PRIMITIVE) { + Property prop = null; + + if (ownType.isRelational()) { + prop = home.getProperty(constraints[i].localProperty()); + } else { + prop = home.getProperty(constraints[i].localName); + } + + if (prop != null) { + child.setString(crel.propName, null); + } + } + } + } + } + + /** + * Returns a map containing the key/value pairs for a specific Node + */ + public Map getKeyParts(INode home) { + Map map = new HashMap(); + for (int i=0; i" + otherType + "]" + c; + String target = otherType == null ? columnName : otherType.toString(); + + return "Relation " + ownType+"."+propName + " -> " + target + c; } /** @@ -781,13 +963,11 @@ public final class Relation { */ class Constraint { String localName; - String tableName; String foreignName; boolean isGroupby; - Constraint(String local, String table, String foreign, boolean groupby) { + Constraint(String local, String foreign, boolean groupby) { localName = local; - tableName = table; foreignName = foreign; isGroupby = groupby; } @@ -806,6 +986,8 @@ public final class Relation { local = ref.getString(homeprop); } + q.append(otherType.getTableName()); + q.append("."); q.append(foreignName); q.append(" = "); @@ -823,6 +1005,11 @@ public final class Relation { foreignName.equalsIgnoreCase(otherType.getIDField()); } + public boolean localKeyIsPrimary(DbMapping homeMapping) { + return (homeMapping == null) || (localName == null) || + localName.equalsIgnoreCase(homeMapping.getIDField()); + } + public String foreignProperty() { return otherType.columnNameToProperty(foreignName); } @@ -832,7 +1019,7 @@ public final class Relation { } public String toString() { - return ownType + "." + localName + "=" + tableName + "." + foreignName; + return localName + "=" + otherType.getTypeName() + "." + foreignName; } } } diff --git a/src/helma/scripting/fesi/ESNode.java b/src/helma/scripting/fesi/ESNode.java index 130b1e4d..3ce462c3 100644 --- a/src/helma/scripting/fesi/ESNode.java +++ b/src/helma/scripting/fesi/ESNode.java @@ -238,7 +238,7 @@ public class ESNode extends ObjectPrototype { } if (!(what[1] instanceof ESNode)) { - throw new EcmaScriptException("Can ony add Node objects as subnodes"); + throw new EcmaScriptException("Can only add Node objects as subnodes"); } ESNode esn = (ESNode) what[1]; @@ -341,23 +341,6 @@ public class ESNode extends ObjectPrototype { ((Node) node).prefetchChildren(start, length); } - /** - * This used to be different from add(), it isn't anymore. It's left here for - * compatibility. - */ - public boolean link(ESValue[] args) { - checkNode(); - - for (int i = 0; i < args.length; i++) { - if (args[i] instanceof ESNode) { - ESNode esn = (ESNode) args[i]; - - node.addNode(esn.getNode()); - } - } - - return true; - } /** * diff --git a/src/helma/scripting/fesi/HopExtension.java b/src/helma/scripting/fesi/HopExtension.java index 9b0ed567..c17b6975 100644 --- a/src/helma/scripting/fesi/HopExtension.java +++ b/src/helma/scripting/fesi/HopExtension.java @@ -107,7 +107,6 @@ public final class HopExtension { esNodePrototype.putHiddenProperty("addAt", new NodeAddAt("addAt", evaluator, fp)); esNodePrototype.putHiddenProperty("remove", new NodeRemove("remove", evaluator, fp)); - esNodePrototype.putHiddenProperty("link", new NodeLink("link", evaluator, fp)); esNodePrototype.putHiddenProperty("list", new NodeList("list", evaluator, fp)); esNodePrototype.putHiddenProperty("set", new NodeSet("set", evaluator, fp)); esNodePrototype.putHiddenProperty("get", new NodeGet("get", evaluator, fp)); @@ -270,19 +269,6 @@ public final class HopExtension { } } - class NodeLink extends BuiltinFunctionObject { - NodeLink(String name, Evaluator evaluator, FunctionPrototype fp) { - super(fp, evaluator, name, 1); - } - - public ESValue callFunction(ESObject thisObject, ESValue[] arguments) - throws EcmaScriptException { - ESNode node = (ESNode) thisObject; - - return ESBoolean.makeBoolean(node.link(arguments)); - } - } - class NodeList extends BuiltinFunctionObject { NodeList(String name, Evaluator evaluator, FunctionPrototype fp) { super(fp, evaluator, name, 0); diff --git a/src/helma/servlet/AbstractServletClient.java b/src/helma/servlet/AbstractServletClient.java index cc6cc2bf..a27e313e 100644 --- a/src/helma/servlet/AbstractServletClient.java +++ b/src/helma/servlet/AbstractServletClient.java @@ -16,6 +16,7 @@ /* Portierung von helma.asp.AspClient auf Servlets */ /* Author: Raphael Spannocchi Datum: 27.11.1998 */ + package helma.servlet; import helma.framework.*; @@ -292,6 +293,7 @@ public abstract class AbstractServletClient extends HttpServlet { if (debug) { sendError(response, response.SC_INTERNAL_SERVER_ERROR, "Error in request handler:" + x); + x.printStackTrace(); } else { sendError(response, response.SC_INTERNAL_SERVER_ERROR, "The server encountered an error while processing your request. " + diff --git a/src/helma/servlet/EmbeddedServletClient.java b/src/helma/servlet/EmbeddedServletClient.java index e2b621b1..7d9f16d4 100644 --- a/src/helma/servlet/EmbeddedServletClient.java +++ b/src/helma/servlet/EmbeddedServletClient.java @@ -32,9 +32,6 @@ public final class EmbeddedServletClient extends AbstractServletClient { private Application app = null; private String appName; - // The path where this servlet is mounted - String mountpoint; - /** * Creates a new EmbeddedServletClient object. */ @@ -56,12 +53,6 @@ public final class EmbeddedServletClient extends AbstractServletClient { if (appName == null) { throw new ServletException("Application name not set in init parameters"); } - - mountpoint = init.getInitParameter("mountpoint"); - - if (mountpoint == null) { - mountpoint = "/" + appName; - } } ResponseTrans execute(RequestTrans req) throws Exception { diff --git a/src/helma/util/CronJob.java b/src/helma/util/CronJob.java index 159fe2af..b232d852 100644 --- a/src/helma/util/CronJob.java +++ b/src/helma/util/CronJob.java @@ -38,7 +38,7 @@ package helma.util; * software without prior written permission. For written * permission, please contact support@protomatter.com. * - * 5. Products derived from this software may not be called "Protomatter", + * 5. Products derived from this software may not be called "Protomatter", * nor may "Protomatter" appear in their name, without prior written * permission of the Protomatter Software Project * (support@protomatter.com). @@ -62,7 +62,7 @@ import java.util.*; /** * A cron entry, derived from Protomatter's CronEntry class. - * This class encapsulates a function call, a timeout value + * This class encapsulates a function call, a timeout value * and a specification for when the given event should be * delivered to the given topics. The specification of when * the event should be delivered is based on the UNIX cron @@ -72,17 +72,18 @@ import java.util.*; public class CronJob { - // used as the value in hashtables - private static Object value = new Object(); - private static Hashtable all = new Hashtable (); + private static HashSet all = new HashSet (2); private static String ALL_VALUE = "*"; + static { + all.add (ALL_VALUE); + } - private Hashtable year; - private Hashtable month; - private Hashtable day; - private Hashtable weekday; - private Hashtable hour; - private Hashtable minute; + private HashSet year; + private HashSet month; + private HashSet day; + private HashSet weekday; + private HashSet hour; + private HashSet minute; private String name = null; private String function = null; @@ -161,21 +162,22 @@ public class CronJob { */ - public static CronJob newJob (String functionName, String year, String month, String day, String weekday, String hour, String minute) { + public static CronJob newJob (String functionName, String year, String month, + String day, String weekday, String hour, String minute) { CronJob job = new CronJob (functionName); job.setFunction (functionName); if (year != null) - parseYear (job, year); + job.parseYear (year); if (month != null) - parseMonth (job, month); + job.parseMonth (month); if (day != null) - parseDay (job, day); + job.parseDay (day); if (weekday != null) - parseWeekDay (job, weekday); + job.parseWeekDay (weekday); if (hour != null) - parseHour (job, hour); + job.parseHour (hour); if (minute != null) - parseMinute (job, minute); + job.parseMinute (minute); return job; } @@ -203,19 +205,19 @@ public class CronJob { if (jobSpec.equalsIgnoreCase("function")) { job.setFunction(value); } else if (jobSpec.equalsIgnoreCase("year")) { - parseYear (job, value); + job.parseYear (value); } else if (jobSpec.equalsIgnoreCase("month")) { - parseMonth (job, value); + job.parseMonth (value); } else if (jobSpec.equalsIgnoreCase("day")) { - parseDay (job, value); + job.parseDay (value); } else if (jobSpec.equalsIgnoreCase("weekday")) { - parseWeekDay (job, value); + job.parseWeekDay (value); } else if (jobSpec.equalsIgnoreCase("hour")) { - parseHour (job, value); + job.parseHour (value); } else if (jobSpec.equalsIgnoreCase("minute")) { - parseMinute (job, value); + job.parseMinute (value); } else if (jobSpec.equalsIgnoreCase("timeout")) { - parseTimeout (job, value); + job.parseTimeout (value); } } catch (NoSuchElementException nsee) { } @@ -249,9 +251,9 @@ public class CronJob { } - public static void parseYear (CronJob job, String value) { + public void parseYear (String value) { if (value.equals("*")) { - job.setAllYears(true); + setAllYears(true); } else { StringTokenizer st = new StringTokenizer(value.trim(), ","); while (st.hasMoreTokens()) { @@ -260,54 +262,54 @@ public class CronJob { int start = Integer.parseInt(s.substring(0, s.indexOf("-"))); int finish = Integer.parseInt(s.substring(s.indexOf("-") +1)); for (int i=start; i<=finish; i++) { - job.addYear(i); + addYear(i); } } else { int y = Integer.parseInt(s); - job.addYear(y); + addYear(y); } } } } - public static void parseMonth (CronJob job, String value) { + public void parseMonth (String value) { if (value.equals("*")) { - job.setAllMonths(true); + setAllMonths(true); } else { StringTokenizer st = new StringTokenizer(value.trim(), ","); while (st.hasMoreTokens()) { String m = st.nextToken(); if (m.equalsIgnoreCase("january")) - job.addMonth(Calendar.JANUARY); + addMonth(Calendar.JANUARY); if (m.equalsIgnoreCase("february")) - job.addMonth(Calendar.FEBRUARY); + addMonth(Calendar.FEBRUARY); if (m.equalsIgnoreCase("march")) - job.addMonth(Calendar.MARCH); + addMonth(Calendar.MARCH); if (m.equalsIgnoreCase("april")) - job.addMonth(Calendar.APRIL); + addMonth(Calendar.APRIL); if (m.equalsIgnoreCase("may")) - job.addMonth(Calendar.MAY); + addMonth(Calendar.MAY); if (m.equalsIgnoreCase("june")) - job.addMonth(Calendar.JUNE); + addMonth(Calendar.JUNE); if (m.equalsIgnoreCase("july")) - job.addMonth(Calendar.JULY); + addMonth(Calendar.JULY); if (m.equalsIgnoreCase("august")) - job.addMonth(Calendar.AUGUST); + addMonth(Calendar.AUGUST); if (m.equalsIgnoreCase("september")) - job.addMonth(Calendar.SEPTEMBER); + addMonth(Calendar.SEPTEMBER); if (m.equalsIgnoreCase("october")) - job.addMonth(Calendar.OCTOBER); + addMonth(Calendar.OCTOBER); if (m.equalsIgnoreCase("november")) - job.addMonth(Calendar.NOVEMBER); + addMonth(Calendar.NOVEMBER); if (m.equalsIgnoreCase("december")) - job.addMonth(Calendar.DECEMBER); + addMonth(Calendar.DECEMBER); } } } - public static void parseDay (CronJob job, String day) { + public void parseDay (String day) { if (day.equals("*")) { - job.setAllDays(true); + setAllDays(true); } else { StringTokenizer st = new StringTokenizer(day.trim(), ","); while (st.hasMoreTokens()) { @@ -316,46 +318,46 @@ public class CronJob { int start = Integer.parseInt(s.substring(0, s.indexOf("-"))); int finish = Integer.parseInt(s.substring(s.indexOf("-") +1)); for (int i=start; i<=finish; i++) { - job.addDay(i); + addDay(i); } } else { int d = Integer.parseInt(s); - job.addDay(d); + addDay(d); } } } } - public static void parseWeekDay (CronJob job, String weekday) { + public void parseWeekDay (String weekday) { if (weekday.equals("*")) { - job.setAllWeekdays(true); + setAllWeekdays(true); } else { StringTokenizer st = new StringTokenizer(weekday.trim(), ","); while (st.hasMoreTokens()) { String d = st.nextToken(); if (d.equalsIgnoreCase("monday")) - job.addWeekday(Calendar.MONDAY); + addWeekday(Calendar.MONDAY); if (d.equalsIgnoreCase("tuesday")) - job.addWeekday(Calendar.TUESDAY); + addWeekday(Calendar.TUESDAY); if (d.equalsIgnoreCase("wednesday")) - job.addWeekday(Calendar.WEDNESDAY); + addWeekday(Calendar.WEDNESDAY); if (d.equalsIgnoreCase("thursday")) - job.addWeekday(Calendar.THURSDAY); + addWeekday(Calendar.THURSDAY); if (d.equalsIgnoreCase("friday")) - job.addWeekday(Calendar.FRIDAY); + addWeekday(Calendar.FRIDAY); if (d.equalsIgnoreCase("saturday")) - job.addWeekday(Calendar.SATURDAY); + addWeekday(Calendar.SATURDAY); if (d.equalsIgnoreCase("sunday")) - job.addWeekday(Calendar.SUNDAY); + addWeekday(Calendar.SUNDAY); } } } - public static void parseHour (CronJob job, String hour) { + public void parseHour (String hour) { if (hour.equals("*")) { - job.setAllHours(true); + setAllHours(true); } else { StringTokenizer st = new StringTokenizer(hour.trim (), ","); while (st.hasMoreTokens()) { @@ -364,20 +366,20 @@ public class CronJob { int start = Integer.parseInt(s.substring(0, s.indexOf("-"))); int finish = Integer.parseInt(s.substring(s.indexOf("-") +1)); for (int i=start; i<=finish; i++) { - job.addHour(i); + addHour(i); } } else { int h = Integer.parseInt(s); - job.addHour(h); + addHour(h); } } } } - public static void parseMinute (CronJob job, String minute) { + public void parseMinute (String minute) { if (minute.equals("*")) { - job.setAllMinutes(true); + setAllMinutes(true); } else { StringTokenizer st = new StringTokenizer(minute.trim (), ","); while (st.hasMoreTokens()) { @@ -386,46 +388,44 @@ public class CronJob { int start = Integer.parseInt(s.substring(0, s.indexOf("-"))); int finish = Integer.parseInt(s.substring(s.indexOf("-") +1)); for (int i=start; i<=finish; i++) { - job.addMinute(i); + addMinute(i); } } else { int m = Integer.parseInt(s); - job.addMinute(m); + addMinute(m); } } } } - - public static void parseTimeout (CronJob job, String timeout) { + public void parseTimeout (String timeout) { long timeoutValue = 1000 * Long.valueOf(timeout).longValue (); - job.setTimeout (timeoutValue); + setTimeout (timeoutValue); } - public static long nextFullMinute () { - long now = System.currentTimeMillis(); - long millisAfterMinute = (now % 60000); - return (now + 60000 - millisAfterMinute); - } + public static long nextFullMinute () { + long now = System.currentTimeMillis(); + long millisAfterMinute = (now % 60000); + return (now + 60000 - millisAfterMinute); + } - public static long millisToNextFullMinute () { - long now = System.currentTimeMillis(); - long millisAfterMinute = (now % 60000); - return (60000 - millisAfterMinute); - } + public static long millisToNextFullMinute () { + long now = System.currentTimeMillis(); + long millisAfterMinute = (now % 60000); + return (60000 - millisAfterMinute); + } /** * Create an empty CronJob. */ public CronJob (String name) { this.name = name; - all.put (ALL_VALUE, value); - year = new Hashtable (all); - month = new Hashtable (all); - day = new Hashtable (all); - weekday = new Hashtable (all); - hour = new Hashtable (all); - minute = new Hashtable (all); + year = new HashSet (all); + month = new HashSet (all); + day = new HashSet (all); + weekday = new HashSet (all); + hour = new HashSet (all); + minute = new HashSet (all); } /** @@ -439,29 +439,29 @@ public class CronJob { // try and short-circuit as fast as possible. Integer theYear = new Integer(cal.get(Calendar.YEAR)); - if (!year.containsKey(ALL_VALUE) && !year.containsKey(theYear)) + if (!year.contains(ALL_VALUE) && !year.contains(theYear)) return false; Integer theMonth = new Integer(cal.get(Calendar.MONTH)); - if (!month.containsKey(ALL_VALUE) && !month.containsKey(theMonth)) + if (!month.contains(ALL_VALUE) && !month.contains(theMonth)) return false; - + Integer theDay = new Integer(cal.get(Calendar.DAY_OF_MONTH)); - if (!day.containsKey(ALL_VALUE) && !day.containsKey(theDay)) + if (!day.contains(ALL_VALUE) && !day.contains(theDay)) return false; - + Integer theWeekDay = new Integer(cal.get(Calendar.DAY_OF_WEEK)); - if (!weekday.containsKey(ALL_VALUE) && !weekday.containsKey(theWeekDay)) + if (!weekday.contains(ALL_VALUE) && !weekday.contains(theWeekDay)) return false; - + Integer theHour = new Integer(cal.get(Calendar.HOUR_OF_DAY)); - if (!hour.containsKey(ALL_VALUE) && !hour.containsKey(theHour)) + if (!hour.contains(ALL_VALUE) && !hour.contains(theHour)) return false; - + Integer theMinute = new Integer(cal.get(Calendar.MINUTE)); - if (!minute.containsKey(ALL_VALUE) && !minute.containsKey(theMinute)) + if (!minute.contains(ALL_VALUE) && !minute.contains(theMinute)) return false; - + return true; } @@ -472,7 +472,7 @@ public class CronJob { public void addYear(int year) { this.year.remove(ALL_VALUE); - this.year.put(new Integer(year), value); + this.year.add(new Integer(year)); } /** @@ -494,7 +494,7 @@ public class CronJob { public void setAllYears(boolean set) { if (set) - this.year.put(ALL_VALUE, value); + this.year.add(ALL_VALUE); else this.year.remove(ALL_VALUE); } @@ -508,7 +508,7 @@ public class CronJob { public void addMonth(int month) { this.month.remove(ALL_VALUE); - this.month.put(new Integer(month), value); + this.month.add(new Integer(month)); } /** @@ -532,7 +532,7 @@ public class CronJob { public void setAllMonths(boolean set) { if (set) - this.month.put(ALL_VALUE, value); + this.month.add(ALL_VALUE); else this.month.remove(ALL_VALUE); } @@ -544,7 +544,7 @@ public class CronJob { public void addDay(int day) { this.day.remove(ALL_VALUE); - this.day.put(new Integer(day), value); + this.day.add(new Integer(day)); } /** @@ -566,7 +566,7 @@ public class CronJob { public void setAllDays(boolean set) { if (set) - this.day.put(ALL_VALUE, value); + this.day.add(ALL_VALUE); else this.day.remove(ALL_VALUE); } @@ -580,7 +580,7 @@ public class CronJob { public void addWeekday(int weekday) { this.weekday.remove(ALL_VALUE); - this.weekday.put(new Integer(weekday), value); + this.weekday.add(new Integer(weekday)); } /** @@ -604,7 +604,7 @@ public class CronJob { public void setAllWeekdays(boolean set) { if (set) - this.weekday.put(ALL_VALUE, value); + this.weekday.add(ALL_VALUE); else this.weekday.remove(ALL_VALUE); } @@ -616,7 +616,7 @@ public class CronJob { public void addHour(int hour) { this.hour.remove(ALL_VALUE); - this.hour.put(new Integer(hour), value); + this.hour.add(new Integer(hour)); } /** @@ -638,7 +638,7 @@ public class CronJob { public void setAllHours(boolean set) { if (set) - this.hour.put(ALL_VALUE, value); + this.hour.add(ALL_VALUE); else this.hour.remove(ALL_VALUE); } @@ -650,7 +650,7 @@ public class CronJob { public void addMinute(int minute) { this.minute.remove(ALL_VALUE); - this.minute.put(new Integer(minute), value); + this.minute.add(new Integer(minute)); } /** @@ -672,12 +672,11 @@ public class CronJob { public void setAllMinutes(boolean set) { if (set) - this.minute.put(ALL_VALUE, value); + this.minute.add(ALL_VALUE); else this.minute.remove(ALL_VALUE); } - /** * Set this entry's name */ @@ -727,9 +726,10 @@ public class CronJob { { return this.timeout; } - - public String toString () { - return "[CronJob " + name + "]"; - } - + + public String toString () + { + return "[CronJob " + name + "]"; + } + } diff --git a/src/helma/util/HtmlEncoder.java b/src/helma/util/HtmlEncoder.java index 6bfbdf8b..9c1bb6c4 100644 --- a/src/helma/util/HtmlEncoder.java +++ b/src/helma/util/HtmlEncoder.java @@ -310,6 +310,7 @@ public final class HtmlEncoder { swallowTwo.add("ul"); /// to be treated as block level elements + swallowTwo.add("br"); swallowTwo.add("dd"); swallowTwo.add("dt"); swallowTwo.add("frameset"); @@ -322,6 +323,26 @@ public final class HtmlEncoder { swallowAll.add("tr"); } + // set of tags that are always empty + static final HashSet emptyTags = new HashSet(); + + static { + emptyTags.add("area"); + emptyTags.add("base"); + emptyTags.add("basefont"); + emptyTags.add("br"); + emptyTags.add("col"); + emptyTags.add("frame"); + emptyTags.add("hr"); + emptyTags.add("img"); + emptyTags.add("input"); + emptyTags.add("isindex"); + emptyTags.add("link"); + emptyTags.add("meta"); + emptyTags.add("param"); + } + + /** * Do "smart" encodging on a string. This means that valid HTML entities and tags, * Helma macros and HTML comments are passed through unescaped, while @@ -470,9 +491,12 @@ public final class HtmlEncoder { continue; } else if (t > 1) { for (int k = 1; k < t; k++) { - ret.append(""); + Object tag = openTags.pop(); + if (!emptyTags.contains(tag)) { + ret.append(""); + } } } @@ -496,7 +520,7 @@ public final class HtmlEncoder { // if (i < l-2) } - if ((linebreaks > 0) && !Character.isWhitespace(c)) { + if ((linebreaks > 0 || swallowLinebreaks > 0) && !Character.isWhitespace(c)) { if (!insidePreTag && (linebreaks > swallowLinebreaks)) { linebreaks -= swallowLinebreaks; @@ -659,9 +683,12 @@ public final class HtmlEncoder { if (o > 0) { for (int k = 0; k < o; k++) { - ret.append(""); + Object tag = openTags.pop(); + if (!emptyTags.contains(tag)) { + ret.append(""); + } } } diff --git a/src/helma/util/StringUtils.java b/src/helma/util/StringUtils.java new file mode 100644 index 00000000..34ddea38 --- /dev/null +++ b/src/helma/util/StringUtils.java @@ -0,0 +1,51 @@ +/* + * Helma License Notice + * + * The contents of this file are subject to the Helma License + * Version 2.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://adele.helma.org/download/helma/license.txt + * + * Copyright 1998-2003 Helma Software. All Rights Reserved. + * + * $RCSfile$ + * $Author$ + * $Revision$ + * $Date$ + */ + +package helma.util; + + +import java.util.StringTokenizer; + +/** + * Utility class for String manipulation. + */ +public class StringUtils { + + + /** + * Split a string into an array of strings. Use comma and space + * as delimiters. + */ + public static String[] split(String str) { + return split(str, ", \t\n\r\f"); + } + + /** + * Split a string into an array of strings. + */ + public static String[] split(String str, String delim) { + if (str == null) { + return new String[0]; + } + StringTokenizer st = new StringTokenizer(str, delim); + String[] s = new String[st.countTokens()]; + for (int i=0; i