diff --git a/apps/manage/Application/actions.js b/apps/manage/Application/actions.js
new file mode 100644
index 00000000..ddbedc75
--- /dev/null
+++ b/apps/manage/Application/actions.js
@@ -0,0 +1,67 @@
+/**
+* renders AppManager
+*/
+function main_action() {
+ if (checkAddress() == false)
+ return;
+ if (checkAuth(this) == false)
+ return;
+
+ res.data.body = this.renderSkinAsString("main");
+ renderSkin("global");
+}
+
+
+/**
+* prints session- and thread-stats for mrtg-tool
+* doesn't check username or password, so that we don't have
+* to write them cleartext in a mrtg-configfile but checks the
+* remote address.
+*/
+function mrtg_action() {
+ if (checkAddress() == false)
+ return;
+
+ if (this.isActive() == false) {
+ res.write("0\n0\n0\n0\n");
+ return;
+ }
+
+ if (req.data.action == "sessions") {
+
+ res.write(this.sessions.size());
+ res.write("\n0\n0\n0\n");
+
+ } else if (req.data.action == "threads") {
+
+ res.write(this.countActiveEvaluators() + "\n");
+ res.write(this.countEvaluators() + "\n");
+ res.write("0\n0\n");
+
+ } else if (req.data.action == "cache") {
+
+ res.write(this.getCacheUsage() + "\n");
+ res.write(this.getProperty("cachesize", "1000") + "\n");
+ res.write("0\n0\n");
+
+ } else if (req.data.action == "requests") {
+
+ // res.write (
+
+ } else {
+ res.write("0\n0\n0\n0\n");
+ }
+
+}
+
+/**
+* performs a redirect to the public site
+* (workaround, we can't access application object from docapplication for some reason)
+* @see application.url_macro
+*/
+function redirectpublic_action() {
+ if (checkAddress() == false) return;
+ if (checkAuth(this) == false) return;
+
+ res.redirect(this.url_macro());
+}
diff --git a/apps/manage/Application/functions.js b/apps/manage/Application/functions.js
new file mode 100644
index 00000000..248fb68a
--- /dev/null
+++ b/apps/manage/Application/functions.js
@@ -0,0 +1,42 @@
+/**
+ * construct an application object so that we can use
+ * skins for non-active applications too
+ * @arg name
+ */
+function constructor(name) {
+ this.name = name;
+}
+
+/**
+ * return true/false to determine if application is running
+ */
+function isActive() {
+ if (root.getApplication(this.name) == null)
+ return false;
+ else
+ return true;
+}
+
+/**
+ * Method used by Helma for URL composition.
+ */
+function href(action) {
+ var base = root.href() + this.name + "/";
+ return action ? base + action : base;
+}
+
+/**
+ * Method used by Helma for URL composition.
+ */
+function getParentElement() {
+ return root;
+}
+
+/**
+ * Method used by Helma request path resolution.
+ */
+function getChildElement(name) {
+ if (name == "api")
+ return this.getDoc();
+ return null;
+}
diff --git a/apps/manage/Application/head.skin b/apps/manage/Application/head.skin
new file mode 100644
index 00000000..539aac0e
--- /dev/null
+++ b/apps/manage/Application/head.skin
@@ -0,0 +1,11 @@
+
AppManager <% this.title %>
+<% this.description prefix="
" %>
+
+ ->
+ /read">showAPI |
+ /render">renderAPI |
+ public |
+ ?app=<% this.title %>&action=flush">flush |
+ ?app=<% this.title %>&action=restart">restart |
+ ?app=<% this.title %>&action=stop">stop
+
diff --git a/apps/manage/Application/macros.js b/apps/manage/Application/macros.js
new file mode 100644
index 00000000..22c32903
--- /dev/null
+++ b/apps/manage/Application/macros.js
@@ -0,0 +1,210 @@
+/**
+ * macro rendering a skin
+ * @param name name of skin
+ */
+function skin_macro(par) {
+ if (par && par.name) {
+ this.renderSkin(par.name);
+ }
+}
+
+
+/**
+ * macro-wrapper for href-function
+ * @param action name of action to call on this prototype, default main
+ */
+function href_macro(par) {
+ return this.href((par && par.action) ? par.action : "main");
+}
+
+
+/**
+ * Macro returning the URL of an application.
+ * using absoluteURI if set in app.properties, otherwise we go for the href calculated by
+ * the application (which is using baseURI if set)
+ */
+function url_macro() {
+ var str = this.getProperty("absoluteuri");
+ if (str != null && str != "") {
+ return str;
+ } else {
+ return this.getRootHref();
+ }
+}
+
+
+/**
+ * Macro returning the title of an application
+ */
+function title_macro() {
+ var title = this.name;
+ return(title);
+}
+
+
+/**
+ * Macro rendering a description of an application from a
+ * file called description.txt or doc.html located in the
+ * app's root. Limits description to 200 chars.
+ * @param Object Macro parameter object
+ */
+function description_macro(param) {
+ var str = "";
+ var appHome = this.getAppDir();
+ var f = new File(this.getAppDir().toString(), "description.txt");
+ if (!f.exists())
+ f = new File(this.getAppDir().toString(), "doc.html");
+ if (f.exists()) {
+ str = f.readAll();
+ if (str.length > 200)
+ str = str.substring(0, 200) + "...";
+ }
+ return(str);
+}
+
+
+/**
+ * Macro returning the server's uptime nicely formatted
+ */
+function uptime_macro() {
+ return formatAge((java.lang.System.currentTimeMillis() - this.starttime) / 1000);
+}
+
+
+/**
+ * Macro returning the number of active sessions.
+ * parameter used by global.formatCount
+ * @see global.formatCount
+ */
+function countSessions_macro(par) {
+ if (this.isActive() == true)
+ return this.sessions.size() + formatCount(this.sessions.size(), par);
+ else
+ return 0;
+}
+
+
+/**
+ * Macro returning the number of logged-in users or the list
+ * of logged-in users if http-parameter showusers is set to true.
+ * Makes use of this.countUsers_macro and this.listUsers_macro
+ * @see application.countUsers_macro
+ * @see application.listUsers_macro
+ */
+function users_macro(par) {
+ if (req.data.showusers == "true") {
+ this.listUsers_macro(par);
+ } else {
+ this.countUsers_macro(par);
+ }
+}
+
+
+/**
+ * Macro returning the number of logged-in users if application
+ * is active
+ * parameter used by global.formatCount
+ * @see global.formatCount
+ */
+function countUsers_macro(par) {
+ if (this.isActive() == true)
+ return this.activeUsers.size() + formatCount(this.activeUsers.size(), par);
+ else
+ return 0;
+}
+
+
+/**
+ * Macro rendering the list of logged-in users if application is active
+ * @param separator html-code written between elements
+ */
+function listUsers_macro(par) {
+ var separator = (par && par.separator) ? par.separator : ", ";
+ if (this.activeUsers.size() == 0)
+ return "";
+ var users = this.activeUsers.iterator();
+ while (users.hasNext()) {
+ res.write(users.next().__name__);
+ if (users.hasNext()) {
+ res.write(separator);
+ }
+ }
+}
+
+
+/**
+ * Macro returning the number of active evaluators (=threads)
+ */
+function countActiveEvaluators_macro() {
+ return this.countActiveEvaluators();
+}
+
+
+/**
+ * Macro returning the number of free evaluators (=threads)
+ */
+function countFreeEvaluators_macro() {
+ return this.countFreeEvaluators();
+}
+
+
+/**
+ * Macro returning the current number of objects in the cache
+ */
+function cacheusage_macro(param) {
+ return this.getCacheUsage();
+}
+
+
+/**
+ * Macro returning the number of objects allowed in the cache
+ */
+function cachesize_macro(param) {
+ return this.getProperty("cachesize", "1000");
+}
+
+
+/**
+ * Macro formatting the number of requests in the last 5 minutes
+ */
+function requestCount_macro(par) {
+ if (app.data.stat == null || app.data.stat[this.name] == null)
+ return "not available";
+ if (this.isActive()) {
+ var obj = app.data.stat[this.name];
+ return obj.requestCount + formatCount(obj.requestCount, par);
+ } else {
+ return 0 + formatCount(0, par);
+ }
+}
+
+
+/**
+ * Macro formatting the number of errors in the last 5 minutes
+ */
+function errorCount_macro(par) {
+ if (app.data.stat == null || app.data.stat[this.name] == null)
+ return "not available";
+ if (this.isActive()) {
+ var obj = app.data.stat[this.name];
+ return obj.errorCount + formatCount(obj.errorCount, par);
+ } else {
+ return 0 + formatCount(0, par);
+ }
+}
+
+
+/**
+* Macro formatting app.properties
+*/
+function properties_macro(par) {
+ formatProperties(this.getProperties(), par);
+}
+
+
+function repositories_macro(param) {
+ var repos = this.getRepositories().iterator();
+ while (repos.hasNext())
+ res.writeln(repos.next().getName());
+}
+
diff --git a/apps/manage/Application/main.skin b/apps/manage/Application/main.skin
new file mode 100644
index 00000000..455e77d2
--- /dev/null
+++ b/apps/manage/Application/main.skin
@@ -0,0 +1,62 @@
+<% this.skin name="head" %>
+
+
+
+
+ active sessions |
+ |
+ <% this.countSessions %> |
+
+
+ ?showusers=true">logged-in users |
+ |
+ <% this.users %> |
+
+
+ active evaluators |
+ |
+ <% this.countActiveEvaluators %> |
+
+
+ free evaluators |
+ |
+ <% this.countFreeEvaluators %> |
+
+
+ requests / 5 min |
+ |
+ <% this.requestCount %> |
+
+
+ errors / 5 min |
+ |
+ <% this.errorCount %> |
+
+
+ cache usage |
+ |
+ <% this.cacheusage %> objects of <% this.cachesize %> |
+
+
+ uptime |
+ |
+ <% this.uptime %> |
+
+
+ repositories |
+ |
+ <% this.repositories %> |
+
+
+
+
+
+ app.properties |
+
+<% this.properties itemprefix='' separator=' | | ' itemsuffix=' |
' %>
+
+
diff --git a/apps/manage/Application/navig_active.skin b/apps/manage/Application/navig_active.skin
new file mode 100644
index 00000000..3c24b3bf
--- /dev/null
+++ b/apps/manage/Application/navig_active.skin
@@ -0,0 +1,18 @@
+
+
"><% this.title %>
+
<% this.countSessions singular=" Session" plural=" Sessions" %>,
+ <% this.requestCount singular=" Request" plural=" Requests" %>/5min
+
+
diff --git a/apps/manage/Application/navig_disabled.skin b/apps/manage/Application/navig_disabled.skin
new file mode 100644
index 00000000..c49223d1
--- /dev/null
+++ b/apps/manage/Application/navig_disabled.skin
@@ -0,0 +1,10 @@
+
diff --git a/apps/manage/DocApplication/actions.js b/apps/manage/DocApplication/actions.js
new file mode 100644
index 00000000..7c1efcfa
--- /dev/null
+++ b/apps/manage/DocApplication/actions.js
@@ -0,0 +1,59 @@
+function read_action() {
+ this.readApplication();
+ res.redirect(this.href("main"));
+}
+
+function main_action() {
+ if (checkAddress() == false)
+ return;
+ if (checkAuth(this.getParentElement()) == false)
+ return;
+ this.renderSkin("frameset");
+}
+
+
+function prototypes_action() {
+ if (checkAddress() == false)
+ return;
+ if (checkAuth(this.getParentElement()) == false)
+ return;
+ res.data.body = this.renderSkinAsString("prototypes");
+ renderSkin("api");
+}
+
+
+function summary_action() {
+ if (checkAddress() == false)
+ return;
+ if (checkAuth(this.getParentElement()) == false)
+ return;
+ res.data.body = this.renderSkinAsString("summary");
+ renderSkin("api");
+}
+
+
+function functionindex_action() {
+ if (checkAddress() == false)
+ return;
+ if (checkAuth(this.getParentElement()) == false)
+ return;
+ res.data.body = this.renderSkinAsString("functionindex");
+ renderSkin("api");
+}
+
+
+function render_action() {
+ // set res.data.rendering, this will suppress the link back to the manage
+ // console in the apidocs actions
+ res.data.rendering = true;
+ if (checkAddress() == false)
+ return;
+ if (checkAuth(this.getParentElement()) == false)
+ return;
+ var ct = this.renderApi();
+ res.data.body = 'rendering API ...
wrote ' + ct + ' files
';
+ res.data.body += 'back to manage console';
+ res.data.title = "rendering helma api";
+ res.data.head = renderSkinAsString("head");
+ renderSkin("basic");
+}
diff --git a/apps/manage/DocApplication/frameset.skin b/apps/manage/DocApplication/frameset.skin
new file mode 100644
index 00000000..7b43c942
--- /dev/null
+++ b/apps/manage/DocApplication/frameset.skin
@@ -0,0 +1,31 @@
+
+
+ helma api / <% this.name %>
+
+
+
+
+
+
+Frame Alert
+
+
+This document is designed to be viewed using the frames feature. If you see this message, you are using a non-frame-capable web client.
+
+
+
+
diff --git a/apps/manage/DocApplication/functionindex.skin b/apps/manage/DocApplication/functionindex.skin
new file mode 100644
index 00000000..fbc6e3d0
--- /dev/null
+++ b/apps/manage/DocApplication/functionindex.skin
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Application <% this.headline %>
+ |
+
+
+
+">SUMMARY |
+">INDEX |
+A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|
+
+
+ <% this.functions skin="asIndexItem"
+ separator="![]() |
"
+ %>
+
+
+">SUMMARY |
+">INDEX |
+A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|
+
diff --git a/apps/manage/DocApplication/functions.js b/apps/manage/DocApplication/functions.js
new file mode 100644
index 00000000..dec28856
--- /dev/null
+++ b/apps/manage/DocApplication/functions.js
@@ -0,0 +1,97 @@
+/**
+ * Get the prototype of any doc-object (either a prototype, a function or a tag)
+ */
+function getDocPrototype(obj) {
+ var tmp = obj;
+ while (tmp != null && tmp.getType() != this.PROTOTYPE) {
+ tmp = tmp.getParentElement();
+ }
+ return tmp;
+}
+
+
+/**
+ * Get a prototype of this docapplication, ie get on of the children of this object
+ */
+function getPrototype(name) {
+ return this.getChildElement("prototype_" + name);
+}
+
+/**
+ * Method used by Helma for URL composition.
+ */
+function href(action) {
+ var base = this.getParentElement().href() + "api/";
+ return action ? base + action : base;
+}
+
+function getDir(dir, obj) {
+ dir.mkdir();
+ if (obj.getType() == this.APPLICATION) {
+ return dir;
+ } else {
+ var protoObj = this.getDocPrototype(obj);
+ var dir = new File (dir, protoObj.getElementName());
+ dir.mkdir();
+ return dir;
+ }
+}
+
+function renderApi() {
+ var prefix = this.href("");
+ this.storePage(this, "main", "", "index.html");
+ this.storePage(this, "prototypes");
+ this.storePage(this, "summary");
+ this.storePage(this, "functionindex");
+ var ct = 4;
+ var arr = this.listChildren();
+ for (var i = 0; i < arr.length; i++) {
+ this.storePage(arr[i], "list", "../");
+ this.storePage(arr[i], "main", "../");
+ ct += 2;
+ var subarr = arr[i].listChildren();
+ for (var j = 0; j < subarr.length; j++) {
+ this.storePage(subarr[j], "main", "../", subarr[j].getElementName() + ".html");
+ ct += 1;
+ }
+ }
+ return ct;
+}
+
+
+function storePage(obj, action, backPath, filename) {
+ if (filename == null)
+ var filename = action + ".html";
+ var str = this.getPage(obj, action, backPath);
+ var appObj = this.getParentElement();
+ var dir = new File (appObj.getAppDir().getAbsolutePath(), ".docs");
+ dir = this.getDir(dir, obj);
+ var f = new File (dir, filename);
+ f.remove();
+ f.open();
+ f.write(str);
+ f.close();
+ app.log("wrote file " + f.getAbsolutePath());
+}
+
+
+function getPage(obj, action, backPath) {
+ backPath = (backPath == null) ? "" : backPath;
+ res.pushStringBuffer();
+ eval("obj." + action + "_action ();");
+ var str = res.popStringBuffer();
+ // get the baseURI out of the url and replace
+ // it with the given relative prefix
+ // (keep anchors in regex!)
+ var reg = new RegExp ("href=\"" + this.href("") + "([^\"#]+)([^\"]*)\"", "gim");
+ str = str.replace(reg, "href=\"" + backPath + "$1.html$2\"");
+ var reg = new RegExp ("src=\"" + this.href("") + "([^\"#]+)([^\"]*)\"", "gim");
+ str = str.replace(reg, "src=\"" + backPath + "$1.html$2\"");
+ // shorten links, so that function files can move up one directory
+ // in the hierarchy
+ var reg = new RegExp ("(prototype_[^/]+/[^/]+)/main.html", "gim");
+ str = str.replace(reg, "$1.html");
+ return str;
+}
+
+
diff --git a/apps/manage/DocApplication/indexSeparator.skin b/apps/manage/DocApplication/indexSeparator.skin
new file mode 100644
index 00000000..beb8ee73
--- /dev/null
+++ b/apps/manage/DocApplication/indexSeparator.skin
@@ -0,0 +1,3 @@
+
+<% param.letter %>
+ |
\ No newline at end of file
diff --git a/apps/manage/DocApplication/macros.js b/apps/manage/DocApplication/macros.js
new file mode 100644
index 00000000..945419bf
--- /dev/null
+++ b/apps/manage/DocApplication/macros.js
@@ -0,0 +1,106 @@
+/**
+* macro rendering a skin
+* @param name name of skin
+*/
+function skin_macro(par) {
+ if (par && par.name) {
+ this.renderSkin(par.name);
+ }
+}
+
+/**
+ * macro-wrapper for href-function
+ * @param action name of action to call on this prototype, default main
+ */
+function href_macro(param) {
+ return this.href((param && param.action) ? param.action : "main");
+}
+
+function comment_macro(param) {
+ return renderComment(this, param);
+}
+
+function content_macro(param) {
+ return this.getContent();
+}
+
+function tags_macro(param) {
+ return renderTags(this, param);
+}
+
+function location_macro(param) {
+ return renderLocation(this, param);
+}
+
+function link_macro(param) {
+ return renderLink(this, param);
+}
+
+//// END OF COPIED FUNCTIONS
+
+
+function linkToManage_macro(param) {
+ if (res.data.rendering != true) {
+ return ('back to manage console');
+ }
+}
+
+function headline_macro(param) {
+ res.write(this.getName());
+}
+
+
+function hrefRoot_macro(param) {
+ var obj = this.getChildElement("prototype_root");
+ if (obj == null) {
+ var obj = this.getChildElement("prototype_Root");
+ }
+ if (obj != null) {
+ var action = (param.action) ? param.action : "main";
+ return obj.href(action);
+ }
+}
+
+
+/**
+ * list all prototypes of this application
+ * @param skin name of skin to render on prototype
+ * @param separator
+ */
+function prototypes_macro(param) {
+ var skin = (param.skin) ? param.skin : "asPrototypeList";
+ var separator = (param.separator) ? param.separator : "";
+ var arr = this.listChildren();
+ for (var i = 0; i < arr.length; i++) {
+ arr[i].renderSkin(skin);
+ if (i < arr.length - 1)
+ res.write(separator);
+ }
+}
+
+
+/**
+ * list all methods of all prototypes, sort them alphabetically
+ * @param skin name of skin to render on each method
+ * @param skinSeparator name of skin to render as separator between each letters
+ */
+function functions_macro(param) {
+ var skinname = (param.skin) ? param.skin : "asListItem";
+ var skinIndexSeparator = (param.indexSeparator) ? param.indexSeparator : "indexSeparator";
+ var separator = (param.separator) ? param.separator : "";
+ var arr = this.listFunctions();
+ var lastLetter = "";
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].getName().substring(0, 1) != lastLetter) {
+ lastLetter = arr[i].getName().substring(0, 1);
+ var tmp = new Object ();
+ tmp.letter = lastLetter.toUpperCase();
+ this.renderSkin(skinIndexSeparator, tmp);
+ }
+ arr[i].renderSkin(skinname);
+ if (i < arr.length - 1)
+ res.write(separator);
+ }
+}
+
+
diff --git a/apps/manage/DocApplication/prototypes.skin b/apps/manage/DocApplication/prototypes.skin
new file mode 100644
index 00000000..2ff758fb
--- /dev/null
+++ b/apps/manage/DocApplication/prototypes.skin
@@ -0,0 +1,10 @@
+<% this.linkToManage suffix="
" %>
+
+Application " target="main"><% this.name %>
+
+<% this.prototypes %>
+
+
+
+
+
diff --git a/apps/manage/DocApplication/summary.skin b/apps/manage/DocApplication/summary.skin
new file mode 100644
index 00000000..816380b7
--- /dev/null
+++ b/apps/manage/DocApplication/summary.skin
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Application <% this.headline %>
+ |
+
+
+
+">SUMMARY |
+">INDEX |
+
+
+
+<% this.comment encoding="html" %>
+
+
+
+
+ <% this.prototypes skin="asSummary"
+ separator="![]() |
"
+ %>
+
+
+
+">SUMMARY |
+">INDEX |
+
diff --git a/apps/manage/DocFunction/actions.js b/apps/manage/DocFunction/actions.js
new file mode 100644
index 00000000..6bbf0bea
--- /dev/null
+++ b/apps/manage/DocFunction/actions.js
@@ -0,0 +1,11 @@
+function main_action() {
+ if (checkAddress() == false)
+ return;
+ if (checkAuth() == false)
+ return;
+ res.data.body = this.renderSkinAsString("main");
+ renderSkin("api");
+}
+
+
+
diff --git a/apps/manage/DocFunction/asIndexItem.skin b/apps/manage/DocFunction/asIndexItem.skin
new file mode 100644
index 00000000..95a998ea
--- /dev/null
+++ b/apps/manage/DocFunction/asIndexItem.skin
@@ -0,0 +1,7 @@
+
+<% this.link handler="false" %>
+- <% this.type %> in <% docprototype.name %>
+
+<% this.comment length="200" %>
+ |
+
diff --git a/apps/manage/DocFunction/asLargeListItem.skin b/apps/manage/DocFunction/asLargeListItem.skin
new file mode 100644
index 00000000..e11013b3
--- /dev/null
+++ b/apps/manage/DocFunction/asLargeListItem.skin
@@ -0,0 +1,5 @@
+
+" target="main"><% this.link %>
+<% this.comment length="200" %>
+ |
+
diff --git a/apps/manage/DocFunction/asLargeListItemSkin.skin b/apps/manage/DocFunction/asLargeListItemSkin.skin
new file mode 100644
index 00000000..c659c611
--- /dev/null
+++ b/apps/manage/DocFunction/asLargeListItemSkin.skin
@@ -0,0 +1,8 @@
+
+" target="main"><% this.link %>
+<% this.comment length="200" %>
+<% this.skinparameters separator=", "%>
+ |
+
+
+
diff --git a/apps/manage/DocFunction/asListItem.skin b/apps/manage/DocFunction/asListItem.skin
new file mode 100644
index 00000000..30df0ea1
--- /dev/null
+++ b/apps/manage/DocFunction/asListItem.skin
@@ -0,0 +1,2 @@
+<% this.link %>
+
diff --git a/apps/manage/DocFunction/asParentListItem.skin b/apps/manage/DocFunction/asParentListItem.skin
new file mode 100644
index 00000000..0822c4cc
--- /dev/null
+++ b/apps/manage/DocFunction/asParentListItem.skin
@@ -0,0 +1 @@
+<% this.link %>
\ No newline at end of file
diff --git a/apps/manage/DocFunction/functions.js b/apps/manage/DocFunction/functions.js
new file mode 100644
index 00000000..eafd288c
--- /dev/null
+++ b/apps/manage/DocFunction/functions.js
@@ -0,0 +1,10 @@
+/**
+ * Method used by Helma for URL composition.
+ */
+function href(action) {
+ var base = this.getParentElement().href()
+ + this.getElementName() + "/";
+ return action ? base + action : base;
+}
+
+
diff --git a/apps/manage/DocFunction/macros.js b/apps/manage/DocFunction/macros.js
new file mode 100644
index 00000000..9443f0eb
--- /dev/null
+++ b/apps/manage/DocFunction/macros.js
@@ -0,0 +1,150 @@
+/**
+* macro rendering a skin
+* @param name name of skin
+*/
+function skin_macro(par) {
+ if (par && par.name) {
+ this.renderSkin(par.name);
+ }
+}
+
+/**
+ * macro-wrapper for href-function
+ * @param action name of action to call on this prototype, default main
+ */
+function href_macro(param) {
+ return this.href((param && param.action) ? param.action : "main");
+}
+
+function comment_macro(param) {
+ return renderComment(this, param);
+}
+function content_macro(param) {
+ return this.getContent();
+}
+function tags_macro(param) {
+ return renderTags(this, param);
+}
+function location_macro(param) {
+ return renderLocation(this, param);
+}
+function link_macro(param) {
+ return renderLink(this, param);
+}
+
+//// END OF COPIED FUNCTIONS
+
+
+function headline_macro(param) {
+ var p = this.getParentElement();
+ var handler = (p != null) ? p.getName() : "";
+ if (this.getType() == this.ACTION) {
+ res.write("/" + this.getName());
+ } else if (this.getType() == this.FUNCTION) {
+ if (handler != "" && handler != "global")
+ res.write(handler + ".");
+ res.write(this.getName() + " (");
+ var arr = this.listParameters();
+ for (var i = 0; i < arr.length; i++) {
+ res.write(arr[i]);
+ if (i < arr.length - 1) {
+ res.write(", ");
+ }
+ }
+ res.write(")");
+ } else if (this.getType() == this.MACRO) {
+ res.write("<% ");
+ if (handler != "" && handler != "global")
+ res.write(handler + ".");
+ var name = this.getName();
+ if (name.indexOf("_macro") > -1)
+ name = name.substring(0, name.length - 6);
+ res.write(name);
+ res.write(" %>");
+ } else if (this.getType() == this.SKIN) {
+ if (handler != "" && handler != "global")
+ res.write(handler + "/");
+ res.write(this.getName());
+ res.write(".skin");
+ } else if (this.getType() == this.PROPERTIES) {
+ res.write(this.getName());
+ }
+}
+
+
+function skinparameters_macro(param) {
+ if (this.getType() == this.SKIN) {
+ this.parameters_macro(param);
+ }
+}
+
+
+function parameters_macro(param) {
+ var separator = (param.separator) ? param.separator : ", ";
+ var arr = this.listParameters();
+ for (var i = 0; i < arr.length; i++) {
+ res.write(arr[i]);
+ if (i < arr.length - 1)
+ res.write(separator);
+ }
+}
+
+
+function type_macro(param) {
+ return this.getTypeName();
+}
+
+
+/**
+ * macro returning nicely formatted sourcecode of this method.
+ * code is encoded, >% %<-tags are colorcoded, line numbers are added
+ */
+function source_macro(param) {
+ var sourcecode = this.getContent();
+ if (param.as == "highlighted") {
+
+ sourcecode = encode(sourcecode);
+
+ // highlight macro tags
+ r = new RegExp("<%", "gim");
+ sourcecode = sourcecode.replace(r, '<%');
+
+ r = new RegExp("%>", "gim");
+ sourcecode = sourcecode.replace(r, '%>');
+
+ // highlight js-comments
+ r = new RegExp("^([ \\t]*//.*)", "gm");
+ sourcecode = sourcecode.replace(r, '$1');
+
+ // highlight quotation marks, but not for skins
+ if (this.getTypeName() != "Skin") {
+ r = new RegExp("(".*?")", "gm");
+ sourcecode = sourcecode.replace(r, '$1');
+ r = new RegExp("(\'[\']*\')", "gm");
+ sourcecode = sourcecode.replace(r, '$1');
+ }
+
+ // remove all CR and LF, just
remains
+ var r = new RegExp("[\\r\\n]", "gm");
+ sourcecode = sourcecode.replace(r, "");
+
+ var arr = sourcecode.split("
");
+ var line = this.getStartLine ? this.getStartLine() : 1;
+ for (var i = 0; i < arr.length; i++) {
+ res.write('' + (line++) + ': ');
+ if (i < 99) {
+ res.write(' ');
+ }
+ if (i < 9) {
+ res.write(' ');
+ }
+ res.write(arr[i] + "\n");
+ }
+
+ } else {
+ res.write(sourcecode);
+ }
+}
+
+
+
diff --git a/apps/manage/DocFunction/main.skin b/apps/manage/DocFunction/main.skin
new file mode 100644
index 00000000..da43ed06
--- /dev/null
+++ b/apps/manage/DocFunction/main.skin
@@ -0,0 +1,34 @@
+
+
+
+ <% this.headline %>
+ |
+
+
+
+
+ <% this.comment suffix="
" %>
+ <% this.skinparameters prefix="general parameters used in this skin: " %>
+
+ <% this.tags type="param" skin="parameter" %>
+ <% this.tags type="return" skin="return" %>
+ <% this.tags type="author" skin="author" %>
+ <% this.tags type="see" skin="see" %>
+ <% this.tags type="deprecated" skin="deprecated" %>
+ <% this.tags type="overrides" skin="overrides" %>
+
+ |
+
+
+
+
+
+ Sourcecode in <% this.location %>:
+ <% this.source as="highlighted" %>
+ |
+
+
+
+
+
+
diff --git a/apps/manage/DocPrototype/actions.js b/apps/manage/DocPrototype/actions.js
new file mode 100644
index 00000000..a9b9c873
--- /dev/null
+++ b/apps/manage/DocPrototype/actions.js
@@ -0,0 +1,18 @@
+function list_action() {
+ if (checkAddress() == false)
+ return;
+ if (checkAuth() == false)
+ return;
+ res.data.body = this.renderSkinAsString("list");
+ renderSkin("api");
+}
+
+function main_action() {
+ if (checkAddress() == false)
+ return;
+ if (checkAuth() == false)
+ return;
+ res.data.body = this.renderSkinAsString("main");
+ renderSkin("api");
+}
+
diff --git a/apps/manage/DocPrototype/asInheritanceLink.skin b/apps/manage/DocPrototype/asInheritanceLink.skin
new file mode 100644
index 00000000..42449c89
--- /dev/null
+++ b/apps/manage/DocPrototype/asInheritanceLink.skin
@@ -0,0 +1 @@
+extends Prototype "><% this.name %>
\ No newline at end of file
diff --git a/apps/manage/DocPrototype/asParentList.skin b/apps/manage/DocPrototype/asParentList.skin
new file mode 100644
index 00000000..34208c8a
--- /dev/null
+++ b/apps/manage/DocPrototype/asParentList.skin
@@ -0,0 +1,16 @@
+
+Inherited from prototype <% this.link %>:
|
+
+
+
+<% this.methods separator=", " filter="actions" skin="asParentListItem" prefix="Actions: " suffix=" " %>
+<% this.methods separator=", " filter="functions" skin="asParentListItem" prefix="Functions: " suffix=" " %>
+<% this.methods separator=", " filter="macros" skin="asParentListItem" prefix="Macros: " suffix=" " %>
+<% this.methods separator=", " filter="skins" skin="asParentListItem" prefix="Skins: " suffix=" " %>
+
+ |
+
+
+
+
+
diff --git a/apps/manage/DocPrototype/asPrototypeList.skin b/apps/manage/DocPrototype/asPrototypeList.skin
new file mode 100644
index 00000000..0f3762aa
--- /dev/null
+++ b/apps/manage/DocPrototype/asPrototypeList.skin
@@ -0,0 +1,10 @@
+
+" onClick="parent.changePrototypeList(this);" target="main"><% this.name %>
+
+<%
+ this.inheritance action="main" target="main"
+ onClick="parent.changePrototypeList(this);" hopobject="false"
+ prefix=" (extends " suffix=")"
+%>
+
+
diff --git a/apps/manage/DocPrototype/asSummary.skin b/apps/manage/DocPrototype/asSummary.skin
new file mode 100644
index 00000000..5ce50bd5
--- /dev/null
+++ b/apps/manage/DocPrototype/asSummary.skin
@@ -0,0 +1,4 @@
+
+"><% this.name %>
+<% this.comment length="200" %>
+ |
diff --git a/apps/manage/DocPrototype/functions.js b/apps/manage/DocPrototype/functions.js
new file mode 100644
index 00000000..ad9ef068
--- /dev/null
+++ b/apps/manage/DocPrototype/functions.js
@@ -0,0 +1,30 @@
+function translateType(filter) {
+ if (filter == "actions")
+ return Packages.helma.doc.DocElement.ACTION;
+ else if (filter == "functions")
+ return Packages.helma.doc.DocElement.FUNCTION;
+ else if (filter == "macros")
+ return Packages.helma.doc.DocElement.MACRO;
+ else if (filter == "skins")
+ return Packages.helma.doc.DocElement.SKIN;
+ else if (filter == "properties")
+ return Packages.helma.doc.DocElement.PROPERTIES;
+ else
+ return -1;
+}
+
+/**
+ * Get the application we're part of.
+ */
+function getApplication() {
+ return this.getParentElement();
+}
+
+/**
+ * Method used by Helma for URL composition.
+ */
+function href(action) {
+ var base = this.getParentElement().href()
+ + this.getElementName() + "/";
+ return action ? base + action : base;
+}
diff --git a/apps/manage/DocPrototype/list.skin b/apps/manage/DocPrototype/list.skin
new file mode 100644
index 00000000..0225a667
--- /dev/null
+++ b/apps/manage/DocPrototype/list.skin
@@ -0,0 +1,10 @@
+Prototype " target="main"><% this.name %>
+<% this.inheritance action="list" %>
+<% this.inheritance deep="true" hopobject="true" action="main" target="main" onClick="parent.changePrototypeList(this);" separator=", " prefix="extends: " suffix="
" %>
+
+
+<% this.methods filter="actions" skin="asListItem" prefix="Actions:
" suffix="
" %>
+<% this.methods filter="functions" skin="asListItem" prefix="Functions:
" suffix="
" %>
+<% this.methods filter="macros" skin="asListItem" prefix="Macros:
" suffix="
" %>
+<% this.methods filter="skins" skin="asListItem" prefix="Skins:
" suffix="
" %>
+
diff --git a/apps/manage/DocPrototype/macros.js b/apps/manage/DocPrototype/macros.js
new file mode 100644
index 00000000..7b2915e1
--- /dev/null
+++ b/apps/manage/DocPrototype/macros.js
@@ -0,0 +1,165 @@
+/**
+* macro rendering a skin
+* @param name name of skin
+*/
+function skin_macro(par) {
+ if (par && par.name) {
+ this.renderSkin(par.name);
+ }
+}
+
+/**
+ * macro-wrapper for href-function
+ * @param action name of action to call on this prototype, default main
+ */
+function href_macro(param) {
+ return this.href((param && param.action) ? param.action : "main");
+}
+
+function comment_macro(param) {
+ return renderComment(this, param);
+}
+
+function content_macro(param) {
+ return this.getContent();
+}
+
+function tags_macro(param) {
+ return renderTags(this, param);
+}
+
+function location_macro(param) {
+ return renderLocation(this, param);
+}
+
+function link_macro(param) {
+ return renderLink(this, param);
+}
+
+//// END OF COPIED FUNCTIONS
+
+
+function headline_macro(param) {
+ res.write(this.getName());
+}
+
+/**
+ * macro formatting list of methods of this prototype
+ * @param filter actions | functions | macros | skins
+ * @param skin skin to apply to the docfunction object
+ * @param separator
+ * @param desc Description that is passed on to the called skin
+ */
+function methods_macro(param) {
+ var skinname = (param.skin) ? param.skin : "list";
+ var separator = (param.separator) ? param.separator : "";
+ var arr = this.listChildren();
+ var type = this.translateType(param.filter);
+ var sb = new java.lang.StringBuffer ();
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].getType() == type) {
+ sb.append(arr[i].renderSkinAsString(skinname, param));
+ sb.append(separator);
+ }
+ }
+ var str = sb.toString();
+ if (str.length > 0)
+ return str.substring(0, str.length - separator.length);
+ else
+ return str;
+}
+
+
+function inheritance_macro(param) {
+ var action = param.action ? param.action : "main";
+ var target = param.target ? ('target="' + param.target + '" ') : '';
+ var obj = this.getParentPrototype();
+ if (obj != null) {
+ obj = this.inheritanceUtil(obj, param);
+ }
+ if (param.deep == "true") {
+ while (obj != null) {
+ obj = this.inheritanceUtil(obj, param);
+ }
+ }
+}
+
+function inheritanceUtil(obj, param) {
+ if (obj.getName() == "hopobject" && param.hopobject != "true")
+ return null;
+ var tmp = new Object ();
+ for (var i in param)
+ tmp[i] = param[i];
+ tmp.href = obj.href((param.action) ? param.action : "main");
+ delete tmp.hopobject;
+ delete tmp.action;
+ delete tmp.deep;
+ delete tmp.separator;
+ res.write(renderLinkTag(tmp));
+ res.write(obj.getName() + "");
+ if (obj.getParentPrototype())
+ res.write(param.separator);
+ return obj.getParentPrototype();
+}
+
+
+/**
+ * loops through the parent prototypes and renders a skin on each
+ * if it has got any functions.
+ * @param skin
+ */
+function parentPrototype_macro(param) {
+ var skinname = (param.skin) ? param.skin : "asParentList";
+ var obj = this.getParentPrototype();
+ while (obj != null) {
+ if (obj.listChildren().length > 0) {
+ obj.renderSkin(skinname);
+ }
+ obj = obj.getParentPrototype();
+ }
+}
+
+/**
+ * macro rendering a skin depending on wheter this prototype has got
+ * type-properties or not.
+ * @param skin
+ */
+function typeProperties_macro(param) {
+ var props = this.getTypeProperties();
+ var iter = props.getResources();
+ while (iter.hasNext()) {
+ var tmp = this.renderTypePropertiesResource(iter.next(), props);
+ var skinname = (param.skinname) ? param.skinname : "typeproperties";
+ this.renderSkin(skinname, tmp);
+ }
+}
+
+function renderTypePropertiesResource(res, props) {
+ if (res.getContent() != "") {
+ var sb = new java.lang.StringBuffer ();
+ // map of all mappings....
+ var mappings = props.getMappings();
+ // parse type.properties linewise:
+ var arr = res.getContent().split("\n");
+ for (var i = 0; i < arr.length; i++) {
+ arr [i] = arr[i].trim();
+ // look up in mappings table if line matches:
+ for (var e = mappings.keys(); e.hasMoreElements();) {
+ var key = e.nextElement();
+ var reg = new RegExp ('^' + key + '\\s');
+ if (arr[i].match(reg)) {
+ // it matched, wrap line in a link to that prototype:
+ var docProtoObj = this.getApplication().getPrototype(mappings.getProperty(key));
+ if (docProtoObj != null) {
+ arr[i] = '' + arr[i] + '';
+ }
+ }
+ }
+ sb.append(arr[i] + "\n");
+ }
+ var tmp = new Object ();
+ tmp.content = sb.toString();
+ tmp.source = res.getName();
+ return tmp;
+ }
+}
diff --git a/apps/manage/DocPrototype/main.skin b/apps/manage/DocPrototype/main.skin
new file mode 100644
index 00000000..0542d1d2
--- /dev/null
+++ b/apps/manage/DocPrototype/main.skin
@@ -0,0 +1,81 @@
+
+
+
+ Prototype <% this.headline %>
+ <% this.inheritance deep="true" hopobject="true" action="main" target="main" onClick="parent.changePrototypeList(this);" separator=", " prefix="extends: " suffix=" " %>
+ |
+
+
+
+ACTIONS |
+FUNCTIONS |
+MACROS |
+SKINS |
+TYPE.PROPERTIES
+
+
+
+
+
+
+
+ <% this.comment suffix="
" %>
+
+ <% this.tags type="author" skin="author" %>
+ <% this.tags type="see" skin="see" %>
+ <% this.tags type="deprecated" skin="deprecated" %>
+ <% this.tags type="overrides" skin="overrides" %>
+
+ |
+
+
+
+
+
+ <% this.methods separator="![]() |
"
+ filter="actions"
+ skin="asLargeListItem"
+ prefix="Actions |
"
+ suffix=" |
"
+ %>
+
+ <% this.methods separator="![]() |
"
+ filter="functions"
+ skin="asLargeListItem"
+ prefix="Functions |
"
+ suffix=" |
"
+ %>
+
+ <% this.methods separator="![]() |
"
+ filter="macros"
+ skin="asLargeListItem"
+ prefix="Macros |
"
+ suffix=" |
"
+ %>
+
+ <% this.methods separator="![]() |
"
+ filter="skins"
+ skin="asLargeListItemSkin"
+ prefix="Skins |
"
+ suffix=" |
"
+ %>
+
+ <% this.methods separator="![]() |
"
+ filter="properties"
+ skin="asLargeListItemSkin"
+ prefix="type.properties |
"
+ suffix=" |
"
+ %>
+
+ <% this.parentPrototype skin="asParentList" %>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/manage/DocPrototype/typeproperties.skin b/apps/manage/DocPrototype/typeproperties.skin
new file mode 100644
index 00000000..e1975b32
--- /dev/null
+++ b/apps/manage/DocPrototype/typeproperties.skin
@@ -0,0 +1,9 @@
+
+
+ type.properties in <% param.source %> |
+
+
+ <% param.content %> |
+
+
+
diff --git a/apps/manage/DocTag/author.skin b/apps/manage/DocTag/author.skin
new file mode 100644
index 00000000..c319829d
--- /dev/null
+++ b/apps/manage/DocTag/author.skin
@@ -0,0 +1,2 @@
+Author
+<% this.text %>
\ No newline at end of file
diff --git a/apps/manage/DocTag/deprecated.skin b/apps/manage/DocTag/deprecated.skin
new file mode 100644
index 00000000..57cb4d4e
--- /dev/null
+++ b/apps/manage/DocTag/deprecated.skin
@@ -0,0 +1,2 @@
+Deprecated
+<% this.text %>
diff --git a/apps/manage/DocTag/main.skin b/apps/manage/DocTag/main.skin
new file mode 100644
index 00000000..dca81089
--- /dev/null
+++ b/apps/manage/DocTag/main.skin
@@ -0,0 +1 @@
+<% this.text %>
\ No newline at end of file
diff --git a/apps/manage/DocTag/overrides.skin b/apps/manage/DocTag/overrides.skin
new file mode 100644
index 00000000..4616af7f
--- /dev/null
+++ b/apps/manage/DocTag/overrides.skin
@@ -0,0 +1,3 @@
+Overrides
+<% param.link %>
+
diff --git a/apps/manage/DocTag/parameter.skin b/apps/manage/DocTag/parameter.skin
new file mode 100644
index 00000000..deeed157
--- /dev/null
+++ b/apps/manage/DocTag/parameter.skin
@@ -0,0 +1,2 @@
+Parameter <% this.name %>
:
+<% this.text %>
\ No newline at end of file
diff --git a/apps/manage/DocTag/return.skin b/apps/manage/DocTag/return.skin
new file mode 100644
index 00000000..a421d33c
--- /dev/null
+++ b/apps/manage/DocTag/return.skin
@@ -0,0 +1,2 @@
+Returns
+<% this.text %>
diff --git a/apps/manage/DocTag/see.skin b/apps/manage/DocTag/see.skin
new file mode 100644
index 00000000..5e66957d
--- /dev/null
+++ b/apps/manage/DocTag/see.skin
@@ -0,0 +1,2 @@
+See also
+<% param.link %>
diff --git a/apps/manage/Global/api.skin b/apps/manage/Global/api.skin
new file mode 100644
index 00000000..9fb798d9
--- /dev/null
+++ b/apps/manage/Global/api.skin
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+<% response.body %>
+
+
+
diff --git a/apps/manage/Global/basic.skin b/apps/manage/Global/basic.skin
new file mode 100644
index 00000000..8b300a8a
--- /dev/null
+++ b/apps/manage/Global/basic.skin
@@ -0,0 +1,5 @@
+
+
+<% response.head %>
+<% response.body %>
+
diff --git a/apps/manage/Global/functions.js b/apps/manage/Global/functions.js
new file mode 100644
index 00000000..7c40b5b7
--- /dev/null
+++ b/apps/manage/Global/functions.js
@@ -0,0 +1,262 @@
+/**
+* scheduler function, runs global.appStat every minute
+*/
+function scheduler() {
+ appStat();
+ return 60000;
+}
+
+
+/**
+ * initializes app.data.stat storage on startup,
+ * creates app.data.addressFilter
+ */
+function onStart() {
+ app.data.addressFilter = createAddressFilter();
+ app.data.addressString = root.getProperty("allowadmin");
+}
+
+/**
+ * initializes addressFilter from app.properties,
+ * hostnames are converted, wildcards are only allowed in ip-addresses
+ * (so, no network-names, sorry)
+ */
+function createAddressFilter() {
+ var filter = new Packages.helma.util.InetAddressFilter();
+ var str = root.getProperty("allowadmin");
+ if (str != null && str != "") {
+ var arr = str.split(",");
+ for (var i in arr) {
+ str = new java.lang.String(arr[i]);
+ try {
+ filter.addAddress(str.trim());
+ } catch (a) {
+ try {
+ str = java.net.InetAddress.getByName(str.trim()).getHostAddress();
+ filter.addAddress(str);
+ } catch (b) {
+ app.log("error using address " + arr[i] + ": " + b);
+ }
+ }
+ }
+ } else {
+ app.log("no addresses allowed for app manage, all access will be denied");
+ }
+ return filter;
+}
+
+
+/**
+ * updates the stats in app.data.stat every 5 minutes
+ */
+function appStat() {
+ if (app.data.stat == null)
+ app.data.stat = new HopObject ();
+ if ((new Date() - 300000) < app.data.stat.lastRun)
+ return;
+ var arr = root.getApplications();
+ for (var i = 0; i < arr.length; i++) {
+ var tmp = app.data.stat[arr[i].getName()];
+ if (tmp == null) {
+ tmp = new HopObject();
+ tmp.lastTotalRequestCount = 0;
+ tmp.lastTotalErrorCount = 0;
+ }
+ tmp.requestCount = arr[i].getRequestCount() - tmp.lastTotalRequestCount;
+ tmp.lastTotalRequestCount = arr[i].getRequestCount();
+ tmp.errorCount = arr[i].getErrorCount() - tmp.lastTotalErrorCount;
+ tmp.lastTotalErrorCount = arr[i].getErrorCount();
+ app.data.stat[arr[i].getName()] = tmp;
+ }
+ app.data.stat.lastRun = new Date();
+}
+
+
+/**
+ * utility function to sort object-arrays by name
+ */
+function sortByName(a, b) {
+ if (a.name > b.name)
+ return 1;
+ else if (a.name == b.name)
+ return 0;
+ else
+ return -1;
+}
+
+
+/**
+ * utility function to sort property-arrays by key
+ */
+function sortProps(a, b) {
+ if (a > b)
+ return 1;
+ else if (a == b)
+ return 0;
+ else
+ return -1;
+}
+
+/**
+ * check access to an application or the whole server, authenticate against md5-encrypted
+ * properties of base-app or the particular application. if username or password aren't set
+ * go into stealth-mode and return a 404. if username|password are wrong, prepare response-
+ * object for http-auth and return false.
+ * @arg appObj application object to check against (if adminUsername etc are set in app.properties)
+ */
+function checkAuth(appObj) {
+ if (res && res.data.noWeb == true) {
+ return true;
+ }
+ var ok = false;
+
+ // check against root
+ var adminAccess = root.getProperty("adminAccess");
+
+ if (adminAccess == null || adminAccess == "") {
+ res.redirect(root.href("makekey"));
+ }
+
+ var uname = req.username;
+ var pwd = req.password;
+
+ if (uname == null || uname == "" || pwd == null || pwd == "")
+ return forceAuth();
+
+ var md5key = Packages.helma.util.MD5Encoder.encode(uname + "-" + pwd);
+
+ if (md5key == adminAccess)
+ return true;
+
+ if (appObj != null && appObj.isActive()) {
+ // check against application
+ adminAccess = appObj.getProperty("adminAccess");
+ if (md5key == adminAccess)
+ return true;
+ }
+ return forceAuth();
+}
+
+
+/**
+ * check access to the manage-app by ip-addresses
+ */
+function checkAddress() {
+ if (res && res.data.noWeb == true) {
+ return true;
+ }
+ // if allowadmin value in server.properties has changed,
+ // re-construct the addressFilter
+ if (app.data.addressString != root.getProperty("allowadmin")) {
+ app.data.addressFilter = createAddressFilter();
+ app.data.addressString = root.getProperty("allowadmin");
+ }
+ if (!app.data.addressFilter.matches(java.net.InetAddress.getByName(req.data.http_remotehost))) {
+ app.log("denied request from " + req.data.http_remotehost);
+ // forceStealth seems a bit like overkill here.
+ // display a message that the ip address must be added to server.properties
+ res.write("Access from address " + req.data.http_remotehost + " denied.");
+ return false;
+ } else {
+ return true;
+ }
+}
+
+
+/**
+ * response is reset to 401 / authorization required
+ * @arg realm realm for http-auth
+ */
+function forceAuth(realm) {
+ res.reset();
+ res.status = 401;
+ res.realm = (realm != null) ? realm : "helma";
+ res.write("Authorization Required. The server could not verify that you are authorized to access the requested page.");
+ return false;
+}
+
+
+/**
+* macro-utility: formatting property lists
+*/
+function formatProperties(props, par) {
+ if (props.size() == 0)
+ return "";
+ var e = props.keys();
+ var arr = new Array();
+ while (e.hasMoreElements()) {
+ arr[arr.length] = e.nextElement();
+ }
+ arr.sort(sortProps);
+ for (var i in arr) {
+ // don't print the admin-password
+ if (arr[i].toLowerCase() == "adminusername" || arr[i].toLowerCase() == "adminpassword") continue;
+ res.write(par.itemprefix + arr[i] + par.separator + props.getProperty(arr[i]) + par.itemsuffix);
+ }
+}
+
+
+/**
+ * macro-utility: formatting an integer value as human readable bytes
+ */
+function formatBytes(bytes) {
+ if (bytes > Math.pow(2, 30)) {
+ res.write(Math.round(100 * bytes / Math.pow(2, 30)) / 100 + "gb");
+ } else if (bytes > Math.pow(2, 20)) {
+ res.write(Math.round(100 * bytes / Math.pow(2, 20)) / 100 + "mb");
+ } else {
+ res.write(Math.round(100 * bytes / Math.pow(2, 10)) / 100 + "kb");
+ }
+}
+
+
+/**
+ * macro-utility: formatting time in millis as human readable age
+ */
+function formatAge(age) {
+ var str = "";
+ var days = Math.floor(age / 86400);
+ age = age - days * 86400;
+ var hours = Math.floor(age / 3600);
+ age = age - hours * 3600;
+ var minutes = Math.floor(age / 60);
+ var seconds = Math.floor(age - minutes * 60);
+ if (days > 0)
+ str += (days + " days, ");
+ if (hours > 0)
+ str += (hours + "h, ");
+ str += (minutes + "min");
+ if (days == 0) str += (", " + seconds + "sec");
+ return(str);
+}
+
+
+/**
+ * macro-utility: formatting a number-suffix by choosing between singular and plural
+ * @arg value number to be formatted
+ * @arg param-object object to get fields
+ * @param singular string used for value==1
+ * @param plural string used for value!=1
+ */
+function formatCount(ct, par) {
+ if (!par || !par.singular || !par.plural) {
+ return "";
+ }
+ if (ct == 1)
+ return par.singular;
+ else
+ return par.plural;
+}
+
+
+/**
+ * tries to make out if this server is running linux from java's system properties
+ */
+function isLinux() {
+ var str = java.lang.System.getProperty("os.name");
+ return (str != null && str.toLowerCase().indexOf("linux") != -1);
+}
+
+
+
+
diff --git a/apps/manage/Global/global.skin b/apps/manage/Global/global.skin
new file mode 100644
index 00000000..a80b0da1
--- /dev/null
+++ b/apps/manage/Global/global.skin
@@ -0,0 +1,14 @@
+
+
+<% skin name="head" %>
+
+
+
+
+ <% skin name="navig" %> |
+ <% response.body %> |
+
+
+
+
+
diff --git a/apps/manage/Global/head.skin b/apps/manage/Global/head.skin
new file mode 100644
index 00000000..45680b8e
--- /dev/null
+++ b/apps/manage/Global/head.skin
@@ -0,0 +1,59 @@
+
+ <% response.title %>
+
+
\ No newline at end of file
diff --git a/apps/manage/Global/macros.js b/apps/manage/Global/macros.js
new file mode 100644
index 00000000..ee3fe97a
--- /dev/null
+++ b/apps/manage/Global/macros.js
@@ -0,0 +1,18 @@
+/**
+ * macro rendering a skin
+ * @param name name of skin
+ */
+function skin_macro(par) {
+ if (par && par.name) {
+ renderSkin(par.name);
+ }
+}
+
+
+/**
+ * Macro returning the actual date and time.
+ */
+function now_macro() {
+ var date = new Date();
+ return(date.format("dd.MM.yyyy, HH:mm'h' zzz"));
+}
diff --git a/apps/manage/Global/navig.skin b/apps/manage/Global/navig.skin
new file mode 100644
index 00000000..b7d93776
--- /dev/null
+++ b/apps/manage/Global/navig.skin
@@ -0,0 +1,30 @@
+
+">
" title="helma" border="0" width="174" height="35" align="baseline" style="border-width:3px;border-color:white;">
+
+
+
+
+<% root.appList filter="active" %>
+
+
+
+ disabled apps:
+
+
+<% root.appList filter="disabled" skin="navig_disabled" %>
+
+
+
+ Information on helma.org:
+
reference
+ mailinglist
+ svn
+ download
+
+
+
+
">generate server password
+
diff --git a/apps/manage/Global/pwdfeedback.skin b/apps/manage/Global/pwdfeedback.skin
new file mode 100644
index 00000000..f5003a67
--- /dev/null
+++ b/apps/manage/Global/pwdfeedback.skin
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+Generated username and password for helma's manager:
+
+Please copy/paste this line into the server.properties file of your
+helma installation.
+
+<% param.propsString %>
+
+After that proceed to ">the manage console,
+enter your credentials and you should be allowed in.
+
+ |
+
+
+
diff --git a/apps/manage/Global/pwdform.skin b/apps/manage/Global/pwdform.skin
new file mode 100644
index 00000000..79b7c602
--- /dev/null
+++ b/apps/manage/Global/pwdform.skin
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+Username and password for helma's manager:
+
+Please choose an username and password combination to access the
+manage application of this server. They will be printed md5-encoded
+in a format that you've got to copy/paste into the server.properties
+file.
+
+<% param.msg %>
+
+
+Warning: The used http-authorization transmits username and password
+in an unsafe cleartext way. Therefore you're strongly discouraged to
+use any given combination that is normally protected through SSH.
+
+ |
+
+
+
diff --git a/apps/manage/Global/renderFunctions.js b/apps/manage/Global/renderFunctions.js
new file mode 100644
index 00000000..280c5168
--- /dev/null
+++ b/apps/manage/Global/renderFunctions.js
@@ -0,0 +1,153 @@
+function renderLink(docEl, param) {
+ var text = "";
+ if (docEl.getType() == docEl.APPLICATION || docEl.getType() == docEl.PROTOTYPE) {
+ text = docEl.getName();
+ } else if (docEl.getType() == docEl.SKIN) {
+ text = docEl.getName() + ".skin";
+ } else if (docEl.getType() == docEl.MACRO) {
+ if (param.handler != "false" && docEl.getParentElement() && docEl.getParentElement().getName() != "global") {
+ text = docEl.getParentElement().getName() + ".";
+ }
+ var str = docEl.getName();
+ if (str.indexOf("_macro")) {
+ text += str.substring(0, str.length - 6);
+ }
+ } else if (docEl.getType() == docEl.FUNCTION) {
+ text = docEl.getName() + "(";
+ var arr = docEl.listParameters();
+ for (var i = 0; i < arr.length; i++) {
+ text += arr[i];
+ if (i < arr.length - 1)
+ text += ", ";
+ }
+ text += ")";
+ } else {
+ text = docEl.getName();
+ }
+ param.href = docEl.href("main");
+ if (!param.target) {
+ param.target = "main";
+ }
+ return renderLinkTag(param) + text + '';
+}
+
+
+function renderLinkTag(param) {
+ var sb = new java.lang.StringBuffer ();
+ sb.append('');
+ return sb.toString();
+}
+
+/**
+ * renders the name of the location of a doc element.
+ */
+function renderLocation (docEl, param) {
+ return docEl.toString();
+}
+
+/**
+* renders tag list.
+* @param param.skin skin to render on found DocTags
+* @param param.separator String printed between tags
+* @param param.type type string (param|return|author|version|see) to filter tags.
+*/
+function renderTags(docEl, param) {
+ var skinname = (param.skin) ? param.skin : "main";
+ var type = param.type;
+ if (type == "params")
+ type = "param";
+ else if (type == "returns")
+ type = "return";
+ else if (type == "arg")
+ type = "param";
+ var str = "";
+ var arr = docEl.listTags();
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].getType() == type) {
+ if (type == "see" || type == "overrides") {
+ param.link = renderReference(arr[i], docEl);
+ }
+ str += arr[i].renderSkinAsString(skinname, param);
+ str += (param.separator) ? param.separator : "";
+ }
+ }
+ return str;
+}
+
+
+/**
+ * renders a reference to functions in other prototypes, masks
+ * urls in a see tag
+ * (see- and overrides-tags)
+ * @param docTagObj
+ * @param docEl needed to be able to walk up to application object
+ */
+function renderReference(docTagObj, docEl) {
+ // prepare the text:
+ var text = docTagObj.getText();
+ text = new java.lang.String (text);
+ text = text.trim();
+ if (text.indexOf("http://") == 0) {
+ // an url is a simple job
+ return '' + text + '';
+ } else {
+ // make sure we only use the first item in the text so that unlinked comments
+ // can follow, store & split the that
+ var tok = new java.util.StringTokenizer (text);
+ var tmp = tok.nextToken();
+ text = " " + text.substring(tmp.length + 1);
+ var parts = tmp.split(".");
+ // try to find the application object
+ var obj = docEl;
+ while (obj != null) {
+ if (obj.getType() == Packages.helma.doc.DocElement.APPLICATION) {
+ var appObj = obj;
+ break;
+ }
+ obj = obj.getParentElement();
+ }
+ var protoObj = appObj.getChildElement("prototype_" + parts[0]);
+ if (protoObj == null) {
+ // prototype wasn't found, return the unlinked tag
+ return tmp + text;
+ }
+ if (parts.length == 1) {
+ // no function specified, return the linked prototype
+ return '' + format(tmp) + '' + text;
+ }
+ // try to find a function object:
+ var arr = protoObj.listChildren();
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].getName() == parts [1]) {
+ return '' + format(tmp) + '' + text;
+ }
+ }
+ // function not found:
+ return tmp + text;
+ }
+}
+
+
+/**
+* function rendering a comment.
+* @param param.length comment is shortened to the given length.
+* @returns string
+*/
+function renderComment(docEl, param) {
+ var str = docEl.getComment();
+ if (param.length) {
+ if (param.length < str.length) {
+ return str.substring(0, param.length) + " ...";
+ }
+ }
+ return str;
+}
+
diff --git a/apps/manage/Root/actions.js b/apps/manage/Root/actions.js
new file mode 100644
index 00000000..aac54dc7
--- /dev/null
+++ b/apps/manage/Root/actions.js
@@ -0,0 +1,130 @@
+/**
+ * main action, show server-stats
+ * perform start, stop, restart and flush-action
+ *
+ */
+function main_action() {
+
+ if (checkAddress() == false) return;
+
+ if (req.data.app != null && req.data.app != "" && req.data.action != null && req.data.action != "") {
+
+ var appObj = root.getApp(req.data.app);
+ // check access for application. md5-encoded uname/pwd can also be put in
+ // app.properties to limit access to a single app
+ if (checkAuth(appObj) == false) return;
+
+ if (req.data.action == "start") {
+ this.startApplication(req.data.app);
+ res.redirect(appObj.href("main"));
+
+ } else if (req.data.action == "stop") {
+ if (checkAuth() == false) return;
+ this.stopApplication(req.data.app);
+ res.redirect(root.href("main"));
+
+ } else if (req.data.action == "restart") {
+ this.stopApplication(req.data.app);
+ this.startApplication(req.data.app);
+ res.redirect(appObj.href("main"));
+
+ } else if (req.data.action == "flush") {
+ appObj.clearCache();
+ res.redirect(appObj.href("main"));
+
+ }
+
+ }
+
+ // output only to root
+ if (checkAuth() == false) return;
+
+ res.data.title = "Helma Object Publisher - Serverinfo";
+ res.data.body = this.renderSkinAsString("main");
+ renderSkin("global");
+}
+
+/**
+* return the helma object publisher logo, built into hop core
+* to be independent of static html-paths
+*/
+function image_action() {
+ if (checkAddress() == false) return;
+
+ res.contentType = "image/gif";
+ res.writeBinary(Packages.helma.util.Logo.hop);
+}
+
+function makekey_action() {
+
+ if (checkAddress() == false)
+ return;
+
+ var obj = new Object();
+ obj.msg = "";
+ if (req.data.username != null && req.data.password != null) {
+
+ // we have input from webform
+ if (req.data.username == "")
+ obj.msg += "username can't be left empty!
";
+ if (req.data.password == "")
+ obj.msg += "password can't be left empty!
";
+ if (obj.msg != "") {
+ obj.username = req.data.username;
+ res.reset();
+ res.data.body = renderSkinAsString("pwdform", obj);
+ } else {
+ // render the md5-string:
+ obj.propsString = "adminAccess=" + Packages.helma.util.MD5Encoder.encode(req.data.username + "-" + req.data.password) + "
\n";
+ res.data.body = renderSkinAsString("pwdfeedback", obj);
+ }
+
+ } else {
+
+ // no input from webform, so print it
+ res.data.body = renderSkinAsString("pwdform", obj);
+
+ }
+
+ res.data.title = "username & password on " + root.hostname_macro();
+ res.data.head = renderSkinAsString("head");
+ renderSkin("basic");
+}
+
+/**
+* prints server-stats for mrtg-tool.
+* doesn't check username or password, so that we don't have
+* to write them cleartext in a mrtg-configfile but checks the
+* remote address.
+*/
+function mrtg_action() {
+
+ if (checkAddress() == false)
+ return;
+
+
+ if (req.data.action == "memory") {
+
+ res.write(this.jvmTotalMemory_macro() + "\n");
+ res.write(this.jvmFreeMemory_macro() + "\n");
+ res.write("0\n0\n");
+
+ } else if (req.data.action == "netstat" && isLinux()) {
+
+ var str = (new File("/proc/net/tcp")).readAll();
+ var arr = str.split("\n");
+ res.write(arr.length - 2 + "\n");
+ res.write("0\n0\n0\n");
+
+ } else if (req.data.action == "loadavg" && isLinux()) {
+
+ // load average of last 5 minutes:
+ var str = (new File("/proc/loadavg")).readAll();
+ var arr = str.split(" ");
+ res.write(arr[1] * 100 + "\n");
+ res.write("0\n0\n0\n");
+
+ } else {
+ res.write("0\n0\n0\n0\n");
+ }
+}
\ No newline at end of file
diff --git a/apps/manage/Root/functions.js b/apps/manage/Root/functions.js
new file mode 100644
index 00000000..9e8ead34
--- /dev/null
+++ b/apps/manage/Root/functions.js
@@ -0,0 +1,76 @@
+/**
+* renders the api of a given application. used from commandline.
+*/
+function renderApi(appName) {
+
+ // supress security checks when accessing actions
+ res.data.noWeb = true;
+
+ // start the application
+ this.startApplication(appName);
+ var appObj = this.getApp(appName);
+ var docApp = appObj.getChildElement("api");
+
+ // now render the api
+ var ct = docApp.renderApi();
+ writeln("rendered " + ct + " files");
+
+ // cleanup
+ this.stopApplication(appName);
+}
+
+
+/**
+ * lists all applications in appdir.
+ * for active apps use this.getApplications() = helma.main.Server.getApplications()
+ */
+function getAllApplications() {
+ var appsDir = this.getAppsHome();
+ var dir = appsDir.listFiles();
+ var arr = new Array();
+ var seen = {};
+ // first check apps directory for apps directories
+ if (dir) {
+ for (var i = 0; i < dir.length; i++) {
+ if (dir[i].isDirectory() && dir[i].name.toLowerCase() != "cvs") {
+ arr[arr.length] = this.getApp(dir[i].name);
+ seen[dir[i].name] = true;
+ }
+ }
+ }
+ // then check entries in apps.properties for apps not currently running
+ var props = wrapJavaMap(root.getAppsProperties(null));
+ for (var i in props) {
+ if (i.indexOf(".") < 0 && !seen[i] && !root.getApplication(i)) {
+ arr[arr.length] = this.getApp(i);
+ }
+ }
+ return arr;
+}
+
+
+/**
+ * get application by name, constructs an hopobject of the prototype application
+ * if the app is not running (and therefore can't be access through
+ * helma.main.ApplicationManager).
+ * ATTENTION: javascript should not overwrite helma.main.Server.getApplication() which
+ * retrieves active applications.
+ * @arg name of application
+ */
+function getApp(name) {
+ if (name == null || name == "")
+ return null;
+ var appObj = this.getApplication(name);
+ if (appObj == null)
+ appObj = new Application(name);
+ return appObj;
+}
+
+/**
+ * Method used by Helma path resolution.
+ */
+function getChildElement(name) {
+ return this.getApp(name);
+}
+
+
diff --git a/apps/manage/Root/macros.js b/apps/manage/Root/macros.js
new file mode 100644
index 00000000..9fc795e0
--- /dev/null
+++ b/apps/manage/Root/macros.js
@@ -0,0 +1,284 @@
+/**
+ * macro rendering a skin
+ * @param name name of skin
+ */
+function skin_macro(par) {
+ if (par && par.name) {
+ this.renderSkin(par.name);
+ }
+}
+
+
+/**
+ * macro-wrapper for href-function
+ * @param action name of action to call on this prototype, default main
+ */
+function href_macro(par) {
+ return this.href((par && par.action)?par.action:"main");
+}
+
+
+/**
+ * macro returning the total number of sessions on this server
+ * @see global.formatCount
+ */
+function countSessions_macro(par) {
+ var arr = this.getApplications();
+ var sum = 0;
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].getName() != app.__app__.getName()) {
+ sum += arr[i].sessions.size();
+ }
+ }
+ return sum + formatCount(sum, par);
+}
+
+
+/**
+ * macro returning the number of requests during the last 5 minutes
+ * @see global.formatCount
+ */
+function requestCount_macro(par) {
+ if (app.data.stat == null) {
+ return;
+ }
+ var arr = this.getApplications();
+ var sum = 0;
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].getName() != app.__app__.getName()) { // don't include manage app
+ var obj = app.data.stat[arr[i].name];
+ if (obj != null) {
+ sum += obj.requestCount;
+ }
+ }
+ }
+ return sum + formatCount(sum, par);
+}
+
+
+/**
+ * macro returning the number of errors during the last 5 minutes
+ * @see global.formatCount
+ */
+function errorCount_macro(par) {
+ if (app.data.stat == null) {
+ return;
+ }
+ var arr = this.getApplications();
+ var sum = 0;
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].getName() != app.__app__.getName()) { // don't include manage app
+ var obj = app.data.stat[arr[i].name];
+ if (obj != null) {
+ sum += obj.errorCount;
+ }
+ }
+ }
+ return sum + formatCount(sum, par);
+}
+
+
+function extensions_macro(par) {
+ var vec = this.getExtensions();
+ var str = "";
+ for (var i = 0; i < vec.size(); i++) {
+ str += vec.elementAt(i).getClass().getName();
+ if (i != (vec.size() - 1)) {
+ str += (par && par.separator) ? par.separator : ", ";
+ }
+ }
+ return (str == "") ? null : str;
+}
+
+/**
+ * Macro returning hostname of this machine
+ */
+function hostname_macro(par) {
+ return java.net.InetAddress.getLocalHost().getHostName()
+}
+
+
+/**
+ * Macro returning address of this machine
+ */
+function hostaddress_macro(par) {
+ return java.net.InetAddress.getLocalHost().getHostAddress()
+}
+
+
+/**
+ * Macro returning the number of running applications,
+ * the manage application being excluded.
+ */
+function appCount_macro(par) {
+ if (par && par.filter == "active") {
+ var ct = root.getApplications().length - 1;
+ } else if (par && par.filter == "disabled") {
+ var ct = root.getAllApplications().length - root.getApplications().length;
+ } else {
+ var ct = root.getAllApplications().length - 1;
+ }
+ return ct + formatCount(ct, par);
+}
+
+
+/**
+ * Macro that lists all running applications,
+ * the manage application being excluded (-1).
+ * @param skin skin of application that will be used.
+ */
+function appList_macro(par) {
+ var skin = (par && par.skin) ? par.skin : "navig_active";
+ var apps = new Array();
+ if (par && par.filter == "active") {
+ var arr = root.getApplications();
+ for (var i = 0; i < arr.length; i++) {
+ apps[apps.length] = arr[i];
+ }
+ } else if (par && par.filter == "disabled") {
+ var arr = root.getAllApplications();
+ for (var i in arr) {
+ if (arr[i].isActive() == false) {
+ apps[apps.length] = arr[i];
+ }
+ }
+ } else {
+ var apps = root.getAllApplications();
+ }
+ apps = apps.sort(sortByName);
+ var html = "";
+ var param = new Object();
+ for (var n in apps) {
+ var a = apps[n];
+ if (apps[n].name == app.__app__.getName())
+ continue;
+ var item = a.renderSkinAsString(skin);
+ html += item;
+ }
+ return(html);
+}
+
+
+/**
+ * Macro that returns the server's uptime nicely formatted
+ */
+function uptime_macro() {
+ return formatAge((java.lang.System.currentTimeMillis() - this.starttime) / 1000);
+}
+
+
+/**
+ * Macro that returns the server's version string
+ */
+function version_macro() {
+ return this.version;
+}
+
+
+/**
+ * Macro that returns the home directory of the hop installation
+ */
+function home_macro() {
+ return this.getHopHome().toString();
+}
+
+
+/**
+ * Macro that returns the free memory in the java virtual machine
+ * @param format if "hr", value will be printed human readable
+ */
+function jvmFreeMemory_macro(param) {
+ var m = java.lang.Runtime.getRuntime().freeMemory();
+ return (param && param.hr) ? formatBytes(m) : m;
+}
+
+
+/**
+ * Macro that returns the total memory available to the java virtual machine
+ * @param format if "hr", value will be printed human readable
+ */
+function jvmTotalMemory_macro(param) {
+ var m = java.lang.Runtime.getRuntime().totalMemory();
+ return (param && param.hr) ? formatBytes(m) : m;
+}
+
+
+/**
+ * Macro that returns the used memory in the java virtual machine
+ * @param format if "hr", value will be printed human readable
+ */
+function jvmUsedMemory_macro(param) {
+ var m = java.lang.Runtime.getRuntime().totalMemory() - java.lang.Runtime.getRuntime().freeMemory();
+ return (param && param.hr) ? formatBytes(m) : m;
+}
+
+
+/**
+ * Macro that returns the version and type of the java virtual machine
+ */
+function jvm_macro() {
+ return java.lang.System.getProperty("java.version") + " " + java.lang.System.getProperty("java.vendor");
+}
+
+
+/**
+ * Macro that returns the home directory of the java virtual machine
+ */
+function jvmHome_macro() {
+ return java.lang.System.getProperty("java.home");
+}
+
+
+/**
+ * Macro that greps all jar-files from the class path variable and lists them.
+ * @param separator string that is printed between the items
+ */
+function jvmJars_macro(par) {
+ var separator = (par && par.separator ) ? par.separator : ", ";
+ var str = java.lang.System.getProperty("java.class.path");
+ var arr = str.split(".jar");
+ for (var i in arr) {
+ var str = arr[i];
+ var pos = ( str.lastIndexOf('\\') > str.lastIndexOf('/') ) ? str.lastIndexOf('\\') : str.lastIndexOf('/');
+ res.write(arr[i].substring(pos + 1) + ".jar");
+ if (i < arr.length - 1) res.write(separator);
+ }
+}
+
+
+/**
+ * Macro that returns the name and version of the server's os
+ */
+function os_macro() {
+ return java.lang.System.getProperty("os.name") + " " + java.lang.System.getProperty("os.arch") + " " + java.lang.System.getProperty("os.version");
+}
+
+
+/**
+ * Macro that returns anything from server.properties
+ */
+function property_macro(par) {
+ if (par && par.key) {
+ return this.getProperty(key);
+ } else {
+ return "";
+ }
+}
+
+
+/**
+ * Macro formatting server.properties
+ */
+function properties_macro(par) {
+ formatProperties(this.getProperties(), par);
+}
+
+
+/**
+ * Macro that returns the timezone of this server
+ */
+function timezone_macro(par) {
+ return java.util.TimeZone.getDefault().getDisplayName(false, java.util.TimeZone.LONG) + " (" + java.util.TimeZone.getDefault().getID() + ")";
+}
+
+
diff --git a/apps/manage/Root/main.skin b/apps/manage/Root/main.skin
new file mode 100644
index 00000000..2bcebc29
--- /dev/null
+++ b/apps/manage/Root/main.skin
@@ -0,0 +1,111 @@
+<% this.hostname %> (<% this.hostaddress %>)
+
+
+
+
+ helma |
+
+
+
+ uptime: |
+ |
+ <% this.uptime %> |
+
+
+
+ version: |
+ |
+ <% this.version %> |
+
+
+
+ homedir: |
+ |
+ <% this.home %> |
+
+
+
+ total active sessions: |
+ |
+ <% this.countSessions default=" " %> |
+
+
+ total requests / 5 min: |
+ |
+ <% this.requestCount default=" " %> |
+
+
+ total errors / 5 min: |
+ |
+ <% this.errorCount default=" " %> |
+
+
+ loaded extensions: |
+ |
+ <% this.extensions default=" " %> |
+
+
+
+ jre |
+
+
+
+ free memory: |
+ |
+ <% this.jvmFreeMemory hr="true" %> |
+
+
+
+
+ used memory: |
+ |
+ <% this.jvmUsedMemory hr="true" %> |
+
+
+
+ total memory: |
+ |
+ <% this.jvmTotalMemory hr="true" %> |
+
+
+
+ java: |
+ |
+ <% this.jvm %> |
+
+
+ javahome: |
+ |
+ <% this.jvmHome %> |
+
+
+ os: |
+ |
+ <% this.os %> |
+
+
+ localtime: |
+ |
+ <% now %> |
+
+
+ timezone: |
+ |
+ <% this.timezone %> |
+
+
+ loaded Jars: |
+ |
+ <% this.jvmJars %> |
+
+
+
+ server.properties |
+
+
+<% this.properties itemprefix='' separator=' | | ' itemsuffix=' |
' %>
+
+
+
+
+
diff --git a/apps/manage/app.properties b/apps/manage/app.properties
new file mode 100644
index 00000000..83e08c43
--- /dev/null
+++ b/apps/manage/app.properties
@@ -0,0 +1,12 @@
+# Set baseURI for application generated URLs, if necessary.
+# baseURI = /manage
+
+# A short description of what this application is about:
+_description = Helma's server management console. Start applications, introspection etc.
+
+# use higher request timeout because rendering the apidocs
+# might take more than one minute on a slow computer
+requestTimeout = 300
+
+# run scheduler function each minute
+cron.scheduler.function = scheduler
diff --git a/apps/manage/class.properties b/apps/manage/class.properties
new file mode 100644
index 00000000..4f48d3cc
--- /dev/null
+++ b/apps/manage/class.properties
@@ -0,0 +1,27 @@
+#
+# define the root class of this application
+#
+root = helma.main.Server
+
+#
+# root.factory is used to retrieve the root class of the application
+# when the instance has already been created when the application comes up.
+# this is true for the helma server that is scripted here.
+#
+root.factory.class = helma.main.Server
+root.factory.method = getServer
+
+#
+# Map Java classes to the prototype used to script them
+# and, yes, map the root class again.
+#
+
+helma.main.Server = Root
+helma.framework.core.Application = Application
+helma.doc.DocApplication = DocApplication
+helma.doc.DocPrototype = DocPrototype
+helma.doc.DocFunction = DocFunction
+helma.doc.DocProperties = DocFunction
+helma.doc.DocSkin = DocFunction
+helma.doc.DocTag = DocTag
+
diff --git a/apps/manage/readme.txt b/apps/manage/readme.txt
new file mode 100644
index 00000000..8e271c95
--- /dev/null
+++ b/apps/manage/readme.txt
@@ -0,0 +1,17 @@
+To get the manage-application to work you must:
+
+ - add it to the apps.properties file with the following line:
+ manage
+
+ - use helma distribution 1.5 or later.
+
+ - add the following properties to server.properties:
+
+ allowAdmin = [comma-separated list of hosts or ip-addresses that
+ are allowed to access this application. wildcards
+ are only allowed in addresses, not in hostnames!]
+ adminAccess =
+
+ Creating the credentials can be done after you've got the application
+ up and running at this address: http:///manage/makekey
+
diff --git a/modules/core/Array.js b/modules/core/Array.js
new file mode 100644
index 00000000..f709c3c0
--- /dev/null
+++ b/modules/core/Array.js
@@ -0,0 +1,83 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Array.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Adds useful methods to the JavaScript Array type.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/Array.js')
+ *
+ * @addon
+ */
+
+
+/**
+ * Check if this array contains a specific value.
+ * @param {Object} val the value to check
+ * @return {boolean} true if the value is contained
+ */
+Array.prototype.contains = function(val) {
+ return this.indexOf(val) > -1;
+};
+
+/**
+ * Retrieve the union set of a bunch of arrays
+ * @param {Array} array1,... the arrays to unify
+ * @return {Array} the union set
+ */
+Array.union = function() {
+ var result = [];
+ var map = {};
+ for (var i=0; i
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/Date.js')
+ */
+
+Date.ONESECOND = 1000;
+Date.ONEMINUTE = 60 * Date.ONESECOND;
+Date.ONEHOUR = 60 * Date.ONEMINUTE;
+Date.ONEDAY = 24 * Date.ONEHOUR;
+Date.ONEWEEK = 7 * Date.ONEDAY;
+Date.ONEMONTH = 30 * Date.ONEDAY;
+Date.ONEYEAR = 12 * Date.ONEMONTH;
+Date.ISOFORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+
+/**
+ * Format a Date to a string.
+ * For details on the format pattern, see
+ * http://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html
+ *
+ * @param String Format pattern
+ * @param Object Java Locale Object (optional)
+ * @param Object Java TimeZone Object (optional)
+ * @return String formatted Date
+ * @see http://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html
+ */
+Date.prototype.format = function (format, locale, timezone) {
+ if (!format)
+ return this.toString();
+ var sdf = locale ? new java.text.SimpleDateFormat(format, locale)
+ : new java.text.SimpleDateFormat(format);
+ if (timezone && timezone != sdf.getTimeZone())
+ sdf.setTimeZone(timezone);
+ return sdf.format(this);
+};
+
+
+/**
+ * set the date/time to UTC by subtracting
+ * the timezone offset
+ */
+Date.prototype.toUtc = function() {
+ this.setMinutes(this.getMinutes() + this.getTimezoneOffset());
+};
+
+
+/**
+ * set the date/time to local time by adding
+ * the timezone offset
+ */
+Date.prototype.toLocalTime = function() {
+ this.setMinutes(this.getMinutes() - this.getTimezoneOffset());
+};
+
+
+/**
+ * returns the difference between this and another
+ * date object in milliseconds
+ */
+Date.prototype.diff = function(dateObj) {
+ return this.getTime() - dateObj.getTime();
+};
+
+
+/**
+ * return the timespan to current date/time or a different Date object
+ * @param Object parameter object containing optional properties:
+ * .now = String to use if difference is < 1 minute
+ * .day|days = String to use for single|multiple day(s)
+ * .hour|hours = String to use for single|multiple hour(s)
+ * .minute|minutes = String to use for single|multiple minute(s)
+ * .date = Date object to use for calculating the timespan
+ * @return Object containing properties:
+ * .isFuture = (Boolean)
+ * .span = (String) timespan
+ * @see Date.prototype.getAge
+ * @see Date.prototype.getExpiry
+ */
+Date.prototype.getTimespan = function(param) {
+ if (!param)
+ param = {date: new Date()};
+ else if (!param.date)
+ param.date = new Date();
+
+ var result = {isFuture: this > param.date};
+ var diff = Math.abs(param.date.diff(this));
+ var age = {days: Math.floor(diff / Date.ONEDAY),
+ hours: Math.floor((diff % Date.ONEDAY) / Date.ONEHOUR),
+ minutes: Math.floor((diff % Date.ONEHOUR) / Date.ONEMINUTE)};
+
+ res.push();
+ if (diff < Date.ONEMINUTE)
+ res.write(param.now || "now");
+ else {
+ var arr = [{one: "day", many: "days"},
+ {one: "hour", many: "hours"},
+ {one: "minute", many: "minutes"}];
+ for (var i in arr) {
+ var value = age[arr[i].many];
+ if (value != 0) {
+ var prop = (value == 1 ? arr[i].one : arr[i].many);
+ res.write(value);
+ res.write(" ");
+ res.write(param[prop] || prop);
+ if (i < arr.length -1)
+ res.write(param.delimiter || ", ");
+ }
+ }
+ }
+ result.span = res.pop();
+ return result;
+};
+
+
+/**
+ * return the past timespan between this Date object and
+ * the current Date or a different Date object
+ * @see Date.prototype.getTimespan
+ */
+Date.prototype.getAge = function(param) {
+ var age = this.getTimespan(param);
+ if (!age.isFuture)
+ return age.span;
+ return null;
+};
+
+
+/**
+ * return the future timespan between this Date object and
+ * the current Date or a different Date object
+ * @see Date.prototype.getTimespan
+ */
+Date.prototype.getExpiry = function(param) {
+ var age = this.getTimespan(param);
+ if (age.isFuture)
+ return age.span;
+ return null;
+};
+
+
+/**
+ * checks if a date object equals another date object
+ * @param Object Date object to compare
+ * @param Int indicating how far the comparison should go
+ * @return Boolean
+ */
+Date.prototype.equals = function(date, extend) {
+ if (!extend)
+ var extend = Date.ONEDAY;
+ switch (extend) {
+ case Date.ONESECOND:
+ if (this.getSeconds() != date.getSeconds())
+ return false;
+ case Date.ONEMINUTE:
+ if (this.getMinutes() != date.getMinutes())
+ return false;
+ case Date.ONEHOUR:
+ if (this.getHours() != date.getHours())
+ return false;
+ case Date.ONEDAY:
+ if (this.getDate() != date.getDate())
+ return false;
+ case Date.ONEMONTH:
+ if (this.getMonth() != date.getMonth())
+ return false;
+ case Date.ONEYEAR:
+ if (this.getFullYear() != date.getFullYear())
+ return false;
+ }
+ return true;
+};
+
+
+// prevent any newly added properties from being enumerated
+for (var i in Date)
+ Date.dontEnum(i);
+for (var i in Date.prototype)
+ Date.prototype.dontEnum(i);
diff --git a/modules/core/Filters.js b/modules/core/Filters.js
new file mode 100644
index 00000000..f520ed49
--- /dev/null
+++ b/modules/core/Filters.js
@@ -0,0 +1,209 @@
+/*
+ * 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-2007 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile$
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Implements some useful macro filters.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/Filters.js')
+ */
+
+app.addRepository('modules/core/String.js');
+
+/**
+ * Transforms a string to lowercase.
+ *
+ * @see String.prototype.toLowerCase
+ */
+function lowercase_filter(input) {
+ return (input || "").toString().toLowerCase();
+}
+
+
+/**
+ * Transforms a string to uppercase.
+ *
+ * @see String.prototype.toUpperCase
+ */
+function uppercase_filter(input) {
+ return (input || "").toString().toUpperCase();
+}
+
+
+/**
+ * Transforms the first Character of a string to uppercase.
+ *
+ * @see String.prototype.capitalize
+ */
+function capitalize_filter(input) {
+ return (input || "").toString().capitalize();
+}
+
+
+/**
+ * Transforms the first Character of each word in a string
+ * to uppercase.
+ *
+ * @see String.prototype.titleize
+ */
+function titleize_filter(input) {
+ return (input || "").toString().titleize();
+}
+
+
+/**
+ * Cuts a String at a certain position, and
+ * optionally appends a suffix, if truncation
+ * has occurred.
+ *
+ * @see String.prototype.head
+ * @param limit Maximum length
+ * @param clipping Appended String, default is the empty String
+ */
+function truncate_filter(input, param, limit, clipping) {
+ var limit = param.limit != null ? param.limit : limit;
+ var clipping = param.clipping || clipping || "";
+ return (input || "").toString().head(limit, clipping);
+}
+
+
+/**
+ * Removes leading and trailing whitespaces.
+ *
+ * @see String.prototype.trim
+ */
+function trim_filter(input) {
+ return (input || "").toString().trim();
+}
+
+
+/**
+ * Removes all tags from a String.
+ * Currently simply wraps Helma's stripTags-method.
+ *
+ * @see global.stripTags
+ */
+function stripTags_filter(input) {
+ return stripTags((input || "").toString());
+};
+
+
+/**
+ * Escapes the characters in a String using XML entities.
+ * Currently simply wraps Helma's encodeXml-method.
+ *
+ * @see global.encodeXml
+ */
+function escapeXml_filter(input) {
+ return encodeXml((input || "").toString());
+}
+
+
+/**
+ * Escapes the characters in a String using HTML entities.
+ *
+ * @see http://www.google.com/codesearch?q=escapeHtml
+ */
+function escapeHtml_filter(input) {
+ var replace = Packages.org.mortbay.util.StringUtil.replace;
+ var str = (input || "").toString();
+ return replace(replace(replace(replace(str, '&', '&'), '"', '"'), '>', '>'), '<', '<');
+}
+
+var h_filter = escapeHtml_filter;
+
+
+/**
+ * Escapes the characters in a String to be suitable
+ * to use as an HTTP parameter value.
+ *
+ * @see http://www.google.com/codesearch?q=escapeUrl
+ * @param charset Optional String. The name of a supported
+ * character encoding.
+ */
+function escapeUrl_filter(input, param, charset) {
+ var charset = param.charset || charset || app.getCharset();
+ return java.net.URLEncoder.encode(input || "", charset);
+}
+
+
+/**
+ * Escapes a string so it may be used in JavaScript String
+ * definitions.
+ */
+function escapeJavaScript_filter(input) {
+ var replace = Packages.org.mortbay.util.StringUtil.replace;
+ var str = (input || "").toString();
+ return replace(replace(replace(replace(replace(str, '"', '\\"'), "'", "\\'"), '\n', '\\n'), '\r', '\\r'), '\t', '\\t');
+}
+
+
+/**
+ * Replaces linebreaks with HTML linebreaks.
+ */
+function linebreakToHtml_filter(input) {
+ var replace = Packages.org.mortbay.util.StringUtil.replace;
+ var str = (input || "").toString();
+ return replace(str, '\n', '
');
+}
+
+
+/**
+ * Performs a string replacement.
+ *
+ * @param old
+ * @param new
+ */
+function replace_filter(input, param, oldString, newString) {
+ var str = (input || "").toString();
+ var oldString = param["old"] != null ? param["old"] : oldString;
+ var newString = param["new"] != null ? param["new"] : newString;
+ var replace = Packages.org.mortbay.util.StringUtil.replace;
+ return replace(str, oldString, newString);
+}
+
+
+/**
+ * Returns a substring. Simply wraps the javascript
+ * method 'substring'.
+ *
+ * @see String.prototype.substring
+ * @param from
+ * @param to
+ */
+function substring_filter(input, param, from, to) {
+ var from = param.from != null ? param.from : from;
+ var to = param.to != null ? param.to : to;
+ var str = (input || "").toString();
+ return str.substring(from, to);
+}
+
+
+/**
+ * Returns a formatted string representation of a Date.
+ * Simply wraps javascripts Date.format-method.
+ *
+ * @see Date.prototype.format
+ * @param format
+ */
+function dateFormat_filter(input, param, format) {
+ var format = param.format || format;
+ if (!input) {
+ return;
+ } else {
+ return input.format(format);
+ }
+}
diff --git a/modules/core/Global.js b/modules/core/Global.js
new file mode 100644
index 00000000..18dcd43a
--- /dev/null
+++ b/modules/core/Global.js
@@ -0,0 +1,79 @@
+/*
+ * 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-2005 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Global.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Adds useful global macros.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/Global.js')
+ */
+
+app.addRepository("modules/core/String.js");
+
+
+/**
+ * write out a property contained in app.properties
+ * @param Object containing the name of the property
+ */
+function property_macro(param, name) {
+ res.write(getProperty(name || param.name) || String.NULL);
+ return;
+}
+
+
+/**
+ * wrapper to output a string from within a skin
+ * just to be able to use different encodings
+ * @param Object containing the string as text property
+ */
+function write_macro(param, text) {
+ res.write(param.text || text || String.NULL);
+ return;
+}
+
+
+/**
+ * renders the current datetime
+ * @param Object containing a formatting string as format property
+ */
+function now_macro(param) {
+ var d = new Date();
+ if (param.format) {
+ try {
+ res.write(d.format(param.format));
+ } catch (e) {
+ res.write('[Invalid date format]');
+ }
+ } else if (param.as == "timestamp") {
+ res.write(d.getTime());
+ } else {
+ res.write(d);
+ }
+ return;
+}
+
+
+/**
+ * renders a global skin
+ */
+var skin_macro = function(param, name) {
+ var skinName = name || param.name;
+ if (skinName) {
+ renderSkin(skinName, param);
+ }
+ return;
+}
+
diff --git a/modules/core/HopObject.js b/modules/core/HopObject.js
new file mode 100644
index 00000000..871de378
--- /dev/null
+++ b/modules/core/HopObject.js
@@ -0,0 +1,196 @@
+/*
+ * 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-2005 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: HopObject.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Adds useful methods to Helma's built-in HopObject prototype.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/HopObject.js')
+ */
+
+app.addRepository("modules/core/Number.js");
+app.addRepository("modules/core/String.js");
+
+
+/**
+ * Iterates over each child node of the HopObject.
+ * @param {Function} callback The callback function to be
+ * called for each child node. On every call the first
+ * argument of this function is set to the current value
+ * of the counter variable i
.
+ */
+HopObject.prototype.forEach = function(callback) {
+ if (!callback || callback instanceof Function == false) {
+ return;
+ }
+ for (var i=0; iparam
+ * argument:
+ *
+ * - param.none - not a single child node
+ * - param.one - exactly one child node
+ * - param.many - more than one child node
+ *
+ * @param {Object} param The default macro parameter
+ * @param {String} name The default name for a child node
+ */
+HopObject.prototype.size_macro = function(param, name) {
+ var EMPTYSTR = "";
+ var n = this.size();
+ if (name) {
+ var text;
+ var plural = name.endsWith("s") ? "es" : "s";
+ if (n > 0) {
+ if (n > 1) {
+ text = n + " " + name + plural;
+ } else {
+ text = (param.one !== null) ? param.one : "one " + name;
+ }
+ } else {
+ text = (param.none !== null) ? param.none : "no " + name + plural;
+ }
+ res.write(text);
+ } else {
+ res.write(n);
+ }
+ return;
+};
diff --git a/modules/core/JSON.js b/modules/core/JSON.js
new file mode 100644
index 00000000..5074b53f
--- /dev/null
+++ b/modules/core/JSON.js
@@ -0,0 +1,179 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: JSON.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Adds JSON methods to the Object, Array and String prototypes.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/JSON.js')
+ */
+
+/*
+ json.js
+ 2006-04-28 [http://www.json.org/json.js]
+
+ This file adds these methods to JavaScript:
+
+ object.toJSON()
+
+ This method produces a JSON text from an object. The
+ object must not contain any cyclical references.
+
+ array.toJSON()
+
+ This method produces a JSON text from an array. The
+ array must not contain any cyclical references.
+
+ string.parseJSON()
+
+ This method parses a JSON text to produce an object or
+ array. It will return false if there is an error.
+*/
+
+(function () {
+ var m = {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+
+ s = {
+ /**
+ * @ignore
+ */
+ array: function (x) {
+ var a = ['['], b, f, i, l = x.length, v;
+ for (i = 0; i < l; i += 1) {
+ v = x[i];
+ f = s[typeof v];
+ if (f) {
+ v = f(v);
+ if (typeof v == 'string') {
+ if (b) {
+ a[a.length] = ',';
+ }
+ a[a.length] = v;
+ b = true;
+ }
+ }
+ }
+ a[a.length] = ']';
+ return a.join('');
+ },
+
+ 'boolean': function (x) {
+ return String(x);
+ },
+
+ 'null': function (x) {
+ return "null";
+ },
+
+ /**
+ * @ignore
+ */
+ number: function (x) {
+ return isFinite(x) ? String(x) : 'null';
+ },
+
+ /**
+ * @ignore
+ */
+ object: function (x) {
+ if (x) {
+ if (x instanceof Array) {
+ return s.array(x);
+ }
+ var a = ['{'], b, f, i, v;
+ for (i in x) {
+ v = x[i];
+ f = s[typeof v];
+ if (f) {
+ v = f(v);
+ if (typeof v == 'string') {
+ if (b) {
+ a[a.length] = ',';
+ }
+ a.push(s.string(i), ':', v);
+ b = true;
+ }
+ }
+ }
+ a[a.length] = '}';
+ return a.join('');
+ }
+ return 'null';
+ },
+
+ /**
+ * @ignore
+ */
+ string: function (x) {
+ if (/["\\\x00-\x1f]/.test(x)) {
+ x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
+ var c = m[b];
+ if (c) {
+ return c;
+ }
+ c = b.charCodeAt();
+ return '\\u00' +
+ Math.floor(c / 16).toString(16) +
+ (c % 16).toString(16);
+ });
+ }
+ return '"' + x + '"';
+ }
+ };
+
+ /**
+ * This method produces a JSON text from an object.
+ * The object must not contain any cyclical references.
+ */
+ Object.prototype.toJSON = function () {
+ return s.object(this);
+ };
+
+ /**
+ * This method produces a JSON text from an array.
+ * The array must not contain any cyclical references.
+ */
+ Array.prototype.toJSON = function () {
+ return s.array(this);
+ };
+
+ Object.prototype.dontEnum("toJSON");
+ Array.prototype.dontEnum("toJSON");
+ return;
+})();
+
+
+/**
+ * This method parses a JSON text to produce an object or
+ * array. It will return false if there is an error.
+ */
+String.prototype.parseJSON = function () {
+ try {
+ return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(this.replace(/"(\\.|[^"\\])*"/g, ''))) && eval('(' + this + ')');
+ } catch (e) {
+ return false;
+ }
+};
+
+String.prototype.dontEnum("parseJSON");
diff --git a/modules/core/Number.js b/modules/core/Number.js
new file mode 100644
index 00000000..c9318e6e
--- /dev/null
+++ b/modules/core/Number.js
@@ -0,0 +1,80 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Number.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Adds useful methods to the JavaScript Number type.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/Number.js')
+ */
+
+/**
+ * format a Number to a String
+ * @param String Format pattern
+ * @param java.util.Locale An optional Locale instance
+ * @return String Number formatted to a String
+ */
+Number.prototype.format = function(fmt, locale) {
+ var symbols;
+ if (locale != null) {
+ symbols = new java.text.DecimalFormatSymbols(locale);
+ } else {
+ symbols = new java.text.DecimalFormatSymbols();
+ }
+ var df = new java.text.DecimalFormat(fmt || "###,##0.##", symbols);
+ return df.format(0 + this); // addition with 0 prevents exception
+};
+
+
+/**
+ * return the percentage of a Number
+ * according to a given total Number
+ * @param Int Total
+ * @param String Format Pattern
+ * @param java.util.Locale An optional Locale instance
+ * @return Int Percentage
+ */
+Number.prototype.toPercent = function(total, fmt, locale) {
+ if (!total)
+ return (0).format(fmt, locale);
+ var p = this / (total / 100);
+ return p.format(fmt, locale);
+};
+
+
+/**
+ * factory to create functions for sorting objects in an array
+ * @param String name of the field each object is compared with
+ * @param Number order (ascending or descending)
+ * @return Function ready for use in Array.prototype.sort
+ */
+Number.Sorter = function(field, order) {
+ if (!order)
+ order = 1;
+ return function(a, b) {
+ return (a[field] - b[field]) * order;
+ };
+};
+
+Number.Sorter.ASC = 1;
+Number.Sorter.DESC = -1;
+
+
+// prevent any newly added properties from being enumerated
+for (var i in Number)
+ Number.dontEnum(i);
+for (var i in Number.prototype)
+ Number.prototype.dontEnum(i);
diff --git a/modules/core/Object.js b/modules/core/Object.js
new file mode 100644
index 00000000..a6d4cfa1
--- /dev/null
+++ b/modules/core/Object.js
@@ -0,0 +1,111 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Object.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Adds useful methods to the JavaScript Object type.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/Object.js')
+ */
+
+/**
+ * copy the properties of an object into
+ * a new object
+ * @param Object the source object
+ * @param Object the (optional) target object
+ * @return Object the resulting object
+ */
+Object.prototype.clone = function(clone, recursive) {
+ if (!clone)
+ clone = new this.constructor();
+ var value;
+ for (var propName in this) {
+ value = this[propName];
+ if (recursive && (value.constructor == HopObject || value.constructor == Object)) {
+ clone[propName] = value.clone(new value.constructor(), recursive);
+ } else {
+ clone[propName] = value;
+ }
+ }
+ return clone;
+};
+
+
+/**
+ * reduce an extended object (ie. a HopObject)
+ * to a generic javascript object
+ * @param HopObject the HopObject to be reduced
+ * @return Object the resulting generic object
+ */
+Object.prototype.reduce = function(recursive) {
+ var result = {};
+ for (var i in this) {
+ if (this[i] instanceof HopObject == false)
+ result[i] = this[i];
+ else if (recursive)
+ result[i] = this.reduce(true);
+ }
+ return result;
+};
+
+
+/**
+ * print the contents of an object for debugging
+ * @param Object the object to dump
+ * @param Boolean recursive flag (if true, dump child objects, too)
+ */
+Object.prototype.dump = function(recursive) {
+ var beginList = "";
+ var beginItem = "";
+ var endItem = "";
+ var beginKey = "";
+ var endKey = ": ";
+ res.write(beginList);
+ for (var p in this) {
+ res.write(beginItem);
+ res.write(beginKey);
+ res.write(p);
+ res.write(endKey);
+ if (recursive && typeof this[p] == "object") {
+ var recurse = true;
+ var types = [Function, Date, String, Number];
+ for (var i in types) {
+ if (this[p] instanceof types[i]) {
+ recurse = false
+ break;
+ }
+ }
+ if (recurse == true)
+ this[p].dump(true);
+ else {
+ res.write(this[p].toSource());
+ }
+ } else if (this[p]) {
+ res.write(encode(this[p].toSource()));
+ }
+ res.write(endItem);
+ }
+ res.write(endList);
+ return;
+};
+
+
+// prevent any newly added properties from being enumerated
+for (var i in Object)
+ Object.dontEnum(i);
+for (var i in Object.prototype)
+ Object.prototype.dontEnum(i);
diff --git a/modules/core/String.js b/modules/core/String.js
new file mode 100644
index 00000000..62d2c8ef
--- /dev/null
+++ b/modules/core/String.js
@@ -0,0 +1,673 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: String.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+
+String.ANUMPATTERN = /[^a-zA-Z0-9]/;
+String.APATTERN = /[^a-zA-Z]/;
+String.NUMPATTERN = /[^0-9]/;
+String.FILEPATTERN = /[^a-zA-Z0-9-_\. ]/;
+String.HEXPATTERN = /[^a-fA-F0-9]/;
+// Email and URL RegExps contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/
+// licensed unter MIT license - http://www.opensource.org/licenses/mit-license.php
+String.EMAILPATTERN = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i;
+String.URLPATTERN = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
+String.LEFT = -1
+String.BALANCE = 0
+String.RIGHT = 1
+String.ISOFORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
+String.SPACE = " ";
+String.EMPTY = "";
+String.NULL = String.EMPTY; // to be deprecated?
+
+/**
+ * @fileoverview Adds useful methods to the JavaScript String type.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/core/String.js')
+ */
+
+/**
+ * checks if a date format pattern is correct
+ * @return Boolean true if the pattern is correct
+ */
+String.prototype.isDateFormat = function() {
+ try {
+ new java.text.SimpleDateFormat(this);
+ return true;
+ } catch (err) {
+ return false;
+ }
+};
+
+
+/**
+ * parse a timestamp into a date object. This is used when users
+ * want to set createtime explicitly when creating/editing stories.
+ * @param String date format to be applied
+ * @param Object Java TimeZone Object (optional)
+ * @return Object contains the resulting date
+ */
+String.prototype.toDate = function(format, timezone) {
+ var sdf = res.data._dateformat;
+ if (!sdf) {
+ sdf = new java.text.SimpleDateFormat(format);
+ res.data._dateformat = sdf;
+ } else if (format != sdf.toPattern())
+ sdf.applyPattern(format);
+ if (timezone && timezone != sdf.getTimeZone())
+ sdf.setTimeZone(timezone);
+ try {
+ return new Date(sdf.parse(this).getTime());
+ } catch (err) {
+ return null;
+ }
+};
+
+
+/**
+ * function checks if the string passed contains any characters that
+ * are forbidden in URLs and tries to create a java.net.URL from it
+ * FIXME: probably deprecated -> helma.Url
+ * @return Boolean
+ * @see helma.Url.PATTERN
+ */
+String.prototype.isUrl = function() {
+ return String.URLPATTERN.test(this);
+};
+
+
+/**
+ * function checks if the string passed contains any characters
+ * that are forbidden in image- or filenames
+ * @return Boolean
+ */
+String.prototype.isFileName = function() {
+ return !String.FILEPATTERN.test(this);
+};
+
+
+/**
+ * function cleans the string passed as argument from any characters
+ * that are forbidden or shouldn't be used in filenames
+ * @return Boolean
+ */
+String.prototype.toFileName = function() {
+ return this.replace(new RegExp(String.FILEPATTERN.source, "g"), String.NULL);
+};
+
+
+/**
+ * function checks a string for a valid color value in hexadecimal format.
+ * it may also contain # as first character
+ * @returns Boolean false, if string length (without #) > 6 or < 6 or
+ * contains any character which is not a valid hex value
+ */
+String.prototype.isHexColor = function() {
+ var str = this;
+ if (this.indexOf("#") == 0)
+ str = this.substring(1);
+ if (str.length != 6)
+ return false;
+ return !String.HEXPATTERN.test(str);
+};
+
+
+/**
+ * converts a string into a hexadecimal color
+ * representation (e.g. "ffcc33"). also knows how to
+ * convert a color string like "rgb (255, 204, 51)".
+ * @return String the resulting hex color (w/o "#")
+ */
+String.prototype.toHexColor = function() {
+ if (this.startsWith("rgb")) {
+ res.push();
+ var col = this.replace(/[^0-9,]/g, String.NULL);
+ var parts = col.split(",");
+ for (var i in parts) {
+ var num = parseInt(parts[i], 10);
+ var hex = num.toString(16);
+ res.write(hex.pad("0", 2, String.LEFT));
+ }
+ return res.pop();
+ }
+ var col = this.replace(new RegExp(String.HEXPATTERN.source), String.NULL);
+ return col.toLowerCase().pad("0", 6, String.LEFT);
+};
+
+
+/**
+ * function returns true if the string contains
+ * only a-z and 0-9 (case insensitive!)
+ * @return Boolean true in case string is alpha, false otherwise
+ */
+String.prototype.isAlphanumeric = function() {
+ if (!this.length)
+ return false;
+ return !String.ANUMPATTERN.test(this);
+};
+
+
+/**
+ * function cleans a string by throwing away all
+ * non-alphanumeric characters
+ * @return cleaned string
+ */
+String.prototype.toAlphanumeric = function() {
+ return this.replace(new RegExp(String.ANUMPATTERN.source, "g"), String.NULL);
+};
+
+
+/**
+ * function returns true if the string contains
+ * only characters a-z
+ * @return Boolean true in case string is alpha, false otherwise
+ */
+String.prototype.isAlpha = function() {
+ if (!this.length)
+ return false;
+ return !String.APATTERN.test(this);
+};
+
+
+/**
+ * function returns true if the string contains
+ * only 0-9
+ * @return Boolean true in case string is numeric, false otherwise
+ */
+String.prototype.isNumeric = function() {
+ if (!this.length)
+ return false;
+ return !String.NUMPATTERN.test(this);
+};
+
+
+/**
+ * transforms the first n characters of a string to uppercase
+ * @param Number amount of characters to transform
+ * @return String the resulting string
+ */
+String.prototype.capitalize = function(limit) {
+ if (limit == null)
+ limit = 1;
+ var head = this.substring(0, limit);
+ var tail = this.substring(limit, this.length);
+ return head.toUpperCase() + tail.toLowerCase();
+};
+
+
+/**
+ * transforms the first n characters of each
+ * word in a string to uppercase
+ * @return String the resulting string
+ */
+String.prototype.titleize = function() {
+ var parts = this.split(" ");
+ res.push();
+ for (var i in parts) {
+ res.write(parts[i].capitalize());
+ if (i < parts.length-1)
+ res.write(" ");
+ }
+ return res.pop();
+};
+
+
+/**
+ * translates all characters of a string into HTML entities
+ * @return String translated result
+ */
+String.prototype.entitize = function() {
+ res.push();
+ for (var i=0; i/g, replacement);
+ return str;
+};
+
+
+/**
+ * function calculates the md5 hash of a string
+ * @return String md5 hash of the string
+ */
+String.prototype.md5 = function() {
+ return Packages.helma.util.MD5Encoder.encode(this);
+};
+
+
+/**
+ * function repeats a string the specified amount of times
+ * @param Int amount of repetitions
+ * @return String resulting string
+ */
+String.prototype.repeat = function(multiplier) {
+ res.push();
+ for (var i=0; i -1)
+ return true;
+ return false;
+};
+
+
+/**
+ * function compares a string with the one passed as argument
+ * using diff
+ * @param String String to compare against String object value
+ * @param String Optional regular expression string to use for
+ * splitting. If not defined, newlines will be used.
+ * @return Object Array containing one JS object for each line
+ * with the following properties:
+ * .num Line number
+ * .value String line if unchanged
+ * .deleted Obj Array containing deleted lines
+ * .inserted Obj Array containing added lines
+ */
+String.prototype.diff = function(mod, separator) {
+ // if no separator use line separator
+ var regexp = (typeof(separator) == "undefined") ?
+ new RegExp("\r\n|\r|\n") :
+ new RegExp(separator);
+ // split both strings into arrays
+ var orig = this.split(regexp);
+ var mod = mod.split(regexp);
+ // create the Diff object
+ var diff = new Packages.helma.util.Diff(orig, mod);
+ // get the diff.
+ var d = diff.diff();
+ if (!d)
+ return null;
+
+ var max = Math.max(orig.length, mod.length);
+ var result = new Array();
+ for (var i=0;i -1) {
+ count += 1;
+ offset += 1;
+ }
+ return count;
+};
+
+
+/**
+ * returns the string encoded using the base64 algorithm
+ */
+String.prototype.enbase64 = function() {
+ var bytes = new java.lang.String(this) . getBytes();
+ return new Packages.sun.misc.BASE64Encoder().encode(bytes);
+};
+
+
+/**
+ * returns the decoded string using the base64 algorithm
+ */
+String.prototype.debase64 = function() {
+ var bytes = new Packages.sun.misc.BASE64Decoder().decodeBuffer(this);
+ return String(new java.lang.String(bytes));
+};
+
+
+// wrapper methods for string-related
+// global helma functions
+
+String.prototype.encode = function() {
+ return encode(this);
+};
+
+String.prototype.encodeXml = function() {
+ return encodeXml(this);
+};
+
+String.prototype.encodeForm = function() {
+ return encodeForm(this);
+};
+
+String.prototype.format = function() {
+ return format(this);
+};
+
+String.prototype.stripTags = function() {
+ return stripTags(this);
+};
+
+/**
+ * factory to create functions for sorting objects in an array
+ * @param String name of the field each object is compared with
+ * @param Number order (ascending or descending)
+ * @return Function ready for use in Array.prototype.sort
+ */
+String.Sorter = function(field, order) {
+ if (!order)
+ order = 1;
+ var key = field + ":" + order;
+ if (!String.Sorter.cache[key]) {
+ String.Sorter.cache[key] = function(a, b) {
+ var str1 = String(a[field] || String.NULL).toLowerCase();
+ var str2 = String(b[field] || String.NULL).toLowerCase();
+ if (str1 > str2)
+ return order * 1;
+ if (str1 < str2)
+ return order * -1;
+ return 0;
+ };
+ }
+ return String.Sorter.cache[key];
+};
+
+String.Sorter.ASC = 1;
+String.Sorter.DESC = -1;
+String.Sorter.cache = {};
+
+
+/**
+ * create a string from a bunch of substrings
+ * @param String one or more strings as arguments
+ * @return String the resulting string
+ */
+String.compose = function() {
+ res.push();
+ for (var i=0; i
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Aspects.js')
+ */
+
+/**
+ * Define the global namespace if not existing
+ */
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Library for adding Aspects
+ *
+ * Provides static methods to wrap existing functions
+ * inside a javascript closure in order to add additional
+ * behavior without overriding the existing one.
+ *
+ * Based on code by roman porotnikov,
+ * http://www.jroller.com/page/deep/20030701
+ *
+ * Note: Each prototype that uses aspects must implement a method
+ * onCodeUpdate() to prevent aspects being lost when the prototype
+ * is re-compiled
+ *
+ * @constructor
+ */
+helma.Aspects = function() {
+ return this;
+};
+
+
+/** @ignore */
+helma.Aspects.toString = function() {
+ return "[helma.Aspects]";
+};
+
+
+/** @ignore */
+helma.Aspects.prototype.toString = function() {
+ return "[helma.Aspects Object]";
+};
+
+
+/**
+ * Adds a function to be called before the orginal function.
+ *
+ * The return value of the added function needs to provide the
+ * array of arguments that is passed to the original function.
+ * The added function receives an array of the original arguments,
+ * the original function and the scope object of the original
+ * function as its parameters.
+ *
+ * @param {Object} obj The object of which the original function is a property
+ * @param {String} fname The property name of the original function
+ * @param {Function} before The function to be called before the original function
+ * @returns Function A new function, wrapping the original function
+ * @type Function
+ */
+helma.Aspects.prototype.addBefore = function(obj, fname, before) {
+ var oldFunc = obj[fname];
+ obj[fname] = function() {
+ return oldFunc.apply(this, before(arguments, oldFunc, this));
+ }
+ return;
+};
+
+
+/**
+ * Adds a function to be called after an existing function.
+ *
+ * The return value of the original function is passed to the
+ * added function as its first argument. In addition, the added
+ * function also receives an array of the original arguments,
+ * the original function and the scope object of the original
+ * function as additional parameters.
+ *
+ * @param {Object} obj as Object, the object of which the original function is a property
+ * @param {String} fname as String, the property name of the original function
+ * @param {Function} after as Function, the function to be called after the original function
+ * @returns Function A new function, wrapping the original function
+ * @type Function
+ */
+helma.Aspects.prototype.addAfter = function(obj, fname, after) {
+ var oldFunc = obj[fname];
+ obj[fname] = function() {
+ return after(oldFunc.apply(this, arguments), arguments, oldFunc, this);
+ }
+ return;
+};
+
+
+/**
+ * Wraps an additional function around the original function.
+ *
+ * The added function receives as its arguments an array of the original
+ * arguments, the original function and the scope object of the original
+ * function. The original function is not called directly and needs
+ * to be invoked by the added function.
+ *
+ * @param {Object} obj as Object, the object of which the original function is a property
+ * @param {String} fname as String, the property name of the original function
+ * @param {Function} around as Function, the function to be called inside the original function
+ * @returns Function A new function, wrapping the original function
+ * @type Function
+ */
+helma.Aspects.prototype.addAround = function(obj, fname, around) {
+ var oldFunc = obj[fname];
+ obj[fname] = function() {
+ return around(arguments, oldFunc, this);
+ }
+ return;
+};
+
+
+helma.lib = "Aspects";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
+
+
+helma.aspects = new helma.Aspects();
+helma.dontEnum("aspects");
diff --git a/modules/helma/Chart.js b/modules/helma/Chart.js
new file mode 100644
index 00000000..c05d3515
--- /dev/null
+++ b/modules/helma/Chart.js
@@ -0,0 +1,201 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Chart.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+
+/**
+ * @fileoverview Fields and methods of the helma.Chart prototype
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Chart.js')
+ */
+
+// take care of any dependencies
+app.addRepository('modules/helma/jxl.jar');
+
+/**
+ * Define the global namespace if not existing
+ */
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Creates a new instance of helma.Chart
+ * @class Instances of this class are capable of reading
+ * Excel spreadsheets and rendering them as XHTML table. Internally
+ * helma.Chart uses the Java Excel API
+ * by Andy Khan.
+ * @param {String} fpath The path to the spreadsheet file
+ * @param {String} prefix An optional prefix to use for all
+ * stylesheet classes within the rendered table
+ * @param {String} sheetName The name of the sheet within the
+ * spreadsheet file to render. If this argument is omitted, the
+ * first sheet is rendered.
+ * @returns A newly created helma.Chart instance.
+ * @constructor
+ * @author Tobi Schaefer
+ */
+helma.Chart = function(fpath, prefix, sheetName) {
+ var JXLPKG = Packages.jxl.Workbook;
+ var JXLPKGNAME = "jxl.jar";
+ var JXLPKGURL = "http://www.andykhan.com/jexcelapi/";
+
+ var workbook, file;
+ try {
+ file = new java.io.File(fpath);
+ workbook = JXLPKG.getWorkbook(file);
+ } catch (e) {
+ if (e instanceof TypeError == false)
+ throw(e);
+ throw("helma.Chart needs " + JXLPKGNAME +
+ " in lib/ext or application directory " +
+ "[" + JXLPKGURL + "]");
+ }
+
+ var getCellStyle = function(c) {
+ if (!c)
+ return;
+ var result = new Object();
+ var format = c.getCellFormat();
+ var font = format.getFont();
+ if (font.getBoldWeight() > 400)
+ result.bold = true;
+ result.italic = font.isItalic();
+ result.wrap = format.getWrap();
+ var type = c.getType();
+ var align = format.getAlignment().getDescription();
+ if (align == "right" || type == "Number" || type == "Date")
+ result.align = "right";
+ else if (align == "centre")
+ result.align = "center";
+ return result;
+ }
+
+ if (sheetName) {
+ var sheet = workbook.getSheet(sheetName);
+ } else {
+ var sheet = workbook.getSheet(0);
+ }
+ if (!sheet)
+ return;
+
+ prefix = prefix ? prefix + "_" : "chart_";
+
+ /**
+ * Renders the Excel spreadsheet as XHTML table.
+ */
+ this.render = function() {
+ res.write('\n');
+
+ var rowBuf = [];
+ var rows = sheet.getRows();
+ var max = 0;
+ for (var i=0; i max)
+ max = row.length;
+ rowBuf.push(row);
+ }
+
+ for (var i in rowBuf) {
+ res.write('\n');
+ for (var n=0; n");
+ if (style.bold)
+ res.write("");
+ if (style.italic)
+ res.write("");
+ }
+ else
+ res.write(">");
+ res.write(str);
+ if (style) {
+ if (style.italic)
+ res.write("");
+ if (style.bold)
+ res.write("");
+ }
+ res.write('\n');
+ }
+ res.write('
\n');
+ }
+
+ res.write('
\n');
+ workbook.close();
+ };
+
+ /**
+ * Returns the spreadsheet as rendered XHTML table.
+ * @returns The rendered spreadsheet table
+ * @type String
+ */
+ this.renderAsString = function() {
+ res.push();
+ this.render();
+ return res.pop();
+ };
+
+ /** @ignore */
+ this.toString = function() {
+ return "[helma.Chart " + file + "]";
+ };
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+}
+
+
+/** @ignore */
+helma.Chart.toString = function() {
+ return "[helma.Chart]";
+};
+
+
+/**
+ * A simple example for using helma.Chart that renders
+ * the passed file as XHTML table to response.
+ * @param {String} file The path to the Excel spreadsheet file
+ */
+helma.Chart.example = function(file) {
+ // var file = "/path/to/file.xls";
+ var chart = new helma.Chart(file);
+ chart.render();
+ return;
+};
+
+
+helma.lib = "Chart";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Color.js b/modules/helma/Color.js
new file mode 100644
index 00000000..32e011ae
--- /dev/null
+++ b/modules/helma/Color.js
@@ -0,0 +1,396 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Color.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+
+/**
+ * @fileoverview Fields and methods of the helma.Chart prototype
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Color.js')
+ */
+
+// take care of any dependencies
+app.addRepository("modules/core/String.js");
+
+/**
+ * Define the global namespace if not existing
+ */
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Constructs a new instance of helma.Color.
+ * @class Instances of this class provide methods for
+ * converting HTML color names into their corresponding
+ * RGB values and vice versa, or retrieving single RGB color values.
+ * @param {Number|String} R Either the red fraction of the color,
+ * or the name of the color.
+ * @param {Number} G The green fraction
+ * @param {Number} B The blue fraction
+ * @returns A newly created helma.Color instance
+ * @constructor
+ */
+helma.Color = function(R, G, B) {
+ var value = null;
+ var name = null;
+ var hex = null;
+ var rgb = null;
+
+ /**
+ * Returns the decimal value of this color, or of a specified
+ * color channel.
+ * @param {String} channel An optional color channel which
+ * decimal value should be returned. Must be either "red",
+ * "green" or "blue". If no channel is specified this
+ * method returns the decimal value of the color itself.
+ * @returns The decimal value of this color or a single channel.
+ * @type Number
+ */
+ this.valueOf = function(channel) {
+ if (channel) {
+ if (!rgb) {
+ var compose = function(n, bits) {
+ var div = Math.pow(2, bits);
+ remainder = n % div;
+ return Math.floor(n/div);
+ }
+ var remainder = value;
+ rgb = {
+ red: compose(remainder, 16),
+ green: compose(remainder, 8),
+ blue: compose(remainder, 0)
+ };
+ }
+ return rgb[channel];
+ }
+ return value;
+ };
+
+ /**
+ * Returns the hexidecimal value of this color (without
+ * a leading hash sign).
+ * @returns The hexidecimal value of this color
+ * @type String
+ */
+ this.toString = function() {
+ if (!value)
+ return null;
+ if (!hex)
+ hex = value.toString(16).pad("0", 6, String.LEFT);
+ return hex;
+ };
+
+
+ /**
+ * Returns the trivial name of this color
+ * @returns The trivial name of this color
+ * @type String
+ */
+ this.getName = function() {
+ return helma.Color.COLORVALUES[value];
+ };
+
+ /**
+ * Main constructor body
+ */
+ if (arguments.length % 2 == 0)
+ throw("Insufficient arguments for creating Color");
+ if (arguments.length == 1) {
+ if (R.constructor == Number) {
+ value = R;
+ } else if (R.constructor == String) {
+ R = R.toLowerCase();
+ if (helma.Color.COLORNAMES[R]) {
+ this.name = R;
+ value = helma.Color.COLORNAMES[R];
+ } else {
+ if (R.startsWith("#")) {
+ R = R.substring(1);
+ }
+ value = parseInt(R, 16);
+ this.name = helma.Color.COLORVALUES[value];
+ }
+ }
+ } else {
+ value = R * Math.pow(2, 16) + G * Math.pow(2, 8) + B;
+ }
+
+ if (value == null || isNaN(value))
+ throw("helma.Color: invalid argument " + R);
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+};
+
+
+/**
+ * Creates a new helma.Color instance based on a color name.
+ * @param {String} name The color name (eg. "darkseagreen")
+ * @returns An instance of helma.Color representing the color specified
+ * @type helma.Color
+ */
+helma.Color.fromName = function(name) {
+ var value = helma.Color.COLORNAMES[name.toLowerCase()];
+ return new helma.Color(value || 0);
+};
+
+
+/**
+ * Creates a new helma.Color instance based on a HSL color
+ * representation. This method is adapted from the HSLtoRGB
+ * conversion method as described at
+ * http://www1.tip.nl/~t876506/ColorDesign.html#hr.
+ * @param {Number} H The hue fraction of the color definition
+ * @param {Number} S The saturation fraction
+ * @param {Number} L The lightness fraction
+ * @returns An instance of helma.Color representing the corresponding
+ * RGB color definition.
+ * @type helma.Color
+ */
+helma.Color.fromHsl = function(H,S,L) {
+ function H1(H,S,L) {
+ var R = 1; var G = 6*H; var B = 0;
+ G = G*S + 1 - S; B = B*S + 1 - S;
+ R = R*L; G = G*L; B = B*L;
+ return [R,G,B];
+ }
+
+ function H2(H,S,L) {
+ var R = 1-6*(H - 1/6); var G = 1; var B = 0;
+ R = R*S + 1 - S; B = B*S + 1 - S;
+ R = R*L; G = G*L; B = B*L;
+ return [R,G,B];
+ }
+
+ function H3(H,S,L) {
+ var R = 0; var G = 1; var B = 6*(H - 1/3);
+ R = R*S + 1 - S; B = B*S + 1 - S;
+ R = R*L; G = G*L; B = B*L
+ return [R,G,B];
+ }
+
+ function H4(H,S,L) {
+ var R = 0; var G = 1-6*(H - 1/2); var B = 1;
+ R = R*S + 1 - S; G = G*S + 1 - S;
+ R = R*L; G = G*L; B = B*L;
+ return [R,G,B];
+ }
+
+ function H5(H,S,L) {
+ var R = 6*(H - 2/3); var G = 0; var B = 1;
+ R = R*S + 1 - S; G = G*S + 1 - S;
+ R = R*L; G = G*L; B = B*L;
+ return [R,G,B];
+ }
+
+ function H6(H,S,L) {
+ var R = 1; var G = 0; var B = 1-6*(H - 5/6);
+ G = G*S + 1 - S; B = B*S + 1 - S;
+ R = R*L; G = G*L; B = B*L;
+ return [R,G,B];
+ }
+
+ // H [0-1] is divided into 6 equal sectors.
+ // From within each sector the proper conversion function is called.
+ var rgb;
+ if (H < 1/6) rgb = H1(H,S,L);
+ else if (H < 1/3) rgb = H2(H,S,L);
+ else if (H < 1/2) rgb = H3(H,S,L);
+ else if (H < 2/3) rgb = H4(H,S,L);
+ else if (H < 5/6) rgb = H5(H,S,L);
+ else rgb = H6(H,S,L);
+
+ return new helma.Color(
+ Math.round(rgb[0]*255),
+ Math.round(rgb[1]*255),
+ Math.round(rgb[2]*255)
+ );
+};
+
+
+/**
+ * Contains the hexadecimal values of named colors.
+ * @type Object
+ * @final
+ */
+helma.Color.COLORNAMES = {
+ black: 0x000000,
+ maroon: 0x800000,
+ green: 0x008000,
+ olive: 0x808000,
+ navy: 0x000080,
+ purple: 0x800080,
+ teal: 0x008080,
+ silver: 0xc0c0c0,
+ gray: 0x808080,
+ red: 0xff0000,
+ lime: 0x00ff00,
+ yellow: 0xffff00,
+ blue: 0x0000ff,
+ fuchsia: 0xff00ff,
+ aqua: 0x00ffff,
+ white: 0xffffff,
+ aliceblue: 0xf0f8ff,
+ antiquewhite: 0xfaebd7,
+ aquamarine: 0x7fffd4,
+ azure: 0xf0ffff,
+ beige: 0xf5f5dc,
+ blueviolet: 0x8a2be2,
+ brown: 0xa52a2a,
+ burlywood: 0xdeb887,
+ cadetblue: 0x5f9ea0,
+ chartreuse: 0x7fff00,
+ chocolate: 0xd2691e,
+ coral: 0xff7f50,
+ cornflowerblue: 0x6495ed,
+ cornsilk: 0xfff8dc,
+ crimson: 0xdc143c,
+ darkblue: 0x00008b,
+ darkcyan: 0x008b8b,
+ darkgoldenrod: 0xb8860b,
+ darkgray: 0xa9a9a9,
+ darkgreen: 0x006400,
+ darkkhaki: 0xbdb76b,
+ darkmagenta: 0x8b008b,
+ darkolivegreen: 0x556b2f,
+ darkorange: 0xff8c00,
+ darkorchid: 0x9932cc,
+ darkred: 0x8b0000,
+ darksalmon: 0xe9967a,
+ darkseagreen: 0x8fbc8f,
+ darkslateblue: 0x483d8b,
+ darkslategray: 0x2f4f4f,
+ darkturquoise: 0x00ced1,
+ darkviolet: 0x9400d3,
+ deeppink: 0xff1493,
+ deepskyblue: 0x00bfff,
+ dimgray: 0x696969,
+ dodgerblue: 0x1e90ff,
+ firebrick: 0xb22222,
+ floralwhite: 0xfffaf0,
+ forestgreen: 0x228b22,
+ gainsboro: 0xdcdcdc,
+ ghostwhite: 0xf8f8ff,
+ gold: 0xffd700,
+ goldenrod: 0xdaa520,
+ greenyellow: 0xadff2f,
+ honeydew: 0xf0fff0,
+ hotpink: 0xff69b4,
+ indianred: 0xcd5c5c,
+ indigo: 0x4b0082,
+ ivory: 0xfffff0,
+ khaki: 0xf0e68c,
+ lavender: 0xe6e6fa,
+ lavenderblush: 0xfff0f5,
+ lawngreen: 0x7cfc00,
+ lemonchiffon: 0xfffacd,
+ lightblue: 0xadd8e6,
+ lightcoral: 0xf08080,
+ lightcyan: 0xe0ffff,
+ lightgoldenrodyellow: 0xfafad2,
+ lightgreen: 0x90ee90,
+ lightgrey: 0xd3d3d3,
+ lightpink: 0xffb6c1,
+ lightsalmon: 0xffa07a,
+ lightseagreen: 0x20b2aa,
+ lightskyblue: 0x87cefa,
+ lightslategray: 0x778899,
+ lightsteelblue: 0xb0c4de,
+ lightyellow: 0xffffe0,
+ limegreen: 0x32cd32,
+ linen: 0xfaf0e6,
+ mediumaquamarine: 0x66cdaa,
+ mediumblue: 0x0000cd,
+ mediumorchid: 0xba55d3,
+ mediumpurple: 0x9370db,
+ mediumseagreen: 0x3cb371,
+ mediumslateblue: 0x7b68ee,
+ mediumspringgreen: 0x00fa9a,
+ mediumturquoise: 0x48d1cc,
+ mediumvioletred: 0xc71585,
+ midnightblue: 0x191970,
+ mintcream: 0xf5fffa,
+ mistyrose: 0xffe4e1,
+ moccasin: 0xffe4b5,
+ navajowhite: 0xffdead,
+ oldlace: 0xfdf5e6,
+ olivedrab: 0x6b8e23,
+ orange: 0xffa500,
+ orangered: 0xff4500,
+ orchid: 0xda70d6,
+ palegoldenrod: 0xeee8aa,
+ palegreen: 0x98fb98,
+ paleturquoise: 0xafeeee,
+ palevioletred: 0xdb7093,
+ papayawhip: 0xffefd5,
+ peachpuff: 0xffdab9,
+ peru: 0xcd853f,
+ pink: 0xffc0cb,
+ plum: 0xdda0dd,
+ powderblue: 0xb0e0e6,
+ rosybrown: 0xbc8f8f,
+ royalblue: 0x4169e1,
+ saddlebrown: 0x8b4513,
+ salmon: 0xfa8072,
+ sandybrown: 0xf4a460,
+ seagreen: 0x2e8b57,
+ seashell: 0xfff5ee,
+ sienna: 0xa0522d,
+ skyblue: 0x87ceeb,
+ slateblue: 0x6a5acd,
+ slategray: 0x708090,
+ snow: 0xfffafa,
+ springgreen: 0x00ff7f,
+ steelblue: 0x4682b4,
+ tan: 0xd2b48c,
+ thistle: 0xd8bfd8,
+ tomato: 0xff6347,
+ turquoise: 0x40e0d0,
+ violet: 0xee82ee,
+ wheat: 0xf5deb3,
+ whitesmoke: 0xf5f5f5,
+ yellowgreen: 0x9acd32
+};
+
+
+/**
+ * Contains the color names for specific hex values
+ * @type Object
+ * @final
+ */
+helma.Color.COLORVALUES = {};
+
+for (var i in helma.Color.COLORNAMES) {
+ helma.Color.COLORVALUES[helma.Color.COLORNAMES[i]] = i;
+}
+
+
+/** @ignore */
+helma.Color.toString = function() {
+ return "[helma.Color]";
+};
+
+
+helma.lib = "Color";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Database.js b/modules/helma/Database.js
new file mode 100644
index 00000000..23fb8150
--- /dev/null
+++ b/modules/helma/Database.js
@@ -0,0 +1,341 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Database.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Properties and methods of the helma.Database prototype.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Database.js')
+ */
+
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Constructor for Database objects, providing access through relational
+ * databases through JDBC. It is usually simpler to use one of the factory
+ * methods {@link #createInstance} or {@link #getInstance}.
+ * @class This class provides access to a relational database through JDBC.
+ * There are two convenient ways to create instances of this class.
+ *
+ * The first is to use {@link #getInstance helma.Database.getInstance()}
+ * to obtain a connection to a DB that is defined in the application's
+ * db.properties and managed by Helma. The second way is to define and create
+ * a database connection locally using
+ * {@link #createInstance helma.Database.createInstance()} and passing it
+ * all necessary parameters.
+ *
+ * This class provides two ways of interaction:
+ * The {@link #query} method allows to issue SQL queries, returning a result set.
+ * The {@link #execute} provides a way to issue SQL statements that do not
+ * return a result set.
+ *
+ * Database connections allocated by this class are be managed and eventually
+ * disposed by Helma.
+ *
+ * @param {DbSource} source instance of a helma.objectmodel.db.DbSource
+ * @constructor
+ */
+helma.Database = function(source) {
+ var Types = java.sql.Types;
+ var DbSource = Packages.helma.objectmodel.db.DbSource;
+
+ if (typeof(source) == "string")
+ source = app.getDbSource(source);
+ if (!(source instanceof DbSource))
+ throw "helma.Database requires a helma.objectmodel.db.DbSource argument";
+
+ /**
+ * Get the java.sql.Connection for this Database instance. This can be used
+ * to operate on the connection directly, without going through the helma.Database
+ * class.
+ * @return {java.sql.Connection} the JDBC connection
+ */
+ this.getConnection = function() {
+ return source.getConnection();
+ };
+
+ /**
+ * Returns the lower case name of the underlying database product.
+ * @return {String} the name of the DB product
+ */
+ this.getProductName = function() {
+ return source.getConnection().getMetaData().getDatabaseProductName().toLowerCase();
+ };
+
+ /**
+ * Returns true if this is an Oracle database.
+ * @return {boolean} true if this is an Oracle database.
+ */
+ this.isOracle = function() {
+ return source.isOracle();
+ };
+
+ /**
+ * Returns true if this is a MySQL database.
+ * @return {boolean} true if this is an MySQL database.
+ */
+ this.isMySql = function() {
+ return source.isMySQL();
+ };
+
+ /**
+ * Returns true if this is a PostgreSQL database.
+ * @return {boolean} true if this is a PostgreSQL database.
+ */
+ this.isPostgreSql = function() {
+ return source.isPostgreSQL();
+ };
+
+ /**
+ * Executes the given SQL statement. The result set is returned
+ * as JavaScript Array containing a JavaScript Object for each result.
+ * @param {String} sql an SQL query statement
+ * @return {Array} an Array containing the result set
+ */
+ this.query = function(sql) {
+ var isLogSqlEnabled = (getProperty("logSQL", "false").toLowerCase() == "true");
+ var logTimeStart = isLogSqlEnabled ? java.lang.System.currentTimeMillis() : 0;
+ var connection = source.getConnection();
+ connection.setReadOnly(true);
+ var statement = connection.createStatement();
+ var resultSet = statement.executeQuery(sql);
+ var metaData = resultSet.getMetaData();
+ var max = metaData.getColumnCount();
+ var types = [];
+ for (var i=1; i <= max; i++) {
+ types[i] = metaData.getColumnType(i);
+ }
+ var result = [];
+ while (resultSet.next()) {
+ var row = {}
+ for (var i=1; i<=max; i+=1) {
+ switch (types[i]) {
+ case Types.BIT:
+ case Types.BOOLEAN:
+ row[metaData.getColumnLabel(i)] = resultSet.getBoolean(i);
+ break;
+ case Types.TINYINT:
+ case Types.BIGINT:
+ case Types.SMALLINT:
+ case Types.INTEGER:
+ row[metaData.getColumnLabel(i)] = resultSet.getLong(i);
+ break;
+ case Types.REAL:
+ case Types.FLOAT:
+ case Types.DOUBLE:
+ case Types.DECIMAL:
+ case Types.NUMERIC:
+ row[metaData.getColumnLabel(i)] = resultSet.getDouble(i);
+ break;
+ case Types.VARBINARY:
+ case Types.BINARY:
+ case Types.LONGVARBINARY:
+ case Types.LONGVARCHAR:
+ case Types.CHAR:
+ case Types.VARCHAR:
+ case Types.CLOB:
+ case Types.OTHER:
+ row[metaData.getColumnLabel(i)] = resultSet.getString(i);
+ break;
+ case Types.DATE:
+ case Types.TIME:
+ case Types.TIMESTAMP:
+ row[metaData.getColumnLabel(i)] = resultSet.getTimestamp(i);
+ break;
+ case Types.NULL:
+ row[metaData.getColumnLabel(i)] = null;
+ break;
+ default:
+ row[metaData.getColumnLabel(i)] = resultSet.getString(i);
+ break;
+ }
+ }
+ result[result.length] = row;
+ }
+ var logTimeStop = isLogSqlEnabled ? java.lang.System.currentTimeMillis() : 0;
+ if (isLogSqlEnabled) {
+ var tableName = metaData.getColumnCount() > 0 ? metaData.getTableName(1) : null;
+ app.getLogger("helma." + app.name + ".sql").info("SQL DIRECT_QUERY " + (tableName || "-") + " " + (logTimeStop - logTimeStart) + ": " + sql);
+ }
+ try {
+ statement.close();
+ resultSet.close();
+ } catch (error) {
+ // ignore
+ }
+ return result;
+ };
+
+ /**
+ * Executes the given SQL statement, which may be an INSERT, UPDATE,
+ * or DELETE statement or an SQL statement that returns nothing,
+ * such as an SQL data definition statement. The return value is an integer that
+ * indicates the number of rows that were affected by the statement.
+ * @param {String} sql an SQL statement
+ * @return {int} either the row count for INSERT, UPDATE or
+ * DELETE statements, or 0 for SQL statements that return nothing
+ */
+ this.execute = function(sql) {
+ var isLogSqlEnabled = (getProperty("logSQL", "false").toLowerCase() == "true");
+ var logTimeStart = isLogSqlEnabled ? java.lang.System.currentTimeMillis() : 0;
+ var connection = source.getConnection();
+ connection.setReadOnly(false);
+ var statement = connection.createStatement();
+ var result;
+ try {
+ result = statement.executeUpdate(sql);
+ } finally {
+ try {
+ statement.close();
+ } catch (error) {
+ // ignore
+ }
+ }
+ var logTimeStop = isLogSqlEnabled ? java.lang.System.currentTimeMillis() : 0;
+ if (isLogSqlEnabled) {
+ app.getLogger("helma." + app.name + ".sql").info("SQL DIRECT_EXECUTE - " + (logTimeStop - logTimeStart) + ": " + sql);
+ }
+ return result;
+ };
+
+ /**
+ * Return the name of the Helma DbSource object.
+ * @return {String} the DbSource name
+ */
+ this.getName = function() {
+ return source.getName();
+ };
+
+ /**
+ * Return the name of the JDBC driver used by this Database instance.
+ * @return {String} the JDBC driver name
+ */
+ this.getDriverName = function() {
+ return source.getDriverName();
+ };
+
+ /**
+ * @ignore
+ */
+ this.toString = function() {
+ return "[helma.Database " + this.getName() + "]";
+ };
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+};
+
+/**
+ * Create a new Database instance using the given parameters.
+ * Some of the parameters support shortcuts for known database products.
+ * The url
parameter recognizes the values "mysql", "oracle" and
+ * "postgresql". For those databases, it is also possible to pass just
+ * hostname
or hostname:port
as url
+ * parameters instead of the full JDBC URL.
+ * @param {String} driver the class name of the JDBC driver. As
+ * shortcuts, the values "mysql", "oracle" and "postgresql" are
+ * recognized.
+ * @param {String} url the JDBC URL.
+ * @param {String} name the name of the database to use
+ * @param {String} user the the username
+ * @param {String} password the password
+ * @return {helma.Database} a helma.Database instance
+ */
+helma.Database.createInstance = function(driver, url, name, user, password) {
+ var DbSource = Packages.helma.objectmodel.db.DbSource;
+
+ if (!driver || !url || !name)
+ throw("Insufficient arguments to create helma.db.Connection");
+ if (typeof password != "string")
+ password = "";
+
+ var MYSQL = "mysql";
+ var ORACLE = "oracle";
+ var POSTGRESQL = "postgresql";
+ var JDBC = "jdbc:";
+ var DRIVER_MYSQL = "com.mysql.jdbc.Driver";
+ var DRIVER_ORACLE = "oracle.jdbc.driver.OracleDriver";
+ var DRIVER_POSTGRESQL = "org.postgresql.Driver";
+
+ if (driver == MYSQL) {
+ driver = DRIVER_MYSQL;
+ if (url.indexOf(JDBC) != 0)
+ url = "jdbc:mysql://" + url + "/" + name;
+ } else if (driver == ORACLE) {
+ driver = DRIVER_ORACLE;
+ if (url.indexOf(JDBC) != 0)
+ url = "jdbc:oracle:thin:@" + url + ":" + name;
+ } else if (driver == POSTGRESQL) {
+ driver = DRIVER_POSTGRESQL;
+ if (url.indexOf(JDBC) != 0)
+ url = "jdbc:postgresql://" + url + "/" + name;
+ }
+ var props = new Packages.helma.util.ResourceProperties();
+ props.put(name + ".url", url);
+ props.put(name + ".driver", driver);
+ if (user) {
+ props.put(name + ".user", user)
+ }
+ if (password) {
+ props.put(name + ".password", password);
+ }
+ return new helma.Database(new DbSource(name, props));
+}
+
+/**
+ * Get a Database instance using the Database source defined in the
+ * application's db.properties file with the given name.
+ * @param {String} name the name of the DB source as defined in db.properties
+ * @return {helma.Database} a helma.Database instance
+ */
+helma.Database.getInstance = function(name) {
+ return new helma.Database(app.getDbSource(name));
+}
+
+/**
+ * @ignore
+ */
+helma.Database.toString = function() {
+ return "[helma.Database]";
+};
+
+/**
+ * @ignore
+ */
+helma.Database.example = function() {
+ var type = "mysql";
+ var host = "localhost";
+ var user = "root";
+ var pw = "";
+ var name = "mysql";
+ var db = new helma.Database(type, host, user, pw, name);
+ var result = db.query("select count(*) from db");
+ res.write(result.toSource());
+ return;
+};
+
+
+helma.lib = "Database";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/File.js b/modules/helma/File.js
new file mode 100644
index 00000000..c5ad66f6
--- /dev/null
+++ b/modules/helma/File.js
@@ -0,0 +1,760 @@
+/*
+ * 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-2007 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: File.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+
+/**
+ * @fileoverview Default properties and methods of the File prototype.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/File.js')
+ */
+
+
+/**
+ * Define the global namespace if not existing
+ */
+if (!global.helma) {
+ global.helma = {};
+}
+
+
+/**
+ * Constructor for File objects, providing read and
+ * write access to the file system.
+ * @class This class represents a local file or directory
+ * @param {String} path as String, can be either absolute or relative to the helma home directory
+ * @constructor
+ */
+helma.File = function(path) {
+ var BufferedReader = java.io.BufferedReader;
+ var File = java.io.File;
+ var Writer = java.io.Writer;
+ var FileReader = java.io.FileReader;
+ var PrintWriter = java.io.PrintWriter;
+ var FileOutputStream = java.io.FileOutputStream;
+ var OutputStreamWriter = java.io.OutputStreamWriter;
+ var FileInputStream = java.io.FileInputStream;
+ var InputStreamReader = java.io.InputStreamReader;
+ var EOFException = java.io.EOFException;
+ var IllegalStateException = java.lang.IllegalStateException;
+ var IllegalArgumentException = java.lang.IllegalArgumentException
+
+ var self = this;
+
+ var file;
+ try {
+ // immediately convert to absolute path - java.io.File is
+ // incredibly stupid when dealing with relative file names
+ if (arguments.length > 1)
+ file = new File(path, arguments[1]).getAbsoluteFile();
+ else
+ file = new File(path).getAbsoluteFile();
+ } catch (e) {
+ throw(e);
+ }
+
+ var readerWriter;
+ var atEOF = false;
+ var lastLine = null;
+
+ var setError = function(e) {
+ self.lastError = e;
+ };
+
+ this.lastError = null;
+
+ /** @ignore */
+ this.toString = function() {
+ return file.toString();
+ };
+
+ /**
+ * Returns the name of the file or directory represented by this File object.
+ *
+ * This is just the last name in the pathname's name sequence.
+ * If the pathname's name sequence is empty, then the empty
+ * string is returned.
+ *
+ * @returns String containing the name of the file or directory
+ * @type String
+ */
+ this.getName = function() {
+ var name = file.getName();
+ return (name == null ? "" : name);
+ };
+
+ /**
+ * Returns true if the file represented by this File object
+ * is currently open.
+ *
+ * @returns Boolean
+ * @type Boolean
+ */
+ this.isOpened = function() {
+ return (readerWriter != null);
+ };
+
+ /**
+ * Opens the file represented by this File object. If the file exists,
+ * it is used for reading, otherwise it is opened for writing.
+ * If the encoding argument is specified, it is used to read or write
+ * the file. Otherwise, the platform's default encoding is used.
+ *
+ * @param {Object} options an optional argument holder object.
+ * The following options are supported:
+ * - charset name of encoding to use for reading or writing
+ * - append whether to append to the file if it exists
+ * @returns Boolean true if the operation succeeded
+ * @type Boolean
+ */
+ this.open = function(options) {
+ if (self.isOpened()) {
+ setError(new IllegalStateException("File already open"));
+ return false;
+ }
+ // We assume that the BufferedReader and PrintWriter creation
+ // cannot fail except if the FileReader/FileWriter fails.
+ // Otherwise we have an open file until the reader/writer
+ // get garbage collected.
+ var charset = options && options.charset;
+ var append = options && options.append;
+ try {
+ if (file.exists() && !append) {
+ if (charset) {
+ readerWriter = new BufferedReader(
+ new InputStreamReader(new FileInputStream(file), charset));
+ } else {
+ readerWriter = new BufferedReader(new FileReader(file));
+ }
+ } else {
+ if (append && charset) {
+ readerWriter = new PrintWriter(
+ new OutputStreamWriter(new FileOutputStream(file, true), charset));
+ } else if (append) {
+ readerWriter = new PrintWriter(
+ new OutputStreamWriter(new FileOutputStream(file, true)));
+ } else if (charset) {
+ readerWriter = new PrintWriter(file, charset);
+ } else {
+ readerWriter = new PrintWriter(file);
+ }
+ }
+ return true;
+ } catch (e) {
+ setError(e);
+ return false;
+ }
+ return;
+ };
+
+ /**
+ * Tests whether the file or directory represented by this File object exists.
+ *
+ * @returns Boolean true if the file or directory exists; false otherwise
+ * @type Boolean
+ */
+ this.exists = function() {
+ return file.exists();
+ };
+
+ /**
+ * Returns the pathname string of this File object's parent directory.
+ *
+ * @returns String containing the pathname of the parent directory
+ * @type String
+ */
+ this.getParent = function() {
+ if (!file.getParent())
+ return null;
+ return new helma.File(file.getParent());
+ };
+
+ /**
+ * This methods reads characters until an end of line/file is encountered
+ * then returns the string for these characters (without any end of line
+ * character).
+ *
+ * @returns String of the next unread line in the file
+ * @type String
+ */
+ this.readln = function() {
+ if (!self.isOpened()) {
+ setError(new IllegalStateException("File not opened"));
+ return null;
+ }
+ if (!(readerWriter instanceof BufferedReader)) {
+ setError(new IllegalStateException("File not opened for reading"));
+ return null;
+ }
+ if (atEOF) {
+ setError(new EOFException());
+ return null;
+ }
+ if (lastLine != null) {
+ var line = lastLine;
+ lastLine = null;
+ return line;
+ }
+ var reader = readerWriter;
+ // Here lastLine is null, return a new line
+ try {
+ var line = readerWriter.readLine();
+ if (line == null) {
+ atEOF = true;
+ setError(new EOFException());
+ }
+ return line;
+ } catch (e) {
+ setError(e);
+ return null;
+ }
+ return;
+ };
+
+ /**
+ * Appends a string to the file represented by this File object.
+ *
+ * @param {String} what as String, to be written to the file
+ * @returns Boolean
+ * @type Boolean
+ * @see #writeln
+ */
+ this.write = function(what) {
+ if (!self.isOpened()) {
+ setError(new IllegalStateException("File not opened"));
+ return false;
+ }
+ if (!(readerWriter instanceof PrintWriter)) {
+ setError(new IllegalStateException("File not opened for writing"));
+ return false;
+ }
+ if (what != null) {
+ readerWriter.print(what.toString());
+ }
+ return true;
+ };
+
+ /**
+ * Appends a string with a platform specific end of
+ * line to the file represented by this File object.
+ *
+ * @param {String} what as String, to be written to the file
+ * @returns Boolean
+ * @type Boolean
+ * @see #write
+ */
+ this.writeln = function(what) {
+ if (self.write(what)) {
+ readerWriter.println();
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * Tests whether this File object's pathname is absolute.
+ *
+ * The definition of absolute pathname is system dependent.
+ * On UNIX systems, a pathname is absolute if its prefix is "/".
+ * On Microsoft Windows systems, a pathname is absolute if its prefix
+ * is a drive specifier followed by "\\", or if its prefix is "\\".
+ *
+ * @returns Boolean if this abstract pathname is absolute, false otherwise
+ * @type Boolean
+ */
+ this.isAbsolute = function() {
+ return file.isAbsolute();
+ };
+
+ /**
+ * Deletes the file or directory represented by this File object.
+ *
+ * @returns Boolean
+ * @type Boolean
+ */
+ this.remove = function() {
+ if (self.isOpened()) {
+ setError(new IllegalStateException("An openened file cannot be removed"));
+ return false;
+ }
+ return file["delete"]();
+ };
+
+ /**
+ * List of all files within the directory represented by this File object.
+ *
+ * You may pass a RegExp Pattern to return just files matching this pattern.
+ *
+ * Example: var xmlFiles = dir.list(/.*\.xml/);
+ *
+ * @param {RegExp} pattern as RegExp, optional pattern to test each file name against
+ * @returns Array the list of file names
+ * @type Array
+ */
+ this.list = function(pattern) {
+ if (self.isOpened())
+ return null;
+ if (!file.isDirectory())
+ return null;
+ if (pattern) {
+ var fileList = file.list();
+ var result = [];
+ for (var i in fileList) {
+ if (pattern.test(fileList[i]))
+ result.push(fileList[i]);
+ }
+ return result;
+ }
+ return file.list();
+ };
+
+ /**
+ * Purges the content of the file represented by this File object.
+ *
+ * @returns Boolean
+ * @type Boolean
+ */
+ this.flush = function() {
+ if (!self.isOpened()) {
+ setError(new IllegalStateException("File not opened"));
+ return false;
+ }
+ if (readerWriter instanceof Writer) {
+ try {
+ readerWriter.flush();
+ } catch (e) {
+ setError(e);
+ return false;
+ }
+ } else {
+ setError(new IllegalStateException("File not opened for write"));
+ return false; // not supported by reader
+ }
+ return true;
+ };
+
+ /**
+ * Closes the file represented by this File object.
+ *
+ * @returns Boolean
+ * @type Boolean
+ */
+ this.close = function() {
+ if (!self.isOpened())
+ return false;
+ try {
+ atEOF = false;
+ lastLine = null;
+ readerWriter.close();
+ readerWriter = null;
+ return true;
+ } catch (e) {
+ setError(e);
+ readerWriter = null;
+ return false;
+ }
+ };
+
+ /**
+ * Returns the pathname string of this File object.
+ *
+ * The resulting string uses the default name-separator character
+ * to separate the names in the name sequence.
+ *
+ * @returns String of this file's pathname
+ * @type String
+ */
+ this.getPath = function() {
+ var path = file.getPath();
+ return (path == null ? "" : path);
+ };
+
+ /**
+ * Contains the last error that occured, if any.
+ * @returns String
+ * @type String
+ * @see #clearError
+ */
+ this.error = function() {
+ if (this.lastError == null) {
+ return "";
+ } else {
+ var exceptionName = this.lastError.getClass().getName();
+ var l = exceptionName.lastIndexOf(".");
+ if (l > 0)
+ exceptionName = exceptionName.substring(l + 1);
+ return exceptionName + ": " + this.lastError.getMessage();
+ }
+ };
+
+ /**
+ * Clears any error message that may otherwise be returned by the error method.
+ *
+ * @see #error
+ */
+ this.clearError = function() {
+ this.lastError = null;
+ return;
+ };
+
+ /**
+ * Tests whether the application can read the file
+ * represented by this File object.
+ *
+ * @returns Boolean true if the file exists and can be read; false otherwise
+ * @type Boolean
+ */
+ this.canRead = function() {
+ return file.canRead();
+ };
+
+ /**
+ * Tests whether the file represented by this File object is writable.
+ *
+ * @returns Boolean true if the file exists and can be modified; false otherwise.
+ * @type Boolean
+ */
+ this.canWrite = function() {
+ return file.canWrite();
+ };
+
+ /**
+ * Returns the absolute pathname string of this file.
+ *
+ * If this File object's pathname is already absolute, then the pathname
+ * string is simply returned as if by the getPath() method. If this
+ * abstract pathname is the empty abstract pathname then the pathname
+ * string of the current user directory, which is named by the system
+ * property user.dir, is returned. Otherwise this pathname is resolved
+ * in a system-dependent way. On UNIX systems, a relative pathname is
+ * made absolute by resolving it against the current user directory.
+ * On Microsoft Windows systems, a relative pathname is made absolute
+ * by resolving it against the current directory of the drive named by
+ * the pathname, if any; if not, it is resolved against the current user
+ * directory.
+ *
+ * @returns String The absolute pathname string
+ * @type String
+ */
+ this.getAbsolutePath = function() {
+ var absolutPath = file.getAbsolutePath();
+ return (absolutPath == null ? "" : absolutPath);
+ };
+
+ /**
+ * Returns the length of the file represented by this File object.
+ *
+ * The return value is unspecified if this pathname denotes a directory.
+ *
+ * @returns Number The length, in bytes, of the file, or 0L if the file does not exist
+ * @type Number
+ */
+ this.getLength = function() {
+ return file.length();
+ };
+
+ /**
+ * Tests whether the file represented by this File object is a directory.
+ *
+ * @returns Boolean true if this File object is a directory and exists; false otherwise
+ * @type Boolean
+ */
+ this.isDirectory = function() {
+ return file.isDirectory();
+ };
+
+ /**
+ * Tests whether the file represented by this File object is a normal file.
+ *
+ * A file is normal if it is not a directory and, in addition, satisfies
+ * other system-dependent criteria. Any non-directory file created by a
+ * Java application is guaranteed to be a normal file.
+ *
+ * @returns Boolean true if this File object is a normal file and exists; false otherwise
+ * @type Boolean
+ */
+ this.isFile = function() {
+ return file.isFile();
+ };
+
+ /**
+ * Returns the time when the file represented by this File object was last modified.
+ *
+ * A number representing the time the file was last modified,
+ * measured in milliseconds since the epoch (00:00:00 GMT, January 1, 1970),
+ * or 0L if the file does not exist or if an I/O error occurs.
+ *
+ * @returns Number in milliseconds since 00:00:00 GMT, January 1, 1970
+ * @type Number
+ */
+ this.lastModified = function() {
+ return file.lastModified();
+ };
+
+ /**
+ * Creates the directory represented by this File object.
+ *
+ * @returns Boolean true if the directory was created; false otherwise
+ * @type Boolean
+ */
+ this.makeDirectory = function() {
+ if (self.isOpened())
+ return false;
+ // don't do anything if file exists or use multi directory version
+ return (file.exists() || file.mkdirs());
+ };
+
+ /**
+ * Renames the file represented by this File object.
+ *
+ * Whether or not this method can move a file from one
+ * filesystem to another is platform-dependent. The return
+ * value should always be checked to make sure that the
+ * rename operation was successful.
+ *
+ * @param {FileObject} toFile as FileObject of the new path
+ * @returns true if the renaming succeeded; false otherwise
+ * @type Boolean
+ */
+ this.renameTo = function(toFile) {
+ if (toFile == null) {
+ setError(new IllegalArgumentException("Uninitialized target File object"));
+ return false;
+ }
+ if (self.isOpened()) {
+ setError(new IllegalStateException("An openened file cannot be renamed"));
+ return false;
+ }
+ if (toFile.isOpened()) {
+ setError(new IllegalStateException("You cannot rename to an openened file"));
+ return false;
+ }
+ return file.renameTo(new java.io.File(toFile.getAbsolutePath()));
+ };
+
+ /**
+ * Returns true if the file represented by this File object
+ * has been read entirely and the end of file has been reached.
+ *
+ * @returns Boolean
+ * @type Boolean
+ */
+ this.eof = function() {
+ if (!self.isOpened()) {
+ setError(new IllegalStateException("File not opened"));
+ return true;
+ }
+ if (!(readerWriter instanceof BufferedReader)) {
+ setError(new IllegalStateException("File not opened for read"));
+ return true;
+ }
+ if (atEOF)
+ return true;
+ if (lastLine != null)
+ return false;
+ try {
+ lastLine = readerWriter.readLine();
+ if (lastLine == null)
+ atEOF = true;
+ return atEOF;
+ } catch (e) {
+ setError(e);
+ return true;
+ }
+ };
+
+ /**
+ * This methods reads all the lines contained in the
+ * file and returns them.
+ *
+ * @return String of all the lines in the file
+ * @type String
+ */
+ this.readAll = function() {
+ // Open the file for readAll
+ if (self.isOpened()) {
+ setError(new IllegalStateException("File already open"));
+ return null;
+ }
+ try {
+ if (file.exists()) {
+ readerWriter = new BufferedReader(new FileReader(file));
+ } else {
+ setError(new IllegalStateException("File does not exist"));
+ return null;
+ }
+ if (!file.isFile()) {
+ setError(new IllegalStateException("File is not a regular file"));
+ return null;
+ }
+
+ // read content line by line to setup proper eol
+ var buffer = new java.lang.StringBuffer(file.length() * 1.10);
+ while (true) {
+ var line = readerWriter.readLine();
+ if (line == null)
+ break;
+ if (buffer.length() > 0)
+ buffer.append("\n"); // EcmaScript EOL
+ buffer.append(line);
+ }
+
+ // Close the file
+ readerWriter.close();
+ readerWriter = null;
+ return buffer.toString();
+ } catch (e) {
+ readerWriter = null;
+ setError(e);
+ return null;
+ }
+ };
+
+
+ /**
+ * This method removes a directory recursively .
+ *
+ * DANGER! DANGER! HIGH VOLTAGE!
+ * The directory is deleted recursively without
+ * any warning or precautious measures.
+ */
+ this.removeDirectory = function() {
+ if (!file.isDirectory())
+ return false;
+ var arr = file.list();
+ for (var i=0; i
+ * Useful for passing it to a function instead of an request object.
+ */
+ this.toByteArray = function() {
+ if (!this.exists())
+ return null;
+ var body = new java.io.ByteArrayOutputStream();
+ var stream = new java.io.BufferedInputStream(
+ new java.io.FileInputStream(this.getAbsolutePath())
+ );
+ var buf = java.lang.reflect.Array.newInstance(
+ java.lang.Byte.TYPE, 1024
+ );
+ var read;
+ while ((read = stream.read(buf)) > -1)
+ body.write(buf, 0, read);
+ stream.close();
+ return body.toByteArray();
+ };
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+}
+
+
+/** @ignore */
+helma.File.toString = function() {
+ return "[helma.File]";
+};
+
+
+helma.File.separator = java.io.File.separator;
+
+
+helma.lib = "File";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Ftp.js b/modules/helma/Ftp.js
new file mode 100644
index 00000000..67296bfa
--- /dev/null
+++ b/modules/helma/Ftp.js
@@ -0,0 +1,568 @@
+/*
+ * 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-2007 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Ftp.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+
+/**
+ * @fileoverview Default properties and methods of the FTP prototype.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Ftp.js')
+ */
+
+// requires helma.File
+app.addRepository("modules/helma/File.js");
+
+/**
+ * Define the global namespace if not existing
+ */
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Constructor for FTP client objects, to send and receive files from an FTP server.
+ *
+ * @class This class represents a FTP client, providing
+ * access to an FTP server.
+ *
+ * @example var ftp = new helma.Ftp("ftp.mydomain.com");
+ * @param {String} server as String, the address of the FTP Server to connect to
+ * @constructor
+ */
+helma.Ftp = function(server) {
+ var OK = 0;
+ var SOCKET = 1;
+ var TIMEOUT = 2;
+ var LOGIN = 10;
+ var LOGOUT = 11;
+ var BINARY = 20;
+ var ASCII = 21;
+ var ACTIVE = 22;
+ var PASSIVE = 23;
+ var CD = 30;
+ var LCD = 31;
+ var PWD = 32;
+ var DIR = 33;
+ var MKDIR = 34;
+ var RMDIR = 35;
+ var GET = 40;
+ var PUT = 41;
+ var DELETE = 42;
+ var RENAME = 43;
+
+ var FTP = Packages.org.apache.commons.net.ftp.FTP;
+ var FtpClient = Packages.org.apache.commons.net.ftp.FTPClient;
+ var BufferedInputStream = java.io.BufferedInputStream;
+ var BufferedOutputStream = java.io.BufferedOutputStream;
+ var FileInputStream = java.io.FileInputStream;
+ var FileOutputStream = java.io.FileOutputStream;
+ var ByteArrayInputStream = java.io.ByteArrayInputStream;
+ var ByteArrayOutputStream = java.io.ByteArrayOutputStream;
+
+ var self = this;
+ var className = "helma.Ftp";
+
+ var ftpclient = new FtpClient();
+ var localDir;
+
+ var error = function(methName, errMsg) {
+ var tx = java.lang.Thread.currentThread();
+ tx.dumpStack();
+ app.log("Error in " + className + ":" + methName + ": " + errMsg);
+ return;
+ };
+
+ var debug = function(methName, msg) {
+ msg = msg ? " " + msg : "";
+ app.debug(className + ":" + methName + msg);
+ return;
+ };
+
+ var setStatus = function(status) {
+ if (self.status === OK) {
+ self.status = status;
+ }
+ return;
+ };
+
+ var getStatus = function() {
+ return self.status;
+ };
+
+ this.server = server;
+ this.status = OK;
+
+ /** @ignore */
+ this.toString = function() {
+ return "[helma.Ftp " + server + "]";
+ };
+
+ /**
+ * Set the default timeout in milliseconds to use when opening a socket.
+ */
+ this.setReadTimeout = function(timeout) {
+ try {
+ ftpclient.setDefaultTimeout(timeout);
+ debug("setReadTimeout", timeout);
+ return true;
+ } catch(x) {
+ error("setReadTimeout", x);
+ setStatus(SOCKET);
+ }
+ return false;
+ };
+
+ /**
+ * Sets the timeout in milliseconds to use when reading from the data connection.
+ */
+ this.setTimeout = function(timeout) {
+ try {
+ ftpclient.setDataTimeout(timeout);
+ debug("setTimeout", timeout);
+ return true;
+ } catch(x) {
+ error("setTimeout", x);
+ setStatus(TIMEOUT);
+ }
+ return false;
+ };
+
+ /**
+ * Logs in to the FTP server.
+ *
+ * @param {String} username as String
+ * @param {String} password as String
+ * @return Boolean true if the login was successful, otherwise false
+ * @type Boolean
+ */
+ this.login = function(username, password) {
+ try {
+ ftpclient.connect(this.server);
+ var result = ftpclient.login(username, password);
+ debug("login", username + "@" + server);
+ return result;
+ } catch(x) {
+ error("login", x);
+ setStatus(LOGIN);
+ }
+ return false;
+ };
+
+ /**
+ * Sets transfer mode to binary for transmitting images and other non-text files.
+ *
+ * @example ftp.binary();
+ */
+ this.binary = function() {
+ try {
+ var result = ftpclient.setFileType(FTP.BINARY_FILE_TYPE);
+ debug("binary");
+ return result;
+ } catch(x) {
+ error("binary", x);
+ setStatus(BINARY);
+ }
+ return false;
+ };
+
+ /**
+ * Sets transfer mode to ascii for transmitting text-based data.
+ *
+ * @example ftp.ascii();
+ */
+ this.ascii = function() {
+ try {
+ var result = ftpclient.setFileType(FTP.ASCII_FILE_TYPE);
+ debug("ascii");
+ return result;
+ } catch(x) {
+ error("ascii", x);
+ setStatus(ASCII);
+ }
+ return false;
+ };
+
+ /**
+ * Switches the connection to use active mode.
+ *
+ * @example ftp.active();
+ */
+ this.active = function() {
+ try {
+ ftpclient.enterLocalActiveMode();
+ debug("active");
+ return true;
+ } catch(x) {
+ error("active", x);
+ setStatus(ACTIVE);
+ }
+ return false;
+ };
+
+ /**
+ * Switches the connection to use passive mode.
+ *
+ * @example ftp.passive();
+ */
+ this.passive = function() {
+ try {
+ ftpclient.enterLocalPassiveMode();
+ debug("passive");
+ return true;
+ } catch(x) {
+ error("passive", x);
+ setStatus(PASSIVE);
+ }
+ return false;
+ };
+
+ /**
+ * Returns the path of the current working directory.
+ *
+ * @example var remotepath = ftp.pwd();
+ * @type String
+ * @return String containing the current working directory path
+ */
+ this.pwd = function() {
+ try {
+ debug("pwd");
+ return ftpclient.printWorkingDirectory();
+ } catch(x) {
+ error("pwd", x);
+ setStatus(PWD);
+ }
+ return;
+ };
+
+ /**
+ * Returns a listing of the files contained in a directory on the FTP server.
+ *
+ * Lists the files contained in the current working
+ * directory or, if an alternative path is specified, the
+ * files contained in the specified directory.
+ *
+ * @example var filelist = ftp.dir();
+ * @param {String} path as String, optional alternative directory
+ * @return Array containing the list of files in that directory
+ * @type Array
+ */
+ this.dir = function(path) {
+ try {
+ debug("dir", path);
+ return ftpclient.listNames(path ? path : ".");
+ } catch(x) {
+ error("dir", x);
+ setStatus(DIR);
+ }
+ return;
+ };
+
+ /**
+ * Creates a new directory on the server.
+ *
+ * The name of the directory is determined as the function's
+ * string parameter. Returns false when an error occured
+ * (e.g. due to access restrictions, directory already
+ * exists etc.), otherwise true.
+ *
+ * @param {String} dir as String, the name of the directory to be created
+ * @return Boolean true if the directory was successfully created, false if there was an error
+ * @type Boolean
+ */
+ this.mkdir = function(dir) {
+ try {
+ var result = ftpclient.makeDirectory(dir);
+ debug("mkdir", dir);
+ return result;
+ } catch(x) {
+ error("mkdir", x);
+ setStatus(MKDIR);
+ }
+ return false;
+ };
+
+ /**
+ * Deletes a directory on the FTP server.
+ *
+ * @param {String} dir as String, the name of the directory to be deleted
+ * @return Boolean true if the deletion was successful, false otherwise
+ * @type Boolean
+ */
+ this.rmdir = function(dir) {
+ try {
+ var result = ftpclient.removeDirectory(dir);
+ debug("rmdir", dir);
+ return result;
+ } catch(x) {
+ error("rmdir", x);
+ setStatus(RMDIR);
+ }
+ return false;
+ };
+
+ /**
+ * Changes the working directory on the FTP server.
+ *
+ * @example ftp.cd("/home/users/fred/www"); // use absolute pathname
+ * @example ftp.cd(".."); // change to parent directory
+ * @example ftp.cd("images"); // use relative pathname
+ * @param {String} dir as String, the path that the remote working directory should be changed to
+ */
+ this.cd = function(path) {
+ try {
+ var result = ftpclient.changeWorkingDirectory(path);
+ debug("cd", path);
+ return result;
+ } catch(x) {
+ error("cd", x);
+ setStatus(CD);
+ }
+ return false;
+ };
+
+ /**
+ * Changes the working directory of the local machine when being connected to an FTP server.
+ *
+ * @example ftp.lcd("/home/users/fred/www"); // use absolute pathname
+ * @example ftp.lcd(".."); // change to parent directory
+ * @example ftp.lcd("images"); // use relative pathname
+ * @param {String} dir as String, the path that the local working directory should be changed to
+ */
+ this.lcd = function(dir) {
+ try {
+ localDir = new helma.File(dir);
+ if (!localDir.exists()) {
+ localDir.mkdir();
+ debug("lcd", dir);
+ }
+ return true;
+ } catch(x) {
+ error("lcd", x);
+ setStatus(LCD);
+ }
+ return false;
+ };
+
+ /**
+ * Transfers a file from the local file system to the remote server.
+ *
+ * Returns true if the transmission was successful, otherwise false.
+ *
+ * @param {String} localFile as String, the name of the file to be uploaded
+ * @param {String} remoteFile as String, the name of the remote destination file
+ * @return Boolean true if the file was successfully uploaded, false if there was an error
+ * @type Boolean
+ */
+ this.putFile = function(localFile, remoteFile) {
+ try {
+ if (localFile instanceof File || localFile instanceof helma.File) {
+ var f = localFile;
+ } else if (typeof localFile == "string") {
+ if (localDir == null)
+ var f = new helma.File(localFile);
+ else
+ var f = new helma.File(localDir, localFile);
+ }
+ var stream = new BufferedInputStream(
+ new FileInputStream(f.getPath())
+ );
+ if (!remoteFile) {
+ remoteFile = f.getName();
+ }
+ var result = ftpclient.storeFile(remoteFile, stream);
+ stream.close();
+ debug("putFile", remoteFile);
+ return result;
+ } catch(x) {
+ error("putFile", x);
+ setStatus(PUT);
+ }
+ return false;
+ };
+
+ /**
+ * Transfers text from a string to a file on the FTP server.
+ *
+ * @example ftp.putString("Hello, World!", "message.txt");
+ * @param {String} str as String, the text content that should be uploaded
+ * @param {String} remoteFile as String, the name of the remote destination file
+ * @param {String} charset as String, optional
+ * @return Boolean true if the file was successfully uploaded, false if there was an error
+ * @type Boolean
+ */
+ this.putString = function(str, remoteFile, charset) {
+ try {
+ str = new java.lang.String(str);
+ var bytes = charset ? str.getBytes(charset) : str.getBytes();
+ var stream = ByteArrayInputStream(bytes);
+ var result = ftpclient.storeFile(remoteFile, stream);
+ debug("putString", remoteFile);
+ return result;
+ } catch(x) {
+ error("putString", x);
+ setStatus(PUT);
+ }
+ return false;
+ };
+
+ /**
+ * Transfers a byte array to a file on the FTP server.
+ * @param {Array} bytes The byte array that should be uploaded
+ * @param {String} remoteFile The name of the remote destination file
+ * @return Boolean True if the file was successfully uploaded, false if there was an error
+ * @type Boolean
+ */
+ this.putBytes = function(bytes, remoteFile) {
+ try {
+ var stream = ByteArrayInputStream(bytes);
+ var result = ftpclient.storeFile(remoteFile, stream);
+ debug("putBytes", remoteFile);
+ return result;
+ } catch(x) {
+ error("putBytes", x);
+ setStatus(PUT);
+ }
+ return false;
+ };
+
+ /**
+ * Transfers a file from the FTP server to the local file system.
+ *
+ * @example ftp.getFile(".htaccess", "htaccess.txt");
+ * @param {String} remoteFile as String, the name of the file that should be downloaded
+ * @param {String} localFile as String, the name which the file should be stored under
+ * @see #cd
+ * @see #lcd
+ */
+ this.getFile = function(remoteFile, localFile) {
+ try {
+ if (localDir == null)
+ var f = new helma.File(localFile);
+ else
+ var f = new helma.File(localDir, localFile);
+ var stream = new BufferedOutputStream(
+ new FileOutputStream(f.getPath())
+ );
+ var result = ftpclient.retrieveFile(remoteFile, stream);
+ stream.close();
+ debug("getFile", remoteFile);
+ return result;
+ } catch(x) {
+ error("getFile", x);
+ setStatus(GET);
+ }
+ return false;
+ };
+
+ /**
+ * Retrieves a file from the FTP server and returns it as string.
+ *
+ * @example var str = ftp.getString("messages.txt");
+ * @param {String} remoteFile as String, the name of the file that should be downloaded
+ * @return String containing the data of the downloaded file
+ * @type String
+ * @see #cd
+ */
+ this.getString = function(remoteFile) {
+ try {
+ var stream = ByteArrayOutputStream();
+ ftpclient.retrieveFile(remoteFile, stream);
+ debug("getString", remoteFile);
+ return stream.toString();
+ } catch(x) {
+ error("getString", x);
+ setStatus(GET);
+ }
+ return;
+ };
+
+ /**
+ * Deletes a file on the FTP server.
+ *
+ * @example var str = ftp.deleteFile("messages.txt");
+ * @param {String} remoteFile as String, the name of the file to be deleted
+ * @return Boolean true if the deletion was successful, false otherwise
+ * @type Boolean
+ */
+ this.deleteFile = function(remoteFile) {
+ try {
+ var result = ftpclient.deleteFile(remoteFile);
+ debug("deleteFile", remoteFile);
+ return result;
+ } catch(x) {
+ error("deleteFile", x);
+ setStatus(DELETE);
+ }
+ return false;
+ };
+
+ /**
+ * Renames a file on the FTP server.
+ *
+ * @example var success = ftp.renameFile("messages.tmp", "messages.txt");
+ * @param {String} from the name of the original file
+ * @param {String} to the new name the original file should get
+ * @return Boolean true if renaming the remote file was successful, false otherwise
+ * @type Boolean
+ */
+ this.renameFile = function(from, to) {
+ try {
+ var result = ftpclient.rename(from, to);
+ debug("renameFile", from + "->" + to);
+ return result;
+ } catch(x) {
+ error("renameFile", x);
+ setStatus(RENAME);
+ }
+ return false;
+ };
+
+ /**
+ * Terminates the current FTP session.
+ */
+ this.logout = function() {
+ try {
+ var result = ftpclient.logout();
+ ftpclient.disconnect();
+ debug("logout");
+ return result;
+ } catch(x) {
+ error("logout", x);
+ setStatus(LOGOUT);
+ }
+ return false;
+ };
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+}
+
+
+/** @ignore */
+helma.Ftp.toString = function() {
+ return "[helma.Ftp]";
+};
+
+
+helma.lib = "Ftp";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Group.js b/modules/helma/Group.js
new file mode 100644
index 00000000..a15081dc
--- /dev/null
+++ b/modules/helma/Group.js
@@ -0,0 +1,875 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Group.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview A JavaScript library wrapping
+ * Packages.helma.extensions.helmagroups
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Group.js')
+ */
+
+// Define the global namespace if not existing
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Constructs a new helma.Group Object.
+ * @class This is what is retrieved through groups.get(groupName),
+ * wrapping the root object of each group tree.
+ * @param {FIXME} javaGroup FIXME
+ * @constructor
+ */
+helma.Group = function(javaGroup) {
+ // private variable containing the wrapper object
+ var groupRoot = new helma.Group.GroupObject(javaGroup.getRoot());
+
+ /**
+ * @returns the wrapped java object Group
+ */
+ this.getJavaObject = function() {
+ return javaGroup;
+ };
+
+ /**
+ * sets a key/value pair on the group's root,
+ * wraps the function of the wrapper object
+ */
+ this.set = function(key, val, sendMode) {
+ groupRoot.set(key, val, sendMode);
+ return;
+ };
+
+ /**
+ * removes a key from the group's root,
+ * wraps the function of the root GroupObject
+ */
+ this.remove = function(key, sendMode) {
+ return groupRoot.remove(key, sendMode);
+ };
+
+ /**
+ * retrieves a key from the group's root,
+ * wraps the function of the root GroupObject
+ */
+ this.get = function(key) {
+ return groupRoot.get(key);
+ };
+
+ /**
+ * @see helma.Group.GroupObject.listChildren
+ */
+ this.listChildren = function() {
+ return groupRoot.listChildren();
+ };
+
+ /**
+ * @see helma.Group.GroupObject.listProperties
+ */
+ this.listProperties = function() {
+ return groupRoot.listProperties();
+ };
+
+ /**
+ * @see helma.Group.GroupObject.countChildren
+ */
+ this.countChildren = function() {
+ return groupRoot.countChildren();
+ };
+
+ /**
+ * @see helma.Group.GroupObject.countProperties
+ */
+ this.countProperties = function() {
+ return groupRoot.countProperties();
+ };
+
+ /**
+ * calls a function in all connected applications
+ * (to be specific: in all registered localClients).
+ * @param method name of the method in xmlrpc-style: test
+ * is called as root.test(), stories.137.render
+ * is called as root.stories.get("137").render() etc etc.
+ * @param argArr array of arguments to the remote method
+ * @param sendMode as defined for helma.Group.GroupObject
+ * @returns array of result objects
+ */
+ this.callFunction = function(method, argArr, sendMode) {
+ groups.checkWriteAccess(javaGroup);
+ if (sendMode == null) {
+ sendMode = helma.Group.GroupObject.DEFAULT_GET;
+ }
+ var argVec = new java.util.Vector();
+ for (var i=0; i
+ * a String
+ * a String containing slashes
+ * an Array containing String keys
+ *
+ * @param {Number} The value to set the property to.
+ * @param {Object} The mode to use when committing the change to
+ * the helma.Group
+ */
+ this.set = function(key, val, sendMode) {
+ if (!key) {
+ throw "helma.Group.GroupObject.set(): key can't be null";
+ }
+ checkWriteAccess();
+ // check content type of value:
+ var ok = false;
+ if (val == null)
+ ok = true;
+ else if (typeof(val) == "string")
+ ok = true;
+ else if (typeof(val) == "number")
+ ok = true;
+ else if (typeof(val) == "boolean")
+ ok = true;
+ else if (val instanceof Date)
+ ok = true;
+ else if (val instanceof helma.Group.GroupObject)
+ ok = true;
+ if (ok == false) {
+ throw "only primitive values, Date and helma.Group.GroupObject allowed in helma.Group.GroupObject.set()";
+ }
+ if (sendMode == null) {
+ sendMode = helma.Group.GroupObject.DEFAULT_GET;
+ }
+
+ if (keyIsPath(key)) {
+ var obj = createPath(this, key);
+ if (obj != null) {
+ obj.set(getLastKeyElement(key), val, sendMode);
+ }
+ } else {
+ // set a property/child of this object
+ if (val == null) {
+ // null values aren't permitted in the group,
+ // setting a property to null is the same as deleting it
+ this.remove(key, sendMode);
+ } else if (val instanceof helma.Group.GroupObject) {
+ // replicate helma.Group.GroupObject
+ javaGroupObject.put(key, val.getJavaObject(), sendMode);
+ } else {
+ // put the primitive property (or maybe replicate,
+ // decision's up to helma.Group.GroupObject)
+ if (val instanceof Date) {
+ // convert javascript dates to java dates
+ val = new java.util.Date(val.getTime());
+ }
+ javaGroupObject.put(key, val, sendMode);
+ }
+ }
+ return;
+ };
+
+
+
+ /**
+ * Removes a property or a child GroupObject from this instance.
+ * The Key may be a String, an Array or a String with separator characters ("/").
+ * In the latter two cases the argument is considered a path and
+ * the function walks down that path to find the GroupObject and
+ * deletes it.
+ * @param {Object} key Either
+ *
+ * - a String
+ * - a String containing slashes
+ * - an Array containing String keys
+ *
+ * @param {Number} The mode to use when committing the change to
+ * the helma.Group
+ */
+ this.remove = function(key, sendMode) {
+ checkWriteAccess();
+ if (sendMode == null) {
+ sendMode = helma.Group.GroupObject.DEFAULT_GET;
+ }
+ if (keyIsPath(key)) {
+ var obj = walkPath(this, key);
+ if (obj != null) {
+ obj.remove(getLastKeyElement(key));
+ }
+ } else {
+ javaGroupObject.remove(key, sendMode);
+ }
+ return;
+ };
+
+
+ /**
+ * Returns either a property or a child GroupObject from
+ * this GroupObject instance. The key passed as argument
+ * may be a String, an Array containing Strings or a
+ * String containing separator characters ("/"). In the latter
+ * two cases the argument is considered a path and
+ * the function walks down that path to find the requested
+ * GroupObject.
+ * @param {Object} key Either
+ *
+ * - a String
+ * - a String containing slashes
+ * - an Array containing String keys
+ *
+ * @return Depending on the argument either the appropriate property
+ * value or a helma.Group.GroupObject
+ * @type Object
+ */
+ this.get = function(key) {
+ if (key == null) {
+ return null;
+ }
+ if (keyIsPath(key)) {
+ var obj = walkPath(this, key);
+ if (obj != null) {
+ return obj.get(getLastKeyElement(key));
+ } else {
+ return null;
+ }
+ } else if (javaGroupObject.hasProperty(key)) {
+ // we got a primitive property
+ var val = javaGroupObject.getProperty(key);
+ if (val instanceof java.util.Date) {
+ // convert java dates to javascript dates
+ val = new Date(val);
+ }
+ return val;
+ } else if (javaGroupObject.hasChild(key)) {
+ // we got a child object
+ return new helma.Group.GroupObject(javaGroupObject.getChild(key));
+ }
+ return null;
+ };
+
+
+ /**
+ * Gets a property from this GroupObject. The key passed as argument
+ * is always considered a property even if it contains a slash.
+ * This is actually a workaround for the fact that other
+ * instances of the group not using the javascript extension aren't forbidden
+ * to add properties containing a slash in the property's name.
+ * So, using this extension we can at least read the property.
+ * @param {String} key The name of the property to return
+ * @returns The value of the property
+ * @type Object
+ */
+ this.getProperty = function(key) {
+ if (key == null) {
+ return null;
+ } else if (javaGroupObject.hasProperty(key)) {
+ // we got a primitive property
+ var val = javaGroupObject.getProperty(key);
+ if (val instanceof java.util.Date) {
+ // convert java dates to javascript dates
+ val = new Date(val);
+ }
+ return val;
+ }
+ return null;
+ }
+
+
+ /**
+ * Exchanges this GroupObject with the one passed
+ * as argument. This is done by exchanging the wrapped
+ * instance of helma.extensions.helmagroups.GroupObject
+ * @param {GroupObject} The GroupObject to use
+ * @returns The GroupObject with the exchanged wrapped java object
+ * @type GroupObject
+ */
+ this.wrap = function(newGroupObject) {
+ checkWriteAccess();
+ if (javaGroupObject.getState() != helmagroups.GroupObject.REPLICATED) {
+ throw "helma.Group.GroupObject.wrap() may only be called on replicated GroupObjects";
+ }
+ if (newGroupObject == null || !(newGroupObject instanceof helma.Group.GroupObject)) {
+ throw "helma.Group.GroupObject.wrap() requires a helma.Group.GroupObject as an argument";
+ }
+ javaGroupObject.wrap(newGroupObject.getJavaObject());
+ return this;
+ };
+
+ /**
+ * Clones this GroupObject and returns it.
+ * This method should be considered if many properties
+ * of a GroupObject must be set or modified since every
+ * change to an already replicated GroupObject will
+ * result in immediate network traffic. Using unwrap
+ * one can modify several properties and then commit
+ * the GroupObject at once using {@link #wrap).
+ * @returns A clone of this GroupObject
+ * @type GroupObject
+ */
+ this.unwrap = function() {
+ var javaGroupObjectClone = javaGroupObject.clone();
+ javaGroupObjectClone.setChildren(new java.util.Hashtable());
+ javaGroupObjectClone.setState(helmagroups.GroupObject.LOCAL);
+ javaGroupObjectClone.setPath(null);
+ return new helma.Group.GroupObject(javaGroupObjectClone);
+ };
+
+ /**
+ * Converts this GroupObject into a vanilla Object
+ * @returns An Object containing all properties of this GroupObject
+ * @type Object
+ */
+ this.toJSObject = function() {
+ var key;
+ var obj = {};
+ var e = javaGroupObject.properties();
+ while(e.hasMoreElements()) {
+ obj[key = e.nextElement()] = javaGroupObject.getProperty(key);
+ }
+ return obj;
+ };
+
+ /**
+ * Returns an Array containing all child GroupObjects
+ * @returns An Array containing GroupObjects
+ * @type Array
+ */
+ this.listChildren = function() {
+ var arr = [];
+ var e = javaGroupObject.children();
+ while(e.hasMoreElements()) {
+ arr.push(e.nextElement());
+ }
+ return arr;
+ };
+
+ /**
+ * Returns an Array containing all property
+ * names of this GroupObject instance
+ * @returns An Array containing property names
+ * @type Array
+ */
+ this.listProperties = function() {
+ var arr = [];
+ var e = javaGroupObject.properties();
+ while(e.hasMoreElements()) {
+ arr.push(e.nextElement());
+ }
+ return arr;
+ };
+
+ /**
+ * Returns the number of child GroupObjects
+ * @returns The number of child GroupObjects of this
+ * helma.Group.GroupObject instance
+ * @type Number
+ */
+ this.countChildren = function() {
+ var ht = javaGroupObject.getChildren();
+ if (ht == null) {
+ return 0;
+ } else {
+ return ht.size();
+ }
+ };
+
+ /**
+ * Returns the number of properties of this GroupObject
+ * @return The number of properties
+ * @type Number
+ */
+ this.countProperties = function() {
+ var ht = javaGroupObject.getProperties();
+ return (ht == null) ? 0 : ht.size();
+ };
+
+ /**
+ * Returns true if the GroupObject is not replicated
+ * @returns True if this GroupObject is still local
+ * @type Boolean
+ */
+ this.isLocal = function() {
+ return (javaGroupObject.getState()
+ == helmagroups.GroupObject.LOCAL);
+ };
+
+ /** @ignore */
+ this.toString = function() {
+ return javaGroupObject.toString();
+ };
+
+ return this;
+};
+
+/**
+ * Static properties of GroupObject constructor function.
+ * These values determine if and for how many confirmation of the
+ * group members this instance waits after a modification.
+ * These values are passed through to org.jgroups.blocks.GroupRequest,
+ * for further comments see the sourcecode of that class
+ */
+// wait just for the first response
+helma.Group.GroupObject.GET_FIRST = 1;
+// wait until all members have responded
+helma.Group.GroupObject.GET_ALL = 2;
+// wait for majority (50% + 1) to respond
+helma.Group.GroupObject.GET_MAJORITY = 3;
+// wait for majority of all members (may block!)
+helma.Group.GroupObject.GET_ABS_MAJORITY = 4;
+// don't wait for any response (fire & forget)
+helma.Group.GroupObject.GET_NONE = 6;
+// default: wait for all responses
+helma.Group.GroupObject.DEFAULT_GET = helma.Group.GroupObject.GET_ALL;
+
+/**
+ * This is mounted as "groups".
+ * @class The root for all groups started in this application
+ * @constructor
+ */
+helma.Group.Manager = function() {
+ var helmagroups = Packages.helma.extensions.helmagroups;
+ var extension = helmagroups.GroupExtension.self;
+
+ if (extension == null) {
+ throw("helma.Group.Manager requires the HelmaGroups Extension \
+ located in lib/ext or the application's top-level directory \
+ [http://adele.helma.org/download/helma/contrib/helmagroups/]");
+ }
+
+
+ /**
+ * get a java object Group for a groupname.
+ * object is fetched regardless of connection status
+ * @returns null if group is not defined
+ */
+ var getJavaGroup = function(name) {
+ return extension.checkAppLink(app.name).get(name);
+ };
+
+
+ /**
+ * visible to scripting env: get a group, wrapped as a javascript helma.Group object.
+ * the group must be defined in app.properties: group.nameXX =
+ * and can then be accessed like this group.get("nameXX")
+ * @returns null if group is not defined or not connected
+ */
+ this.get = function(name) {
+ var javaGroup = getJavaGroup(name);
+ if (javaGroup == null || javaGroup.isConnected() == false) {
+ return null;
+ } else {
+ return new helma.Group(javaGroup);
+ }
+ };
+
+
+ /**
+ * checks for write access to a group according to app.properties
+ * group.nameXX.writable must be true so that this function returns
+ * @param nameOrJGroup can be the name of a group or a java Group itself
+ * @throws an error if group is not writable
+ */
+ this.checkWriteAccess = function(nameOrJGroup) {
+ var extension = helmagroups.GroupExtension.self;
+ var jAppLink = extension.checkAppLink(app.name);
+ if (nameOrJGroup instanceof helmagroups.Group) {
+ // arg was a Group
+ var javaGroup = nameOrJGroup;
+ } else {
+ // arg was the name of the group
+ var javaGroup = jAppLink.get(nameOrJGroup);
+ }
+ if (javaGroup != null && jAppLink.isWritable(javaGroup) == false) {
+ throw("tried to access write-protected group");
+ }
+ return true;
+ };
+
+
+ /**
+ * try to connect a group
+ * @returns false if group is not found
+ */
+ this.connect = function(name) {
+ var javaGroup = getJavaGroup(name);
+ if (javaGroup == null) {
+ return false;
+ }
+ javaGroup.connect();
+ return true;
+ };
+
+
+ /**
+ * try to disconnect from a group
+ * @returns false if group is not found
+ */
+ this.disconnect = function(name) {
+ var javaGroup = getJavaGroup(name);
+ if (javaGroup == null) {
+ return false;
+ }
+ javaGroup.disconnect();
+ return true;
+ };
+
+
+ /**
+ * try to disconnect and connect again to a group
+ * @returns false if group is not found
+ */
+ this.reconnect = function(name) {
+ var javaGroup = getJavaGroup(name);
+ if (javaGroup == null) {
+ return false;
+ }
+ javaGroup.reconnect();
+ return true;
+ };
+
+
+ /**
+ * try to reset a group (if application may write in group).
+ * all instances of the group empty their cache.
+ * @returns false if group is not found
+ */
+ this.reset = function(name) {
+ var javaGroup = getJavaGroup(name);
+ if (javaGroup == null) {
+ return false;
+ }
+ groups.checkWriteAccess(javaGroup);
+ javaGroup.reset();
+ return true;
+ };
+
+
+ /**
+ * try to destroy a group (if application may write in group).
+ * all other instances of the group disconnect
+ * @returns false if group is not found
+ */
+ this.destroy = function(name) {
+ var javaGroup = getJavaGroup(name);
+ if (javaGroup == null) {
+ return false;
+ }
+ groups.checkWriteAccess(javaGroup);
+ javaGroup.destroy();
+ return true;
+ };
+
+
+ /**
+ * try to restart a group (if application may write in group).
+ * all other instances of the group disconnect and reconnect - each app after a different pause
+ * so that they don't all come up at the same moment
+ * @returns false if group is not found
+ */
+ this.restart = function(name) {
+ var javaGroup = getJavaGroup(name);
+ if (javaGroup == null) {
+ return false;
+ }
+ groups.checkWriteAccess(javaGroup);
+ javaGroup.restart();
+ return true;
+ };
+
+
+ /**
+ * list the members of this group (ie instances of Group, one helma server is one instance)
+ * @returns array of strings, false if group is not found
+ */
+ this.listMembers = function(name) {
+ var javaGroup = getJavaGroup(name);
+ if (javaGroup == null) {
+ return false;
+ }
+ var addrArr = javaGroup.info.listMembers();
+ var arr = [];
+ for (var i=0; i
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Html.js')
+ */
+
+// take care of any dependencies
+app.addRepository('modules/core/String.js');
+app.addRepository('modules/core/Object.js');
+app.addRepository('modules/core/Array.js');
+
+/**
+ * Define the global namespace if not existing
+ */
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Creates a new instance of helma.Html
+ * @class This class provides various methods for rendering
+ * X/Html tags.
+ * @returns A newly created instance of helma.Html
+ * @constructor
+ */
+helma.Html = function() {
+ return this;
+};
+
+/**
+ * Static helper method that renders an arbitrary markup part.
+ * @param {String} name The element's name
+ * @param {String} start Prefix of each rendered element
+ * @param {String} end Suffix of each rendered element
+ * @param {Object} attr Optional element attributes
+ */
+helma.Html.renderMarkupPart = function(name, start, end, attr) {
+ res.write(start);
+ res.write(name);
+ if (attr) {
+ for (var i in attr) {
+ if (i == "prefix" || i == "suffix" ||
+ i == "default" || attr[i] == null) {
+ continue;
+ }
+ res.write(" ");
+ res.write(i);
+ res.write("=\"");
+ res.write(attr[i]);
+ res.write("\"");
+ }
+ }
+ res.write(end);
+ return;
+};
+
+/**
+ * Static helper method used in helma.Html.checkBox
+ * and helma.Html.dropDown to check if a current value
+ * matches against one or more selected values passed
+ * as argument
+ * @param {String} value The current value to check
+ * @param {String|Array} selectedValue Either a single
+ * value to check against the current value, or an array
+ * containing values.
+ * @returns True in case the value is among the selected
+ * values, false otherwise
+ * @type Boolean
+ */
+helma.Html.isSelected = function(value, selectedValue) {
+ if (selectedValue == null || value == null)
+ return false;
+ if (selectedValue instanceof Array)
+ return selectedValue.contains(value);
+ return value == selectedValue;
+};
+
+
+/** @ignore */
+helma.Html.prototype.toString = function() {
+ return "[helma.Html]";
+};
+
+/**
+ * Renders the opening tag of an arbitrary x/html tag
+ * @param {String} name The tag name
+ * @param {Object} attr An optional object containing element attributes
+ */
+helma.Html.prototype.openTag = function(name, attr) {
+ helma.Html.renderMarkupPart(name, "<", ">", attr);
+ return;
+};
+
+/**
+ * Returns the opening tag of an arbitrary x/html tag
+ * @param {String} name The tag name
+ * @param {Object} attr An optional object containing element attributes
+ * @returns The rendered x/html opening tag
+ * @type String
+ * @see #openTag
+ */
+helma.Html.prototype.openTagAsString = function(name, attr) {
+ res.push();
+ helma.Html.renderMarkupPart(name, "<", ">", attr);
+ return res.pop();
+};
+
+/**
+ * Renders the closing tag of an arbitrary x/html tag
+ * @param {String} name The tag name
+ */
+helma.Html.prototype.closeTag = function(name) {
+ helma.Html.renderMarkupPart(name, "", ">", null);
+ return;
+};
+
+/**
+ * Returns the closing tag of an arbitray x/html element
+ * @param {String} name The tag name
+ * @returns The rendered closing tag
+ * @type String
+ * @see #closeTag
+ */
+helma.Html.prototype.closeTagAsString = function(name) {
+ res.push();
+ helma.Html.renderMarkupPart(name, "", ">", null);
+ return res.pop();
+};
+
+/**
+ * Renders an empty arbitrary x/html tag ("contentless tag")
+ * @param {String} name The tag name
+ * @param {Object} attr An optional object containing tag attributes
+ */
+helma.Html.prototype.tag = function(name, attr) {
+ helma.Html.renderMarkupPart(name, "<", " />", attr);
+ return;
+};
+
+/**
+ * Returns an empty arbitrary x/html tag ("contentless tag")
+ * @param {String} name The tag name
+ * @param {Object} attr An optional object containing tag attributes
+ * @returns The rendered element
+ * @type String
+ * @see #tag
+ */
+helma.Html.prototype.tagAsString = function(name, attr) {
+ res.push();
+ helma.Html.renderMarkupPart(name, "<", " />", attr);
+ return res.pop();
+};
+
+/**
+ * Renders an arbitrary x/html element
+ * @param {String} name The element name
+ * @param {String} str The content of the element
+ * @param {Object} attr An optional object containing element attributes
+ */
+helma.Html.prototype.element = function(name, str, attr) {
+ helma.Html.renderMarkupPart(name, "<", ">", attr);
+ res.write(str);
+ helma.Html.renderMarkupPart(name, "", ">");
+ return;
+};
+
+/**
+ * Return an arbitrary x/html element
+ * @param {String} name The element name
+ * @param {String} str The content of the element
+ * @param {Object} attr An optional object containing element attributes
+ * @returns The rendered element
+ * @type String
+ * @see #element
+ */
+helma.Html.prototype.elementAsString = function(name, str, attr) {
+ res.push();
+ this.element(name, str, attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html link tag
+ * @param {Object} attr An object containing the link attributes
+ * @param {String} text The text to appear as link
+ */
+helma.Html.prototype.link = function(attr, text) {
+ if (!attr) {
+ res.write("[Html.link: insufficient arguments]");
+ return;
+ }
+ this.openTag("a", attr);
+ res.write(text);
+ this.closeTag("a");
+ return;
+};
+
+/**
+ * Returns a rendered x/html link tag
+ * @param {Object} attr An object containing the link attributes
+ * @param {String} text The text to appear as link
+ * @returns The rendered link tag
+ * @type String
+ * @see #link
+ */
+helma.Html.prototype.linkAsString = function(attr, text) {
+ res.push();
+ this.link(attr, text);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html input tag of type "hidden"
+ * @param {Object} param An object containing the tag attributes
+ */
+helma.Html.prototype.hidden = function(param) {
+ if (!param) {
+ res.write("[Html.hidden: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ attr.type = "hidden";
+ attr.value = (attr.value != null) ? encodeForm(attr.value) : "";
+ this.tag("input", attr);
+ return;
+};
+
+/**
+ * Returns a rendered x/html input tag of type "hidden"
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered input element
+ * @type String
+ * @see #hidden
+ */
+helma.Html.prototype.hiddenAsString = function(attr) {
+ res.push();
+ this.hidden(attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html text input tag
+ * @param {Object} param An object containing the tag attributes
+ */
+helma.Html.prototype.input = function(param) {
+ if (!param) {
+ res.write("[Html.input: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ attr.type = "text";
+ if (!attr.size)
+ attr.size = 20;
+ attr.value = (attr.value != null) ? encodeForm(attr.value) : "";
+ this.tag("input", attr);
+ return;
+};
+
+/**
+ * Returns a rendered x/html text input tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered text input tag
+ * @type String
+ * @see #input
+ */
+helma.Html.prototype.inputAsString = function(attr) {
+ res.push();
+ this.input(attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html textarea tag
+ * @param {Object} param An object containing the tag attributes
+ */
+helma.Html.prototype.textArea = function(param) {
+ if (!param) {
+ res.write("[Html.textArea: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ var value = (attr.value != null) ? encodeForm(attr.value) : "";
+ delete attr.value;
+ this.openTag("textarea", attr);
+ res.write(value);
+ this.closeTag("textarea");
+ return;
+};
+
+/**
+ * Returns a rendered x/html textarea tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered textarea tag
+ * @type String
+ * @see #textArea
+ */
+helma.Html.prototype.textAreaAsString = function(attr) {
+ res.push();
+ this.textArea(attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html checkbox input tag
+ * @param {Object} param An object containing the tag attributes
+ */
+helma.Html.prototype.checkBox = function(param) {
+ if (!param) {
+ res.write("[Html.checkBox: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ attr.type = "checkbox";
+ if (attr.selectedValue != null) {
+ if (helma.Html.isSelected(param.value, param.selectedValue))
+ attr.checked = "checked";
+ else
+ delete attr.checked;
+ delete attr.selectedValue;
+ }
+ this.tag("input", attr);
+ return;
+};
+
+/**
+ * Returns a rendered x/html checkbox input tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered checkbox tag
+ * @type String
+ * @see #checkBox
+ */
+helma.Html.prototype.checkBoxAsString = function(attr) {
+ res.push();
+ this.checkBox(attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html radiobutton input tag
+ * @param {Object} param An object containing the tag attributes
+ */
+helma.Html.prototype.radioButton = function(param) {
+ if (!param) {
+ res.write("[Html.radioButton: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ attr.type = "radio";
+ if (attr.selectedValue != null) {
+ if (attr.value == attr.selectedValue)
+ attr.checked = "checked";
+ else
+ delete attr.checked;
+ delete attr.selectedValue;
+ }
+ this.tag("input", attr);
+ return;
+};
+
+/**
+ * Returns a rendered x/html radio input tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered element
+ * @type String
+ * @see #radioButton
+ */
+helma.Html.prototype.radioButtonAsString = function(attr) {
+ res.push();
+ this.radioButton(attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html submit input tag
+ * @param {Object} param An object containing the tag attributes
+ */
+helma.Html.prototype.submit = function(param) {
+ if (!param) {
+ res.write("[Html.submit: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ attr.type = "submit";
+ if (!attr.name)
+ attr.name = attr.type;
+ attr.value = (attr.value != null) ? encodeForm(attr.value) : attr.type;
+ this.tag("input", attr);
+ return;
+};
+
+/**
+ * Returns a rendered x/html submit input tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered submit input tag
+ * @type String
+ * @see #submit
+ */
+helma.Html.prototype.submitAsString = function(attr) {
+ res.push();
+ this.submit(attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html button input tag
+ * @param {Object} param An object containing the tag attributes
+ */
+helma.Html.prototype.button = function(param) {
+ if (!param) {
+ res.write("[Html.button: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ attr.type = "button";
+ if (!attr.name)
+ attr.name = attr.type;
+ attr.value = (attr.value != null) ? encodeForm(attr.value) : attr.type;
+ this.tag("input", attr);
+ return;
+};
+
+/**
+ * Returns a rendered x/html button input tag
+ * @param {Object} param An object containing the tag attributes
+ * @returns The rendered button input tag
+ * @type String
+ * @see #button
+ */
+helma.Html.prototype.buttonAsString = function(attr) {
+ res.push();
+ this.button(attr);
+ return res.pop();
+};
+
+/**
+ * Renders a x/html drop down select box
+ * @param {Object} param An object containing the tag attributes
+ * @param {Array} options Either an array of strings, an array with
+ * several {value: v, display: d}
objects, or a collection
+ * of ["value", "display"]
arrays in an array
+ * @param {String} selectedValue The value to pre-select
+ * @param {String} firstOption An optional first option to display in the
+ * select box (this option will always have no value)
+ */
+helma.Html.prototype.dropDown = function(param, options, selectedValue, firstOption) {
+ if (!param) {
+ res.write("[Html.dropDown: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ if (!attr.size)
+ attr.size = 1;
+ this.openTag("select", attr);
+ res.write("\n ");
+ if (firstOption) {
+ this.openTag("option", {value: ""});
+ res.write(firstOption);
+ this.closeTag("option");
+ res.write("\n ");
+ }
+ for (var i in options) {
+ var attr = new Object();
+ var display = "";
+ if ((options[i] instanceof Array) && options[i].length > 0) {
+ // option is an array
+ attr.value = options[i][0];
+ display = options[i][1];
+ } else if (options[i].value != null && options[i].display != null) {
+ // option is an object
+ attr.value = options[i].value;
+ if (options[i]["class"] != null) {
+ attr["class"] = options[i]["class"];
+ }
+ display = options[i].display;
+ } else {
+ // assume option is a string
+ attr.value = i;
+ display = options[i];
+ }
+ if (helma.Html.isSelected(attr.value, selectedValue))
+ attr.selected = "selected";
+ this.openTag("option", attr);
+ res.write(display);
+ this.closeTag("option");
+ res.write("\n ");
+ }
+ this.closeTag("select");
+ res.write("\n ");
+ return;
+};
+
+/**
+ * Returns a rendered x/html drop down select box
+ * @param {Object} param An object containing the tag attributes
+ * @param {Array} options Either an array of strings, an array with
+ * several {value: v, display: d}
objects, or a collection
+ * of ["value", "display"]
arrays in an array
+ * @param {String} selectedValue The value to pre-select
+ * @param {String} firstOption An optional first option to display in the
+ * select box (this option will always have no value)
+ * @returns The rendered drop down select box
+ * @type String
+ * @see #dropDown
+ */
+helma.Html.prototype.dropDownAsString = function(attr, options, selectedValue, firstOption) {
+ res.push();
+ this.dropDown(attr, options, selectedValue, firstOption);
+ return res.pop();
+};
+
+/**
+ * Renders an image map based on an array containing the map parameters.
+ * @param {String} name The name of the image map
+ * @param {Array} param An array containing objects, where each of them
+ * contains the attributes for a single image map entry
+ */
+helma.Html.prototype.map = function(name, param) {
+ if (!name || !param) {
+ res.write("[Html.map: insufficient arguments]");
+ return;
+ }
+ this.openTag("map", {name: name});
+ var attr = Object.prototype.reduce.call(param);
+ for (var i in areas) {
+ if (!areas[i].alt)
+ areas[i].alt = "";
+ if (!areas[i].shape)
+ areas[i].shape = "rect";
+ this.openTag("area", areas[i]);
+ }
+ this.closeTag("map");
+ return;
+};
+
+/**
+ * Returns a rendered image map based on an array containing the map parameters.
+ * @param {String} name The name of the image map
+ * @param {Array} areas An array containing objects, where each of them
+ * contains the attributes for a single image map entry
+ * @returns The rendered image map
+ * @type String
+ * @see #map
+ */
+helma.Html.prototype.mapAsString = function(name, areas) {
+ res.push();
+ this.map(name, areas);
+ return res.pop();
+};
+
+/**
+ * Renders a complete x/html table.
+ * @param {Array} headers An array containing table headers
+ * @param {Array} data A two-dimensional array containing the table data
+ * @param {Object} param An object containing the following properties:
+ *
+ * table
: Attributes to render within the opening <table>
tag
+ * tr
: Attributes to render within each <tr>
tag
+ * td
: Attributes to render within each <td>
tag
+ * th
: Attributes to render within each <th>
tag
+ * trHead
: Attributes to render within each <tr>
tag
+ in the header area of the table
+ * trEven
: Attributes to render within each even <tr>
tag
+ * trOdd
: Attributes to render within each odd <tr>
tag
+ * tdEven
: Attributes to render within each even <td>
tag
+ * tdOdd
: Attributes to render within each odd <td>
tag
+ * thEven
: Attributes to render within each even <th>
tag
+ * thOdd
: Attributes to render within each odd <th>
tag
+ *
+ */
+helma.Html.prototype.table = function(headers, data, param) {
+ if (!param) {
+ res.write("[Html.table: insufficient arguments]");
+ return;
+ }
+ var attr = Object.prototype.reduce.call(param);
+ if (!attr.trHead) attr.trHead = attr.tr;
+ if (!attr.trEven) attr.trEven = attr.tr;
+ if (!attr.trOdd) attr.trOdd = attr.tr;
+ if (!attr.tdEven) attr.tdEven = attr.td;
+ if (!attr.tdOdd) attr.tdOdd = attr.td;
+ if (!attr.thEven) attr.thEven = attr.th;
+ if (!attr.thOdd) attr.thOdd = attr.th;
+ this.openTag("table", attr.table);
+ if (headers) {
+ this.openTag("tr", attr.trHead);
+ for (var i in headers) {
+ var evenOdd = i % 2 == 0 ? "Even" : "Odd";
+ this.openTag("th", attr["th"+evenOdd]);
+ res.write(headers[i]);
+ this.closeTag("th");
+ }
+ this.closeTag("tr");
+ }
+ for (var i in data) {
+ var evenOdd = i % 2 == 0 ? "Even" : "Odd";
+ this.openTag("tr", attr["tr"+evenOdd]);
+ for (var j in data[i]) {
+ var evenOddCell = j % 2 == 0 ? "Even" : "Odd";
+ this.openTag("td", attr["td"+evenOddCell]);
+ res.write(data[i][j]);
+ this.closeTag("td");
+ }
+ this.closeTag("tr");
+ }
+ this.closeTag("table");
+ return;
+};
+
+/**
+ * Returns a rendered x/html table
+ * @param {Array} headers An array containing table headers
+ * @param {Array} data A two-dimensional array containing the table data
+ * @param {Object} attr For a description see {@link #table}
+ * @returns The rendered table
+ * @type String
+ * @see #table
+ */
+helma.Html.prototype.tableAsString = function(headers, data, attr) {
+ res.push();
+ this.table(headers, data, attr);
+ return res.pop();
+};
+
+/*********************************************************************/
+/* */
+/* the following functions should be deliberately altered or removed */
+/* (most of these can easily be replaced by the methods they call) */
+/* */
+/*********************************************************************/
+
+/**
+ * Renders an x/html opening link tag
+ * @param {Object} attr An object containing the tag attributes
+ */
+helma.Html.prototype.openLink = function(attr) {
+ this.openTag("a", attr);
+ return;
+};
+
+/**
+ * Returns an x/html opening link tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered open link tag
+ * @type String
+ * @see #openTag
+ */
+helma.Html.prototype.openLinkAsString = function(attr) {
+ return this.openTagAsString("a", attr);
+};
+
+/**
+ * Renders an x/html closing link tag
+ */
+helma.Html.prototype.closeLink = function() {
+ this.closeTag("a");
+ return;
+};
+
+/**
+ * Returns a rendered x/html closing link tag
+ * @returns Rhe rendered closing link tag
+ * @type String
+ * @see #closeLink
+ */
+helma.Html.prototype.closeLinkAsString = function() {
+ return this.closeTagAsString("a");
+};
+
+/**
+ * Renders a color definition string. If the string passed as
+ * argument contains only hex characters it will be prefixed with a
+ * hash sign if necessary, otherwise this method assumes that the
+ * value is a named color (eg. "yellow").
+ * @param {String} c The color definintion
+ * @deprecated
+ */
+helma.Html.prototype.color = function(c) {
+ if (c) {
+ var nonhex = /[^0-9,a-f]/gi;
+ if (!c.match(nonhex)) {
+ c = c.pad("0", 6);
+ res.write("#");
+ }
+ }
+ res.write(c);
+ return;
+};
+
+/**
+ * Returns a color definition.
+ * @param {String} c The color definintion
+ * @returns The rendered color definition
+ * @type String
+ * @see #color
+ * @deprecated
+ */
+helma.Html.prototype.colorAsString = function(c) {
+ res.push();
+ this.color(c);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html opening form tag
+ * @param {Object} attr An object containing the tag attributes
+ */
+helma.Html.prototype.form = function(attr) {
+ this.openTag("form", attr);
+ return;
+};
+
+/**
+ * Returns an x/html opening form tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered opening form tag
+ * @type String
+ * @see #form
+ */
+helma.Html.prototype.formAsString = function(attr) {
+ res.push();
+ this.form(attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html password input tag
+ * @param {Object} attr An object containing the tag attributes
+ */
+helma.Html.prototype.password = function(attr) {
+ if (!attr) {
+ res.write("[Html.password: insufficient arguments]");
+ return;
+ }
+ attr.type = "password";
+ if (!attr.size)
+ attr.size = 20;
+ this.tag("input", attr);
+ return;
+};
+
+/**
+ * Returns a rendered x/html password input tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered password input tag
+ * @type String
+ * @see #password
+ */
+helma.Html.prototype.passwordAsString = function(attr) {
+ res.push();
+ this.password(attr);
+ return res.pop();
+};
+
+/**
+ * Renders an x/html file input tag
+ * @param {Object} attr An object containing the tag attributes
+ */
+helma.Html.prototype.file = function(attr) {
+ if (!attr) {
+ res.write("[Html.file: insufficient arguments]");
+ return;
+ }
+ attr.type = "file";
+ this.tag("input", attr);
+ return;
+};
+
+/**
+ * Returns a rendered x/html file input tag
+ * @param {Object} attr An object containing the tag attributes
+ * @returns The rendered file input tag
+ * @type String
+ * @see #file
+ */
+helma.Html.prototype.fileAsString = function(attr) {
+ res.push();
+ this.file(attr);
+ return res.pop();
+};
+
+/**
+ * Parses the string passed as argument and converts any
+ * URL in it into a link tag
+ * @param {String} str The string wherein URLs should be
+ * converted into link tags
+ * @returns The string containing URLs converted into link tags
+ * @type String
+ */
+helma.Html.prototype.activateUrls = function(str) {
+ var re = /(^|\/>|\s+)([fhtpsr]+:\/\/[^\s]+?)([\.,;:\)\]\"]?)(?=[\s<]|$)/gim;
+ var func = function(str, p1, p2, p3) {
+ res.push();
+ res.write(p1);
+ res.write('');
+ res.write(p2.clip(50, "...", true));
+ res.write('');
+ res.write(p3);
+ return res.pop();
+ };
+ return str.replace(re, func);
+};
+
+/**
+ * Creates a new TableWriter instance
+ * @class This class provides various methods for
+ * programmatically creating an x/html table.
+ * @param {Number} numberOfColumns The number of columns in the table
+ * @param {Object} attr An object containing attributes to use when
+ * rendering the single table elements. For a description see {@link #table}.
+ * @returns An instance of TableWriter
+ * @constructor
+ */
+helma.Html.TableWriter = function(numberOfColumns, attr) {
+ if (isNaN(numberOfColumns))
+ throw "Illegal argument in TableWriter(): first argument must be a number";
+ if (numberOfColumns < 1)
+ throw "Illegal argument in TableWriter(): first argument must be > 1";
+ /** @private */
+ this.ncols = numberOfColumns;
+ /** @private */
+ this.written = 0;
+ // if no attributes object given, create an empty one
+ if (!attr)
+ attr = {};
+ if (!attr.trEven) attr.trEven = attr.tr;
+ if (!attr.trOdd) attr.trOdd = attr.tr;
+ if (!attr.trHead) attr.trHead = attr.trEven;
+ if (!attr.tdEven) attr.tdEven = attr.td;
+ if (!attr.tdOdd) attr.tdOdd = attr.td;
+ if (!attr.thEven) attr.thEven = attr.th;
+ if (!attr.thOdd) attr.thOdd = attr.th;
+ /** @private */
+ this.attr = attr;
+
+ /**
+ * If set to true the first row of the table data is rendered
+ * using <th>
tags (defaults to false).
+ * @type Boolean
+ */
+ this.writeHeader = false;
+
+ /**
+ * If set to true the TableWriter returns the rendered table
+ * as string, otherwise the table is written directly to response,
+ * which is the default.
+ * @type Boolean
+ */
+ this.writeString = false;
+
+ this.dontEnum("ncols", "written", "attr", "writeHeader", "writeString");
+
+ return this;
+};
+
+/** @ignore */
+helma.Html.TableWriter.prototype.toString = function() {
+ return "[helma.Html.TableWriter]";
+}
+
+/**
+ * Writes a single table cell to response.
+ * @param {String} text The content of the table cess
+ * @param {Object} attr An optional object containig attributes
+ * to render for this table cell
+ */
+helma.Html.TableWriter.prototype.write = function(text, attr) {
+ // set up some variables
+ var isHeaderRow = (this.writeHeader && this.written < this.ncols);
+ var isNewRow = (this.written % this.ncols == 0);
+ var isEvenRow = ((this.written / this.ncols) % 2 == 0);
+ var isEvenCol = ((this.written % this.ncols) % 2 == 0);
+ // write out table and table row tags
+ if (this.written == 0) {
+ if (this.writeString)
+ res.push();
+ helma.Html.prototype.openTag.call(this, "table", this.attr.table);
+ helma.Html.prototype.openTag.call(this, "tr", this.attr.trHead);
+ } else if (isNewRow) {
+ helma.Html.prototype.closeTag.call(this, "tr");
+ if (isEvenRow)
+ helma.Html.prototype.openTag.call(this, "tr", this.attr.trEven);
+ else
+ helma.Html.prototype.openTag.call(this, "tr", this.attr.trOdd);
+ }
+ // get the attribute object for the table cell
+ if (!attr) {
+ // no explicit attribute given
+ if (isEvenCol) {
+ attr = isHeaderRow ? this.attr.thEven : this.attr.tdEven;
+ } else {
+ attr = isHeaderRow ? this.attr.thOdd : this.attr.tdOdd;
+ }
+ }
+ // write out table cell tag
+ helma.Html.prototype.openTag.call(this, isHeaderRow ? "th" : "td", attr);
+ // write out table cell contents
+ if (text) {
+ res.write(text);
+ }
+ // close table cell
+ helma.Html.prototype.closeTag.call(this, isHeaderRow ? "th" : "td");
+ if (attr && !isNaN(attr.colspan)) {
+ this.written += attr.colspan;
+ } else {
+ this.written += 1;
+ }
+ return;
+};
+
+/**
+ * Closes all open table tags. If {@link #writeString} is set to
+ * true, this method returns the rendered table.
+ * @returns The rendered table, if {@link #writeString} is set to
+ * true, otherwise void.
+ * @type String
+ */
+helma.Html.TableWriter.prototype.close = function() {
+ if (this.written > 0) {
+ while (this.written++ % this.ncols != 0)
+ res.write(" | ");
+ res.write("");
+ this.written = 0;
+ }
+ if (this.writeString)
+ return res.pop();
+ return;
+};
+
+
+
+helma.lib = "Html";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+for (var i in helma[helma.lib].TableWriter.prototype)
+ helma[helma.lib].TableWriter.prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Http.js b/modules/helma/Http.js
new file mode 100644
index 00000000..628ae100
--- /dev/null
+++ b/modules/helma/Http.js
@@ -0,0 +1,817 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Http.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+
+/**
+ * @fileoverview Fields and methods of the helma.Http class.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Http.js')
+ */
+
+// take care of any dependencies
+app.addRepository('modules/core/Date.js');
+
+
+/**
+ * Define the global namespace if not existing
+ */
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Creates a new instance of helma.Http
+ * @class This class provides functionality to programatically issue
+ * an Http request based on java.net.HttpUrlConnection.
+ * By default the request will use method GET
.
+ * @returns A newly created helma.Http instance
+ * @constructor
+ */
+helma.Http = function() {
+ var self = this;
+ var proxy = null;
+ var content = "";
+ var userAgent = "Helma Http Client";
+ var method = "GET";
+ var cookies = null;
+ var credentials = null;
+ var followRedirects = true;
+ var binaryMode = false;
+ var headers = {};
+ var timeout = {
+ "connect": 0,
+ "socket": 0
+ };
+ var maxResponseSize = null;
+
+ var responseHandler = function(connection, result) {
+ var input;
+ try {
+ input = new java.io.BufferedInputStream(connection.getInputStream());
+ } catch (error) {
+ input = new java.io.BufferedInputStream(connection.getErrorStream());
+ }
+ if (input) {
+ var body = new java.io.ByteArrayOutputStream();
+ var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 1024);
+ var len;
+ var currentSize = 0;
+ while ((len = input.read(buf)) > -1) {
+ body.write(buf, 0, len);
+ currentSize += len;
+ if (maxResponseSize && currentSize > maxResponseSize) {
+ throw new Error("Maximum allowed response size is exceeded");
+ }
+ }
+ try {
+ input.close();
+ } catch (error) {
+ // safe to ignore
+ }
+ if (binaryMode && (result.code >= 200 && result.code < 300)) {
+ // only honor binaryMode if the request succeeded
+ result.content = body.toByteArray();
+ } else {
+ result.content = result.charset ?
+ body.toString(result.charset) :
+ body.toString();
+ }
+ // adjust content length
+ if (result.content) {
+ result.length = result.content.length;
+ }
+ }
+ };
+
+ /** @private */
+ var setTimeout = function(type, value) {
+ var v = java.lang.System.getProperty("java.specification.version");
+ if (parseFloat(v, 10) >= 1.5) {
+ timeout[type] = value;
+ } else {
+ app.logger.warn("helma.Http: Timeouts can only be set with Java Runtime version >= 1.5");
+ }
+ return true;
+ }
+
+ /**
+ * Sets the proxy host and port for later use. The argument must
+ * be in host:port
format (eg. "proxy.example.com:3128").
+ * @param {String} proxyString The proxy to use for this request
+ * @see #getProxy
+ */
+ this.setProxy = function(proxyString) {
+ var idx = proxyString.indexOf(":");
+ var host = proxyString.substring(0, idx);
+ var port = proxyString.substring(idx+1);
+ if (java.lang.Class.forName("java.net.Proxy") != null) {
+ // construct a proxy instance
+ var socket = new java.net.InetSocketAddress(host, port);
+ proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, socket);
+ } else {
+ // the pre jdk1.5 way: set the system properties
+ var sys = java.lang.System.getProperties();
+ if (host) {
+ app.logger.warn("[Helma Http Client] WARNING: setting system http proxy to " + host + ":" + port);
+ sys.put("http.proxySet", "true");
+ sys.put("http.proxyHost", host);
+ sys.put("http.proxyPort", port);
+ }
+ }
+ return;
+ };
+
+ /**
+ * Returns the proxy in host:port
format
+ * @return The proxy defined for this request
+ * @type String
+ * @see #setProxy
+ */
+ this.getProxy = function() {
+ if (proxy != null) {
+ return proxy.address().getHostName() + ":" + proxy.address().getPort();
+ } else if (sys.get("http.proxySet") == "true") {
+ return sys.get("http.proxyHost") + ":" + sys.get("http.proxyPort");
+ } else {
+ return null;
+ }
+ };
+
+ /**
+ * Sets the credentials for basic http authentication
+ * @param {String} username The username
+ * @param {String} password The password
+ */
+ this.setCredentials = function(username, password) {
+ var str = new java.lang.String(username + ":" + password);
+ credentials = (new Packages.sun.misc.BASE64Encoder()).encode(str.getBytes());
+ return;
+ }
+
+ /**
+ * Sets the content to send to the remote server within this request.
+ * @param {String|Object} stringOrObject The content of the request, which
+ * can be either a string or an object. In the latter case all properties
+ * and their values are concatenated into a single string.
+ * If a property is an array, then for each value the propertyname and value pair is added.
+ * If the name of an array property ends with "_array" then the _array part is removed.
+ */
+ this.setContent = function(stringOrObject) {
+ if (stringOrObject != null) {
+ if (stringOrObject.constructor == Object) {
+ res.push();
+ var value;
+ for (var key in stringOrObject) {
+ value = stringOrObject[key];
+ if (value instanceof Array) {
+ if (key.substring(key.length - 6) == "_array")
+ key = key.substring(0,key.length - 6);
+ for (var i = 0; i < value.length; i++) {
+ res.write(encodeURIComponent(key));
+ res.write("=");
+ res.write(encodeURIComponent(value[i]));
+ res.write("&");
+ }
+ } else {
+ res.write(encodeURIComponent(key));
+ res.write("=");
+ res.write(encodeURIComponent(value));
+ res.write("&");
+ }
+ }
+ content = res.pop();
+ content = content.substring(0, content.length-1);
+ } else {
+ content = stringOrObject.toString();
+ }
+ } else {
+ content = null;
+ }
+ return;
+ };
+
+ /**
+ * Sets the request method to use.
+ * @param {String} m The method to use (GET
, POST
...)
+ * @see #getMethod
+ */
+ this.setMethod = function(m) {
+ method = m;
+ return;
+ };
+
+ /**
+ * Returns the currently defined request method.
+ * @returns The method used
+ * @type String
+ * @see #setMethod
+ */
+ this.getMethod = function() {
+ return method;
+ };
+
+ /**
+ * Sets a single HTTP request header field
+ * @param {String} name The name of the header field
+ * @param {String} value The value of the header field
+ * @see #getHeader
+ */
+ this.setHeader = function(name, value) {
+ headers[name] = value;
+ return;
+ };
+
+ /**
+ * Returns the value of the request header field with the given name
+ * @param {String} name The name of the request header field
+ * @returns The value of the request header field
+ * @type String
+ * @see #setHeader
+ */
+ this.getHeader = function(name) {
+ return headers[name];
+ };
+
+ /**
+ * Adds a cookie with the name and value passed as arguments
+ * to the list of cookies to send to the remote server.
+ * @param {String} name The name of the cookie
+ * @param {String} value The value of the cookie
+ * @see #getCookie
+ * @see #getCookies
+ */
+ this.setCookie = function(name, value) {
+ if (name != null && value != null) {
+ // store the cookie in the cookies map
+ if (!cookies) {
+ cookies = {};
+ }
+ cookies[name] = new helma.Http.Cookie(name, value);
+ }
+ return;
+ };
+
+ /**
+ * Returns the value of the cookie with the given name
+ * @param {String} name The name of the cookie
+ * @returns The value of the cookie
+ * @type String
+ * @see #setCookie
+ */
+ this.getCookie = function(name) {
+ return (cookies != null) ? cookies[name] : null;
+ };
+
+ /**
+ * Adds the cookies passed as argument to the list of cookies to send
+ * to the remote server.
+ * @param {Array} cookies An array containing objects with the properties
+ * "name" (the name of the cookie) and "value" (the value of the cookie) set.
+ */
+ this.setCookies = function(cookies) {
+ if (cookies != null) {
+ for (var i=0; i
+ * url
: (String) The Url of the request
+ * location
: (String) The value of the location header field
+ * code
: (Number) The HTTP response code
+ * message
: (String) An optional HTTP response message
+ * length
: (Number) The content length of the response
+ * type
: (String) The mimetype of the response
+ * charset
: (String) The character set of the response
+ * encoding
: (String) An optional encoding to use with the response
+ * lastModified
: (String) The value of the lastModified response header field
+ * eTag
: (String) The eTag as received from the remote server
+ * cookie
: (helma.Http.Cookie) An object containing the cookie parameters, if the remote
+ server has set the "Set-Cookie" header field
+ * headers
: (java.util.Map) A map object containing the headers, access them using get("headername")
+ * content
: (String|ByteArray) The response received from the server. Can be either
+ a string or a byte array (see #setBinaryMode)
+ *
+ */
+ this.getUrl = function(url, opt) {
+ if (typeof url == "string") {
+ if (!(url = helma.Http.evalUrl(url)))
+ throw new Error("'" + url + "' is not a valid URL.");
+ } else if (!(url instanceof java.net.URL)) {
+ throw new Error("'" + url + "' is not a valid URL.");
+ }
+
+ var conn = proxy ? url.openConnection(proxy) : url.openConnection();
+ // Note: we must call setInstanceFollowRedirects() instead of
+ // static method setFollowRedirects(), as the latter will
+ // set the default value for all url connections, and will not work for
+ // url connections that have already been created.
+ conn.setInstanceFollowRedirects(followRedirects);
+ conn.setAllowUserInteraction(false);
+ conn.setRequestMethod(method);
+ conn.setRequestProperty("User-Agent", userAgent);
+
+ if (opt) {
+ if (opt instanceof Date)
+ conn.setIfModifiedSince(opt.getTime());
+ else if ((typeof opt == "string") && (opt.length > 0))
+ conn.setRequestProperty("If-None-Match", opt);
+ }
+
+ var userinfo;
+ if (userinfo = url.getUserInfo()) {
+ userinfo = userinfo.split(/:/, 2);
+ this.setCredentials(userinfo[0], userinfo[1]);
+ }
+ if (credentials != null) {
+ conn.setRequestProperty("Authorization", "Basic " + credentials);
+ }
+ // set timeouts
+ if (parseFloat(java.lang.System.getProperty("java.specification.version"), 10) >= 1.5) {
+ conn.setConnectTimeout(timeout.connect);
+ conn.setReadTimeout(timeout.socket);
+ }
+ // set header fields
+ for (var i in headers) {
+ conn.setRequestProperty(i, headers[i]);
+ }
+ // set cookies
+ if (cookies != null) {
+ var arr = [];
+ for (var i in cookies) {
+ arr[arr.length] = cookies[i].getFieldValue();
+ }
+ conn.setRequestProperty("Cookie", arr.join(";"));
+ }
+ // set content
+ if (content) {
+ conn.setRequestProperty("Content-Length", content.length);
+ conn.setDoOutput(true);
+ var out = new java.io.OutputStreamWriter(conn.getOutputStream());
+ out.write(content);
+ out.flush();
+ out.close();
+ }
+
+ var result = {
+ url: conn.getURL(),
+ location: conn.getHeaderField("location"),
+ code: conn.getResponseCode(),
+ message: conn.getResponseMessage(),
+ length: conn.getContentLength(),
+ type: conn.getContentType(),
+ encoding: conn.getContentEncoding(),
+ lastModified: null,
+ eTag: conn.getHeaderField("ETag"),
+ cookies: null,
+ headers: conn.getHeaderFields(),
+ content: null,
+ }
+
+ // parse all "Set-Cookie" header fields into an array of
+ // helma.Http.Cookie instances
+ var setCookies = conn.getHeaderFields().get("Set-Cookie");
+ if (setCookies != null) {
+ var arr = [];
+ var cookie;
+ for (var i=0; i 0) {
+ result.cookies = arr;
+ }
+ }
+
+ var lastmod = conn.getLastModified();
+ if (lastmod) {
+ result.lastModified = new Date(lastmod);
+ }
+
+ if (maxResponseSize && result.length > maxResponseSize) {
+ throw new Error("Maximum allowed response size is exceeded");
+ }
+
+ if (result.type && result.type.indexOf("charset=") != -1) {
+ var charset = result.type.substring(result.type.indexOf("charset=") + 8);
+ charset = charset.replace(/[;"]/g, '').trim();
+ result.charset = charset;
+ }
+
+ // invoke response handler
+ responseHandler(conn, result);
+
+ conn.disconnect();
+ return result;
+ }
+
+ /** @ignore */
+ this.toString = function() {
+ return "[Helma Http Client]";
+ };
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+};
+
+
+/**
+ * Evaluates the url passed as argument.
+ * @param {String} url The url or uri string to evaluate
+ * @returns If the argument is a valid url, this method returns
+ * a new instance of java.net.URL, otherwise it returns null.
+ * @type java.net.URL
+ */
+helma.Http.evalUrl = function(url) {
+ try {
+ return new java.net.URL(url);
+ } catch (err) {
+ return null;
+ }
+};
+
+
+/**
+ * Sets the global http proxy setting. If no proxy definition
+ * is passed to this method, any existing proxy setting is
+ * cleared. Internally this method sets the system properties
+ * http.proxySet
, http.proxyHost
and
+ * http.proxyPort
. Keep in mind that this is valid for
+ * the whole Java Virtual Machine, therefor using this method
+ * can potentially influence other running Helma applications too!
+ * @param {String} proxyString A proxy definition in host:port
+ * format (eg. "proxy.example.com:3128");
+ * @member helma.Http
+ */
+helma.Http.setProxy = function(proxyString) {
+ var sys = java.lang.System.getProperties();
+ if (proxyString) {
+ var idx = proxyString.indexOf(":");
+ var host = proxyString.substring(0, idx);
+ var port = proxyString.substring(idx+1);
+ if (!port)
+ port = "3128";
+ else if (typeof port == "number")
+ port = port.toString();
+ app.logger.info("helma.Http.setProxy " + proxyString);
+ sys.put("http.proxySet", "true");
+ sys.put("http.proxyHost", host);
+ sys.put("http.proxyPort", port);
+ } else {
+ sys.put("http.proxySet", "false");
+ sys.put("http.proxyHost", "");
+ sys.put("http.prodyPort", "");
+ }
+ return;
+
+};
+
+
+/**
+ * Returns the proxy setting of the Java Virtual Machine
+ * the Helma application server is running in. If no
+ * proxy is set, this method returns boolean false.
+ * @returns The global proxy setting in host:port
+ * format (eg. "proxy.example.com:3128"), or boolean false.
+ * @type String|Boolean
+ * @member helma.Http
+ */
+helma.Http.getProxy = function() {
+ var sys = java.lang.System.getProperties();
+ if (sys.get("http.proxySet") == "true")
+ return sys.get("http.proxyHost") + ":" + sys.get("http.proxyPort");
+ return false;
+};
+
+
+/**
+ * Static helper method to check if a request issued agains a
+ * Helma application is authorized or not.
+ * @param {String} name The username to check req.username against
+ * @param {String} pwd The password to check req.password against
+ * @return True if the request is authorized, false otherwise. In
+ * the latter case the current response is reset and the response code
+ * is set to "401" ("Authentication required").
+ * @type Boolean
+ */
+helma.Http.isAuthorized = function(name, pwd) {
+ if (!req.username || !req.password ||
+ req.username != name || req.password != pwd) {
+ res.reset();
+ res.status = 401;
+ res.realm = "Helma Http Authorization";
+ res.write("Authorization required.");
+ return false;
+ } else {
+ return true;
+ }
+};
+
+/** @ignore */
+helma.Http.toString = function() {
+ return "[helma.Http]";
+};
+
+/**
+ * Creates a new instance of helma.Http.Cookie
+ * @class Instances of this object represent a HTTP cookie
+ * @param {String} name The name of the cookie
+ * @param {String} value The value of the cookie
+ * @returns A newly created Cookie instance
+ * @constructor
+ */
+helma.Http.Cookie = function(name, value) {
+ /**
+ * The name of the Cookie
+ * @type String
+ */
+ this.name = name;
+
+ /**
+ * The value of the Cookie
+ * @type String
+ */
+ this.value = value;
+
+ /**
+ * An optional date defining the lifetime of this cookie
+ * @type Date
+ */
+ this.expires = null;
+
+ /**
+ * An optional path where this cookie is valid
+ * @type String
+ */
+ this.path = null;
+
+ /**
+ * An optional domain where this cookie is valid
+ * @type String
+ */
+ this.domain = null;
+
+ return this;
+}
+
+/**
+ * An instance of java.text.SimpleDateFormat used for both parsing
+ * an "expires" string into a date and vice versa
+ * @type java.text.SimpleDateFormat
+ * @final
+ */
+helma.Http.Cookie.DATEFORMAT = new java.text.SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z");
+
+
+/**
+ * A regular expression used for parsing cookie strings
+ * @type RegExp
+ * @final
+ */
+helma.Http.Cookie.PATTERN = /([^=;]+)=?([^;]*)(?:;\s*|$)/g;
+
+
+/**
+ * Parses the cookie string passed as argument into an instance of helma.Http
+ * @param {String} cookieStr The cookie string as received from the remote server
+ * @returns An instance of helma.Http.Cookie containing the cookie parameters
+ * @type helma.Http.Cookie
+ */
+helma.Http.Cookie.parse = function(cookieStr) {
+ if (cookieStr != null) {
+ var cookie = new helma.Http.Cookie;
+ var m = helma.Http.Cookie.PATTERN.exec(cookieStr);
+ if (m) {
+ cookie.name = m[1].trim();
+ cookie.value = m[2] ? m[2].trim() : "";
+ }
+ while ((m = helma.Http.Cookie.PATTERN.exec(cookieStr)) != null) {
+ var key = m[1].trim();
+ var value = m[2] ? m[2].trim() : "";
+ switch (key.toLowerCase()) {
+ case "expires":
+ // try to parse the expires date string into a date object
+ try {
+ cookie.expires = helma.Http.Cookie.DATEFORMAT.parse(value);
+ } catch (e) {
+ // ignore
+ }
+ break;
+ default:
+ cookie[key.toLowerCase()] = value;
+ break;
+ }
+ }
+ return cookie;
+ }
+ return null;
+};
+
+/**
+ * Returns this cookie in a format useable to set the HTTP header field "Cookie"
+ * @return This cookie formatted as HTTP header field value
+ * @type String
+ */
+helma.Http.Cookie.prototype.getFieldValue = function() {
+ return this.name + "=" + this.value;
+};
+
+/** @ignore */
+helma.Http.Cookie.prototype.toString = function() {
+ return "[helma.Http.Cookie " + this.name + " " + this.value + "]";
+};
+
+helma.lib = "Http";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+for (var i in helma[helma.lib].Cookie.prototype)
+ helma[helma.lib].Cookie.prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Image.js b/modules/helma/Image.js
new file mode 100644
index 00000000..0d82691b
--- /dev/null
+++ b/modules/helma/Image.js
@@ -0,0 +1,150 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Image.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Methods of the helma.Image module.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Image.js')
+ */
+
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Returns an Image object, generated from the specified source.
+ *
+ * If the JIMI package is installed, an instance of
+ * helma.image.jimi.JimiGenerator will be returned. Otherwise,
+ * if the javax.imageio package is available, an instance of
+ * helma.image.imageio.ImageIOGenerator is returned.
+ * Additionally, the class of the ImageGenerator implementation
+ * to be used can be set using the imageGenerator
+ * property in either the app.properties or server.properties
+ * file.
+ *
+ *
+ * @param {helma.File|java.io.File|String} arg image source, filename or url
+ * @return a new Image object
+ * @singleton
+ * @see Packages.helma.image.ImageGenerator
+ * @see Packages.helma.image.jimi.JimiGenerator
+ * @see Packages.helma.image.imageio.ImageIOGenerator
+ */
+helma.Image = function(arg) {
+ // according to
+ // http://grazia.helma.org/pipermail/helma-dev/2004-June/001253.html
+ var generator = Packages.helma.image.ImageGenerator.getInstance();
+ return generator.createImage(arg);
+}
+
+/** @ignore */
+helma.Image.toString = function() {
+ return "[helma.Image]";
+};
+
+
+/**
+ * Returns an ImageInfo object for the specified image file.
+ *
+ * @param {helma.File|java.io.File|String} arg image source, filename or url
+ * @returns an ImageInfo object
+ * @memberof helma.Image
+ * @see Packages.helma.image.ImageInfo
+ */
+helma.Image.getInfo = function(arg) {
+ if (arguments.length != 1) {
+ throw new java.lang.IllegalArgumentException(
+ "Image.getInfo() expects one argument"
+ );
+ }
+
+ var inp, result;
+ var info = new Packages.helma.image.ImageInfo();
+ // FIXME: we need a byte array for class comparison
+ var b = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 0);
+
+ try {
+ if (arg instanceof java.io.InputStream) {
+ inp = new java.io.InputStream(arg);
+ // FIXME: here comes a dirty hack to check for a byte array
+ } else if (arg.getClass && arg.getClass() == b.getClass()) {
+ inp = new java.io.ByteArrayInputStream(arg);
+ } else if (arg instanceof java.io.File) {
+ inp = new java.io.FileInputStream(arg);
+ } else if (arg instanceof helma.File) {
+ inp = new java.io.FileInputStream(arg.getFile());
+ } else if (typeof arg == "string") {
+ var str = arg;
+ // try to interpret argument as URL if it contains a colon,
+ // otherwise or if URL is malformed interpret as file name.
+ if (str.indexOf(":") > -1) {
+ try {
+ var url = new java.net.URL(str);
+ inp = url.openStream();
+ } catch (mux) {
+ inp = new java.io.FileInputStream(str);
+ }
+ } else {
+ inp = new java.io.FileInputStream(str);
+ }
+ }
+ if (inp == null) {
+ var msg = "Unrecognized argument in Image.getInfo(): ";
+ msg += (arg == null ? "null" : arg.getClass().toString());
+ throw new java.lang.IllegalArgumentException(msg);
+ }
+ info.setInput(inp);
+ if (info.check()) {
+ result = info;
+ }
+ } catch (e) {
+ // do nothing, returns null later
+ } finally {
+ if (inp != null) {
+ try {
+ inp.close();
+ } catch (e) {}
+ }
+ }
+
+ return result;
+};
+
+
+/**
+ * Writes a 1x1 pixel transparent spacer GIF image to the
+ * response buffer and sets the content type to image/gif.
+ *
+ * @memberof helma.Image
+ */
+helma.Image.spacer = function() {
+ res.contentType = "image/gif";
+ res.writeBinary([71,73,70,56,57,97,2,0,2,0,-128,-1,0,-64,-64,-64,0,0,0,33,-7,4,1,0,0,0,0,44,0,0,0,0,1,0,1,0,64,2,2,68,1,0,59]);
+ return;
+};
+
+helma.lib = "Image";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
+
+
+
diff --git a/modules/helma/Mail.js b/modules/helma/Mail.js
new file mode 100644
index 00000000..5ea1a3b3
--- /dev/null
+++ b/modules/helma/Mail.js
@@ -0,0 +1,705 @@
+/*
+ * 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-2007 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Mail.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Fields and methods of the helma.Mail class.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Mail.js')
+ */
+
+// take care of any dependencies
+app.addRepository('modules/helma/File.js');
+
+/**
+ * Define the global namespace if not existing
+ */
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Mail client enabling you to send e-mail via SMTP using Packages.javax.mail.
+ *
+ * @class This class provides functionality to sending
+ * Email messages.
+ * A mail client object is created by using the helma.Mail()
+ * constructor. The mail object then can be manipulated and sent
+ * using the methods listed below.
+ *
+ * You will either need to set your mail server via the smtp
+ * property in the app.properties or server.properties file
+ * or pass the hostname of the mail server you want to use as a
+ * parameter to the constructor.
+ *
+ * Note: Make sure that the SMTP server itself is well-configured,
+ * so that it accepts e-mails coming from your server and does
+ * not deny relaying. Best and fastest configuration is of course
+ * if you run your own SMTP server (e.g. postfix) which might be
+ * a bit tricky to set up, however.
+ *
+ * @param {String} smtp as String, the hostname of the mail server
+ * @constructor
+ */
+helma.Mail = function(host, port) {
+ // Error code values for this.status
+ var OK = 0;
+ var SUBJECT = 10;
+ var TEXT = 11;
+ var MIMEPART = 12;
+ var TO = 20;
+ var CC = 21;
+ var BCC = 22;
+ var FROM = 23;
+ var REPLYTO = 24;
+ var SETHEADER = 25;
+ var ADDHEADER = 26;
+ var GETHEADER = 27;
+ var REMOVEHEADER = 28;
+ var SEND = 30;
+ var MAILPKG = Packages.javax.mail;
+
+ var self = this;
+ var errStr = "Error in helma.Mail";
+
+ var System = java.lang.System;
+ var Properties = java.util.Properties;
+ var IOException = java.io.IOException;
+ var Wrapper = Packages.org.mozilla.javascript.Wrapper;
+ var FileDataSource = Packages.javax.activation.FileDataSource;
+ var DataHandler = Packages.javax.activation.DataHandler;
+
+ var MimePart = Packages.helma.util.MimePart;
+ var MimePartDataSource = Packages.helma.util.MimePartDataSource;
+
+ var BodyPart = MAILPKG.BodyPart;
+ var Message = MAILPKG.Message;
+ var Session = MAILPKG.Session;
+ var InternetAddress = MAILPKG.internet.InternetAddress;
+ var AddressException = MAILPKG.internet.AddressException;
+ var MimeBodyPart = MAILPKG.internet.MimeBodyPart;
+ var MimeMessage = MAILPKG.internet.MimeMessage;
+ var MimeUtility = MAILPKG.internet.MimeUtility;
+ var MimeMultipart = MAILPKG.internet.MimeMultipart;
+
+ var buffer, multipart, multipartType = "mixed";
+ var username, password;
+
+ var setStatus = function(status) {
+ if (self.status === OK) {
+ self.status = status;
+ }
+ return;
+ };
+
+ var getStatus = function() {
+ return self.status;
+ };
+
+ var addRecipient = function(addstr, name, type) {
+ if (addstr.indexOf("@") < 0) {
+ throw new AddressException();
+ }
+ if (name != null) {
+ var address = new InternetAddress(addstr,
+ MimeUtility.encodeWord(name.toString()));
+ } else {
+ var address = new InternetAddress(addstr);
+ }
+ message.addRecipient(type, address);
+ return;
+ };
+
+ /**
+ * Adds the content stored in this helma.Mail instance
+ * to the wrapped message.
+ * @private
+ */
+ var setContent = function() {
+ if (buffer != null) {
+ if (multipart != null) {
+ var part = new MimeBodyPart();
+ part.setContent(buffer.toString(), "text/plain");
+ multipart.addBodyPart(part, 0);
+ message.setContent(multipart);
+ } else {
+ message.setText(buffer.toString());
+ }
+ } else if (multipart != null) {
+ message.setContent(multipart);
+ } else {
+ message.setText("");
+ }
+ return;
+ };
+
+ /**
+ * Returns the text buffer of this mail message
+ * @returns The text buffer of this mail message
+ * @type java.lang.StringBuffer
+ * @private
+ */
+ this.getBuffer = function() {
+ return buffer;
+ };
+
+ /**
+ * Returns the mime multipart object of this mail message
+ * @returns The mime multipart object of this mail message
+ * @type javax.mail.internet.MimeMultipart
+ * @private
+ */
+ this.getMultipart = function() {
+ return multipart;
+ };
+
+ /**
+ * Sets username and password to use for SMTP authentication.
+ * @param {String} uname The username to use
+ * @param {String} pwd The password to use
+ */
+ this.setAuthentication = function(uname, pwd) {
+ if (uname && pwd) {
+ username = uname;
+ password = pwd;
+ // enable smtp authentication
+ props.put("mail.smtp.auth", "true");
+ }
+ return;
+ }
+
+ /**
+ * Returns the wrapped message
+ * @returns The wrapped message
+ * @type javax.mail.internet.MimeMessage
+ */
+ this.getMessage = function() {
+ return message;
+ };
+
+ /**
+ * Switches debug mode on or off.
+ * @param {Boolean} debug If true debug mode is enabled
+ */
+ this.setDebug = function(debug) {
+ session.setDebug(debug === true);
+ };
+
+ /**
+ * The status of this Mail object. This equals 0
unless
+ * an error occurred. See {@link helma.Mail Mail.js} source code for a list of
+ * possible error codes.
+ */
+ this.status = OK;
+
+ /**
+ * Sets the sender of an e-mail message.
+ *
+ * The first argument specifies the receipient's
+ * e-mail address. The optional second argument
+ * specifies the name of the recipient.
+ *
+ * @param {String} addstr as String, sender email address
+ * @param {String} name as String, optional sender name
+ */
+ this.setFrom = function(addstr, name) {
+ try {
+ if (addstr.indexOf("@") < 0) {
+ throw new AddressException();
+ }
+ if (name != null) {
+ var address = new InternetAddress(addstr,
+ MimeUtility.encodeWord(name.toString()));
+ } else {
+ var address = new InternetAddress(addstr);
+ }
+ message.setFrom(address);
+ } catch (mx) {
+ app.logger.error(errStr + ".setFrom(): " + mx);
+ setStatus(FROM);
+ }
+ return;
+ };
+
+ /**
+ * Set a header in the e-mail message. If the given header is already set the previous
+ * value is replaced with the new one.
+ * @param name a header name
+ * @param value the header value
+ */
+ this.setHeader = function(name, value) {
+ try {
+ message.addHeader(name, MimeUtility.encodeText(value));
+ } catch (mx) {
+ app.logger.error(errStr + ".setHeader(): " + mx);
+ setStatus(SETHEADER);
+ }
+ return;
+ }
+
+ /**
+ * Set a header in the e-mail message. If the given header is already set the previous
+ * value is replaced with the new one.
+ * @param name a header name
+ * @param value the header value
+ */
+ this.addHeader = function(name, value) {
+ try {
+ message.addHeader(name, MimeUtility.encodeText(value));
+ } catch (mx) {
+ app.logger.error(errStr + ".addHeader(): " + mx);
+ setStatus(ADDHEADER);
+ }
+ return;
+ }
+
+ /**
+ * Get all the headers for this header name.
+ * Returns null if no headers for this header name are available.
+ * @param name a header name
+ * @return {String[]} a string array of header values, or null
+ */
+ this.getHeader = function(name) {
+ var value = null;
+ try {
+ value = message.getHeader(name);
+ if (value && value.length) {
+ for (var i = 0; i < value.length; i++) {
+ value[i] = MimeUtility.decodeText(value[i]);
+ }
+ }
+ } catch (mx) {
+ app.logger.error(errStr + ".getHeader(): " + mx);
+ setStatus(GETHEADER);
+ }
+ return value;
+ }
+
+ /**
+ * Remove all headers with this name.
+ * @param name the header name
+ */
+ this.removeHeader = function(name) {
+ try {
+ message.removeHeader(name);
+ } catch (mx) {
+ app.logger.error(errStr + ".removeHeader(): " + mx);
+ setStatus(REMOVEHEADER);
+ }
+ return;
+ }
+
+ /**
+ * Sets the Reply-To address of an e-mail message.
+ *
+ * @param {String} addstr as String, the reply-to email address
+ */
+ this.setReplyTo = function(addstr) {
+ try {
+ if (addstr.indexOf("@") < 0) {
+ throw new AddressException();
+ }
+ var address = [new InternetAddress(addstr)];
+ message.setReplyTo(address);
+ } catch (mx) {
+ app.logger.error(errStr + ".setReplyTo(): " + mx);
+ setStatus(REPLYTO);
+ }
+ return;
+ }
+
+ /**
+ * Sets the recipient of an e-mail message.
+ *
+ * The first argument specifies the receipient's
+ * e-mail address. The optional second argument
+ * specifies the name of the recipient.
+ *
+ * @param {String} addstr as String, receipients email address
+ * @param {String} name as String, optional receipients name
+ * @see #addTo
+ */
+ this.setTo = function(addstr, name) {
+ try {
+ addRecipient(addstr, name, Message.RecipientType.TO);
+ } catch (mx) {
+ app.logger.error(errStr + ".setTo(): " + mx);
+ setStatus(TO);
+ }
+ return;
+ };
+
+ /**
+ * Adds a recipient to the address list of an e-mail message.
+ *
+ * The first argument specifies the receipient's
+ * e-mail address. The optional second argument
+ * specifies the name of the recipient.
+ *
+ * @param {String} addstr as String, receipients email address
+ * @param {String} name as String, optional receipients name
+ * @see setTo
+ */
+ this.addTo = function(addstr, name) {
+ try {
+ addRecipient(addstr, name, Message.RecipientType.TO);
+ } catch (mx) {
+ app.logger.error(errStr + ".addTo(): " + mx);
+ setStatus(TO);
+ }
+ return;
+ }
+
+ /**
+ * Adds a recipient to the list of addresses to get a "carbon copy"
+ * of an e-mail message.
+ *
+ * The first argument specifies the receipient's
+ * e-mail address. The optional second argument
+ * specifies the name of the recipient.
+ *
+ * @param {String} addstr as String, receipients email address
+ * @param {String} name as String, optional receipients name
+ */
+ this.addCC = function(addstr, name) {
+ try {
+ addRecipient(addstr, name, Message.RecipientType.CC);
+ } catch (mx) {
+ app.logger.error(errStr + ".addCC(): " + mx);
+ setStatus(CC);
+ }
+ return;
+ }
+
+ /**
+ * Adds a recipient to the list of addresses to get a "blind carbon copy" of an e-mail message.
+ *
+ * The first argument specifies the receipient's
+ * e-mail address. The optional second argument
+ * specifies the name of the recipient.
+ *
+ * @param {String} addstr as String, receipients email address
+ * @param {String} name as String, optional receipients name
+ */
+ this.addBCC = function(addstr, name) {
+ try {
+ addRecipient(addstr, name, Message.RecipientType.BCC);
+ } catch (mx) {
+ app.logger.error(errStr + ".addBCC(): " + mx);
+ setStatus(BCC);
+ }
+ return;
+ }
+
+ /**
+ * Sets the subject of an e-mail message.
+ *
+ * @param {String} subject as String, the email subject
+ */
+ this.setSubject = function(subject) {
+ if (!subject) {
+ return;
+ }
+ try {
+ message.setSubject(MimeUtility.encodeWord(subject.toString()));
+ } catch (mx) {
+ app.logger.error(errStr + ".setSubject(): " + mx);
+ setStatus(SUBJECT);
+ }
+ return;
+ };
+
+ /**
+ * Sets the body text of an e-mail message.
+ *
+ * @param {String} text as String, to be appended to the message body
+ * @see #addText
+ */
+ this.setText = function(text) {
+ if (text) {
+ buffer = new java.lang.StringBuffer(text);
+ }
+ return;
+ };
+
+ /**
+ * Appends a string to the body text of an e-mail message.
+ *
+ * @param {String} text as String, to be appended to the message body
+ * @see #setText
+ */
+ this.addText = function(text) {
+ if (buffer == null) {
+ buffer = new java.lang.StringBuffer(text);
+ } else {
+ buffer.append(text);
+ }
+ return;
+ };
+
+ /**
+ * Sets the MIME multiparte message subtype. The default value is
+ * "mixed" for messages of type multipart/mixed. A common value
+ * is "alternative" for the multipart/alternative MIME type.
+ * @param {String} messageType the MIME subtype such as "mixed" or "alternative".
+ * @see #getMultipartType
+ * @see #addPart
+ */
+ this.setMultipartType = function(messageType) {
+ multipartType = messageType;
+ return;
+ };
+
+ /**
+ * Returns the MIME multiparte message subtype. The default value is
+ * "mixed" for messages of type multipart/mixed.
+ * @return the MIME subtype
+ * @type String
+ * @see #setMultipartType
+ * @see #addPart
+ */
+ this.getMultipartType = function(messageType) {
+ return multipartType;
+ };
+
+ /**
+ * Adds an attachment to an e-mail message.
+ *
+ * The attachment needs to be either a helma.util.MimePart Object retrieved
+ * through the global getURL function, or a {@link helma.File} object, or a String.
+ *
+ * Use the getURL() function to retrieve a MIME object or wrap a
+ * helma.File object around a file of the local file system.
+ *
+ * @param {fileOrMimeObjectOrString} obj File, Mime object or String to attach to the email
+ * @param {String} filename optional name of the attachment
+ * @param {String} contentType optional content type (only if first argument is a string)
+ * @see global.getURL
+ * @see helma.util.MimePart
+ * @see helma.File
+ */
+ this.addPart = function(obj, filename, contentType) {
+ try {
+ if (obj == null) {
+ throw new IOException(
+ errStr + ".addPart: method called with wrong number of arguments."
+ );
+ }
+ if (multipart == null) {
+ multipart = new MimeMultipart(multipartType);
+ }
+ if (obj instanceof Wrapper) {
+ obj = obj.unwrap();
+ }
+
+ var part;
+ if (obj instanceof BodyPart) {
+ // we already got a body part, no need to convert it
+ part = obj;
+ } else {
+ part = new MimeBodyPart();
+ if (typeof obj == "string") {
+ part.setContent(obj.toString(), contentType || "text/plain");
+ } else if (obj instanceof File || obj instanceof helma.File) {
+ var source = new FileDataSource(obj.getPath());
+ part.setDataHandler(new DataHandler(source));
+ } else if (obj instanceof MimePart) {
+ var source = new MimePartDataSource(obj);
+ part.setDataHandler(new DataHandler(source));
+ }
+ }
+ if (filename != null) {
+ try {
+ part.setFileName(filename.toString());
+ } catch (x) {}
+ } else if (obj instanceof File || obj instanceof helma.File) {
+ try {
+ part.setFileName(obj.getName());
+ } catch (x) {}
+ }
+ multipart.addBodyPart(part);
+ } catch (mx) {
+ app.logger.error(errStr + ".addPart(): " + mx);
+ setStatus(MIMEPART);
+ }
+ return;
+ };
+
+ /**
+ * Saves this mail RFC 822 formatted into a file. The name of the
+ * file is prefixed with "helma.Mail" followed by the current time
+ * in milliseconds and a random number.
+ * @param {helma.File} dir An optional directory where to save
+ * this mail to. If omitted the mail will be saved in the system's
+ * temp directory.
+ */
+ this.writeToFile = function(dir) {
+ if (!dir || !dir.exists() || !dir.canWrite()) {
+ dir = new java.io.File(System.getProperty("java.io.tmpdir"));
+ }
+ var fileName = "helma.Mail." + (new Date()).getTime() +
+ "." + Math.round(Math.random() * 1000000);
+ var file = new java.io.File(dir, fileName);
+ if (file.exists()) {
+ file["delete"]();
+ }
+ try {
+ setContent();
+ var fos = new java.io.FileOutputStream(file);
+ var os = new java.io.BufferedOutputStream(fos);
+ message.writeTo(os);
+ os.close();
+ app.logger.info("helma.Mail.saveTo(): saved mail to " +
+ file.getAbsolutePath());
+ } catch (e) {
+ app.logger.error(errStr + ".saveTo(): " + e);
+ }
+ return;
+ };
+
+ /**
+ * Returns the source of this mail as RFC 822 formatted string.
+ * @returns The source of this mail as RFC 822 formatted string
+ * @type String
+ */
+ this.getSource = function() {
+ try {
+ setContent();
+ var buf = new java.io.ByteArrayOutputStream();
+ var os = new java.io.BufferedOutputStream(buf);
+ message.writeTo(os);
+ os.close();
+ return buf.toString();
+ } catch (e) {
+ app.logger.error(errStr + ".getSource(): " + e);
+ }
+ return null;
+ };
+
+ /**
+ * Sends an e-mail message.
+ *
+ * This function sends the message using the SMTP
+ * server as specified when the Mail object was
+ * constructed using helma.Mail.
+ *
+ * If no smtp hostname was specified when the Mail
+ * object was constructed, the smtp property in either
+ * the app.properties or server.properties file needs
+ * to be set in order for this to work.
+ *
+ * As a fallback, the message will then be written to
+ * file using the {@link #writeToFile} method.
+ * Additionally, the location of the message files can
+ * be determined by setting smtp.dir in app.properties
+ * to the desired file path.
+ */
+ this.send = function() {
+ if (this.status > OK) {
+ app.logger.error(errStr + ".send(): Status is " + this.status);
+ return;
+ }
+ if (host != null) {
+ try {
+ setContent();
+ message.setSentDate(new Date());
+ var transport = session.getTransport("smtp");
+ if (username && password) {
+ transport.connect(host, username, password);
+ } else {
+ transport.connect();
+ }
+ message.saveChanges();
+ transport.sendMessage(message, message.getAllRecipients());
+ } catch (mx) {
+ app.logger.error(errStr + ".send(): " + mx);
+ setStatus(SEND);
+ } finally {
+ if (transport != null && transport.isConnected()) {
+ transport.close();
+ }
+ }
+ } else {
+ // no smtp host is given, so write the mail to a file
+ this.writeToFile(getProperty("smtp.dir"));
+ }
+ return;
+ };
+
+ /**
+ * Main constructor body
+ */
+ var props = new Properties();
+ if (host || (host = getProperty("smtp"))) {
+ props.put("mail.transport.protocol", "smtp");
+ props.put("mail.smtp.host", String(host));
+ props.put("mail.smtp.port", String(port || 25));
+ props.put("mail.smtp.starttls.enable",
+ getProperty("smtp.tls") || "false");
+ props.put("mail.mime.charset",
+ getProperty("smtp.charset") ||
+ System.getProperty("mail.charset") ||
+ "ISO-8859-15");
+ }
+
+ this.setAuthentication(getProperty("smtp.username"),
+ getProperty("smtp.password"));
+ var session = Session.getInstance(props);
+ var message = new MimeMessage(session);
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+}
+
+
+/** @ignore */
+helma.Mail.toString = function() {
+ return "[helma.Mail]";
+};
+
+
+/** @ignore */
+helma.Mail.prototype.toString = function() {
+ return "[helma.Mail Object]";
+};
+
+helma.Mail.example = function(host, sender, addr, subject, text) {
+ // var smtp = "smtp.host.dom";
+ // var sender = "sender@host.dom";
+ // var addr = "recipient@host.dom";
+ // var subject = "Hello, World!";
+ // var text = "This is a test.";
+ var port = 25;
+ var msg = new helma.Mail(host, port);
+ msg.setFrom(sender);
+ msg.addTo(addr);
+ msg.setSubject(subject);
+ msg.setText(text);
+ msg.send();
+ res.write(msg.status);
+ return;
+};
+
+
+helma.lib = "Mail";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Search.js b/modules/helma/Search.js
new file mode 100644
index 00000000..478fd638
--- /dev/null
+++ b/modules/helma/Search.js
@@ -0,0 +1,1458 @@
+//
+// A wrapper for Apache Lucene for use with Helma Object Publisher
+// Copyright (c) 2005-2006 Robert Gaggl
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+//
+//
+// $Revision$
+// $Author$
+// $Date$
+//
+
+
+/**
+ * @fileoverview Fields and methods of the helma.Search class
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Search.js')
+ */
+
+// take care of any dependencies
+app.addRepository('modules/helma/lucene-core.jar');
+app.addRepository('modules/helma/lucene-analyzers.jar');
+
+
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Constructs a new instance of helma.Search. This merely
+ * checks if the Apache Lucene library is in the application
+ * classpath.
+ * @class This class provides functionality for
+ * creating a fulltext search index based on Apache Lucene.
+ * @returns A newly created instance of this prototype.
+ * @constructor
+ * @author Robert Gaggl
+ */
+helma.Search = function() {
+ try {
+ var c = java.lang.Class.forName("org.apache.lucene.analysis.Analyzer",
+ true, app.getClassLoader());
+ var pkg = Packages.org.apache.lucene.LucenePackage.get();
+ var version = parseFloat(pkg.getSpecificationVersion());
+ if (version < 2.2) {
+ throw "Incompatible version";
+ }
+ } catch (e) {
+ throw "helma.Search needs lucene.jar in version >= 2.2 \
+ in lib/ext or application directory \
+ [http://lucene.apache.org/]";
+ }
+ return this;
+};
+
+
+/***************************************
+ ***** S T A T I C M E T H O D S *****
+ ***************************************/
+
+/** @ignore */
+helma.Search.toString = function() {
+ return "[helma.Search]";
+};
+
+/**
+ * Returns a new Analyzer instance depending on the key
+ * passed as argument. Currently supported arguments are
+ * "br" (BrazilianAnalyzer), "cn" (ChineseAnalyzer), "cz" (CzechAnalyzer),
+ * "nl" (DutchAnalyzer), "fr" (FrenchAnalyzer), "de" (GermanAnalyzer),
+ * "el" (GreekAnalyzer), "keyword" (KeywordAnalyzer), "ru" (RussianAnalyzer),
+ * "simple" (SimpleAnalyzer), "snowball" (SnowballAnalyzer), "stop" (StopAnalyzer)
+ * "whitespace" (WhitespaceAnalyzer). If no argument is given, a StandardAnalyzer
+ * is returned.
+ * @param {String} key The key identifying the analyzer
+ * @returns A newly created Analyzer instance
+ * @type {org.apache.lucene.analysis.Analyzer}
+ */
+helma.Search.getAnalyzer = function(key) {
+ var pkg = Packages.org.apache.lucene;
+ switch (key) {
+ case "br":
+ return new pkg.analysis.br.BrazilianAnalyzer();
+ case "cn":
+ return new pkg.analysis.cn.ChineseAnalyzer();
+ case "cz":
+ return new pkg.analysis.cz.CzechAnalyzer();
+ case "nl":
+ return new pkg.analysis.nl.DutchAnalyzer();
+ case "fr":
+ return new pkg.analysis.fr.FrenchAnalyzer();
+ case "de":
+ return new pkg.analysis.de.GermanAnalyzer();
+ case "el":
+ return new pkg.analysis.el.GreekAnalyzer();
+ case "keyword":
+ return new pkg.analysis.KeywordAnalyzer();
+ case "ru":
+ return new pkg.analysis.ru.RussianAnalyzer();
+ case "simple":
+ return new pkg.analysis.SimpleAnalyzer();
+ case "snowball":
+ return new pkg.analysis.snowball.SnowballAnalyzer();
+ case "stop":
+ return new pkg.analysis.StopAnalyzer();
+ case "whitespace":
+ return new pkg.analysis.WhitespaceAnalyzer();
+ default:
+ return new pkg.analysis.standard.StandardAnalyzer();
+ }
+};
+
+/**
+ * Constructs a new QueryFilter instance. This class
+ * wraps a lucene QueryFilter.
+ * @param {helma.Search.Query} q The query object to use as
+ * the basis for the QueryFilter instance.
+ * @returns A newly created QueryFilter instance.
+ * @constructor
+ */
+helma.Search.QueryFilter = function(q) {
+ var filter = new Packages.org.apache.lucene.search.QueryFilter(q.getQuery());
+
+ /**
+ * Returns the wrapped filter instance
+ * @type org.apache.lucene.search.QueryFilter
+ */
+ this.getFilter = function() {
+ return filter;
+ };
+
+ return this;
+};
+
+/** @ignore */
+helma.Search.QueryFilter.prototype.toString = function() {
+ return this.getFilter().toString();
+};
+
+
+
+/*********************************************
+ ***** P R O T O T Y P E M E T H O D S *****
+ *********************************************/
+
+
+/** @ignore */
+helma.Search.prototype.toString = function() {
+ return helma.Search.toString();
+};
+
+/**
+ * Returns an instance of org.apache.lucene.store.FSDirectory. If
+ * no index is present in the given directory, it is created on the fly.
+ * @param {File|helma.File|java.io.File|String} dir The directory
+ * where the index is located or should be created at.
+ * @param {Boolean} create If true the index will be created, removing
+ * any existing index in the same directory
+ * @returns The index directory.
+ * @type org.apache.lucene.store.FSDirectory
+ */
+helma.Search.prototype.getDirectory = function(dir, create) {
+ if (!dir) {
+ throw "helma.Search.getDirectory(): insufficient arguments.";
+ }
+ var d;
+ if (dir.constructor == String) {
+ d = new java.io.File(dir);
+ } else if (dir.constructor == File || dir.constructor == helma.File) {
+ d = new java.io.File(dir.getAbsolutePath());
+ } else if (!((d = dir) instanceof java.io.File)) {
+ throw "helma.Search.getDirectory(): " + dir + " is not a valid argument.";
+ }
+ return Packages.org.apache.lucene.store.FSDirectory.getDirectory(d,
+ create === true || !d.exists());
+};
+
+/**
+ * Returns a RAM directory object.
+ * @param {File|helma.File|java.io.File|String} dir Optional directory
+ * containing a Lucene index from which this RAM directory should be created.
+ * @returns A RAM directory instance.
+ * @type org.apache.lucene.store.RAMDirectory
+ */
+helma.Search.prototype.getRAMDirectory = function(dir) {
+ if (dir != null) {
+ var d;
+ if (dir.constructor == String) {
+ d = new java.io.File(dir);
+ } else if (dir.constructor == File || dir.constructor == helma.File) {
+ d = new java.io.File(dir.getAbsolutePath());
+ } else if (!((d = dir) instanceof java.io.File)) {
+ throw "helma.Search.getRAMDirectory(): " + dir + " is not a valid argument.";
+ }
+ if (!d.exists()) {
+ throw "helma.Search.getRAMDirectory(): " + dir + " does not exist.";
+ }
+ return Packages.org.apache.lucene.store.RAMDirectory(d);
+ }
+ return Packages.org.apache.lucene.store.RAMDirectory();
+};
+
+/**
+ * Creates a new Lucene index in the directory passed as
+ * argument, using an optional analyzer, and returns an instance
+ * of helma.Search.Index. Any already existing index in this
+ * directory will be preserved.
+ * @param {org.apache.lucene.store.Directory} dir The directory
+ * where the index should be stored. This can be either a
+ * FSDirectory or a RAMDirectory instance.
+ * @param {org.apache.lucene.analysis.Analyzer} analyzer The analyzer to
+ * use for the index. If not specified a StandardAnalyzer will be used.
+ * @returns The index instance.
+ * @type helma.Search.Index
+ */
+helma.Search.prototype.createIndex = function(dir, analyzer) {
+ if (!dir || !(dir instanceof Packages.org.apache.lucene.store.Directory)) {
+ throw "Index directory missing or invalid.";
+ } else if (!analyzer) {
+ // no analyzer given, use a StandardAnalyzer
+ analyzer = helma.Search.getAnalyzer();
+ }
+ var index = new helma.Search.Index(dir, analyzer);
+ if (!Packages.org.apache.lucene.index.IndexReader.indexExists(dir)) {
+ index.create();
+ }
+ return index;
+};
+
+
+/*********************
+ ***** I N D E X *****
+ *********************/
+
+
+/**
+ * Creates a new instance of helma.Search.Index
+ * @class Instances of this class represent a Lucene search index
+ * located in either a directory on disk or in RAM. This class
+ * provides various methods for modifying the underlying Lucene index.
+ * @param {org.apache.lucene.store.Directory} directory The directory
+ * where the Lucene index is located at.
+ * @param {org.apache.lucene.analysis.Analyzer} analyzer The analyzer
+ * to use when modifying the index.
+ * @constructor
+ */
+helma.Search.Index = function(directory, analyzer) {
+
+ /**
+ * Returns an IndexWriter instance that can be used to add documents to
+ * the underlying index or to perform various other modifying operations.
+ * If the index is currently locked this method will try for the next
+ * two seconds to create the IndexWriter, otherwise it will
+ * throw an error.
+ * @param {Boolean} create True to create the index (overwriting an
+ * existing index), false to append to an existing index. Defaults to false
+ * @param {Boolean} autoCommit Enables or disables auto commit (defaults to
+ * false)
+ * @returns An IndexWriter instance.
+ * @type org.apache.lucene.index.IndexWriter
+ */
+ this.getWriter = function(create, autoCommit) {
+ try {
+ return new Packages.org.apache.lucene.index.IndexWriter(directory,
+ (autoCommit === true), analyzer, (create === true));
+ } catch (e) {
+ throw "Failed to get IndexWriter due to active lock (Thread "
+ + java.lang.Thread.currentThread().getId() + ")";
+ }
+ };
+
+ /**
+ * Returns an IndexReader instance. Due to locking issues an
+ * IndexModifier should be used for deleting documents.
+ * @returns An IndexReader instance
+ * @type org.apache.lucene.index.IndexReader
+ */
+ this.getReader = function() {
+ return Packages.org.apache.lucene.index.IndexReader.open(directory);
+ };
+
+ /**
+ * Returns the directory the underlying Lucene index is located at.
+ * @returns The directory of this index
+ * @type org.apache.lucene.store.Directory
+ */
+ this.getDirectory = function() {
+ return directory;
+ };
+
+ /**
+ * Returns the analyzer used within this index.
+ * @returns The analyzer used within this index.
+ * @type org.apache.lucene.analysis.Analyer
+ */
+ this.getAnalyzer = function() {
+ return analyzer;
+ };
+
+ /**
+ * Returns a searcher for querying this index.
+ * @returns A searcher useable for querying the index.
+ * @type helma.Search.Searcher
+ */
+ this.getSearcher = function() {
+ return new helma.Search.Searcher(this);
+ };
+
+ /** @ignore */
+ this.toString = function() {
+ return ("[Lucene Index " + directory + "]");
+ };
+
+ return this;
+};
+
+/**
+ * Merges the indexes passed as argument into this one.
+ * @param {org.apache.lucene.store.Directory} dir At least one
+ * index director to add to this index.
+ */
+helma.Search.Index.prototype.addIndexes = function(dir /* [, dir[, dir...] */) {
+ var dirs = java.lang.reflect.Array.newInstance(
+ Packages.org.apache.lucene.store.Directory,
+ arguments.length);
+ for (var i=0;i 0) {
+ var arr = java.lang.reflect.Array.newInstance(pkg.SortField, this.sortFields.size());
+ var sort = pkg.Sort(this.sortFields.toArray(arr));
+ if (filter) {
+ hits = searcher.search(query, filter, sort);
+ } else {
+ hits = searcher.search(query, sort);
+ }
+ } else if (filter) {
+ hits = searcher.search(query, filter);
+ } else {
+ hits = searcher.search(query);
+ }
+ this.hits = new helma.Search.HitCollection(hits);
+ return this.hits.length();
+};
+
+/**
+ * Sets a field as result sorting field. This method can be called
+ * with a different number of arguments:
+ * sortBy(fieldName)
+ * sortBy(fieldName, type)
+ * sortBy(fieldName, reverse)
+ * sortBy(fieldName, type, reverse)
+ * @param {String} fieldName The name of the field in the index by which
+ * the search result should be ordered.
+ * @param {String} type The type of the field defined by argument fieldName.
+ * Valid arguments are "string", "float", "int", "score", "doc", "auto", "custom".
+ * Default is "auto". See http://lucene.apache.org/java/docs/api/org/apache/lucene/search/SortField.html
+ * for an explanation.
+ */
+helma.Search.Searcher.prototype.sortBy = function(fieldName /** type, reverse */) {
+ var pkg = Packages.org.apache.lucene.search;
+ if (!this.sortFields)
+ this.sortFields = new java.util.Vector();
+ var f = fieldName;
+ var t = pkg.SortField.AUTO;
+ var r = false;
+ if (arguments.length == 3) {
+ t = pkg.SortField[arguments[1].toUpperCase()];
+ r = arguments[2];
+ } else if (arguments.length == 2) {
+ if (arguments[1].constructor == Boolean) {
+ r = arguments[1];
+ } else {
+ t = pkg.SortField[arguments[1].toUpperCase()];
+ }
+ }
+ this.sortFields.add(new pkg.SortField(f, t, r));
+ return;
+};
+
+/**
+ * Closes the wrapped IndexSearcher instance.
+ */
+helma.Search.Searcher.prototype.close = function() {
+ var s = this.getSearcher();
+ if (s != null) {
+ s.close();
+ }
+ return;
+};
+
+/**
+ * Creates a new instance of helma.Search.HitCollection
+ * @class This class provides Helma-like methods for accessing
+ * a collection of search hits.
+ * @param {org.lucene.search.Hits} hits The hit collection returned
+ * by lucene.
+ * @constructor
+ */
+helma.Search.HitCollection = function(hits) {
+ /**
+ * Silently converts the hit at the given index position into
+ * an instance of helma.Search.Document.
+ * @param {Number} idx The index position of the hit
+ * @returns The document object at the given index position
+ * @type helma.Search.Document
+ */
+ this.get = function(idx) {
+ var doc = new helma.Search.Document(hits.doc(idx));
+ doc.id = hits.id(idx);
+ doc.score = hits.score(idx);
+ doc.rank = idx +1;
+ return doc;
+ };
+
+ /**
+ * Returns the number of hits in this collection.
+ * @returns The number of hits.
+ * @type Number
+ */
+ this.size = function() {
+ return (hits != null) ? hits.length() : 0;
+ };
+
+ /**
+ * Returns the number of hits in this collection.
+ * This method is deprecated, use {@link #size} instead.
+ * @returns The number of hits.
+ * @type Number
+ * @deprecated
+ * @see #size
+ */
+ this.length = function() {
+ return this.size();
+ };
+
+ /**
+ * Executes a provided function once per hit.
+ * @param {Function} fun Function to execute for each element
+ * @param {Object} context Object to use as "this" when executing callback.
+ * @see https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/ForEach
+ */
+ this.forEach = function(func, context) {
+ if (typeof func != "function") {
+ throw new TypeError();
+ }
+ var len = this.size();
+ for (var i = 0; i < len; i += 1) {
+ var hit = this.get(i);
+ if (hit) {
+ func.call(context, hit, i, hits);
+ }
+ }
+ return;
+ };
+
+ return this;
+};
+
+/** @ignore */
+helma.Search.HitCollection.prototype.toString = function() {
+ return "[HitCollection]";
+};
+
+/***********************************************
+ ***** Q U E R Y C O N S T R U C T O R S *****
+ ***********************************************/
+
+/**
+ * Creates a new instance of helma.Search.Query
+ * @class Base class for the various query constructors. Don't
+ * call this directly, as it provides just basic methods used
+ * in all of its extends.
+ * @constructor
+ */
+helma.Search.Query = function() {
+ /**
+ * The wrapped query instance
+ * @type org.apache.lucene.search.Query
+ * @private
+ */
+ this.query = null;
+
+ return this;
+};
+
+/**
+ * Returns the wrapped Lucene Query object.
+ * @returns The wrapped query object
+ * @type org.apache.lucene.search.Query
+ */
+helma.Search.Query.prototype.getQuery = function() {
+ return this.query;
+};
+
+/** @ignore */
+helma.Search.Query.prototype.toString = function(field) {
+ return "[" + this.getQuery().toString(field) + "]";
+};
+
+/**
+ * Returns the boost factor of this query.
+ * @type Number
+ */
+helma.Search.Query.prototype.getBoost = function() {
+ return this.getQuery().getBoost();
+};
+
+/**
+ * Sets the boost factor of this query clause to
+ * the given number. Documents matching this query
+ * will have their score multiplied with the given
+ * factor
+ * @param {Number} fact The factor to multiply the score
+ * of matching documents with.
+ */
+helma.Search.Query.prototype.setBoost = function(fact) {
+ this.getQuery().setBoost(fact);
+ return;
+};
+
+/**
+ * Creates a new instance of helma.Search.TermQuery
+ * @class This class represents a simple Term Query.
+ * @param {String} field The name of the field
+ * @param {String} str The value of the field
+ * @constructor
+ * @extends helma.Search.Query
+ */
+helma.Search.TermQuery = function(field, str) {
+ var t = new Packages.org.apache.lucene.index.Term(field, str);
+ /**
+ * Contains the wrapped TermQuery instance
+ * @type org.apache.lucene.search.TermQuery
+ */
+ this.query = new Packages.org.apache.lucene.search.TermQuery(t);
+ return this;
+};
+helma.Search.TermQuery.prototype = new helma.Search.Query;
+
+/**
+ * Creates a new instance of helma.Search.BooleanQuery
+ * @class This class represents a Boolean Query, providing
+ * various methods for combining other Query instances using boolean operators.
+ * @param String name of the field
+ * @param String query string
+ * @returns Object BooleanQuery object
+ * @constructor
+ * @extends helma.Search.Query
+ */
+helma.Search.BooleanQuery = function(field, str, clause, analyzer) {
+ /**
+ * Contains the wrapped BooleanQuery instance
+ * @type org.apache.lucene.search.BooleanQuery
+ */
+ this.query = new Packages.org.apache.lucene.search.BooleanQuery();
+
+ /**
+ * Main constructor body
+ */
+ if (field && str) {
+ this.addTerm.apply(this, arguments);
+ }
+
+ return this;
+};
+helma.Search.BooleanQuery.prototype = new helma.Search.Query;
+
+/**
+ * Adds a term to the wrapped query object. This method can be called
+ * with two, three or four arguments, eg.:
+ * addTerm("fieldname", "querystring")
+ * addTerm("fieldname", "querystring", "and")
+ * addTerm("fieldname", "querystring", helma.Search.getAnalyzer("de"))
+ * addTerm("fieldname", "querystring", "not", helma.Search.getAnalyzer("simple"))
+ * @param {String|Array} field Either a String or an Array containing Strings
+ * that determine the index field(s) to match
+ * @param {String} str Query string to match
+ * @param {String} clause Boolean clause ("or", "not" or "and", default is "and")
+ * @param {org.apache.lucene.analysis.Analyzer} analyzer An analyzer to use
+ */
+helma.Search.BooleanQuery.prototype.addTerm = function(field, str, clause, analyzer) {
+ var pkg = Packages.org.apache.lucene;
+ if (arguments.length == 3 && arguments[2] instanceof pkg.analysis.Analyzer) {
+ analyzer = arguments[2];
+ clause = "or";
+ }
+ if (!analyzer) {
+ analyzer = helma.Search.getAnalyzer();
+ }
+
+ var fields = (field instanceof Array) ? field : [field];
+ var parser = new pkg.queryParser.MultiFieldQueryParser(fields, analyzer);
+ this.addQuery(parser.parse(str), clause);
+ return;
+};
+
+/**
+ * Adds an additional query clause to this query.
+ * @param {helma.Search.Query} q The query to add
+ * @param {String} clause Boolean clause ("or", "not", or "and", default is "and")
+ */
+helma.Search.BooleanQuery.prototype.addQuery = function(q, clause) {
+ var pkg = Packages.org.apache.lucene;
+ var booleanClause;
+ if (q instanceof helma.Search.Query) {
+ q = q.getQuery();
+ }
+ switch (clause) {
+ case "and":
+ booleanClause = pkg.search.BooleanClause.Occur.MUST;
+ break;
+ case "not":
+ booleanClause = pkg.search.BooleanClause.Occur.MUST_NOT;
+ break;
+ default:
+ booleanClause = pkg.search.BooleanClause.Occur.SHOULD;
+ break;
+ }
+ this.getQuery().add(q, booleanClause);
+ return;
+};
+
+/**
+ * Constructs a new helma.Search.PhraseQuery instance that wraps
+ * a Lucene Phrase Query object.
+ * @class Instances of this class represent a phrase query.
+ * @param {String} field The name of the field
+ * @param {String} str The phrase query string
+ * @returns A newly created PhraseQuery instance
+ * @constructor
+ * @extends helma.Search.Query
+ */
+helma.Search.PhraseQuery = function(field, str) {
+ /**
+ * Contains the wrapped PhraseQuery instance
+ * @type org.apache.lucene.search.PhraseQuery
+ */
+ this.query = new Packages.org.apache.lucene.search.PhraseQuery();
+
+ /**
+ * add a term to the end of the phrase query
+ */
+ this.addTerm = function(field, str) {
+ var t = new Packages.org.apache.lucene.index.Term(field, str);
+ this.query.add(t);
+ return;
+ };
+
+ if (field && str)
+ this.addTerm(field, str);
+ delete this.base;
+ return this;
+};
+helma.Search.PhraseQuery.prototype = new helma.Search.Query;
+
+/**
+ * Constructs a new helma.Search.RangeQuery instance.
+ * @class Instances of this class represent a range
+ * query, wrapping a Lucene RangeQuery instance.
+ * @param {String} field The name of the field
+ * @param {String} from The minimum value to match (can be null)
+ * @param {String} to The maximum value to match (can be null)
+ * @param {Boolean} inclusive If true the given min/max values are included
+ * @returns A newly created RangeQuery instance
+ * @constructor
+ * @extends helma.Search.Query
+ */
+helma.Search.RangeQuery = function(field, from, to, inclusive) {
+ if (!field)
+ throw "Missing field name in RangeQuery()";
+ if (arguments.length < 4)
+ inclusive = true;
+ var t1 = from ? new Packages.org.apache.lucene.index.Term(field, from) : null;
+ var t2 = to ? new Packages.org.apache.lucene.index.Term(field, to) : null;
+ /**
+ * Contains the wrapped RangeQuery instance
+ * @type org.apache.lucene.search.RangeQuery
+ */
+ this.query = new Packages.org.apache.lucene.search.RangeQuery(t1, t2, inclusive);
+ return this;
+};
+helma.Search.RangeQuery.prototype = new helma.Search.Query;
+
+/**
+ * Constructs a new helma.Search.FuzzyQuery instance.
+ * @class Instances of this class represent a fuzzy query
+ * @param {String} field The name of the field
+ * @param {String} str The query string to match
+ * @returns A newly created FuzzyQuery instance
+ * @constructor
+ * @extends helma.Search.Query
+ */
+helma.Search.FuzzyQuery = function(field, str) {
+ var t = new Packages.org.apache.lucene.index.Term(field, str);
+ /**
+ * Contains the wrapped FuzzyQuery instance
+ * @type org.apache.lucene.search.FuzzyQuery
+ */
+ this.query = new Packages.org.apache.lucene.search.FuzzyQuery(t);
+ return this;
+};
+helma.Search.FuzzyQuery.prototype = new helma.Search.Query;
+
+/**
+ * Constructs a new helma.Search.PrefixQuery instance.
+ * @class Instances of this class represent a prefix query
+ * @param {String} field The name of the field
+ * @param {String} str The query string to match
+ * @returns A newly created PrefixQuery instance
+ * @constructor
+ * @extends helma.Search.Query
+ */
+helma.Search.PrefixQuery = function(field, str) {
+ var t = new Packages.org.apache.lucene.index.Term(field, str);
+ /**
+ * Contains the wrapped PrefixQuery instance
+ * @type org.apache.lucene.search.PrefixQuery
+ */
+ this.query = new Packages.org.apache.lucene.search.PrefixQuery(t);
+ return this;
+};
+helma.Search.PrefixQuery.prototype = new helma.Search.Query;
+
+/**
+ * Constructs a new helma.Search.WildcardQuery instance.
+ * @class Instances of this class represent a wildcard query.
+ * @param {String} field The name of the field
+ * @param {String} str The query string to match
+ * @returns A newly created WildcardQuery instance
+ * @constructor
+ * @extends helma.Search.Query
+ */
+helma.Search.WildcardQuery = function(field, str) {
+ var t = new Packages.org.apache.lucene.index.Term(field, str);
+ /**
+ * Contains the wrapped WildcardQuery instance
+ * @type org.apache.lucene.search.WildcardQuery
+ */
+ this.query = new Packages.org.apache.lucene.search.WildcardQuery(t);
+ return this;
+};
+helma.Search.WildcardQuery.prototype = new helma.Search.Query;
+
+
+
+/***************************
+ ***** D O C U M E N T *****
+ ***************************/
+
+
+/**
+ * Creates a new instance of helma.Search.Document.
+ * @class Instances of this class represent a single
+ * index document. This class provides various methods for
+ * adding content to such documents.
+ * @param {org.apache.lucene.document.Document} document Optional Lucene Document object
+ * that should be wrapped by this Document instance.
+ * @constructor
+ */
+helma.Search.Document = function(document) {
+ if (!document) {
+ document = new Packages.org.apache.lucene.document.Document();
+ }
+
+ /**
+ * Returns the wrapped Lucene Document object
+ * @returns The wrapped Document object
+ * @type org.apache.lucene.document.Document
+ */
+ this.getDocument = function() {
+ return document;
+ };
+
+ return this;
+};
+
+/**
+ * Adds a field to this document.
+ * @param {String|helma.Search.Document.Field} name The name of the field, or
+ * an instance of {@link helma.Search.Document.Field}, in which case the other
+ * arguments are ignored.
+ * @param {String} value The value of the field
+ * @param {Object} options Optional object containing the following properties
+ * (each of them is optional too):
+ *
+ * store
(String) Defines whether and how the value is stored
+ * in the field. Accepted values are "no", "yes" and "compress" (defaults to "yes")
+ * index
(String) Defines whether and how the value is indexed
+ * in the field. Accepted values are "no", "tokenized", "unTokenized" and
+ * "noNorms" (defaults to "tokenized")
+ * termVector
(String) Defines if and how the fiels should have
+ * term vectors. Accepted values are "no", "yes", "withOffsets", "withPositions"
+ * and "withPositionsAndOffsets" (defaults to "no")
+ *
+ */
+helma.Search.Document.prototype.addField = function(name, value, options) {
+ if (!name) {
+ throw "helma.Search: missing arguments to Document.addField";
+ } else if (arguments.length == 1) {
+ if (arguments[0] instanceof Packages.org.apache.lucene.document.Field) {
+ this.getDocument().add(arguments[0]);
+ } else if (arguments[0] instanceof helma.Search.Document.Field) {
+ this.getDocument().add(arguments[0].getField());
+ }
+ return;
+ }
+
+ // for backwards compatibility only
+ if (options != null) {
+ if (typeof(options.store) === "boolean") {
+ options.store = (options.store === true) ? "yes" : "no";
+ }
+ if (typeof(options.index) === "boolean") {
+ if (options.index === true) {
+ options.index = (options.tokenize === true) ? "tokenized" : "unTokenized";
+ } else {
+ options.index = "no";
+ }
+ delete options.tokenize;
+ }
+ }
+ this.addField(new helma.Search.Document.Field(name, value, options));
+ return;
+};
+
+/**
+ * Returns a single document field.
+ * @param {String} name The name of the field in this document object.
+ * @returns The field with the given name
+ * @type helma.Search.Document.Field
+ */
+helma.Search.Document.prototype.getField = function(name) {
+ var f = this.getDocument().getField(name);
+ if (f != null) {
+ return new helma.Search.Document.Field(f);
+ }
+ return null;
+};
+
+/**
+ * Returns the fields of a document object. If a name is passed as argument,
+ * this method returns only the fields with the given name
+ * @param {String} name Optional name of the fields to return
+ * @returns An array containing all fields in this document object.
+ * @type Array
+ */
+helma.Search.Document.prototype.getFields = function(name) {
+ var fields;
+ if (name != null) {
+ fields = this.getDocument().getFields(name);
+ } else {
+ fields = this.getDocument().getFields().toArray();
+ }
+ var size = fields.length;
+ var result = new Array(size);
+ for (var i=0; i
+ * store
(String) Defines whether and how the value is stored
+ * in the field. Accepted values are "no", "yes" and "compress" (defaults to "yes")
+ * index
(String) Defines whether and how the value is indexed
+ * in the field. Accepted values are "no", "tokenized", "unTokenized" and
+ * "noNorms" (defaults to "tokenized")
+ * termVector
(String) Defines if and how the fiels should have
+ * term vectors. Accepted values are "no", "yes", "withOffsets", "withPositions"
+ * and "withPositionsAndOffsets" (defaults to "no")
+ *
+ */
+helma.Search.Document.Field = function(name, value, options) {
+ var field;
+
+ /**
+ * Contains the name of the field
+ * @type String
+ */
+ this.name = undefined; // for documentation purposes only
+ this.__defineGetter__("name", function() {
+ return field.name();
+ });
+
+ /**
+ * Contains the string value of the field
+ * @type String
+ */
+ this.value = undefined; // for documentation purposes only
+ this.__defineGetter__("value", function() {
+ return field.stringValue();
+ });
+
+ /**
+ * Contains the value of the field converted into a date object.
+ * @type String
+ */
+ this.dateValue = undefined; // for documentation purposes only
+ this.__defineGetter__("dateValue", function() {
+ return Packages.org.apache.lucene.document.DateTools.stringToDate(this.value);
+ });
+
+ /**
+ * Returns the wrapped field instance
+ * @returns The wrapped field
+ * @type org.apache.lucene.document.Field
+ */
+ this.getField = function() {
+ return field;
+ };
+
+ /**
+ * Main constructor body
+ */
+ if (arguments.length == 1 && arguments[0] instanceof Packages.org.apache.lucene.document.Field) {
+ // calling the constructor with a single field argument is
+ // only used internally (eg. in Document.getFields())
+ field = arguments[0];
+ } else {
+ var pkg = Packages.org.apache.lucene.document.Field;
+ // default options
+ var store = pkg.Store.YES;
+ var index = pkg.Index.TOKENIZED;
+ var termVector = pkg.TermVector.NO;
+
+ var opt;
+ if (options != null) {
+ if (options.store != null) {
+ opt = options.store.toUpperCase();
+ if (opt == "YES" || opt == "NO" || opt == "COMPRESS") {
+ store = pkg.Store[opt];
+ } else {
+ app.logger.warn("helma.Search: unknown field storage option '" +
+ options.store + "'");
+ }
+ }
+ if (options.index != null) {
+ opt = options.index.toUpperCase();
+ if (opt == "TOKENIZED" || opt == "NO") {
+ index = pkg.Index[opt];
+ } else if (opt == "UNTOKENIZED") {
+ index = pkg.Index.UN_TOKENIZED;
+ } else if (opt == "NONORMS") {
+ index = pkg.Index.NO_NORMS;
+ } else {
+ app.logger.warn("helma.Search: unknown field indexing option '" +
+ options.index + "'");
+ }
+ }
+ if (options.termVector != null) {
+ opt = options.termVector.toUpperCase();
+ if (opt == "NO" || opt == "YES") {
+ termVector = pkg.TermVector[opt];
+ } else if (opt == "WITHOFFSETS") {
+ termVector = pkg.TermVector.WITH_OFFSETS;
+ } else if (opt == "WITHPOSITIONS") {
+ termVector = pkg.TermVector.WITH_POSITIONS;
+ } else if (opt == "WITHPOSITIONSANDOFFSETS") {
+ termVector = pkg.TermVector.WITH_POSITIONS_OFFSETS;
+ } else {
+ app.logger.warn("helma.Search: unknown field term vector option '" +
+ options.termVector + "'");
+ }
+ }
+ }
+
+ // construct the field instance and add it to this document
+ field = new Packages.org.apache.lucene.document.Field(
+ name, helma.Search.Document.Field.valueToString(value),
+ store, index, termVector);
+ }
+
+ return this;
+};
+
+/**
+ * Converts the value passed as argument to the appropriate string value. For
+ * null values this method returns an empty string.
+ * @param {Object} value The value to convert into a string
+ * @returns The value converted into a string
+ * @type String
+ */
+helma.Search.Document.Field.valueToString = function(value) {
+ var pkg = Packages.org.apache.lucene.document;
+ if (value != null) {
+ if (value.constructor === Date) {
+ return pkg.DateTools.timeToString(value.getTime(), pkg.DateTools.Resolution.MINUTE);
+ } else if (value.constructor !== String) {
+ return String(value);
+ }
+ return value;
+ }
+ return "";
+};
+
+/** @ignore */
+helma.Search.Document.Field.prototype.toString = function() {
+ return "[Field '" + this.name + "' (" + this.getField().toString() + ")]";
+};
+
+/**
+ * Returns the boost factor of this field.
+ * @returns The boost factor of this field
+ * @type Number
+ */
+helma.Search.Document.Field.prototype.getBoost = function() {
+ return this.getField().getBoost();
+};
+
+/**
+ * Sets the boost factor of this field.
+ * @param {Number} boost The boost factor of this field
+ */
+helma.Search.Document.Field.prototype.setBoost = function(boost) {
+ this.getField().setBoost(boost);
+ app.logger.debug("boost is now: " + this.getField().getBoost());
+ return;
+};
+
+/**
+ * Returns true if this field is indexed
+ * @returns True if this field's value is indexed, false otherwise
+ * @type Boolean
+ */
+helma.Search.Document.Field.prototype.isIndexed = function() {
+ return this.getField().isIndexed();
+};
+
+/**
+ * Returns true if this field's value is stored in compressed form in the index
+ * @returns True if this field's value is compressed, false otherwise
+ * @type Boolean
+ */
+helma.Search.Document.Field.prototype.isCompressed = function() {
+ return this.getField().isCompressed();
+};
+
+/**
+ * Returns true if this field's value is stored in the index
+ * @returns True if this field's value is stored, false otherwise
+ * @type Boolean
+ */
+helma.Search.Document.Field.prototype.isStored = function() {
+ return this.getField().isStored();
+};
+
+/**
+ * Returns true if this field's value is tokenized
+ * @returns True if this field's value is tokenized, false otherwise
+ * @type Boolean
+ */
+helma.Search.Document.Field.prototype.isTokenized = function() {
+ return this.getField().isTokenized();
+};
+
+/**
+ * Returns true if this field's term vector is stored in the index
+ * @returns True if this field's term vector is stored, false otherwise
+ * @type Boolean
+ */
+helma.Search.Document.Field.prototype.isTermVectorStored = function() {
+ return this.getField().isTermVectorStored();
+};
+
+
+helma.lib = "Search";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Skin.js b/modules/helma/Skin.js
new file mode 100644
index 00000000..60325858
--- /dev/null
+++ b/modules/helma/Skin.js
@@ -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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Skin.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+
+/**
+ * @fileoverview Fields and methods of the helma.Skin class.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Skin.js')
+ */
+
+
+// define the helma namespace, if not existing
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Constructs a new instance of helma.Skin
+ * @class Instances of this class represent a Helma skin. In addition
+ * to the standard skin functionality this class allows creation of
+ * a skin based on a Base64 encoded source.
+ * @param {String} source The source of the skin
+ * @param {Boolean} encFlag If true the source will be Base64-decoded.
+ * @constructor
+ * @returns A newly created instance of helma.Skin
+ */
+helma.Skin = function(source, encFlag) {
+ /** @ignore */
+ var Base64 = Packages.helma.util.Base64;
+
+ if (!encFlag) {
+ var skin = createSkin(source);
+ } else {
+ var encoded = source;
+ source = new java.lang.String(source);
+ var bytes = Base64.decode(source.toCharArray());
+ var skin = createSkin(new java.lang.String(bytes, "UTF-8"));
+ }
+
+ /** @ignore */
+ this.toString = function() {
+ return source;
+ };
+
+ /**
+ * Returns the source of the skin as Base64 encoded string
+ * @returns The source of the skin as Base64 encoded string
+ * @type String
+ */
+ this.valueOf = function() {
+ if (encFlag) {
+ return encoded;
+ }
+ var bytes = new java.lang.String(source).getBytes("UTF-8");
+ return new java.lang.String(Base64.encode(bytes));
+ };
+
+ /**
+ * Renders the skin.
+ * @param {Object} param An optional parameter object to pass to the skin.
+ */
+ this.render = function(param) {
+ return renderSkin(skin, param);
+ };
+
+ /**
+ * Returns the rendered skin.
+ * @param {Object} param An optional parameter object to pass to the skin.
+ * @type String
+ */
+ this.renderAsString = function(param) {
+ return renderSkinAsString(skin, param);
+ };
+
+ /**
+ * Returns true if the skin contains a macro with the name
+ * and optional handler passed as argument.
+ * @param {String} name The name of the macro
+ * @param {String} handler An optional macro handler name
+ * @returns True if the skin contains this macro at least once,
+ * false otherwise.
+ * @type Boolean
+ */
+ this.containsMacro = function(name, handler) {
+ res.push();
+ res.write("<% *");
+ if (handler) {
+ res.write(handler);
+ res.write(".");
+ }
+ res.write(name);
+ res.write(" *%>");
+ var re = new RegExp(res.pop(), "g");
+ return re.test(source);
+ };
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+};
+
+
+helma.lib = "Skin";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Ssh.js b/modules/helma/Ssh.js
new file mode 100644
index 00000000..875dcecf
--- /dev/null
+++ b/modules/helma/Ssh.js
@@ -0,0 +1,375 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Ssh.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+
+/**
+ * @fileoverview Fields and methods of the helma.Ssh class.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Ssh.js')
+ */
+
+// take care of any dependencies
+app.addRepository('modules/helma/File.js');
+app.addRepository('modules/helma/ganymed-ssh2.jar');
+
+// define the helma namespace, if not existing
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Creates a new instance of helma.Ssh
+ * @class This class provides methods for connecting to a remote
+ * server via secure shell (ssh) and copying files from/to a remote
+ * server using secure copy (scp). It utilizes "Ganymed SSH-2 for Java"
+ * (see http://www.ganymed.ethz.ch/ssh2/).
+ * @param {String} server The server to connect to
+ * @param {helma.File|java.io.File|String} hosts Either a file
+ * containing a list of known hosts, or the path pointing to a
+ * file. This argument is optional.
+ * @constructor
+ * @returns A newly created instance of helma.Ssh
+ * @author Robert Gaggl
+ */
+helma.Ssh = function(server, hosts) {
+ var SSHPKG = Packages.ch.ethz.ssh2;
+ var SSHPKGNAME = "ganymed-ssh2.jar";
+ var SSHPKGURL = "http://www.ganymed.ethz.ch/ssh2";
+ var className = "helma.Ssh";
+ var paranoid = false;
+ var verifier = null;
+ var knownHosts, connection;
+
+ // check if necessary jar file is in classpath
+ try {
+ knownHosts = new SSHPKG.KnownHosts();
+ connection = new SSHPKG.Connection(server);
+ } catch (e) {
+ if (e instanceof TypeError == false)
+ throw(e);
+ throw("helma.Ssh needs " + SSHPKGNAME +
+ " in lib/ext or application directory " +
+ "[" + SSHPKGURL + "]");
+ }
+
+ /**
+ * A simple verifier for verifying host keys
+ * @private
+ */
+ var SimpleVerifier = {
+ verifyServerHostKey: function(hostname, port, serverHostKeyAlgorithm, serverHostKey) {
+ var result = knownHosts.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey);
+ switch (result) {
+ case SSHPKG.KnownHosts.HOSTKEY_IS_OK:
+ debug("verifyServerHostKey", "received known host key, proceeding");
+ return true;
+ case SSHPKG.KnownHosts.HOSTKEY_IS_NEW:
+ if (paranoid == true) {
+ debug("verifyServerHostKey", "received unknown host key, rejecting");
+ return false;
+ } else {
+ debug("verifyServerHostKey", "received new host key, adding temporarily to known hosts");
+ var hn = java.lang.reflect.Array.newInstance(java.lang.String, 1);
+ hn[0] = hostname;
+ knownHosts.addHostkey(hn, serverHostKeyAlgorithm, serverHostKey);
+ return true;
+ }
+ case SSHPKG.KnownHosts.HOSTKEY_HAS_CHANGED:
+ debug("verifyServerHostKey", "WARNING: host key has changed, rejecting");
+ default:
+ return false;
+ }
+ return;
+ }
+ };
+
+ /**
+ * Converts the argument into an instance of java.io.File
+ * @param {helma.File|java.io.File|String} file Either a file
+ * object or the path to a file as string
+ * @returns The argument converted into a file object
+ * @type java.io.File
+ * @private
+ */
+ var getFile = function(file) {
+ if (file instanceof helma.File) {
+ return new java.io.File(file.getAbsolutePath());
+ } else if (file instanceof java.io.File) {
+ return file;
+ } else if (file.constructor == String) {
+ return new java.io.File(file);
+ }
+ return null;
+ };
+
+ /**
+ * Connects to the remote server
+ * @return Boolean
+ * @private
+ */
+ var connect = function() {
+ try {
+ var info = connection.connect(verifier);
+ debug("connect", "connected to server " + server);
+ return true;
+ } catch (e) {
+ error("connect", "connection to " + server + " failed.");
+ }
+ return false;
+ };
+
+ /**
+ * Private helper method for debugging output using app.logger
+ * @param {String} methodName The name of the method
+ * @param {String} msg The debug message to write to event log file
+ * @private
+ */
+ var debug = function(methodName, msg) {
+ var msg = msg ? " " + msg : "";
+ app.logger.debug(className + ":" + methodName + msg);
+ return;
+ };
+
+ /**
+ * Private helper method for error output using app.logger
+ * @param {String} methodName The name of the method
+ * @param {String} msg The error message to write to event log file
+ * @private
+ */
+ var error = function(methodName, msg) {
+ var tx = java.lang.Thread.currentThread();
+ tx.dumpStack();
+ app.logger.error(className + ":" + methodName + ": " + msg);
+ return;
+ };
+
+ /**
+ * Opens the file passed as argument and adds the known hosts
+ * therein to the list of known hosts for this client.
+ * @param {helma.File|java.io.File|String} file Either a file object
+ * or the path to a file containing a list of known hosts
+ * @returns True if adding the list was successful, false otherwise
+ * @type Boolean
+ */
+ this.addKnownHosts = function(file) {
+ try {
+ knownHosts.addHostkeys(getFile(file));
+ verifier = new SSHPKG.ServerHostKeyVerifier(SimpleVerifier);
+ return true;
+ } catch (e) {
+ error("addKnownHosts", "Missing or invalid known hosts file '" + file + "'");
+ }
+ return false;
+ };
+
+ /**
+ * Connects to a remote host using plain username/password authentication.
+ * @param {String} username The username
+ * @param {String} password The password
+ * @returns True in case the connection attempt was successful, false otherwise.
+ * @type Boolean
+ */
+ this.connect = function(username, password) {
+ if (!username || !password) {
+ error("connect", "Insufficient arguments.");
+ } else if (connect() && connection.authenticateWithPassword(username, password)) {
+ debug("connect", "authenticated using password");
+ return true;
+ } else {
+ error("connect", "Authentication failed!");
+ }
+ return false;
+ };
+
+ /**
+ * Connects to a remote host using a private key and the corresponding
+ * passphrase.
+ * @param {String} username The username
+ * @param {helma.File|java.io.File|String} key Either a file object
+ * representing the private key file, or the path to it.
+ * @param {String} passphrase The passphrase of the private key, if necessary.
+ * @returns True in case the connection attempt was successful, false otherwise.
+ * @type Boolean
+ */
+ this.connectWithKey = function(username, key, passphrase) {
+ var keyFile;
+ if (!username || !(keyFile = getFile(key))) {
+ error("connectWithKey", "Insufficient or wrong arguments.");
+ } else if (connect() && connection.authenticateWithPublicKey(username, keyFile, passphrase)) {
+ debug("connectWithKey", "authenticated with key");
+ return true;
+ } else {
+ error("connectWithKey", "Authentication failed!");
+ }
+ return false;
+ };
+
+ /**
+ * Disconnects this client from the remote server.
+ */
+ this.disconnect = function() {
+ connection.close();
+ debug("disconnect", "disconnected from server " + server);
+ return;
+ };
+
+ /**
+ * Returns true if this client is currently connected.
+ * @returns True in case this client is connected, false otherwise.
+ * @type Boolean
+ */
+ this.isConnected = function() {
+ return (connection != null && connection.isAuthenticationComplete());
+ };
+
+ /**
+ * Copies a local file to the remote server
+ * @param {String|Array} localFile Either the path to a single local
+ * file or an array containing multiple file paths that should be
+ * copied to the remote server.
+ * @param {String} remoteDir The path to the remote destination directory
+ * @param {String} mode An optional 4-digit permission mode string (eg.
+ * 0755
);
+ * @returns True in case the operation was successful, false otherwise.
+ * @type Boolean
+ */
+ this.put = function(localFile, remoteDir, mode) {
+ if (!localFile || !remoteDir) {
+ error("put", "Insufficient arguments.");
+ } else if (!this.isConnected()) {
+ error("put", "Not connected. Please establish a connection first.");
+ } else {
+ try {
+ var scp = connection.createSCPClient();
+ if (mode != null)
+ scp.put(localFile, remoteDir, mode);
+ else
+ scp.put(localFile, remoteDir);
+ debug("put", "copied '" + localFile + "' to '" + remoteDir + "'");
+ return true;
+ } catch (e) {
+ error("put", e);
+ }
+ }
+ return false;
+ };
+
+ /**
+ * Retrieves a file from the remote server and stores it locally.
+ * @param {String|Array} remoteFile Either the path to a single remote
+ * file or an array containing multiple file paths that should be
+ * copied onto the local disk.
+ * @param {String} targetDir The path to the local destination directory
+ * @returns True if the copy process was successful, false otherwise.
+ * @type Boolean
+ */
+ this.get = function(remoteFile, targetDir) {
+ if (!remoteFile || !targetDir) {
+ error("get", "Insufficient arguments.");
+ } else if (!this.isConnected()) {
+ error("get", "Not connected. Please establish a connection first.");
+ } else {
+ try {
+ var scp = connection.createSCPClient();
+ scp.get(remoteFile, targetDir);
+ debug("get", "copied '" + remoteFile + "' to '" + targetDir + "'");
+ return true;
+ } catch (e) {
+ error("get", e);
+ }
+ }
+ return false;
+ };
+
+ /**
+ * Executes a single command on the remote server.
+ * @param {String} cmd The command to execute on the remote server.
+ * @return The result of the command execution
+ * @type String
+ */
+ this.execCommand = function(cmd) {
+ if (!this.isConnected()) {
+ error("execCommand", "Not connected. Please establish a connection first.");
+ } else {
+ var session = connection.openSession();
+ try {
+ session.execCommand(cmd);
+ var stdout = new SSHPKG.StreamGobbler(session.getStdout());
+ var br = new java.io.BufferedReader(new java.io.InputStreamReader(stdout));
+ res.push();
+ while (true) {
+ if (!(line = br.readLine()))
+ break;
+ res.writeln(line);
+ }
+ debug("execCommand", "executed command '" + cmd + "'");
+ return res.pop();
+ } catch (e) {
+ error("execCommand", e);
+ } finally {
+ session.close();
+ }
+ }
+ };
+
+ /**
+ * Toggles paranoid mode. If set to true this client tries to
+ * verify the host key against the its list of known hosts
+ * during connection and rejects if the host key is not found
+ * therein or is different.
+ * @param {Boolean} p Either true or false
+ */
+ this.setParanoid = function(p) {
+ paranoid = (p === true);
+ return;
+ };
+
+ /**
+ * Returns true if this client is in paranoid mode.
+ * @return Boolean
+ * @see #setParanoid
+ */
+ this.isParanoid = function() {
+ return paranoid;
+ };
+
+ /**
+ * main constructor body
+ */
+ if (hosts) {
+ this.addKnownHosts(hosts);
+ }
+
+ for (var i in this)
+ this.dontEnum(i);
+ return this;
+};
+
+
+/** @ignore */
+helma.Ssh.toString = function() {
+ return "[helma.Ssh]";
+};
+
+
+helma.lib = "Ssh";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/Url.js b/modules/helma/Url.js
new file mode 100644
index 00000000..081f765d
--- /dev/null
+++ b/modules/helma/Url.js
@@ -0,0 +1,126 @@
+/*
+ * 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-2007 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Url.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Fields and methods of the helma.Url class.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Url.js')
+ */
+
+if (!global.helma) {
+ global.helma = {};
+}
+
+
+/**
+ * Creates a helma.Url object from a provided url string.
+ *
+ * @constructor
+ */
+helma.Url = function(str) {
+ if (!str || !helma.Url.PATTERN.test(str))
+ throw Error("Cannot create helma.Url: insufficient arguments");
+
+ // filter punctuation from user-generated urls
+ // FIXME: a) can this be done in helma.Url.PATTERN?
+ // b) should it be done rather in methods like activateUrls?
+ str = str.replace(/[,.;:]\s/, "");
+
+ var parts = helma.Url.PATTERN.exec(str);
+ /**
+ * Protocol segment of this URL
+ */
+ this.protocol = parts[1];
+
+ if (!parts[2]) {
+ if (parts[3])
+ /**
+ * User name segment of this URL
+ */
+ this.user = parts[3];
+ } else {
+ this.user = parts[2];
+ if (parts[3])
+ /**
+ * Password segment of this URL
+ */
+ this.password = parts[3];
+ }
+
+ if (!parts[4])
+ throw Error("Cannot create helma.Url: missing host part");
+
+ /**
+ * Fully qualified domain name segment of this URL
+ */
+ this.domainName = parts[4]; // actually, the fully-qualified domain name
+ var fqdnParts = this.domainName.split(".");
+ if (fqdnParts.length < 3)
+ this.host = "";
+ else {
+ /**
+ * Host name segment of this URL
+ */
+ this.host = fqdnParts[0];
+ fqdnParts.splice(0, 1);
+ }
+ /**
+ * Top level domain name segment of this URL
+ */
+ this.topLevelDomain = fqdnParts[fqdnParts.length-1];
+ /**
+ * Domain name segment of this URL
+ */
+ this.domain = fqdnParts.join(".");
+
+ /**
+ * Request path segment of this URL as string
+ */
+ this.pathString = parts[5] || "";
+ if (this.pathString.indexOf("/") == 0)
+ this.pathString = this.pathString.substring(1);
+ /**
+ * Request path segment of this URL as array
+ */
+ this.path = this.pathString.split("/");
+ /**
+ * File name segment of this URL
+ */
+ this.file = this.path.pop();
+
+ if (parts[6]) {
+ /**
+ * Query parameter segment of this URL as string
+ */
+ this.queryString = parts[6];
+ var pairs;
+ /**
+ * Query parameter segment of this URL as object
+ */
+ this.query = {};
+ parts = parts[6].split("&");
+ for (var i in parts) {
+ pairs = parts[i].split("=");
+ this.query[pairs[0]] = pairs[1];
+ }
+ }
+
+ return this;
+};
+
+
+helma.Url.PATTERN = /^([^:]*):\/\/+(?:([^\/]*):)?(?:([^\/]*)@)?([\w\-_.]*[^.])(\/[^?]*)?(?:\?(.*))?$/;
diff --git a/modules/helma/Zip.js b/modules/helma/Zip.js
new file mode 100644
index 00000000..921fb254
--- /dev/null
+++ b/modules/helma/Zip.js
@@ -0,0 +1,503 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Zip.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+/**
+ * @fileoverview Fields and methods of the helma.Zip class.
+ *
+ * To use this optional module, its repository needs to be added to the
+ * application, for example by calling app.addRepository('modules/helma/Zip.js')
+ */
+
+// take care of any dependencies
+app.addRepository('modules/helma/File.js');
+
+// define the helma namespace, if not existing
+if (!global.helma) {
+ global.helma = {};
+}
+
+/**
+ * Constructs a new helma.Zip instance
+ * @class Instances of this class represent a single zip archive
+ * and provide various methods for extracting entries or manipulating
+ * the contents of the archive.
+ * @param {helma.File|java.io.File|String} file Either
+ * a file object representing the .zip file on disk, or the
+ * path to the .zip file as string.
+ * @constructor
+ * @returns A newly created instance of helma.Zip.
+ * @author Robert Gaggl
+ */
+helma.Zip = function(file) {
+
+ /**
+ * Private method that extracts the data of a single file
+ * in a .zip archive. If a destination path is given it
+ * writes the extracted data directly to disk using the
+ * name of the ZipEntry Object, otherwise it returns the
+ * byte array containing the extracted data.
+ * @param {java.util.zip.ZipFile} zFile The zip archive to extract
+ * the file from.
+ * @param {java.util.zip.ZipEntry} entry The zip entry to extract
+ * @param {String} destPath The destination path where the extracted
+ * file should be stored.
+ * @returns If no destination path is given, this method returns
+ * the contents of the extracted file as ByteArray, otherwise
+ * it returns null.
+ * @private
+ */
+ var extractEntry = function(zFile, entry, destPath) {
+ var size = entry.getSize();
+ if (entry.isDirectory() || size <= 0)
+ return null;
+
+ var zInStream = new java.io.BufferedInputStream(zFile.getInputStream(entry));
+ var buf = new java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, size);
+ zInStream.read(buf, 0, size);
+ zInStream.close();
+
+ if (!destPath) {
+ // no filesystem destination given, so return
+ // the byte array containing the extracted data
+ return buf;
+ }
+ // extract the file to the given path
+ var dest = new java.io.File(destPath, entry.getName());
+ if (entry.isDirectory())
+ dest.mkdirs();
+ else if (buf) {
+ if (!dest.getParentFile().exists())
+ dest.getParentFile().mkdirs();
+ try {
+ var outStream = new java.io.BufferedOutputStream(new java.io.FileOutputStream(dest));
+ outStream.write(buf, 0, size);
+ } finally {
+ outStream.close();
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Private method for adding a single file to the Zip archive
+ * represented by this helma.Zip instance
+ * @param {java.util.zip.ZipOutputStream} zOutStream The output
+ * stream to write to
+ * @param {java.io.File} f The file that should be added to the
+ * Zip archive.
+ * @param {Number} level The compression-level between 0-9.
+ * @param {String} pathPrefix The path of the directory within the
+ * Zip archive where the file should be added (optional).
+ * @private
+ */
+ var addFile = function(zOutStream, f, level, pathPrefix) {
+ var fInStream = new java.io.BufferedInputStream(new java.io.FileInputStream(f));
+ buf = new java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, f.length());
+ fInStream.read(buf, 0, f.length());
+
+ var name = new java.lang.StringBuffer();
+ if (pathPrefix) {
+ // append clean pathPrefix to name buffer
+ var st = new java.util.StringTokenizer(pathPrefix, "\\/");
+ while (st.hasMoreTokens()) {
+ name.append(st.nextToken());
+ name.append("/");
+ }
+ }
+ name.append(f.getName());
+ var entry = new java.util.zip.ZipEntry(name.toString());
+ entry.setSize(f.length());
+ entry.setTime(f.lastModified());
+ zOutStream.setLevel(level);
+ zOutStream.putNextEntry(entry);
+ zOutStream.write(buf, 0, buf.length);
+ zOutStream.closeEntry();
+ fInStream.close();
+ return;
+ };
+
+ /**
+ * Private helper method that converts the argument into
+ * an instance of java.io.File.
+ * @param {helma.File|java.io.File} f Either a file object or
+ * the path to a file as string
+ * @return The argument converted into a file object
+ * @type java.io.File
+ * @private
+ */
+ var evalFile = function(f) {
+ var result;
+ if (f instanceof java.io.File) {
+ result = f;
+ } else if (f instanceof helma.File || typeof(f) == "string") {
+ result = new java.io.File(f);
+ }
+ if (!result.exists()) {
+ throw "Error creating Zip Object: File '" + f + "' doesn't exist.";
+ }
+ return result;
+ };
+
+ /**
+ * Returns an array containing the entries of the archive
+ * represented by this helma.Zip instance.
+ * @returns The entries stored in the zip archive
+ * @type helma.Zip.Content
+ */
+ this.list = function() {
+ var result = new helma.Zip.Content();
+ var zFile = new java.util.zip.ZipFile(file);
+ var entries = zFile.entries();
+ while (entries.hasMoreElements()) {
+ result.add(new helma.Zip.Entry(entries.nextElement()));
+ }
+ zFile.close();
+ return result;
+ };
+
+ /**
+ * Extracts a single file from the zip archive represented
+ * by this helma.Zip instance. If a destination path is given it
+ * writes the extracted data directly to disk using the
+ * name of the zip entry, otherwise the resulting entry object
+ * contains the extracted data in the property data
.
+ * @param {String} name The name of the file to extract
+ * @param {String} destPath An optional destination path where
+ * the extracted file should be stored.
+ * @returns An object containing the entry's properties
+ * @type helma.Zip.Entry
+ * @see helma.Zip.Entry
+ */
+ this.extract = function(name, destPath) {
+ var zFile = new java.util.zip.ZipFile(file);
+ var entry = zFile.getEntry(name);
+ if (!entry)
+ return null;
+ var result = new helma.Zip.Entry(entry);
+ result.data = extractEntry(zFile, entry, destPath);
+ zFile.close();
+ return result;
+ };
+
+ /**
+ * Extracts all files within the zip archive represented by
+ * this helma.Zip instance. If a destination path is given it
+ * stores the files directly on disk, while preserving any directory
+ * structure within the archive. If no destination path is given,
+ * the resulting entry objects will contain the extracted data
+ * in their property data
.
+ * @param {String} destPath An optional destination path where the
+ * files in the zip archive should be stored.
+ * @returns An object containing the extracted entries.
+ * @type helma.Zip.Content
+ * @see helma.Zip.Entry
+ */
+ this.extractAll = function(destPath) {
+ var result = new helma.Zip.Content();
+ var zFile = new java.util.zip.ZipFile(file);
+ var entries = zFile.entries();
+ while (entries.hasMoreElements()) {
+ var entry = entries.nextElement();
+ var e = new helma.Zip.Entry(entry);
+ e.data = extractEntry(zFile, entry, destPath);
+ result.add(e);
+ }
+ zFile.close();
+ return result;
+ };
+
+ /**
+ * Adds a single file or a whole directory (recursive!) to the zip archive
+ * @param {helma.File|java.io.File|String} f Either a file object
+ * or the path to a file or directory on disk that should be added to the
+ * archive. If the argument represents a directory, its contents will be added
+ * recursively to the archive.
+ * @param {Number} level An optional compression level to use. The argument
+ * must be between zero and 9 (default: 9 = best compression).
+ * @param {String} pathPrefix An optional path prefix to use within the archive.
+ */
+ this.add = function (f, level, pathPrefix) {
+ var f = evalFile(f);
+
+ // evaluate arguments
+ if (arguments.length == 2) {
+ if (typeof arguments[1] == "string") {
+ pathPrefix = arguments[1];
+ level = 9;
+ } else {
+ level = parseInt(arguments[1], 10);
+ pathPrefix = null;
+ }
+ } else if (level == null || isNaN(level)) {
+ level = 9;
+ }
+ // only levels between 0 and 9 are allowed
+ level = Math.max(0, Math.min(9, level));
+
+ if (f.isDirectory()) {
+ // add a whole directory to the zip file (recursive!)
+ var files = (new helma.File(f.getAbsolutePath())).listRecursive();
+ for (var i in files) {
+ var fAdd = new java.io.File(files[i]);
+ if (!fAdd.isDirectory()) {
+ var p = fAdd.getPath().substring(f.getAbsolutePath().length, fAdd.getParent().length);
+ if (pathPrefix)
+ p = pathPrefix + p;
+ addFile(zOutStream, fAdd, level, p);
+ }
+ }
+ } else {
+ addFile(zOutStream, f, level, pathPrefix);
+ }
+ return;
+ };
+
+ /**
+ * Adds a new entry to the zip file.
+ * @param {ByteArray} buf A byte array containing the data to add
+ * to the archive.
+ * @param {String} name The name of the file to add, containing
+ * an optional path prefix
+ * @param {Number} level The compression level to use (0-9, defaults to 9).
+ */
+ this.addData = function(buf, name, level) {
+ var entry = new java.util.zip.ZipEntry(name);
+ entry.setSize(buf.length);
+ entry.setTime(new Date());
+ if (level == null || isNaN(level)) {
+ zOutStream.setLevel(9);
+ } else {
+ zOutStream.setLevel(Math.max(0, Math.min(9, parseInt(level, 10))));
+ }
+ zOutStream.putNextEntry(entry);
+ zOutStream.write(buf, 0, buf.length);
+ zOutStream.closeEntry();
+ return;
+ };
+
+ /**
+ * Closes the zip archive. This method should be called when
+ * all operations have been finished, to ensure that no open
+ * file handles are left.
+ */
+ this.close = function() {
+ zOutStream.close();
+ return;
+ };
+
+ /**
+ * Returns the binary data of the zip archive.
+ * @returns A ByteArray containing the binary data of the zip archive
+ * @type ByteArray
+ */
+ this.getData = function() {
+ return bOutStream.toByteArray();
+ };
+
+ /**
+ * Saves the archive.
+ * @param {String} dest The full destination path including the name
+ * where the zip archive should be saved.
+ */
+ this.save = function(dest) {
+ if (!dest)
+ throw new Error("no destination for ZipFile given");
+ // first of all, close the ZipOutputStream
+ zOutStream.close();
+ var destFile = new java.io.File(dest);
+ try {
+ var outStream = new java.io.FileOutputStream(destFile);
+ bOutStream.writeTo(outStream);
+ } finally {
+ outStream.close();
+ }
+ return;
+ };
+
+ /** @ignore */
+ this.toString = function() {
+ if (file) {
+ return "[helma.Zip " + file.getAbsolutePath() + "]";
+ } else {
+ return "[helma.Zip]";
+ }
+ };
+
+ /**
+ * constructor body
+ */
+ var bOutStream = new java.io.ByteArrayOutputStream();
+ var zOutStream = new java.util.zip.ZipOutputStream(bOutStream);
+ if (file) {
+ file = evalFile(file);
+ }
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+}
+
+/**
+ * Creates a new helma.Zip.Content instance
+ * @class Instances of this class represent the content
+ * of a zip archive.
+ * @constructor
+ * @returns A newly created instance of helma.Zip.Content
+ */
+helma.Zip.Content = function() {
+ /**
+ * The table of contents of the archive
+ * @type Array
+ */
+ this.toc = [];
+
+ /**
+ * The files contained in the zip archive, where
+ * each directory level is a separate object containing
+ * the entries (files and directories) as properties.
+ */
+ this.files = {};
+
+ /**
+ * Adds a zip entry object to the table of contents
+ * and the files collection
+ * @param {helma.Zip.Entry} entry The entry to add to the
+ * zip archive
+ */
+ this.add = function(entry) {
+ // add the file to the table of contents array
+ this.toc[this.toc.length] = entry;
+ // plus add it to the files tree
+ var arr = entry.name.split(/[\\\/]/);
+ var cnt = 0;
+ var curr = this.files;
+ var propName;
+ while (cnt < arr.length-1) {
+ propName = arr[cnt++];
+ if (!curr[propName]) {
+ curr[propName] = {};
+ }
+ curr = curr[propName];
+ }
+ curr[arr[cnt]] = entry;
+ return;
+ };
+
+ for (var i in this)
+ this.dontEnum(i);
+
+ return this;
+};
+
+
+/** @ignore */
+helma.Zip.Content.prototype.toString = function() {
+ return "[helma.Zip.Content]";
+}
+
+
+/**
+ * Creates a new instance of helma.Zip.Entry
+ * @class Instances of this class represent a single zip archive entry,
+ * containing the (meta)data of the entry.
+ * @param {java.util.zip.ZipEntry} entry The zip entry object whose metadata
+ * should be stored in this instance
+ * @constructor
+ * @returns A newly created helma.Zip.Entry instance.
+ */
+helma.Zip.Entry = function(entry) {
+ /**
+ * The name of the zip archive entry
+ * @type String
+ */
+ this.name = entry.getName();
+
+ /**
+ * The size of the entry in bytes
+ * @type Number
+ */
+ this.size = entry.getSize();
+
+ /**
+ * The file date of the entry
+ * @type Date
+ */
+ this.time = entry.getTime() ? new Date(entry.getTime()) : null;
+
+ /**
+ * True if the entry is a directory, false otherwise
+ * @type Boolean
+ */
+ this.isDirectory = entry.isDirectory();
+
+ /**
+ * The data of the zip entry
+ * @type ByteArray
+ */
+ this.data = null;
+
+ for (var i in this)
+ this.dontEnum(i);
+ return this;
+};
+
+
+/** @ignore */
+helma.Zip.Entry.prototype.toString = function() {
+ return "[helma.Zip.Entry]";
+}
+
+
+/**
+ * Extracts all files in the zip archive data passed as argument
+ * and returns them.
+ * @param {ByteArray} zipData A ByteArray containing the data of the zip archive
+ * @returns The entries of the zip archive
+ * @type helma.Zip.Content
+ */
+helma.Zip.extractData = function(zipData) {
+ var zInStream = new java.util.zip.ZipInputStream(new java.io.ByteArrayInputStream(zipData));
+ var result = new helma.Zip.Content();
+
+ var entry;
+ while ((entry = zInStream.getNextEntry()) != null) {
+ var eParam = new helma.Zip.Entry(entry);
+ if (eParam.isDirectory)
+ continue;
+ if (eParam.size == -1)
+ eParam.size = 16384;
+ var bos = new java.io.ByteArrayOutputStream(eParam.size);
+ var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192);
+ var count;
+ while ((count = zInStream.read(buf)) != -1)
+ bos.write(buf, 0, count);
+ eParam.data = bos.toByteArray();
+ eParam.size = bos.size();
+ result.add(eParam);
+ }
+ zInStream.close();
+ return result;
+};
+
+
+helma.lib = "Zip";
+helma.dontEnum(helma.lib);
+for (var i in helma[helma.lib])
+ helma[helma.lib].dontEnum(i);
+for (var i in helma[helma.lib].prototype)
+ helma[helma.lib].prototype.dontEnum(i);
+delete helma.lib;
diff --git a/modules/helma/all.js b/modules/helma/all.js
new file mode 100644
index 00000000..f8a3cdcf
--- /dev/null
+++ b/modules/helma/all.js
@@ -0,0 +1,34 @@
+/*
+ * 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-2006 Helma Software. All Rights Reserved.
+ *
+ * $RCSfile: Aspects.js,v $
+ * $Author$
+ * $Revision$
+ * $Date$
+ */
+
+// convenience SingleFileRepository to load all the
+// Javascript library files in ./modules/helma
+
+app.addRepository('modules/helma/Aspects.js');
+app.addRepository('modules/helma/Chart.js');
+app.addRepository('modules/helma/Color.js');
+app.addRepository('modules/helma/Database.js');
+app.addRepository('modules/helma/File.js');
+app.addRepository('modules/helma/Ftp.js');
+app.addRepository('modules/helma/Html.js');
+app.addRepository('modules/helma/Http.js');
+app.addRepository('modules/helma/Image.js');
+app.addRepository('modules/helma/Mail.js');
+app.addRepository('modules/helma/Search.js');
+app.addRepository('modules/helma/Skin.js');
+app.addRepository('modules/helma/Ssh.js');
+app.addRepository('modules/helma/Url.js');
+app.addRepository('modules/helma/Zip.js');
diff --git a/modules/helma/ganymed-ssh2.jar b/modules/helma/ganymed-ssh2.jar
new file mode 100644
index 00000000..8ca7bd74
Binary files /dev/null and b/modules/helma/ganymed-ssh2.jar differ
diff --git a/modules/helma/jxl.jar b/modules/helma/jxl.jar
new file mode 100644
index 00000000..92dc867a
Binary files /dev/null and b/modules/helma/jxl.jar differ
diff --git a/modules/helma/lucene-analyzers.jar b/modules/helma/lucene-analyzers.jar
new file mode 100644
index 00000000..7e37b731
Binary files /dev/null and b/modules/helma/lucene-analyzers.jar differ
diff --git a/modules/helma/lucene-core.jar b/modules/helma/lucene-core.jar
new file mode 100644
index 00000000..2469481c
Binary files /dev/null and b/modules/helma/lucene-core.jar differ
diff --git a/modules/summary.txt b/modules/summary.txt
new file mode 100644
index 00000000..c32e6357
--- /dev/null
+++ b/modules/summary.txt
@@ -0,0 +1,17 @@
+HelmaLib is organized into two groups of modules:
+
+
+- modules/core which contains extensions to core JavaScript types such as
+Object, Array, or Date.
+- modules/helma which provides new functionality to JavaScript, usually by
+wrapping a Java library.
+
+
+To use a HelmaLib module in your Helma application, you need to add it to the
+app's repositories. The simplest way to do so is by using the app.addRepository()
+function:
+
+ app.addRepository("modules/helma/Search.js");
+
+If you are looking for more Helma libraries, be sure to check out the
+Jala project!
\ No newline at end of file
diff --git a/modules/test/README.txt b/modules/test/README.txt
new file mode 100644
index 00000000..2e554f6e
--- /dev/null
+++ b/modules/test/README.txt
@@ -0,0 +1,10 @@
+To run the tests, add something like this to your apps.properties file:
+
+test
+test.appdir = apps/test/code
+test.repository.0 = apps/test/code
+test.repository.1 = modules/jala/util/Test/code
+
+And you need to have a JDBC driver for your database in lib/ext,
+as well as create the database schema provided in one of the
+db-.sql files.
diff --git a/modules/test/code/Global/global.js b/modules/test/code/Global/global.js
new file mode 100755
index 00000000..b162d4d9
--- /dev/null
+++ b/modules/test/code/Global/global.js
@@ -0,0 +1,33 @@
+function onStart() {
+ root.date = new Date();
+ root.string = "root";
+}
+
+function hello_macro(param) {
+ return "hello";
+}
+
+function echo_macro(param, arg) {
+ return arg || param.what;
+}
+
+function isDate_filter(arg) {
+ return arg instanceof Date;
+}
+
+function isRootDate_filter(arg) {
+ return (arg instanceof Date) &&
+ arg.getTime() == root.date.getTime();
+}
+
+function isResponseDate_filter(arg) {
+ return (arg instanceof Date) && arg == res.data.date;
+}
+
+function makeLongString() {
+ var b = new java.lang.StringBuffer();
+ for (var i = 1; i <= 10000; i++) {
+ b.append(i.toString()).append(" ");
+ }
+ return b.toString();
+}
diff --git a/modules/test/code/Global/subskins.skin b/modules/test/code/Global/subskins.skin
new file mode 100755
index 00000000..379245b9
--- /dev/null
+++ b/modules/test/code/Global/subskins.skin
@@ -0,0 +1 @@
+mainskin<% #subskin1 %>subskin1<% #subskin2 %>subskin2<% #end %>
diff --git a/modules/test/code/Organisation/type.properties b/modules/test/code/Organisation/type.properties
new file mode 100644
index 00000000..e5bbfbf2
--- /dev/null
+++ b/modules/test/code/Organisation/type.properties
@@ -0,0 +1,40 @@
+
+_db = dbcTest
+_table = tb_organisation
+
+_id = org_id
+_parent = root.organisations
+
+persons = collection(Person)
+persons.local = org_id
+persons.foreign = person_org_id
+persons.accessname = person_name
+persons.order = person_name
+
+range = collection(Person)
+range.local = org_id
+range.foreign = person_org_id
+range.accessname = person_name
+range.order = person_name
+range.offset = 100
+range.maxsize = 100
+
+generic = collection(Person)
+generic.local.1 = $prototype
+generic.foreign.1 = person_generic_prototype
+generic.local.2 = $id
+generic.foreign.2 = person_generic_id
+generic.order = person_name
+
+groupedGeneric = collection(Person)
+groupedGeneric.local.1 = $prototype
+groupedGeneric.foreign.1 = person_generic_prototype
+groupedGeneric.local.2 = $id
+groupedGeneric.foreign.2 = person_generic_id
+groupedGeneric.group = person_name
+groupedGeneric.group.order = person_name
+
+name = org_name
+country = org_country
+
+someMountpoint = mountpoint(SomeMountpoint)
diff --git a/modules/test/code/Person/type.properties b/modules/test/code/Person/type.properties
new file mode 100644
index 00000000..c10baede
--- /dev/null
+++ b/modules/test/code/Person/type.properties
@@ -0,0 +1,17 @@
+
+_db = dbcTest
+_table = tb_person
+
+_id = person_id
+_parent = organisation.persons, root.persons
+
+name = person_name
+height = person_height
+dateOfBirth = person_dateofbirth
+
+organisation = object(Organisation)
+organisation.local = person_org_id
+organisation.foreign = org_id
+
+genericPrototype = person_generic_prototype
+genericId = person_generic_id
\ No newline at end of file
diff --git a/modules/test/code/Root/root.js b/modules/test/code/Root/root.js
new file mode 100755
index 00000000..4fe3c512
--- /dev/null
+++ b/modules/test/code/Root/root.js
@@ -0,0 +1,35 @@
+function main_action() {
+ res.redirect(root.href("jala.test"));
+}
+
+function hello_action() {
+ res.contentType = "text/plain";
+ res.write("Hello");
+}
+
+function throwerror_action() {
+ throw Error();
+}
+
+function notfound_action() {
+ res.write("Not found");
+}
+
+function redirect_action() {
+ res.redirect(this.href("hello"));
+}
+
+function error_action() {
+ res.write("Error");
+}
+
+function long_action() {
+ res.write(makeLongString());
+}
+
+function macro_macro(param) {
+ // change suffix
+ if (param.suffix) param.suffix = ".";
+ return this.string;
+}
+
diff --git a/modules/test/code/Root/type.properties b/modules/test/code/Root/type.properties
new file mode 100755
index 00000000..05a31029
--- /dev/null
+++ b/modules/test/code/Root/type.properties
@@ -0,0 +1,16 @@
+date = date
+string = string
+
+
+organisations = collection(Organisation)
+organisations.accessname = org_name
+
+organisationsByCountry = collection(Organisation)
+organisationsByCountry.group = org_country
+organisationsByCountry.group.prototype = Country
+organisationsByCountry.group.order = org_country desc
+organisationsByCountry.order = org_name desc
+
+persons = collection(Person)
+
+someMountpoint = mountpoint(SomeMountpoint)
diff --git a/modules/test/code/app.properties b/modules/test/code/app.properties
new file mode 100755
index 00000000..1d18bda0
--- /dev/null
+++ b/modules/test/code/app.properties
@@ -0,0 +1,3 @@
+baseUri = http://localhost:8080/test/
+# hard-code cache size to default value to make sure there's some cache rotation
+cacheSize = 500
diff --git a/modules/test/code/db.properties b/modules/test/code/db.properties
new file mode 100644
index 00000000..ebceb1a3
--- /dev/null
+++ b/modules/test/code/db.properties
@@ -0,0 +1,15 @@
+
+#MySQL
+dbcTest.url = jdbc:mysql://localhost/helmaTest
+dbcTest.driver = com.mysql.jdbc.Driver
+dbcTest.user = helma
+dbcTest.password = secret
+
+# Oracle
+# dbcTest.url = jdbc:oracle:thin:@localhost:1521:XE
+# dbcTest.driver = oracle.jdbc.driver.OracleDriver
+
+# PostgreSQL
+# dbcTest.url = jdbc:postgresql:helmaTest
+# dbcTest.driver = org.postgresql.Driver
+
diff --git a/modules/test/db-mysql.sql b/modules/test/db-mysql.sql
new file mode 100644
index 00000000..b5559d90
--- /dev/null
+++ b/modules/test/db-mysql.sql
@@ -0,0 +1,26 @@
+
+CREATE DATABASE IF NOT EXISTS helmaTest;
+USE helmaTest;
+
+GRANT ALL ON helmaTest.* TO helma@localhost IDENTIFIED BY 'secret';
+
+DROP TABLE IF EXISTS tb_person;
+DROP TABLE IF EXISTS tb_organisation;
+
+CREATE TABLE tb_person (
+ person_id MEDIUMINT(10) NOT NULL,
+ person_name TINYTEXT,
+ person_height TINYINT unsigned,
+ person_dateofbirth DATETIME,
+ person_org_id MEDIUMINT(10) unsigned,
+ person_generic_prototype VARCHAR(30),
+ person_generic_id MEDIUMINT(10) unsigned,
+ PRIMARY KEY (person_id)
+);
+
+CREATE TABLE tb_organisation (
+ org_id MEDIUMINT(10) unsigned NOT NULL,
+ org_name TINYTEXT,
+ org_country TINYTEXT,
+ PRIMARY KEY (org_id)
+);
diff --git a/modules/test/db-oracle.sql b/modules/test/db-oracle.sql
new file mode 100644
index 00000000..817396e3
--- /dev/null
+++ b/modules/test/db-oracle.sql
@@ -0,0 +1,15 @@
+CREATE TABLE tb_person (
+ person_id NUMBER(10) NOT NULL,
+ person_name VARCHAR2(255),
+ person_height NUMBER(10),
+ person_dateofbirth DATE,
+ person_org_id NUMBER(10),
+ PRIMARY KEY (person_id)
+);
+
+CREATE TABLE tb_organisation (
+ org_id NUMBER(10) NOT NULL,
+ org_name VARCHAR2(255),
+ org_country VARCHAR2(255),
+ PRIMARY KEY (org_id)
+);
diff --git a/modules/test/db-postgresql.sql b/modules/test/db-postgresql.sql
new file mode 100644
index 00000000..d93e9b41
--- /dev/null
+++ b/modules/test/db-postgresql.sql
@@ -0,0 +1,20 @@
+
+CREATE TABLE tb_person (
+ person_id INTEGER NOT NULL,
+ person_name VARCHAR(100),
+ person_height INTEGER,
+ person_dateofbirth TIMESTAMP,
+ person_org_id INTEGER,
+ PRIMARY KEY (person_id)
+);
+
+CREATE TABLE tb_organisation (
+ org_id INTEGER NOT NULL,
+ org_name VARCHAR(100),
+ org_country VARCHAR(100),
+ PRIMARY KEY (org_id)
+);
+
+CREATE USER helma WITH PASSWORD 'secret';
+GRANT insert, delete, select, update ON tb_person, tb_organisation TO helma;
+
diff --git a/modules/test/tests/HopObjectBasicMapping.js b/modules/test/tests/HopObjectBasicMapping.js
new file mode 100644
index 00000000..c8164682
--- /dev/null
+++ b/modules/test/tests/HopObjectBasicMapping.js
@@ -0,0 +1,148 @@
+tests = [
+ "testEquality",
+ "testSimpleMapping",
+ "testSimpleCollection",
+ "testObjectReference",
+ "testCollectionForReference"
+];
+
+function setup() {
+}
+
+function testEquality() {
+ var person = new Person();
+ root.persons.add(person);
+ res.commit();
+ var id = person._id;
+ app.clearCache();
+ var person2 = root.persons.get(id);
+ assertNotNull(person2);
+ assertTrue(person !== person2);
+ assertTrue(person._id === person2._id);
+ assertTrue(person == person2);
+ person.remove();
+}
+
+function testSimpleMapping() {
+
+ var data = {
+ name: "Oliver Stone",
+ dateOfBirth: new Date(1946, 8, 15, 6, 0, 0),
+ height: 182
+ };
+ var person = new Person();
+ person.name = data.name;
+ person.dateOfBirth = data.dateOfBirth;
+ person.height = data.height;
+ root.persons.add(person);
+ var personId = person._id;
+
+ res.commit(); // Commit Transaction
+ app.clearCache(); // Clear cache so that object is refetched
+
+ var person = Person.getById(personId);
+ assertNotNull(person);
+ assertEqual(person._prototype, "Person");
+ assertEqual(person._id, personId);
+ assertEqual(person.name, data.name);
+ assertEqual(person.height, data.height);
+ assertEqual(person.dateOfBirth.valueOf(), data.dateOfBirth.valueOf());
+ res.commit();
+
+ person.remove();
+}
+
+function testSimpleCollection() {
+
+ var data = {
+ name: "Helma"
+ };
+ var orgCount = root.organisations.count();
+ var org = new Organisation();
+ org.name = data.name;
+ root.organisations.add(org);
+ var orgId = org._id;
+
+ assertEqual(orgCount + 1, root.organisations.count());
+ assertNotEqual(root.organisations.indexOf(org), -1);
+
+ // fetch Object via position
+ assertEqual(root.organisations.get(root.organisations.indexOf(org)), org);
+
+ // fetch Object via accessname
+ assertEqual(root.organisations.get(data.name), org);
+
+ // fetch Object via id
+ assertEqual(root.organisations.getById(orgId), org);
+
+ // test list
+ assertEqual(root.organisations.count(), root.organisations.list().length);
+
+ org.remove();
+
+ assertEqual(orgCount, root.organisations.count());
+ assertEqual(root.organisations.indexOf(org), -1);
+ assertNull(root.organisations.get(data.name));
+ assertNull(root.organisations.getById(orgId));
+
+}
+
+
+function testObjectReference() {
+
+ var org = new Organisation();
+ org.name = "Helma";
+ root.organisations.add(org);
+ var orgId = org._id;
+
+ var person = new Person();
+ person.name = "Hannes";
+ person.organisation = org;
+ root.persons.add(person);
+ var personId = person._id;
+
+ res.commit(); // Commit Transaction
+ app.clearCache(); // Clear cache so that object is refetched
+
+ var org = Organisation.getById(orgId);
+ var person = Person.getById(personId);
+ assertEqual(person.organisation, org);
+
+ org.remove();
+ res.commit(); // Commit Transaction
+
+ assertNull(person.organisation);
+
+ person.remove();
+}
+
+
+function testCollectionForReference() {
+
+ var org = new Organisation();
+ org.name = "Helma";
+ root.organisations.add(org);
+ var orgId = org._id;
+ var personCount = org.persons.count();
+
+ var person = new Person();
+ person.name = "Hannes";
+ person.organisation = org;
+ root.persons.add(person);
+ org.persons.add(person);
+
+ assertEqual(personCount + 1, org.persons.count());
+ assertNotEqual(org.persons.indexOf(person), -1);
+
+ org.persons.removeChild(person);
+ person.remove();
+
+ assertEqual(personCount, org.persons.count());
+ assertEqual(org.persons.indexOf(person), -1);
+
+ org.remove();
+}
+
+
+function cleanup() {
+}
diff --git a/modules/test/tests/HopObjectCollection.js b/modules/test/tests/HopObjectCollection.js
new file mode 100644
index 00000000..60ba4737
--- /dev/null
+++ b/modules/test/tests/HopObjectCollection.js
@@ -0,0 +1,183 @@
+tests = [
+ "testSize",
+ "testMaxSize",
+ "testAddSmall",
+ "testAddLarge",
+ "testRemoveSmall",
+ "testRemoveLarge",
+ "testUpdateSmall",
+ "testUpdateLarge",
+ "testListSmall",
+ "testListLarge",
+ "testOrderLarge",
+ "testOrderSmall"
+];
+
+var helma, ikea;
+var small = 3, large = 1234;
+
+function setup() {
+ ikea = makeOrg('Ikea', large);
+ helma = makeOrg('Helma', small);
+}
+
+function testSize() {
+ assertEqual(ikea.persons.size(), large);
+ assertEqual(helma.persons.size(), small);
+}
+
+function testMaxSize() {
+ assertEqual(ikea.range.size(), 100);
+ assertEqual(helma.range.size(), 0);
+ var person = ikea.range.get("Person Ikea 0150");
+ assertNotNull(person);
+ assertEqual(person, ikea.range.get(50));
+ assertEqual(person, ikea.persons.get(150));
+ assertEqual(50, ikea.range.indexOf(person));
+ assertEqual(150, ikea.persons.indexOf(person));
+}
+
+function testAddSmall() {
+ testAdd(helma, small);
+}
+
+function testAddLarge() {
+ testAdd(ikea, large);
+}
+
+// test directly adding to a collection
+function testAdd(org, size) {
+ var person = new Person();
+ person.name = "TestPerson";
+ org.persons.add(person);
+ assertEqual(org.persons.size(), size + 1);
+ assertEqual(org.persons.indexOf(person), size);
+ assertEqual(org.persons.contains(person), size);
+ assertEqual(person.href(), org.persons.href() + "TestPerson/");
+ // make sure the add has set the back-reference on the person object.
+ // note that === comparison will return false if the
+ // collection size exceeds the cache size.
+ assertTrue(person.organisation == org);
+}
+
+function testRemoveSmall() {
+ testRemove(helma, small);
+}
+
+function testRemoveLarge() {
+ testRemove(ikea, large);
+}
+
+// test directly removing from a collection
+function testRemove(org, size) {
+ var person = org.persons.get(org.persons.size() - 1);
+ person.remove();
+ assertEqual(org.persons.size(), size);
+ assertEqual(org.persons.indexOf(person), -1);
+ assertEqual(org.persons.contains(person), -1);
+}
+
+function testUpdateSmall() {
+ testUpdate(helma, small);
+}
+
+function testUpdateLarge() {
+ testUpdate(ikea, large);
+}
+
+// test indirectly adding to and removing form a collection
+function testUpdate(org, size) {
+ var person = new Person();
+ person.name = "TestPerson";
+ person.organisation = org;
+ person.persist();
+ res.commit();
+ assertEqual(org.persons.size(), size + 1);
+ org.persons.prefetchChildren();
+ assertEqual(org.persons.indexOf(person), size);
+ assertEqual(org.persons.contains(person), size);
+ assertEqual(person.href(), org.persons.href() + "TestPerson/");
+ person.remove();
+ res.commit();
+ assertEqual(org.persons.size(), size);
+ assertEqual(org.persons.indexOf(person), -1);
+ assertEqual(org.persons.contains(person), -1);
+}
+
+function testListSmall() {
+ testList(helma, small);
+}
+
+function testListLarge() {
+ testList(ikea, large);
+}
+
+function testList(org, size) {
+ function iterate(list, start, length) {
+ assertEqual(list.length, length);
+ for (var i = 0; i < length; i++) {
+ assertEqual(list[i].name, "Person " + org.name + (start + i).format(" 0000"));
+ }
+ }
+ iterate(org.persons.list(), 0, size);
+ org.persons.invalidate();
+ iterate(org.persons.list(), 0, size);
+ org.persons.invalidate();
+ iterate(org.persons.list(1, size - 2), 1, size - 2);
+}
+
+function testOrderLarge() {
+ testOrder(ikea, ikea.persons.size() - 2);
+ testOrder(ikea, Math.floor(ikea.persons.size() / 2));
+ testOrder(ikea, 2);
+}
+
+function testOrderSmall() {
+ testOrder(helma, helma.persons.size() - 1);
+ testOrder(helma, 1);
+ testOrder(helma, 0);
+}
+
+function testOrder(org, pos) {
+ var person = new Person();
+ person.name = "Person " + org.name + pos.format(" 0000") + "B";
+ person.organisation = org;
+ root.persons.add(person);
+ res.commit();
+ assertEqual(pos + 1, org.persons.indexOf(person));
+}
+
+function cleanup() {
+ var persons = root.persons.list();
+ for each (var person in persons) {
+ person.remove();
+ }
+ ikea.remove();
+ helma.remove();
+}
+
+function makeOrg(name, size) {
+ var org = new Organisation();
+ org.name = name;
+ root.organisations.add(org);
+
+ for (var i = 0; i < size; i++) {
+ var person = new Person();
+ person.name = "Person " + name + i.format(" 0000");
+ person.organisation = org;
+ root.persons.add(person);
+ }
+ res.commit();
+ return org;
+}
+
+// debugging helpers
+function dumpDataChange(message) {
+ res.debug(message + ": ");
+ dumpDataChangeFor("Person");
+ dumpDataChangeFor("Organisation");
+}
+
+function dumpDataChangeFor(name) {
+ res.debug(name + ": " + app.__app__.getDbMapping(name).getLastDataChange());
+}
diff --git a/modules/test/tests/HopObjectGeneric.js b/modules/test/tests/HopObjectGeneric.js
new file mode 100644
index 00000000..c9929f8e
--- /dev/null
+++ b/modules/test/tests/HopObjectGeneric.js
@@ -0,0 +1,93 @@
+tests = [
+ 'testSize',
+ 'testContent',
+ 'testGroupContent',
+ 'testRemove',
+ 'testAdd'
+];
+
+var org;
+var size = 16;
+
+function setup() {
+ org = new Organisation();
+ org.name = "GenericOrg";
+ var i = 0, person;
+
+ function addPerson(collection) {
+ person = new Person();
+ person.name = "GenericPerson " + i.format("00");
+ collection.add(person);
+ i++;
+ }
+
+ // add first half to both collections of transient parent ...
+ while (i < 4)
+ addPerson(org.generic);
+ while (i < 8)
+ addPerson(org.groupedGeneric);
+ root.organisations.add(org);
+ // ... second half to both collections of persistent parent.
+ while (i < 12)
+ addPerson(org.generic);
+ while (i < size)
+ addPerson(org.groupedGeneric);
+
+ res.commit();
+}
+
+function testSize() {
+ assertEqual(org.generic.size(), size);
+ org.invalidate();
+ assertEqual(org.generic.size(), size);
+}
+
+function testContent() {
+ var list = org.generic.list();
+ assertEqual(list.length, size);
+ for (var i = 0; i < list.length; i++) {
+ assertEqual(list[i].name, "GenericPerson " + i.format("00"));
+ }
+}
+
+function testGroupContent() {
+ var list = org.groupedGeneric.list();
+ assertEqual(list.length, size);
+ for (var i = 0; i < list.length; i++) {
+ assertEqual(list[i].groupname, "GenericPerson " + i.format("00"));
+ assertEqual(list[i].size(), 1);
+ assertEqual(list[i].get(0).name, "GenericPerson " + i.format("00"));
+ }
+}
+
+function testRemove() {
+ var person = org.generic.get(size/2);
+ org.generic.removeChild(person);
+ assertEqual(org.generic.size(), size - 1);
+ res.rollback();
+ // note: removeChild does not remove the node, nor does it
+ // unset the constraints between parent and child, so after a rollback
+ // the object is back in place. While this behaviour is disputable,
+ // until this is so we test for it.
+ assertEqual(org.generic.size(), size);
+}
+
+function testAdd() {
+ var person = new Person();
+ org.generic.add(person);
+ assertEqual(org.generic.size(), size + 1);
+ assertEqual(org.groupedGeneric.size(), size);
+ res.commit();
+ // note: even after commit the grouped collection must not grow
+ // since we added a person without a name
+ assertEqual(org.generic.size(), size + 1);
+ assertEqual(org.groupedGeneric.size(), size);
+}
+
+function cleanup() {
+ var persons = org.generic.list();
+ for each (var person in persons) {
+ person.remove();
+ }
+ org.remove();
+}
diff --git a/modules/test/tests/HopObjectGroupBy.js b/modules/test/tests/HopObjectGroupBy.js
new file mode 100644
index 00000000..fc028b12
--- /dev/null
+++ b/modules/test/tests/HopObjectGroupBy.js
@@ -0,0 +1,149 @@
+tests = [
+ "testGroupByAddRemoveCommit",
+ "testGroupByAddRemoveNoCommit",
+ "testGroupOrder",
+ "testGroupTransient"
+];
+
+// todo: run with different sizes
+var size = 1234;
+
+function setup() {
+ for (var i = 0; i < size; i++) {
+ var org = new Organisation();
+ org.name = "Organisation " + i;
+ org.country = "CH" + i.format("0000");
+ // add to different collections
+ if (i % 2 == 0)
+ root.organisations.add(org);
+ else
+ root.organisationsByCountry.add(org);
+ }
+ res.commit();
+}
+
+function testGroupByAddRemoveCommit() {
+ var countryCount = root.organisationsByCountry.count();
+ var org = new Organisation();
+ org.country = "AT" + Math.random();
+ org.name = "Helma" + Math.random();
+ root.organisations.add(org);
+ res.commit(); // Commit Transaction
+
+ var country = root.organisationsByCountry.get(org.country);
+ assertEqual(countryCount + 1, root.organisationsByCountry.count());
+ assertNotNull(country);
+ assertEqual(country._prototype, "Country");
+ assertEqual(country._id, org.country);
+
+ org.remove();
+ res.commit(); // Commit Transaction
+
+ assertNull(root.organisationsByCountry.get(org.country));
+ assertEqual(countryCount, root.organisationsByCountry.count());
+}
+
+function testGroupByAddRemoveNoCommit() {
+
+ var countryCount = root.organisationsByCountry.count();
+ var org = new Organisation();
+ org.country = "AT" + Math.random();
+ org.name = "Helma" + Math.random();
+ root.organisations.add(org);
+ root.organisationsByCountry.add(org);
+
+ // FIXME HELMABUG: count does not get incremented immediately
+ assertEqual(countryCount + 1, root.organisationsByCountry.count());
+
+ var country = root.organisationsByCountry.get(org.country);
+ assertNotNull(country);
+ assertEqual(country._prototype, "Country");
+ assertEqual(country._id, org.country);
+ assertEqual(country.count(), 1);
+ assertEqual(country.get(0), org);
+
+ root.organisationsByCountry.removeChild(org);
+ org.remove();
+
+ // FIXME HELMABUG: country is still accessible at this point
+ // similar to http://helma.org/bugs/show_bug.cgi?id=551
+ assertNull(root.organisationsByCountry.get(org.country));
+
+ assertEqual(countryCount, root.organisationsByCountry.count());
+}
+
+function testGroupOrder() {
+
+ var org1 = new Organisation();
+ org1.country = "AT";
+ org1.name = "Helma" + Math.random();
+ root.organisations.add(org1);
+
+ var org2 = new Organisation();
+ org2.country = "CH01"; // pre-populated items have countries CH0000..C1234
+ org2.name = "Helma" + Math.random();
+ root.organisations.add(org2);
+
+ var org3 = new Organisation();
+ org3.country = "DE";
+ org3.name = "Helma" + Math.random();
+ root.organisations.add(org3);
+
+ var org4 = new Organisation();
+ org4.country = org3.country;
+ org4.name = "Helma" + Math.random();
+ root.organisations.add(org4);
+
+ res.commit();
+
+ // make sure that countries and organisations are sorted in decreasing order (as specified in type.properties)
+ var countries = root.organisationsByCountry.list();
+ assertEqual(countries.length, size + 3);
+ for (var i = 0; i < countries.length; i++) {
+ if (i>0) {
+ assertTrue(root.organisationsByCountry.get(i-1).groupname >= root.organisationsByCountry.get(i).groupname);
+ }
+ for (var j = 0; j < root.organisationsByCountry.get(i); j++) {
+ if (j > 0) {
+ assertTrue(root.organisationsByCountry.get(i).get(j-1).groupname >= root.organisationsByCountry.get(i).get(j).groupname);
+ }
+ }
+ }
+
+ org1.remove();
+ org2.remove();
+ org3.remove();
+ org4.remove();
+}
+
+function testGroupTransient() {
+
+ var temp = new Root();
+ var countryCount = temp.organisationsByCountry.count();
+ var org = new Organisation();
+ org.country = "AT" + Math.random();
+ org.name = "Helma" + Math.random();
+ temp.organisationsByCountry.add(org);
+
+ var country = temp.organisationsByCountry.get(org.country);
+ assertEqual(countryCount + 1, temp.organisationsByCountry.count());
+ assertNotNull(country);
+ assertEqual(country._prototype, "Country");
+ assertEqual(country.groupname, org.country);
+
+ // These don't work as org uses the parent from type.properties
+ // which is root.organisations. Not sure if this is a bug or not.
+ // assertEqual(country, org._parent);
+ // org.remove();
+ country.removeChild(org);
+
+ assertNull(root.organisationsByCountry.get(org.country));
+ assertEqual(countryCount, temp.organisationsByCountry.count());
+}
+
+function cleanup() {
+ var orgs = root.organisations.list();
+ for each (var org in orgs) {
+ org.remove();
+ }
+}
\ No newline at end of file
diff --git a/modules/test/tests/HopObjectHref.js b/modules/test/tests/HopObjectHref.js
new file mode 100644
index 00000000..9515b3b5
--- /dev/null
+++ b/modules/test/tests/HopObjectHref.js
@@ -0,0 +1,56 @@
+tests = [
+ "testSimpleParent",
+ "testFallbackParent",
+ "testMountpoints"
+];
+
+
+var org;
+var person1;
+var person2;
+
+function setup() {
+ org = new Organisation();
+ org.name = "Helma";
+ root.organisations.add(org);
+
+ person1 = new Person();
+ person1.name = "Hannes";
+ person1.organisation = org;
+ root.persons.add(person1);
+
+ person2 = new Person();
+ person2.name = "Michi";
+ root.persons.add(person2);
+}
+
+function testSimpleParent() {
+ assertEqual(org.href(), root.organisations.href() + org.name + "/");
+ assertEqual(root.organisations, org._parent);
+ assertEqual(root, org._parent._parent);
+}
+
+function testFallbackParent() {
+ assertEqual(person1.href(), person1.organisation.persons.href() + person1.name + "/");
+ assertEqual(person1.organisation.persons, person1._parent);
+
+ assertEqual(person2.href(), root.persons.href() + person2._id + "/");
+ assertEqual(root.persons, person2._parent);
+}
+
+function testMountpoints() {
+ assertEqual(root.someMountpoint._prototype, "SomeMountpoint");
+ assertEqual(root.someMountpoint._parent, root);
+ assertEqual(root.someMountpoint.href(), root.href() + "someMountpoint/");
+
+ assertEqual(org.someMountpoint._prototype, "SomeMountpoint");
+ assertEqual(org.someMountpoint._parent, org);
+ // FIXME: Helma-Bug ? mountpoints are converted to lower case ?
+ assertEqual(org.someMountpoint.href(), org.href() + "someMountpoint/");
+}
+
+function cleanup() {
+ org.remove();
+ person1.remove();
+ person2.remove();
+}
diff --git a/modules/test/tests/HopObjectReference.js b/modules/test/tests/HopObjectReference.js
new file mode 100644
index 00000000..e97f8700
--- /dev/null
+++ b/modules/test/tests/HopObjectReference.js
@@ -0,0 +1,40 @@
+tests = [
+ "testForward",
+ "testBackward",
+];
+
+function setup() {
+ var org = new Organisation();
+ var person = new Person();
+ org.name = "Acme Hovercraft";
+ person.name = "Murray Feather";
+ person.organisation = org;
+ org.person = person;
+ person.persist();
+ res.commit();
+}
+
+function testForward() {
+ app.clearCache();
+ person = root.persons.get(0);
+ org = root.organisations.get(0);
+ assertEqual(person.organisation, org);
+ assertEqual(person.organisation.name, org.name);
+ assertEqual("Acme Hovercraft", org.name);
+}
+
+function testBackward() {
+ app.clearCache();
+ var person = root.persons.get(0);
+ var org = root.organisations.get(0);
+ assertEqual(org.person, person);
+ assertEqual(org.person.name, person.name);
+ assertEqual("Murray Feather", person.name);
+}
+
+function cleanup() {
+ var person = root.persons.get(0);
+ var org = root.organisations.get(0);
+ org.remove();
+ person.remove();
+}
diff --git a/modules/test/tests/Skin.js b/modules/test/tests/Skin.js
new file mode 100644
index 00000000..e8f5c3c3
--- /dev/null
+++ b/modules/test/tests/Skin.js
@@ -0,0 +1,319 @@
+var tests = [
+ "testCommentMacro",
+ "testDeepResolve",
+ "testDeepUnhandled",
+ "testDeep",
+ "testDeepFail",
+ "testDeepFailSilent",
+ "testJavaProp",
+ "testJavaMissing",
+ "testJavaMissingSilent",
+ "testJavaMissingVerbose",
+ "testUndefinedHandler",
+ "testJSProp",
+ "testJSMacro",
+ "testJSFunction",
+ "testJSMissing",
+ "testJSMissingSilent",
+ "testJSMissingVerbose",
+ "testDateFilter",
+ "testNestedRootMacro",
+ "testNestedParamMacro",
+ "testNestedResponseMacro",
+ "testNestedPrefixSuffix",
+ "testResponseProp",
+ "testResponseMacro",
+ "testResponseFunction",
+ "testResponseMissing",
+ "testResponseMissingSilent",
+ "testResponseMissingVerbose",
+ "testRootProp",
+ "testRootMacro",
+ "testRootMissing",
+ "testRootMissingSilent",
+ "testRootMissingVerbose",
+ "testSessionProp",
+ "testSessionMacro",
+ "testSessionFunction",
+ "testSessionMissing",
+ "testSessionMissingDefault",
+ "testSessionMissingSilent",
+ "testSessionMissingVerbose",
+ "testSessionDeepMissing",
+ "testSubskins"
+];
+
+var setup = function() {
+ res.handlers.color = new java.awt.Color["(int,int,int)"](0, 255, 0);
+ res.handlers.file = new java.io.File["(java.lang.String,java.lang.String)"](null, "file");
+ res.handlers.jsobject = {
+ banana: "yellow",
+ kiwi_macro: function() { return this.kiwiColor },
+ apple: function() {},
+ kiwiColor: "green"
+ };
+ res.data.date = new Date();
+ res.data.banana = "yellow";
+ res.data.kiwi_macro = function() { return "green" };
+ res.data.apple = function() {};
+ session.data.banana = "yellow";
+ session.data.kiwi_macro = function() { return "green" };
+ session.data.apple = function() {};
+};
+
+var testCommentMacro = function() {
+ var skin = createSkin("<% // this.foo bar=<% this.foobar %> FIXME %>ok");
+ var result = renderSkinAsString(skin);
+ assertEqual(result, "ok");
+}
+
+var testDeepResolve = function() {
+ res.handlers.deep = {
+ getMacroHandler: function(name) {
+ if (name != "foo") return null;
+ return {
+ bar_macro: function() {
+ return "ok";
+ }
+ }
+ }
+ }
+ var result = renderSkinAsString(createSkin("<% deep.foo.bar %>"));
+ assertEqual(result, "ok");
+};
+
+var testDeepUnhandled = function() {
+ res.handlers.deep = {
+ getMacroHandler: function(name) {
+ if (name != "foo") return null;
+ return {
+ onUnhandledMacro: function(name) {
+ if (name == "bar") return "ok";
+ }
+ }
+ }
+ }
+ var result = renderSkinAsString(createSkin("<% deep.foo.bar %>"));
+ assertEqual(result, "ok");
+};
+
+var testDeep = function() {
+ res.handlers.deep = {
+ foo: {
+ bar: "ok"
+ }
+ }
+ var result = renderSkinAsString(createSkin("<% deep.foo.bar %>"));
+ assertEqual(result, "ok");
+};
+
+var testDeepFail = function() {
+ var result = renderSkinAsString(createSkin("<% root.foo.bar %>"));
+ assertStringContains(result, "Unhandled");
+ var result = renderSkinAsString(createSkin("<% root.foo.bar failmode=silent %>"));
+ assertEqual(result, "");
+};
+
+var testDeepFailSilent = function() {
+ res.handlers.deep = {
+ foo: {}
+ };
+ var result = renderSkinAsString(createSkin("<% deep.foo.bar %>"));
+ assertEqual(result, "");
+ var result = renderSkinAsString(createSkin("<% deep.foo.bar failmode=verbose %>"));
+ assertStringContains(result, "Unhandled");
+};
+
+
+var testJavaProp = function() {
+ var result = renderSkinAsString(createSkin("<% color.green %>"));
+ assertEqual(result, "255");
+ result = renderSkinAsString(createSkin("<% color.red %>"));
+ assertEqual(result, "0");
+};
+
+var testJavaMissing = function() {
+ var result = renderSkinAsString(createSkin("<% colo.foo %>"));
+ assertStringContains(result, "Unhandled");
+};
+
+var testJavaMissingSilent = function() {
+ var result = renderSkinAsString(createSkin("<% color.foo failmode=silent default=ok %>"));
+ assertEqual(result, "ok");
+};
+
+var testJavaMissingVerbose = function() {
+ var result = renderSkinAsString(createSkin("<% color.foo failmode=verbose %>"));
+ assertStringContains(result, "Unhandled");
+}
+
+var testUndefinedHandler = function() {
+ var result = renderSkinAsString(createSkin("<% file.parentFile %>"));
+ assertEqual(result, "");
+};
+
+var testJSProp = function() {
+ var result = renderSkinAsString(createSkin("<% jsobject.banana %>"));
+ assertEqual(result, "yellow");
+};
+
+var testJSMacro = function() {
+ var result = renderSkinAsString(createSkin("<% jsobject.kiwi %>"));
+ assertEqual(result, "green");
+};
+
+var testJSFunction = function() {
+ var result = renderSkinAsString(createSkin("<% jsobject.apple failmode=silent %>"));
+ assertEqual(result, "");
+};
+
+var testJSMissing = function() {
+ var result = renderSkinAsString(createSkin("<% jsobject.papaya %>"));
+ assertEqual(result, "");
+};
+
+var testJSMissingSilent = function() {
+ var result = renderSkinAsString(createSkin("<% jsobject.papaya failmode=silent %>"));
+ assertEqual(result, "");
+};
+
+var testJSMissingVerbose = function() {
+ var result = renderSkinAsString(createSkin("<% jsobject.papaya failmode=verbose %>"));
+ assertStringContains(result, "Unhandled");
+};
+
+var testDateFilter = function() {
+ var result = renderSkinAsString(createSkin("<% root.date | isDate %>"));
+ assertEqual(result, "true");
+};
+
+var testNestedRootMacro = function() {
+ var skin = "<% echo <% root.date %> | isRootDate %>";
+ var result = renderSkinAsString(createSkin(skin));
+ assertEqual(result, "true");
+}
+
+var testNestedParamMacro = function() {
+ var skin = "<% echo <% param.date %> | isDate %>";
+ var result = renderSkinAsString(createSkin(skin), { date: new Date() });
+ assertEqual(result, "true");
+}
+
+
+var testNestedResponseMacro = function() {
+ var skin = "<% echo what=<% response.date %> | isResponseDate %>";
+ var result = renderSkinAsString(createSkin(skin));
+ assertEqual(result, "true");
+};
+
+var testNestedPrefixSuffix = function() {
+ var skin = "<% root.macro prefix=<% root.string %> suffix=<% root.macro %> %>";
+ var result = renderSkinAsString(createSkin(skin));
+ // root.macro changes suffix to "."
+ assertEqual(result, "rootroot.");
+}
+
+var testResponseProp = function() {
+ var result = renderSkinAsString(createSkin("<% response.banana %>"));
+ assertEqual(result, "yellow");
+};
+
+var testResponseMacro = function() {
+ var result = renderSkinAsString(createSkin("<% response.kiwi %>"));
+ assertEqual(result, "green");
+};
+
+var testResponseFunction = function() {
+ var result = renderSkinAsString(createSkin("<% response.apple failmode=silent %>"));
+ assertEqual(result, "");
+};
+
+var testResponseMissing = function() {
+ var result = renderSkinAsString(createSkin("<% response.papaya %>"));
+ assertEqual(result, "");
+};
+
+var testResponseMissingSilent = function() {
+ var result = renderSkinAsString(createSkin("<% response.papaya failmode=silent %>"));
+ assertEqual(result, "");
+};
+
+var testResponseMissingVerbose = function() {
+ var result = renderSkinAsString(createSkin("<% response.papaya failmode=verbose %>"));
+ assertStringContains(result, "Unhandled");
+};
+
+var testRootProp = function() {
+ var result = renderSkinAsString(createSkin("<% root.string %>"));
+ assertEqual(result, "root");
+};
+
+var testRootMacro = function() {
+ var result = renderSkinAsString(createSkin("<% root.macro %>"));
+ assertEqual(result, "root");
+};
+
+var testRootMissing = function() {
+ var result = renderSkinAsString(createSkin("<% root.undefinedmacro %>"));
+ assertStringContains(result, "Unhandled");
+};
+
+var testRootMissingSilent = function() {
+ var result = renderSkinAsString(createSkin("<% root.undefinedmacro failmode=silent %>"));
+ assertEqual(result, "");
+};
+
+var testRootMissingVerbose = function() {
+ var result = renderSkinAsString(createSkin("<% root.undefinedmacro failmode=verbose %>"));
+ assertStringContains(result, "Unhandled");
+};
+
+var testSessionProp = function() {
+ var result = renderSkinAsString(createSkin("<% session.banana %>"));
+ assertEqual(result, "yellow");
+};
+
+var testSessionMacro = function() {
+ var result = renderSkinAsString(createSkin("<% session.kiwi %>"));
+ assertEqual(result, "green");
+};
+
+var testSessionFunction = function() {
+ var result = renderSkinAsString(createSkin("<% session.apple failmode=silent %>"));
+ assertEqual(result, "");
+};
+
+var testSessionMissingDefault = function() {
+ var result = renderSkinAsString(createSkin("<% session.papaya default=nope %>"));
+ assertEqual(result, "nope");
+};
+
+var testSessionMissing = function() {
+ var result = renderSkinAsString(createSkin("<% session.papaya %>"));
+ assertEqual(result, "");
+};
+
+var testSessionMissingSilent = function() {
+ var result = renderSkinAsString(createSkin("<% session.papaya failmode=silent %>"));
+ assertEqual(result, "");
+};
+
+var testSessionMissingVerbose = function() {
+ var result = renderSkinAsString(createSkin("<% session.papaya failmode=verbose %>"));
+ assertStringContains(result, "Unhandled");
+};
+
+var testSessionDeepMissing = function() {
+ var result = renderSkinAsString(createSkin("<% session.user.name %>"));
+ assertStringContains(result, "Unhandled");
+ // assertEqual(result, "");
+};
+
+var testSubskins = function() {
+ var result = renderSkinAsString("subskins");
+ assertEqual(result, "mainskin");
+ result = renderSkinAsString("subskins#subskin1");
+ assertEqual(result, "subskin1");
+ result = renderSkinAsString("subskins#subskin2");
+ assertEqual(result, "subskin2");
+};
diff --git a/modules/test/tests/helma.Http.js b/modules/test/tests/helma.Http.js
new file mode 100644
index 00000000..3cd51cf8
--- /dev/null
+++ b/modules/test/tests/helma.Http.js
@@ -0,0 +1,68 @@
+var tests = [
+ "testSimple",
+ "testError",
+ "testNotFound",
+ "testRedirect",
+ "testRedirectNoFollow",
+ "testMaxResponseSize",
+ "testLongResponse"
+];
+
+app.addRepository("modules/helma/Http.js");
+
+var http = new helma.Http();
+
+var testSimple = function() {
+ var result = http.getUrl(root.href("hello"));
+ assertEqual(result.content, "Hello");
+ assertEqual(result.code, 200);
+};
+
+var testError = function() {
+ var result = http.getUrl(root.href("throwerror"));
+ assertEqual(result.content, "Error");
+ assertEqual(result.code, 500);
+};
+
+var testNotFound = function() {
+ var result = http.getUrl(root.href("nonExistingAction"));
+ assertEqual(result.content, "Not found");
+ assertEqual(result.code, 404);
+};
+
+var testRedirect = function() {
+ var result = http.getUrl(root.href("redirect"));
+ assertEqual(result.content, "Hello");
+ assertEqual(result.code, 200);
+};
+
+var testRedirectNoFollow = function() {
+ http.setFollowRedirects(false);
+ var result = null;
+ try {
+ result = http.getUrl(root.href("redirect"));
+ } finally {
+ http.setFollowRedirects(true);
+ }
+ assertEqual(result.content, "");
+ // response codes 302 and 303 are both ok
+ assertTrue(result.code == 302 || result.code == 303);
+};
+
+var testMaxResponseSize = function() {
+ http.setMaxResponseSize(3);
+ var error = null;
+ try {
+ http.getUrl(root.href("hello"));
+ } catch (err) {
+ error = err;
+ } finally {
+ http.setMaxResponseSize(null);
+ }
+ assertNotNull(error);
+};
+
+var testLongResponse = function() {
+ var result = http.getUrl(root.href("long"));
+ assertEqual(result.content, makeLongString());
+};
diff --git a/modules/test/tests/helma.Search.js b/modules/test/tests/helma.Search.js
new file mode 100644
index 00000000..f70a68d7
--- /dev/null
+++ b/modules/test/tests/helma.Search.js
@@ -0,0 +1,347 @@
+var tests = [
+ "testConstructor",
+ "testGetDirectory",
+ "testGetRAMDirectory",
+ "testCreateIndex",
+ "testGetReaderWriter",
+ "testIndexLock",
+ "testDocument",
+ "testAddDocuments",
+ "testSearch"
+];
+
+app.addRepository("modules/helma/Search.js");
+
+var indexName = "testindex";
+var basePath = java.lang.System.getProperty("java.io.tmpdir");
+var index;
+
+/**
+ * Test preliminaries
+ */
+var setup = function() {
+ // check if there is a (leftover) directory with the same name
+ // in the system's temporary directory - if so throw an exception and stop
+ var dir = new helma.File(basePath, indexName);
+ if (dir.exists()) {
+ throw "There is already a directory '" + dir.getAbsolutePath() +
+ "', please remove it before trying to run test again";
+ }
+ return;
+};
+
+/**
+ * Test the helma.Search constructor to make sure Lucene is loaded
+ */
+var testConstructor = function() {
+ // should not throw an exception
+ var s = new helma.Search();
+ return;
+};
+
+/**
+ * Test getDirectory method
+ */
+var testGetDirectory = function() {
+ var search = new helma.Search();
+ assertThrows(function() {
+ search.getDirectory();
+ });
+ var dirs = [
+ new helma.File(basePath, indexName),
+ new File(basePath, indexName),
+ new java.io.File(basePath, indexName),
+ basePath + "/" + indexName
+ ];
+ var dir, fsDir, dirPath;
+ for (var i in dirs) {
+ dir = dirs[i];
+ if (dir.constructor != String) {
+ dirPath = dir.getAbsolutePath();
+ } else {
+ dirPath = dir;
+ }
+ fsDir = search.getDirectory(dir);
+ assertNotNull(fsDir);
+ assertEqual(fsDir.getFile().getAbsolutePath(), dirPath);
+ }
+ return;
+};
+
+/**
+ * Test getRAMDirectory method
+ */
+var testGetRAMDirectory = function() {
+ var search = new helma.Search();
+ assertThrows(function() {
+ search.getDirectory();
+ });
+ var dirs = [
+ new helma.File(basePath, indexName),
+ new File(basePath, indexName),
+ new java.io.File(basePath, indexName),
+ basePath + "/" + indexName
+ ];
+ var dir, ramDir;
+ for (var i in dirs) {
+ dir = dirs[i];
+ ramDir = search.getRAMDirectory(dir);
+ assertNotNull(ramDir);
+ }
+ return;
+};
+
+/**
+ * Test index creation - this method creates a RAMDirectory based
+ * index for testing and stores it in the global variable "index"
+ */
+var testCreateIndex = function() {
+ var search = new helma.Search();
+ // test creating a file based index
+ var fsDir = search.getDirectory(new helma.File(basePath, indexName));
+ index = search.createIndex(fsDir);
+ assertNotNull(index);
+ // explicitly test index.create(true)
+ assertTrue(index.create(true));
+
+ // test creating a ram based index
+ var ramDir = search.getRAMDirectory();
+ index = search.createIndex(ramDir);
+ assertNotNull(index);
+ // explicitly test index.create(true)
+ assertTrue(index.create(true));
+ assertEqual(index.constructor, helma.Search.Index);
+ assertEqual(index.size(), 0);
+ return;
+};
+
+/**
+ * Test getting index reader, writer and modifier
+ */
+var testGetReaderWriter = function() {
+ // test getReader
+ var reader = index.getReader();
+ assertNotNull(reader);
+ reader.close();
+ // test getWriter
+ var writer = index.getWriter();
+ assertNotNull(writer);
+ writer.close();
+ return;
+};
+
+/**
+ * Test index locking
+ */
+var testIndexLock = function() {
+ // test if the index is correctly locked when opening a writer
+ var writer = index.getWriter();
+ assertTrue(index.isLocked());
+ // test if getWriter() throws an exception when trying to open a second writer
+ assertThrows(function() {
+ index.getWriter();
+ });
+ writer.close();
+ assertFalse(index.isLocked());
+ return;
+};
+
+/**
+ * Test document constructor and methods
+ */
+var testDocument = function() {
+ var doc = new helma.Search.Document();
+ var f;
+
+ // test type conversion
+ f = new helma.Search.Document.Field("id", 1);
+ assertEqual(f.value.constructor, String);
+ assertEqual(f.value, "1");
+ var now = new Date();
+ f = new helma.Search.Document.Field("createtime", now);
+ assertEqual(f.dateValue.constructor, Date);
+ assertEqual(f.dateValue.getTime(), now.getTime() - (now.getTime() % 60000));
+
+ // test adding field with default store and index options
+ doc.addField(new helma.Search.Document.Field("id", 1));
+ f = doc.getField("id");
+ assertNotNull(f);
+ assertTrue(f.isStored());
+ assertTrue(f.isIndexed());
+ assertTrue(f.isTokenized());
+
+ // test adding date field with changed field options
+ f = new helma.Search.Document.Field("createtime", new Date(), {
+ store: "no",
+ index: "tokenized"
+ });
+ doc.addField(f);
+ f = doc.getField("createtime");
+ assertNotNull(f);
+ assertFalse(f.isStored());
+ assertTrue(f.isIndexed());
+ assertTrue(f.isTokenized());
+
+ // test deprecated way of calling addField()
+ doc.addField("title", "Just a test", {
+ "store": false,
+ "index": true,
+ "tokenize": false
+ });
+ f = doc.getField("title");
+ assertNotNull(f);
+ assertFalse(f.isStored());
+ assertTrue(f.isIndexed());
+ assertFalse(f.isTokenized());
+
+ // test getFields()
+ var fields = doc.getFields();
+ assertEqual(fields.length, 3);
+ assertEqual(fields[0].name, "id");
+ assertEqual(fields[1].name, "createtime");
+ assertEqual(fields[2].name, "title");
+ return;
+};
+
+/**
+ * Test adding documents
+ */
+var testAddDocuments = function() {
+ // test addDocument()
+ var doc = new helma.Search.Document();
+ doc.addField(new helma.Search.Document.Field("id", 1));
+ index.addDocument(doc);
+ assertEqual(index.size(), 1);
+
+ // test removeDocument()
+ index.removeDocument("id", 1);
+ assertEqual(index.size(), 0);
+
+ // test addDocuments() and removeDocuments() with an array
+ doc = new helma.Search.Document();
+ doc.addField(new helma.Search.Document.Field("id", 2));
+ index.addDocuments([doc]);
+ assertEqual(index.size(), 1);
+ index.removeDocuments("id", [2]);
+ assertEqual(index.size(), 0);
+
+ // test addDocuments() and removeDocuments() with a Hashtable as argument
+ var ht = new java.util.Hashtable();
+ ht.put("doc", doc);
+ index.addDocuments(ht);
+ ht = new java.util.Hashtable();
+ ht.put("id", 1);
+ ht.put("id", 2);
+ index.removeDocuments("id", ht);
+ assertEqual(index.size(), 0);
+
+ // test addDocuments() and removeDocuments() with a Vector as argument
+ var v = new java.util.Vector();
+ v.add(doc);
+ index.addDocuments(v);
+ v = new java.util.Vector();
+ v.add(1);
+ v.add(2);
+ index.removeDocuments("id", v);
+ assertEqual(index.size(), 0);
+
+ // test updateDocument
+ index.addDocument(doc);
+ doc = new helma.Search.Document();
+ doc.addField("id", 2);
+ index.updateDocument(doc, "id");
+ assertEqual(index.size(), 1);
+
+ // test count()
+ doc = new helma.Search.Document();
+ doc.addField("id", 3);
+ index.addDocument(doc);
+ assertEqual(index.count("id", 3), 1);
+
+ return;
+};
+
+/**
+ * Test searching the index
+ */
+var testSearch = function() {
+ // clear the index
+ index.create();
+ assertEqual(index.size(), 0);
+
+ // populate the index with test content
+ var names = [
+ "foo",
+ "bar",
+ "baz"
+ ];
+
+ var now = new Date();
+ var doc;
+ names.forEach(function(name, idx) {
+ doc = new helma.Search.Document();
+ doc.addField("id", idx + 1);
+ doc.addField("parent", idx % 2);
+ doc.addField("name", name);
+ doc.addField("timestamp", new Date(now.getTime() - (idx * 1e6)));
+ index.addDocument(doc);
+ });
+ assertEqual(index.size(), 3);
+
+ var searcher = index.getSearcher();
+ assertNotNull(searcher);
+ assertNull(searcher.sortFields);
+ assertNotNull(searcher.getSearcher());
+ assertTrue(searcher.getSearcher() instanceof Packages.org.apache.lucene.search.IndexSearcher);
+
+ // test basic search
+ var q = new helma.Search.TermQuery("id", 1);
+ assertEqual(searcher.search(q), 1);
+ assertNotNull(searcher.hits);
+ assertEqual(searcher.hits.constructor, helma.Search.HitCollection);
+ assertEqual(searcher.hits.size(), 1);
+ var hit = searcher.hits.get(0);
+ assertNotNull(hit);
+ assertEqual(hit.constructor, helma.Search.Document);
+ assertEqual(hit.getField("name").constructor, helma.Search.Document.Field);
+ assertEqual(hit.getField("name").value, "foo");
+ // test date value conversion
+ assertEqual(hit.getField("timestamp").value.constructor, String);
+ assertEqual(hit.getField("timestamp").dateValue.constructor, Date);
+
+ // test query filter
+ var qf = new helma.Search.QueryFilter(new helma.Search.TermQuery("parent", 0));
+ q = new helma.Search.WildcardQuery("name", "ba*");
+ assertEqual(searcher.search(q, qf), 1);
+ assertEqual(searcher.hits.get(0).getField("name").value, names[2]);
+
+ // test sorting of hits
+ searcher.sortBy("id", true);
+ assertEqual(searcher.search(q), 2);
+ assertEqual(searcher.hits.get(0).getField("name").value, names[2]);
+ assertEqual(searcher.hits.get(1).getField("name").value, names[1]);
+
+ // test boolean query
+ q = new helma.Search.BooleanQuery();
+ q.addTerm("parent", "0");
+ assertEqual(q.toString(), "[(parent:0)]");
+ q = new helma.Search.BooleanQuery();
+ q.addTerm("parent", "0", "and");
+ assertEqual(q.toString(), "[+(parent:0)]");
+ q = new helma.Search.BooleanQuery();
+ q.addTerm("parent", "0", "not");
+ assertEqual(q.toString(), "[-(parent:0)]");
+
+ searcher.close();
+ return;
+};
+
+/**
+ * Cleanup
+ */
+var cleanup = function() {
+ // remove the directory containing the test index
+ var dir = new helma.File(basePath, indexName);
+ dir.removeDirectory();
+ return;
+}
\ No newline at end of file