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" %> + + + + +
application
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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 %>
+ + + + + +<% this.properties itemprefix='' %> +
app.properties
' 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 +
+ + + + +
+ /read">showAPI | + /render">renderAPI | + public | + ?app=<% this.title %>&action=flush">flush | + ?app=<% this.title %>&action=restart">restart +
+
+
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 @@ +
+ + + + +
+ ?app=<% this.title %>&action=start">start +
+ <% this.title %> +
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 %> + + + + + +" name="prototypes"> +" name="functions"> + +" name="main"> + + +<h2> +Frame Alert</h2> + +<p> +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:
  • " separator="
  • " suffix="

" %> +
    + <% 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="" + suffix="" + %> + + <% this.methods separator="" + filter="functions" + skin="asLargeListItem" + prefix="" + suffix="" + %> + + <% this.methods separator="" + filter="macros" + skin="asLargeListItem" + prefix="" + suffix="" + %> + + <% this.methods separator="" + filter="skins" + skin="asLargeListItemSkin" + prefix="" + suffix="" + %> + + <% this.methods separator="" + filter="properties" + skin="asLargeListItemSkin" + prefix="" + suffix="" + %> + + <% this.parentPrototype skin="asParentList" %> +
Actions
 
Functions
 
Macros
 
Skins
 
type.properties
 
+ +

+ + + + + + + + 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.appCount filter="active" singular=" app" plural=" apps"%> on + "><% root.hostname %> (<% root.hostaddress %>) +
    + +<% 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 %> +
    + (username)
    + (password)
    +
    +
    + +

    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 %>)

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<% this.properties itemprefix='' %> +
    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
    ' 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: + *
      + *
    1. param.none - not a single child node
    2. + *
    3. param.one - exactly one child node
    4. + *
    5. param.many - more than one child node
    6. + *
    + * @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 endList = "
    "; + 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