/** * TrimPath Junction. Release 1.0.39. * Copyright (C) 2004, 2005 Metaha. * * TrimPath Junction is licensed under the GNU General Public License * and the Apache License, Version 2.0, as follows: * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var TrimPath; (function(safeEval) { if (TrimPath == null) TrimPath = new Object(); var MANY_ZEROS = "000000000000000000"; var junctionUtil = TrimPath.junctionUtil = { safeEval : safeEval, upperFirst : function(str) { return str.charAt(0).toUpperCase() + str.substring(1); }, lowerFirst : function(str) { return str.charAt(0).toLowerCase() + str.substring(1); }, encodeAngles : function(str) { return str.replace(//g, '>'); }, decodeAngles : function(str) { return str.replace(/</g, '<').replace(/>/g, '>'); }, encodeQuotes : function(str) { // Useful for inline javascript event handler attributes. return str.replace(/'/g, "\\'").replace(/"/g, '"'); }, leftZeroPad : function(val, minLength) { if (typeof(val) != "string") val = String(val); return (MANY_ZEROS.substring(0, minLength - val.length)) + val; }, toLocalDateString : function(date, withTime) { if (typeof(date) == "string") { date = junctionUtil.parseDateString(date); if (date == null) return ""; } date = date || new Date(); var result = junctionUtil.leftZeroPad(date.getFullYear(), 4) + '/' + junctionUtil.leftZeroPad(date.getMonth() + 1, 2) + '/' + junctionUtil.leftZeroPad(date.getDate(), 2); if (withTime == true) result = result + ' ' + junctionUtil.leftZeroPad(date.getHours(), 2) + ':' + junctionUtil.leftZeroPad(date.getMinutes(), 2) + ':' + junctionUtil.leftZeroPad(date.getSeconds(), 2); return result; }, toSQLDateString : function(date) { date = date || new Date(); return junctionUtil.leftZeroPad(date.getUTCFullYear(), 4) + '/' + junctionUtil.leftZeroPad(date.getUTCMonth() + 1, 2) + '/' + junctionUtil.leftZeroPad(date.getUTCDate(), 2) + ' ' + junctionUtil.leftZeroPad(date.getUTCHours(), 2) + ':' + junctionUtil.leftZeroPad(date.getUTCMinutes(), 2) + ':' + junctionUtil.leftZeroPad(date.getUTCSeconds(), 2) + ' UTC'; }, prepSQLParams : function(sqlParams) { if (sqlParams != null) { for (var i = 0; i < sqlParams.length; i++) { if (sqlParams[i] instanceof Date) sqlParams[i] = junctionUtil.toSQLDateString(sqlParams[i]); } } return sqlParams; }, isValidSQLName : function(name) { return (name.search(/^\w+$/) == 0); }, parseDateString : function(s) { var d = new Date(s); if (d != null && isNaN(d.getTime()) == false) return d; return null; }, setMapTreeValue : function(mapTree, path, value) { // Example path is 'order[customer][name]'. if (path != null) { var keys = path.replace(/\]/g, '').split('['); for (var k = 0; k < keys.length; k++) { var key = keys[k]; if (k < keys.length - 1) { if (mapTree[key] == null) mapTree[key] = {}; mapTree = mapTree[key]; } else { mapTree[key] = value; } } } }, copyProps : function(src, dst) { // The dst is always a record-like object. for (var k in src) { if (typeof(src[k]) != 'function' && junctionUtil.isValidSQLName(k)) { dst[k] = src[k]; // By convention, fields with "Id" suffix are integers. var suffix = k.slice(-2); if (suffix == "Id") dst[k] = junctionUtil.nanToNull(parseInt(dst[k])); // By convention, fields with "At" suffix are dates. if (suffix == "At" && src[k] != null) { if (typeof(src[k]) == "string") { var d = junctionUtil.parseDateString(src[k]); if (d != null) dst[k] = junctionUtil.toSQLDateString(d); else dst[k] = null; } if (src[k] instanceof Date) dst[k] = junctionUtil.toSQLDateString(src[k]); } } } return dst; }, nanToNull : function(val) { return isNaN(val) ? null : val; }, findRecordIndex : function(records, id) { // Returns the index of the record with the given id. if (records != null) { for (var i = 0; i < records.length; i++) { if (records[i].id == id) return i; } } return -1; }, beforeFilter : function(funcFilter, scopeObj) { // The remaining varargs are action function names in the scope object. var installBeforeFilter = function(actionName) { var funcOrig = scopeObj[actionName]; scopeObj[actionName] = function(req, res) { if (funcFilter(req, res) == false) return; return funcOrig(req, res); } } for (var i = 2; i < arguments.length; i++) installBeforeFilter(arguments[i]); }, callIfExists : function(obj, methodName, defaultResult) { if (obj[methodName] == null) return defaultResult; return obj[methodName](); }, createMemoryDataProvider : function(data, idGenerator) { var recordChanged = function(tableName, id, op) { var changes = memoryDataProvider.changesFor(tableName); changes[id] = [ op ]; } var memoryDataProvider = { changesFor : function(tableName) { var key = "changes @@ " + tableName; if (data[key] == null) // We store changes into the data hash so that changes are data[key] = {}; // auto-serialized to json along with any normal data. return data[key]; }, query : function(stmt) { return stmt.filter(data); // See TrimQuery API. }, save : function(tableName, obj) { var records = data[tableName]; if (records == null) records = data[tableName] = []; var index = junctionUtil.findRecordIndex(records, obj.id); if (index >= 0) { junctionUtil.copyProps(obj, records[index]); recordChanged(tableName, obj.id, "update"); return true; } if (obj.isNewRecord()) // We use a negative id value to denote client-only existence. obj.id = idGenerator.nextId(); records.push(junctionUtil.copyProps(obj, {})); recordChanged(tableName, obj.id, "insert"); return true; }, destroy : function(tableName, id) { var records = data[tableName]; var index = junctionUtil.findRecordIndex(records, id); if (index >= 0) { records.splice(index, 1); recordChanged(tableName, id, "delete"); return true; } return false; }, resetAll : function() { for (var tableName in data) { if (typeof(data[tableName]) != 'function') data[tableName] = (junctionUtil.isValidSQLName(tableName) ? [] : {}); } if (TrimPath.junction.env.syncDOMData != null) TrimPath.junction.env.syncDOMData(); }, // Attributes that are part of memory data provider only... memoryData : data // Keyed by table name. }; return memoryDataProvider; }, toArray : function(obj, length, filterFunc) { length = length || obj.length; var result = []; for (var i = 0; i < length; i++) if (filterFunc == null || filterFunc(obj[i])) result.push(obj[i]); return result; }, pushAttributes : function(array, attrs) { for (var k in attrs) if (typeof(attrs[k]) != "function") array.push(' ' + k + '="' + attrs[k] + '"'); return array; } }; var ModelErrors = function() { this.clear(); }; ModelErrors.prototype.clear = function() { this.attrErrorsCount = 0; this.attrErrors = {}; } ModelErrors.prototype.add = function(attrName, msg) { if (this.attrErrors[attrName] == null) this.attrErrors[attrName] = []; this.attrErrors[attrName].push(msg || "is invalid"); this.attrErrorsCount++; } ModelErrors.prototype.addToBase = function(msg) { this.add(":base", msg); } ModelErrors.prototype.onBase = function() { return this.on(":base"); } ModelErrors.prototype.on = function(attrName) { return this.attrErrors[attrName]; } ModelErrors.prototype.isInvalid = function(attrName) { return this.on(attrName) != null; } ModelErrors.prototype.count = function() { return this.attrErrorsCount; } ModelErrors.prototype.isEmpty = function() { return this.count() == 0; } ModelErrors.prototype.invalidAttributes = function() { var result = []; for (var attrName in this.attrErrors) { var errs = this.attrErrors[attrName]; if (errs != null && errs instanceof Array) result.push(attrName); } return result; } ModelErrors.prototype.fullMessagesOn = function(attrName) { var msgs = this.on(attrName); if (msgs == null || attrName == ":base") return msgs; var results = []; for (var i = 0; i < msgs.length; i++) results.push(attrName + " " + msgs[i]); return results; } ModelErrors.prototype.fullMessages = function() { var results = []; for (var i = 0, attrs = this.invalidAttributes(); i < attrs.length; i++) results.concat(this.fullMessagesOn(attrs[i]) || []) return results; } TrimPath.junctionCreate = function(env) { var modelInfos = {}; // Keyed by model name, like 'InvoiceLine'. var modelQueryLang = null; var lastFormArgs = null; // For startFormTag() and the other form tag helpers. var getFunc_cache = {}; var getFunc = function(funcName) { try { return safeEval(funcName); } catch (e) {}; return null; // No actual caching, because it prevents in-browser app development. // if (getFunc_cache[funcName] == null) // getFunc_cache[funcName] = safeEval(funcName); // return getFunc_cache[funcName]; } var sqlPrepare_cache = {}; var sqlPrepare = function(sql, sqlParams) { var key = sql + " -- " + sqlParams; if (sqlPrepare_cache[key] == null) { sqlPrepare_value = junction.queryLang().parseSQL(sql, sqlParams); sqlPrepare_cache[key] = sqlPrepare_value; // Note, do not collapse into previous line of code } // because of conflicts with junction.queryLang(forceReset == true). return sqlPrepare_cache[key]; } var conditionsPrep = function(conditions) { if (conditions == null) return ""; if (conditions.slice(0, 9) == "ORDER BY ") return " " + conditions; return " WHERE " + conditions; } var junction = { env : env, queryLang : function(forceReset) { if (modelQueryLang == null || forceReset == true) { modelQueryLang = env.makeQueryLang(env.dataSchemaProvider.getSchema()); sqlPrepare_cache = {}; } return modelQueryLang; }, localSqlExecute : function(sql, optParams) { return env.dataProvider.query(sqlPrepare(sql, junctionUtil.prepSQLParams(optParams))); }, modelFor : function(modelName, func) { // The modelName is like 'InvoiceLine'. var modelInfo = modelInfos[modelName]; if (modelInfo == null || modelInfo.func != getFunc(modelName)) { modelInfo = modelInfos[modelName] = { name : modelName, funcName : modelName, func : func || getFunc(modelName), tableName : modelName, pluralName : modelName + 's', hasOne : {}, hasMany : {}, belongsTo : {}, validations : { save : [], create : [], update : [] } } modelInfo.func.findAll = function(conditions, optParams) { var sql = "SELECT " + modelInfo.tableName + ".* FROM " + modelInfo.tableName + conditionsPrep(conditions); return modelInfo.func.findBySql(sql, optParams); } modelInfo.func.find = function(id) { return modelInfo.func.findFirst(modelInfo.tableName + ".id = " + parseInt(id)); // TODO: SQL injection problem here. } modelInfo.func.findFirst = function(conditions, optParams) { var sql = "SELECT " + modelInfo.tableName + ".* FROM " + modelInfo.tableName + conditionsPrep(conditions); var record = env.dataProvider.query(sqlPrepare(sql, junctionUtil.prepSQLParams(optParams)))[0]; if (record != null) return modelInfo.func.newInstance(record); return null; } modelInfo.func.findBySql = function(sql, optParams) { var records = junction.localSqlExecute(sql, optParams); var result = []; for (var i = 0; i < records.length; i++) result.push(modelInfo.func.newInstance(records[i])); return result; } modelInfo.func.newInstance = function(attrs) { var newObj = junctionUtil.copyProps(attrs, new (modelInfo.func)()); if (newObj != null) newObj.setConventionalAttributes(true); return newObj; } modelInfo.func.prototype.destroy = function() { junctionUtil.callIfExists(this, "beforeDestroy"); return env.dataProvider.destroy(modelInfo.tableName, this.id); } modelInfo.func.prototype.save = function() { var suffix = this.isNewRecord() ? "Create" : "Update"; // Need to capture this before state changes. if (this.isValid() == false) return false; this.setConventionalAttributes(); junctionUtil.callIfExists(this, "beforeSave"); junctionUtil.callIfExists(this, "before" + suffix); if (env.dataProvider.save(modelInfo.tableName, this) == false) return false; junctionUtil.callIfExists(this, "after" + suffix); junctionUtil.callIfExists(this, "afterSave"); return true; } modelInfo.func.prototype.isValid = function() { this.errors().clear(); var suffix = this.isNewRecord() ? "OnCreate" : "OnUpdate"; junctionUtil.callIfExists(this, "beforeValidation"); junctionUtil.callIfExists(this, "beforeValidation" + suffix); this.runValidations("save"); this.runValidations(this.isNewRecord() ? "create" : "update"); junctionUtil.callIfExists(this, "validate"); junctionUtil.callIfExists(this, "validate" + suffix); junctionUtil.callIfExists(this, "afterValidation"); junctionUtil.callIfExists(this, "afterValidation" + suffix); return this.errors().isEmpty(); } modelInfo.func.prototype.runValidations = function(type) { var funcs = modelInfo.validations[type]; for (var i = 0; i < funcs.length; i++) funcs[i](this); } modelInfo.func.prototype.isNewRecord = function() { return this.id == null || this.id == 0; } modelInfo.func.prototype.errors = function() { if (this["@errors"] == null) this["@errors"] = new ModelErrors(); return this["@errors"]; } modelInfo.func.prototype.updateAttributes = function(attrs) { var id = this.id; junctionUtil.copyProps(attrs, this); this.id = id; return this.save(); } modelInfo.func.prototype.setConventionalAttributes = function(skipLockVersion) { var tableSchema = env.dataSchemaProvider.getSchema()[modelInfo.tableName]; if (tableSchema != null) { if (tableSchema['lockVersion']) { if (this.lockVersion == null) this.lockVersion = 0; if (skipLockVersion != true) // Treats null as false. this.lockVersion += 1; } var now; if (tableSchema['createdAt'] && this.createdAt == null) this.createdAt = now = (now || junctionUtil.toSQLDateString(new Date())); if (tableSchema['createdOn'] && this.createdOn == null) this.createdOn = now = (now || junctionUtil.toSQLDateString(new Date())); if (tableSchema['updatedAt']) this.updatedAt = now = (now || junctionUtil.toSQLDateString(new Date())); if (tableSchema['updatedOn']) this.updatedOn = now = (now || junctionUtil.toSQLDateString(new Date())); } } modelInfo.metaAspects = { tableName : function(tableNameVal) { modelInfo.tableName = tableNameVal; }, pluralName : function(pluralNameVal) { modelInfo.pluralName = pluralNameVal; }, hasMany : function(childPluralName, info) { // The childPluralName is like 'TodoItems'. info = info || {}; info.modelName = info.modelName || childPluralName.slice(0, -1); info.foreignKey = info.foreignKey || junctionUtil.lowerFirst(modelName) + "Id"; modelInfo.hasMany[childPluralName] = info; modelInfo.func.prototype['get' + childPluralName] = function(forceReload) { if (forceReload == true || this['_cached_' + childPluralName] == null) { var childModelInfo = modelInfos[info.modelName]; var conditions = childModelInfo.tableName + "." + info.foreignKey + " = " + this.id; if (info.conditions) { if (info.conditions.indexOf("ORDER BY ") != 0) conditions += " AND"; conditions += " " + info.conditions; } this['_cached_' + childPluralName] = childModelInfo.func.findAll(conditions); } return this['_cached_' + childPluralName]; } }, belongsTo : function(parentName, info) { // The parentName is like 'TodoList'. info = info || {}; info.modelName = info.modelName || parentName; info.foreignKey = info.foreignKey || junctionUtil.lowerFirst(parentName) + "Id"; modelInfo.belongsTo[parentName] = info; modelInfo.func.prototype['get' + parentName] = function(forceReload) { if (forceReload == true || this['_cached_' + parentName] == null) { var parentModelInfo = modelInfos[info.modelName]; var conditions = parentModelInfo.tableName + ".id = " + this[info.foreignKey]; this['_cached_' + parentName] = parentModelInfo.func.findFirst(conditions); } return this['_cached_' + parentName]; } }, validatesFormatOf : function(attrName, regexp, msg, on) { modelInfo.validations[on || 'save'].push(function(obj) { if (obj[attrName] != null && String(obj[attrName]).match(regexp) == null) obj.errors().add(attrName, msg || "is invalid"); }); }, validatesInclusionOf : function(attrName, inArray, msg, on) { modelInfo.validations[on || 'save'].push(function(obj) { var val = obj[attrName]; if (val == null) return; for (var i = 0; i < inArray.length; i++) if (val == inArray[i]) return; obj.errors().add(attrName, msg || "is not included in the list"); }); }, validatesPresenceOf : function(attrName, msg, on) { modelInfo.validations[on || 'save'].push(function(obj) { if (obj[attrName] == null || obj[attrName] == "") obj.errors().add(attrName, msg || "can't be empty"); }); }, toString : function() { var result = '{'; for (var k in modelInfo) result = result + k + ': ' + modelInfo(k) + ', '; return result + '}'; } } } return modelInfo.metaAspects; }, scaffold : function(controllerFunc, modelName) { // The modelName is like 'InvoiceLine'. controllerFunc.prototype.index = function(req, res) { var modelInfo = modelInfos[modelName]; res[junctionUtil.lowerFirst(modelInfo.pluralName)] = modelInfo.func.findAll(); } controllerFunc.prototype.show = controllerFunc.prototype.edit = function(req, res) { var modelInfo = modelInfos[modelName]; res[junctionUtil.lowerFirst(modelInfo.name)] = modelInfo.func.find(req['objId']); } controllerFunc.prototype.update = function(req, res) { var modelInfo = modelInfos[modelName]; var key = junctionUtil.lowerFirst(modelInfo.name); res[key] = modelInfo.func.find(req['objId']); if (res[key].updateAttributes(req[key])) { res.flash['notice'] = 'The ' + modelName + ' is updated.'; res.redirectToAction('show', res[key].id); } else res.renderAction('edit'); } controllerFunc.prototype.newInstance = function(req, res) { var modelInfo = modelInfos[modelName]; res[junctionUtil.lowerFirst(modelInfo.name)] = modelInfo.func.newInstance(); } controllerFunc.prototype.create = function(req, res) { var modelInfo = modelInfos[modelName]; var key = junctionUtil.lowerFirst(modelInfo.name); res[key] = modelInfo.func.newInstance(req[key]); if (res[key].save()) { res.flash['notice'] = 'The ' + modelName + ' is created.'; res.redirectToAction('show', res[key].id); } else res.renderAction('newInstance'); } controllerFunc.prototype.destroy = function(req, res) { var modelInfo = modelInfos[modelName]; var obj = modelInfo.func.find(req['objId']); if (obj != null) { obj.destroy(); res.flash['notice'] = 'The ' + modelName + ' is deleted.'; } else res.flash['notice'] = 'We could not delete an unknown ' + modelName + '.'; res.redirectToAction('index'); } }, /////////////////////////////////////////////// localGet : function(controllerName, actionName, objId, req) { return junction.localRequest("get", controllerName, actionName, objId, req); }, localPost : function(controllerName, actionName, objId, req) { return junction.localRequest("post", controllerName, actionName, objId, req); }, localRequest : function(type, controllerName, actionName, objIdOrig, req) { var controllerFuncName = junctionUtil.upperFirst(controllerName) + "Controller"; var controllerFunc = getFunc(controllerFuncName); if (controllerFunc == null || typeof(controllerFunc) != 'function') return env.errorUnknownController(controllerName, controllerFuncName); var objId = env.mapObjId(objIdOrig); if (req == null) req = {}; req.type = type; req.controllerName = controllerName; req.actionName = actionName; req.objId = objId; req.session = env.getSession(); var urlForArgsCheck = function(controllerNameVal, actionNameVal, objIdVal) { return { controllerNameVal : (controllerNameVal || controllerName), actionNameVal : (actionNameVal || actionName), objIdVal : (objIdVal) }; } var resRendered = null; var resRedirect = null; var res = { req : req, session : req.session, flash : req.session.flash || {}, layoutName : controllerFunc.layoutName || 'default', renderAction : function(actionName) { return res.render(controllerName + '/' + actionName); }, render : function(templateName) { if (templateName == null) templateName = controllerName + '/' + actionName; return res.renderTemplate("/app/views/" + templateName + ".jst"); }, renderTemplate : function(templatePath) { return res.renderText(env.templateRenderer(templatePath, res)); }, renderText : function(text) { resRendered = text; return text; }, redirectTo : function(controllerNameVal, actionNameVal, objIdVal) { resRedirect = urlForArgsCheck(controllerNameVal, actionNameVal, objIdVal); }, redirectToAction : function(actionNameVal, objIdVal) { res.redirectTo(controllerName, actionNameVal, objIdVal); }, urlForArgs : function(args) { var result = [ '?controllerName=', args.controllerNameVal, '&actionName=', args.actionNameVal ]; if (args.objIdVal) result.push('&objId=' + args.objIdVal); return result.join(''); }, urlFor : function(controllerNameVal, actionNameVal, objIdVal) { return res.urlForArgs(urlForArgsCheck(controllerNameVal, actionNameVal, objIdVal)); }, htmlOptionsPrepareConfirm : function(htmlOptions) { if (htmlOptions != null && htmlOptions.confirm != null) { if (htmlOptions.confirm == true) htmlOptions.confirm = "Are you sure?"; htmlOptions.onclick = "if (!confirm('" + htmlOptions.confirm + "')) return false;" + (htmlOptions.onclick || ""); htmlOptions.confirm = null; if (htmlOptions['class'] == null) htmlOptions['class'] = 'confirm'; } }, linkToArgs : function(linkText, args, htmlOptions, asButton) { res.htmlOptionsPrepareConfirm(htmlOptions); var result; if (asButton != true) result = [ ''); if (asButton != true) { result.push(linkText); result.push(''); } return result.join(''); }, linkTo : function(linkText, controllerNameVal, actionNameVal, objIdVal, htmlOptions) { return res.linkToArgs(linkText, urlForArgsCheck(controllerNameVal, actionNameVal, objIdVal), htmlOptions); }, linkToLocal : function(linkText, controllerNameVal, actionNameVal, objIdVal, htmlOptions, asButton) { var args = urlForArgsCheck(controllerNameVal, actionNameVal, objIdVal); htmlOptions = htmlOptions || {}; htmlOptions.onclick = htmlOptions.onclick || ""; htmlOptions.onclick = htmlOptions.onclick + ";TrimPath.junction.localGet('" + args.controllerNameVal + "', '" + args.actionNameVal + "'"; if (args.objIdVal) htmlOptions.onclick += ", '" + args.objIdVal + "'"; htmlOptions.onclick += "); return false"; return res.linkToArgs(linkText, args, htmlOptions, asButton); }, buttonToLocal : function(buttonText, controllerNameVal, actionNameVal, objIdVal, htmlOptions) { return res.linkToLocal(buttonText, controllerNameVal, actionNameVal, objIdVal, htmlOptions, true); }, defaultFormId : function(args) { return res.urlForArgs(args).replace(/&/g, '|'); }, startFormTag : function(controllerNameVal, actionNameVal, objIdVal, htmlOptions) { lastFormArgs = urlForArgsCheck(controllerNameVal, actionNameVal, objIdVal); lastFormArgs.htmlOptions = htmlOptions || {}; lastFormArgs.htmlOptions.id = lastFormArgs.htmlOptions.id || res.defaultFormId(lastFormArgs); lastFormArgs.htmlOptions.method = lastFormArgs.htmlOptions.method || 'post'; var result = [ '
'); return result.join(''); }, startFormTagLocal : function(controllerNameVal, actionNameVal, objIdVal, htmlOptions) { args = urlForArgsCheck(controllerNameVal, actionNameVal, objIdVal); htmlOptions = htmlOptions || {}; htmlOptions.id = htmlOptions.id || res.defaultFormId(args); htmlOptions.onsubmit = [ "TrimPath.junction.env.localPostForm('", htmlOptions.id, "', '", args.controllerNameVal, "', '", args.actionNameVal, "'" ]; if (args.objIdVal) htmlOptions.onsubmit.push(", '" + args.objIdVal + "'"); htmlOptions.onsubmit.push('); return false'); htmlOptions.onsubmit = htmlOptions.onsubmit.join(''); return res.startFormTag(args.controllerNameVal, args.actionNameVal, args.objIdVal, htmlOptions); }, endFormTag : function() { lastFormArgs = null; return "
"; }, inputTag : function(name, value, inputType, htmlOptions) { htmlOptions = htmlOptions || {}; res.htmlOptionsPrepareConfirm(htmlOptions); if (value == null) value = ""; value = value.replace(/\"/g, '"'); var result = [''); return result.join(''); }, inputField : function(objName, attrName, inputType, htmlOptions) { htmlOptions = htmlOptions || {}; var value = null; if (res[objName] != null) value = res[objName][attrName]; return res.inputTag(objName + '[' + attrName + ']', value, inputType, htmlOptions); }, textField : function(objName, attrName, htmlOptions) { return res.inputField(objName, attrName, 'text', htmlOptions); }, hiddenField : function(objName, attrName, htmlOptions) { return res.inputField(objName, attrName, 'hidden', htmlOptions); }, passwordField : function(objName, attrName, htmlOptions) { return res.inputField(objName, attrName, 'password', htmlOptions); }, radioButton : function(objName, attrName, tagValue, htmlOptions) { htmlOptions = htmlOptions || {}; if (res[objName] != null && res[objName][attrName] == tagValue) htmlOptions.checked = 'checked'; return res.inputTag(objName + '[' + attrName + ']', tagValue, 'radio', htmlOptions); }, select : function(objName, attrName, choices, htmlOptions) { htmlOptions = htmlOptions || {}; var value = null; if (res[objName] != null) value = res[objName][attrName]; var result = [''); return result.join(''); }, optionsForSelect : function(choices, selected) { var result = []; for (var i = 0; i < choices.length; i++) { var name, value; var choice = choices[i]; if (choice != null && choice instanceof Array) { name = choice[0]; value = choice[1]; } else name = value = choice; if (value == null) value = ''; result.push(''); } return result.join(''); }, textArea : function(objName, attrName, htmlOptions) { var value = null; if (res[objName] != null) value = res[objName][attrName]; if (value == null) value = ""; var result = [''); return result.join(''); }, submitButton : function(name, value, htmlOptions) { return res.inputTag(name, value, 'submit', htmlOptions); }, submitButtonLocal : function(name, value, htmlOptions) { htmlOptions = htmlOptions || {}; htmlOptions.onclick = [ "TrimPath.junction.env.localPostForm('", lastFormArgs.htmlOptions.id, "', '", lastFormArgs.controllerNameVal, "', '", lastFormArgs.actionNameVal, "'" ]; if (lastFormArgs.objIdVal) htmlOptions.onclick.push(", '" + lastFormArgs.objIdVal + "'"); else htmlOptions.onclick.push(", null"); htmlOptions.onclick.push(", null, '" + name + "'"); htmlOptions.onclick.push('); return false'); htmlOptions.onclick = htmlOptions.onclick.join(''); return res.submitButton(name, value, htmlOptions) }, errorMessagesOn : function(objName, attrName, prependText, appendText, cssClass) { if (res[objName] != null && res[objName].errors().isInvalid(attrName)) { prependText = prependText || ""; appendText = appendText || ""; cssClass = cssClass || "formError"; var start = '
' + prependText; var end = appendText + '
'; var msgs = res[objName].errors().fullMessagesOn(attrName); return start + msgs.join(end + start) + end; } return ""; }, errorMessagesFor : function(objName, options) { if (res[objName] == null || res[objName].errors().isEmpty()) return ""; options = options || {}; options['headerTag'] = options['headerTag'] || 'h2'; options['id'] = options['id'] || 'errorExplanation'; options['class'] = options['class'] || 'errorExplanation'; var result = [ '
<', options['headerTag'], '>Errors for ', objName, '
' ]; result.push(res[objName].errors().fullMessages().join('
')); result.push('
'); return result.join(''); }, localDateString : function(d) { if (d == null) return ""; return junctionUtil.toLocalDateString(d); } } res.res = res; res.errorMessageOn = res.errorMessagesOn; try { var controller = new (controllerFunc)(); if (controller && controller[actionName]) controller[actionName](req, res); if (resRedirect != null) { req.session.flash = res.flash; env.syncToServer(); return env.redirect(resRedirect.controllerNameVal, resRedirect.actionNameVal, resRedirect.objIdVal); } if (resRendered == null) res.renderAction(actionName); } catch (e) { resRendered = "[ERROR: controller processing: " + controllerName + ", " + actionName + ", " + objId + ": " + e.toString() + "]"; } if (res.layoutName != null) env.layoutRenderer(res.layoutName, resRendered, req, res); req.session.flash = null; env.syncToServer(); return resRendered; } } return junction; } // A web browser based environment for the default junction is defined here. // Other environments, like for Rhino, would override this. TrimPath.junction = TrimPath.junctionCreate({ appName : null, appInit : function(appName, conversationId, storageRealm, session, dataSchemaId, dataId) { dataSchemaId = dataSchemaId || '__system/dataSchema'; dataId = dataId || '__system/data'; var dataSchema = TrimPath.junction.env.elementJsonData(dataSchemaId); TrimPath.junction.env.dataSchemaProvider = { getSchema : function() { return dataSchema; } }; TrimPath.junction.env.dataProvider = TrimPath.junctionUtil.createMemoryDataProvider(TrimPath.junction.env.elementJsonData(dataId), TrimPath.junction.env); TrimPath.junction.env.appName = appName; TrimPath.junction.env.conversationId = conversationId; // Unique within each session. TrimPath.junction.env.storageRealm = storageRealm; TrimPath.junction.env.session = session; }, nextId_last : null, // Grows monotonically negative. nextId : function() { if (TrimPath.junction.env.nextId_last == null) { var min = 0; var data = TrimPath.junction.env.dataProvider.memoryData; for (var tableName in data) { var records = data[tableName]; if (records != null && typeof(records) != "function") { if (TrimPath.junctionUtil.isValidSQLName(tableName)) { for (var i = 0; i < records.length; i++) min = Math.min(records[i].id, min); } else { // The records must be a change tracking hash, not a real records array. for (var id in records) // We scan the changes anyways to handle the case of deleted records. min = Math.min(TrimPath.junctionUtil.nanToNull(parseInt(id)), min); } } } TrimPath.junction.env.nextId_last = min; } TrimPath.junction.env.nextId_last = TrimPath.junction.env.nextId_last - 1; return TrimPath.junction.env.nextId_last; }, makeQueryLang : TrimPath.makeQueryLang, getSession : function() { if (TrimPath.junction.env.session == null) TrimPath.junction.env.session = {}; return TrimPath.junction.env.session; }, templateCache : {}, templateRenderer : function(templatePath, context) { try { var template = TrimPath.junction.env.templateCache[templatePath]; if (template == null) { var templateEl = document.getElementById(templatePath); if (templateEl == null) return "[ERROR: could not find template: " + templatePath + "]"; var text = TrimPath.junctionUtil.decodeAngles(TrimPath.junction.env.innerText(templateEl)); template = TrimPath.junction.env.templateCache[templatePath] = TrimPath.parseTemplate(text); } return template.process(context); } catch (e) { return "[ERROR: template parsing: " + templatePath + ": " + e.toString() + (e.message ? '; ' + e.message : '') + "]"; } }, layoutRenderer : function(layoutName, content, req, res, junction) { var targetElement = document.getElementById(layoutName); if (targetElement != null) targetElement.innerHTML = content; // Also render and update any subsidiary layout areas. var subsidiaryPrefix = layoutName + '.'; for (var i = 0, divs = document.getElementsByTagName("DIV"); i < divs.length; i++) { var div = divs[i]; if (div.id != null && div.id.substring(0, subsidiaryPrefix.length) == subsidiaryPrefix) { var subsidiaryPath = "/app/views/layouts/" + div.id.substring(subsidiaryPrefix.length) + ".jst"; div.innerHTML = TrimPath.junction.env.templateRenderer(subsidiaryPath, res); } } }, redirect : function(controllerName, actionName, objId) { return TrimPath.junction.localGet(controllerName, actionName, objId); }, localPostForm : function(formId, controllerName, actionName, objId, req, submitButtonName) { req = TrimPath.junction.env.formToReq(formId, submitButtonName, req); if (req != null) return TrimPath.junction.localPost(controllerName, actionName, objId, req); }, formToReq : function(formId, submitButtonName, req) { var formEl = document.getElementById(formId); if (formEl != null) { req = req || {}; for (var i = 0; i < formEl.elements.length; i++) { var element = formEl.elements[i]; if (element.type == "submit") { if (element.name == submitButtonName) junctionUtil.setMapTreeValue(req, element.name, element.value); } else if (element.type == "radio") { if (element.checked) junctionUtil.setMapTreeValue(req, element.name, element.value); } else { var value = (element.type == "checkbox" ? element.checked : element.value); junctionUtil.setMapTreeValue(req, element.name, value); } } return req; } return null; }, errorUnknownController : function(controllerName, controllerFuncName) { var msg = 'Error: unknown controller function: ' + controllerFuncName; alert(msg); return msg; }, syncToServerLast : null, syncToServer : function() { // Update server and locally persistable DOM nodes from record hashes. // Don't bother sync'ing a data delta if we just did it, since localRequest()/syncToServer() // are often called more than once in the same request, due to redirect(). var now = new Date(); if ((TrimPath.junction.env.syncToServerLast != null) && (now.getTime() - TrimPath.junction.env.syncToServerLast.getTime() < 1000)) return; TrimPath.junction.env.syncToServerLast = now; var data = TrimPath.junction.env.dataProvider.memoryData; if (data != null) { var dirty = false; var delta = {}; for (var tableName in data) { if (TrimPath.junctionUtil.isValidSQLName(tableName)) { // Must be legal tableName (not a change tracking entry). var records = data[tableName]; if (records != null && records instanceof Array) { var changes = TrimPath.junction.env.dataProvider.changesFor(tableName); if (changes != null) { delta[tableName] = {}; for (var id in changes) { var ops = changes[id]; if (ops instanceof Array) { dirty = true; var op = ops[ops.length - 1]; if (op == "delete") { delta[tableName][id] = [op]; } else { var index = junctionUtil.findRecordIndex(records, id); if (index >= 0) delta[tableName][id] = [op, records[index]]; } } } } } } } if (dirty) { TrimPath.junction.env.syncDOMData(); if (TrimPath.junction.env.isConnected()) { syncToServer_objIdMap = null; new Ajax.Request("/app/sync_data_delta/" + TrimPath.junction.env.appName, { method : "post", asynchronous : true, parameters : [ 'delta=', encodeURIComponent(junctionUtil.toJsonString(delta)), '&version=0.1', '&conversationId=', encodeURIComponent(TrimPath.junction.env.conversationId), '&storageRealm=', encodeURIComponent(TrimPath.junction.env.storageRealm) ].join(''), onComplete : function(transport) { var result = safeEval('(' + transport.responseText +')'); TrimPath.junction.env.dataProvider = TrimPath.junctionUtil.createMemoryDataProvider(result.data, TrimPath.junction.env); TrimPath.junction.env.syncDOMData(); TrimPath.junction.env.syncToServer_objIdMap = result.objIdMap; } }); } } } }, syncToServer_objIdMap : null, mapObjId : function(objId) { // Map to handle when old pages are refering to outdated negative id's. if (TrimPath.junction.env.syncToServer_objIdMap != null && TrimPath.junction.env.syncToServer_objIdMap[objId] != null) return TrimPath.junction.env.syncToServer_objIdMap[objId]; return objId; }, syncToServerAll : function() { // Synchronize the server with our data and dataSchema. TrimPath.junction.env.syncDOMData(); if (TrimPath.junction.env.isConnected()) { new Ajax.Request("/app/sync_data_all/" + TrimPath.junction.env.appName, { method : "post", asynchronous : true, parameters : [ 'data=', encodeURIComponent(TrimPath.junction.env.innerText(document.getElementById('__system/data'))), '&version=0.1', '&conversationId=', encodeURIComponent(TrimPath.junction.env.conversationId), '&storageRealm=', encodeURIComponent(TrimPath.junction.env.storageRealm) ].join('') }); } }, syncDOMData : function() { // Update locally persistable DOM nodes from our in-memory record hashes. // After this function returns, File->Save Page should work (at least in firefox). TrimPath.junction.env.elementJsonDataUpdate('__system/data', TrimPath.junction.env.dataProvider.memoryData); }, isConnected : function() { var location = String(window.location); return (location.substring(0, 4) == "http" && location.split('?')[0].indexOf(".htm") < 0 && TrimPath.junction.env.appName != null); }, innerTextArray : function(el, out) { // An innerText implementation that fills and returns a String array. out = out || []; for (var i = 0; i < el.childNodes.length; i++) { var childEl = el.childNodes[i]; if (childEl.nodeType == 1) TrimPath.junction.env.innerTextArray(childEl, out); if (childEl.nodeType == 3) out.push(childEl.data); } return out; }, innerText : function(el) { return TrimPath.junction.env.innerTextArray(el).join(''); }, elementJsonData : function(elementId, doc) { // The element should be a pre or div. doc = doc || document; var el = doc.getElementById(elementId); if (el != null) return eval(TrimPath.junction.env.innerText(el).replace(/[\r\n]/g, '')); return null; }, elementJsonDataUpdate : function(elementId, data, doc) { var arr = ['(']; TrimPath.junctionUtil.toJsonStringArray(data, arr); arr.push(')'); TrimPath.junction.env.elementTextUpdate(elementId, arr.join(''), doc); }, elementTextUpdate : function(elementId, elementText, doc) { doc = doc || document; var dataEl = document.getElementById(elementId); if (dataEl != null) { while (dataEl.hasChildNodes()) dataEl.removeChild(dataEl.firstChild); dataEl.appendChild(doc.createTextNode(elementText)); } }, toggleDisplay : function(el, doc, showStr) { showStr = showStr || "block"; if (el instanceof Array) { for (var i = 0; i < el.length; i++) TrimPath.junction.env.toggleDisplay(el[i], doc); return; } var doc = doc || document; if (typeof(el) == "string") el = doc.getElementById(el); if (el != null) el.style.display = (el.style.display != showStr ? showStr : "none"); return el; }, eventually : function(callbackStr, millis) { setTimeout(function() { safeEval(callbackStr); }, millis || 200); return ""; // Empty string is useful when used in jst eval blocks. }, devAreaShow : function(devAreaId, viewOnly, doc) { var doc = doc || document; var devAreaId = devAreaId || "junctionDevArea"; var devAreaEl = TrimPath.junction.env.toggleDisplay(devAreaId, doc); if (devAreaEl != null) { if (devAreaEl.style.display == "block") devAreaEl.innerHTML = TrimPath.junctionUtil.decodeAngles(TrimPath.junction.env.innerText($("/__system/lib/devArea.jst"))).process(); return false; } return true; } }); }) (function(evalExpr) { return eval(evalExpr); }); // The safeEval works in global scope. var modelFor = modelFor || TrimPath.junction.modelFor; var scaffold = scaffold || TrimPath.junction.scaffold; var beforeFilter = beforeFilter || TrimPath.junctionUtil.beforeFilter; var toSQLDateString = toSQLDateString || TrimPath.junctionUtil.toSQLDateString; /* Copyright (c) 2002 JSON.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The Software shall be used for Good, not Evil. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ TrimPath.junctionUtil.toJsonString = function(arg, prefix) { // Put into TrimPath namespace to avoid version conflicts. return TrimPath.junctionUtil.toJsonStringArray(arg, [], prefix).join(''); } TrimPath.junctionUtil.toJsonStringArray = function(arg, out, prefix) { out = out || new Array(); var u; // undefined switch (typeof arg) { case 'object': if (arg) { if (arg.constructor == Array) { out.push('['); for (var i = 0; i < arg.length; ++i) { if (i <= 0) { if (prefix != null && arg.length > 1) out.push(' '); } else { out.push(',\n'); if (prefix != null) out.push(prefix); } TrimPath.junctionUtil.toJsonStringArray(arg[i], out, prefix != null ? prefix + " " : null); } out.push(']'); return out; } else if (typeof arg.toString != 'undefined') { out.push('{'); var first = true; var nextPrefix = prefix != null ? prefix + " " : null; for (var i in arg) { var curr = out.length; // Record position to allow undo when arg[i] is undefined. if (first) { if (prefix != null) out.push(' '); } else { out.push(',\n'); if (prefix != null) out.push(prefix); } TrimPath.junctionUtil.toJsonStringArray(i, out, nextPrefix); if (prefix == null) out.push(':'); else out.push(': '); TrimPath.junctionUtil.toJsonStringArray(arg[i], out, nextPrefix); if (out[out.length - 1] == u) out.splice(curr, out.length - curr); else first = false; } out.push('}'); return out; } return out; } out.push('null'); return out; case 'unknown': case 'undefined': case 'function': out.push(u); return out; case 'string': out.push('"') out.push(arg.replace(/(["\\])/g, '\\$1').replace(/\r/g, '').replace(/\n/g, '\\n')); out.push('"'); return out; default: out.push(String(arg)); return out; } }