/* * ***** BEGIN LICENSE BLOCK ***** * Version: ZAPL 1.1 * * The contents of this file are subject to the Zimbra AJAX Public * License Version 1.1 ("License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.zimbra.com/license * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. * * The Original Code is: Zimbra AJAX Toolkit. * * The Initial Developer of the Original Code is Zimbra, Inc. * Portions created by Zimbra are Copyright (C) 2005 Zimbra, Inc. * All Rights Reserved. * * Contributor(s): * * ***** END LICENSE BLOCK ***** */ // // Factory to create XModelItems from simple attributes (eg: from JS object literals or XML) // /** This class is never instantiated. */ function XModelItemFactory() {} XModelItemFactory.createItem = function (attributes, parentItem, xmodel) { // assign a modelItem to the item var type = attributes.type; constructor = this.getItemTypeConstructor(type || _UNTYPED_); var item = new constructor(); item._setAttributes(attributes); if (item.id != null && item.ref == null) item.ref = item.id; // idPath is mostly used for debugging... var idPath = this.getIdPath(attributes, parentItem); item.__idPath = idPath; item.__xmodel = xmodel; item.__parentItem = parentItem; //DBG.println("XModelItemFactory.createItem(", attributes.id, ") idPath='", idPath, "' type='", item.type,"'"); item.initModelItem(); // add the item to its model's index xmodel.indexItem(item, idPath); return item; } XModelItemFactory.getIdPath = function (attributes, parentItem) { if (attributes.path) return attributes.path; return this.getFullPath(attributes.id, (parentItem ? parentItem.getIdPath() : "")); } XModelItemFactory.getFullPath = function (itemPath, parentPath) { if (itemPath == null) return null; if (parentPath == null) parentPath = ""; var path = itemPath; if (itemPath == ".") { path = parentPath; } else if (itemPath == "..") { parentPath = parentPath.split("/"); path = parentPath.slice(0, parentPath.length - 1).join("/"); } else if (parentPath == "") { path = itemPath; } else { path = parentPath + "/" + itemPath; } return path; } XModelItemFactory.typeConstructorMap = {}; XModelItemFactory.createItemType = function (typeConstant, typeName, constructor, superClassConstructor) { if (constructor == null) constructor = new Function(); if (typeof superClassConstructor == "string") superClassConstructor = this.getItemTypeConstructor(superClassConstructor); if (superClassConstructor == null) superClassConstructor = XModelItem; // initialize the constructor constructor.prototype = new superClassConstructor(); constructor.prototype.type = typeName; constructor.prototype.constructor = constructor; constructor.prototype.toString = new Function("return '[XModelItem:" + typeName + " path=\"' + this.getIdPath() + '\"]'"); constructor.toString = new Function("return '[Class XModelItem:" + typeName + "]'"); // put the item type into the typemap this.registerItemType(typeConstant, typeName, constructor); // return the prototype return constructor; } XModelItemFactory.registerItemType = function(typeConstant, typeName, constructor) { // assign the type constant to the window so everyone else can use it window[typeConstant] = typeName; this.typeConstructorMap[typeName] = constructor; } XModelItemFactory.getItemTypeConstructor = function (typeName) { var typeConstructor = this.typeConstructorMap[typeName]; if (typeConstructor == null) typeConstructor = this.typeConstructorMap["string"]; return typeConstructor; } function XModelItem() {} XModelItemFactory.createItemType("_UNTYPED_", "untyped", XModelItem, Object); // define the base class as the "object" class -- it works, but no type logic is applied XModelItemFactory.registerItemType("_OBJECT_", "object", XModelItem); // set base class defaults XModelItem.prototype.__isXModelItem = true; XModelItem.prototype.getterScope = _INHERIT_; XModelItem.prototype.setterScope = _INHERIT_; // methods XModelItem.prototype._setAttributes = function (attributes) { this._attributes = attributes; for (var prop in attributes) { this[prop] = attributes[prop]; } } XModelItem.prototype.initModelItem = function() { window.status = ''; } // initialize sub-items for this item XModelItem.prototype.initializeItems = function () { var items = this.getItems(); if (items != null) { this.items = this.getModel().initItemList(items, this); } } // // accessors // XModelItem.prototype.getModel = function() { return this.__xmodel; } XModelItem.prototype.getParentItem = function() { return this.__parentItem; } XModelItem.prototype.getIdPath = function() { return this.__idPath; } XModelItem.prototype.getItems = function () { return this.items; } XModelItem.prototype.addItem = function (item) { if (!item.__isXModelItem) item = this.xmodel.initItem(item, this); if (this.items == null) this.items = []; this.items.push(item); } XModelItem.prototype.getConstraints = function() { return this.constraints; } XModelItem.prototype.getRequired = function() { return this.required; } XModelItem.prototype.getReadonly = function() { return this.readonly; } XModelItem.prototype.getReadOnly = XModelItem.prototype.getReadonly; XModelItem.prototype.getDefaultValue = function () {return new Object() }; // // validate this value (i.e. when a formitem that refers to it has changed) // XModel.registerErrorMessage("valueIsRequired", DwtMsg.valueIsRequired); XModelItem.prototype.validate = function (value, form, formItem, instance) { // see if it's required if (value == null || value === "") { if (this.getRequired()) { throw this.getModel().getErrorMessage("valueIsRequired", value); } } // next validate the type // this will throw an exception if something went wrong // also, value may be coerced to a particular type by the validator else { value = this.validateType(value); } // if they defined any constraints, var constraints = this.getConstraints(); if (constraints == null) return value; if (! (AjxUtil.isInstance(constraints, Array))) constraints = [constraints]; for (var i = 0; i < constraints.length; i++) { var constraint = constraints[i]; if (constraint.type == "method") { // The constraint method should either return a value, or throw an // exception. value = constraint.value.call(this, value, form, formItem, instance); } // if (isValid == false) { // throw this.getModel().getErrorMessage(constraint.errorMessageId, value); // } } return value; } XModelItem.prototype.getDefaultErrorMessage = function () { return this.errorMessage; } // generic validators for different data types // we have them here so we can use them in the _LIST_ data type XModelItem.prototype.validateType = function(value) { return value; } // // for validating strings // /** * Datatype facet: length. If not null, the length of the data * value must be equal to this value. Specifying this attribute * ignores the values for {@link XModelItem.prototype.minLength} * and {@link XModelItem.prototype.maxLength}. */ XModelItem.prototype.length = null; /** * Datatype facet: minimum length. If not null, the length of * the data value must not be less than this value. */ XModelItem.prototype.minLength = null; /** * Datatype facet: maximum length. If not null, the length of * the data value must not exceed this value. */ XModelItem.prototype.maxLength = null; /** * Datatype facet: pattern. If not null, specifies an array of * RegExp objects. The data value must match one of * the patterns or an error is signaled during validation. */ XModelItem.prototype.pattern = null; /** * Datatype facet: enumeration. If not null, specifies an array of * literal string values. The data value must match one of the * literals or an error is signaled during validation. */ XModelItem.prototype.enumeration = null; /** * Datatype facet: white space. If not null, specifies how white * space in the value should be processed before returning the * final value. Valid values are: * */ XModelItem.prototype.whiteSpace = null; XModelItem.prototype.getLength = function() { return this.length; } XModelItem.prototype.getMinLength = function () { return this.minLength; } XModelItem.prototype.getMaxLength = function () { return this.maxLength; } XModelItem.prototype.getPattern = function() { if (this.pattern != null && this.pattern.checked == null) { if (AjxUtil.isString(this.pattern)) { this.pattern = [ new RegExp(this.pattern) ]; } else if (AjxUtil.isInstance(this.pattern, RegExp)) { this.pattern = [ this.pattern ]; } else if (AjxUtil.isArray(this.pattern)) { for (var i = 0; i < this.pattern.length; i++) { var pattern = this.pattern[i]; if (AjxUtil.isString(pattern)) { this.pattern[i] = new RegExp(this.pattern[i]); } } } else { // REVISIT: What to do in this case? Do we just // assume that it was specified correctly? } this.pattern.checked = true; } return this.pattern; } XModelItem.prototype.getEnumeration = function() { return this.enumeration; } XModelItem.prototype.getWhiteSpace = function() { return this.whiteSpace; } XModel.registerErrorMessage("notAString", DwtMsg.notAString); XModel.registerErrorMessage("stringLenWrong", DwtMsg.stringLenWrong); XModel.registerErrorMessage("stringTooShort", DwtMsg.stringTooShort); XModel.registerErrorMessage("stringTooLong", DwtMsg.stringTooLong); XModel.registerErrorMessage("stringMismatch", DwtMsg.stringMismatch); XModelItem.prototype.validateString = function(value) { if (value == null) return; if (!AjxUtil.isString(value)) { throw this.getModel().getErrorMessage("notAString", value); } value = this._normalizeAndValidate(value); var length = this.getLength(); if (length !== null) { if (value.length !== length) { throw this.getModel().getErrorMessage("stringLenWrong", value, length); } } else { var maxLength = this.getMaxLength(); if (maxLength !== null && value.length > maxLength) { throw this.getModel().getErrorMessage("stringTooLong", value, maxLength); } var minLength = this.getMinLength(); if (minLength !== null && value.length < minLength) { throw this.getModel().getErrorMessage("stringTooShort", value, minLength); } } return value; } /** * Normalizes value against whiteSpace facet and then validates * against pattern and enumeration facets. * @private */ XModelItem.prototype._normalizeAndValidate = function(value) { var whiteSpace = this.getWhiteSpace(); if (whiteSpace !== null) { if (whiteSpace === "replace" || whiteSpace === "collapse") { value = value.replace(/[\t\r\n]/g, " "); } if (whiteSpace === "collapse") { value = value.replace(/^\s+/,"").replace(/\s+$/,"").replace(/[ ]+/, " "); } } var pattern = this.getPattern(); if (pattern != null) { var matched = false; for (var i = 0; i < pattern.length; i++) { if (pattern[i].test(value)) { matched = true; break; } } if (!matched) { throw this.getModel().getErrorMessage("stringMismatch", value); } } var enumeration = this.getEnumeration(); if (enumeration !== null) { var matched = false; for (var i = 0; i < enumeration.length; i++) { if (enumeration[i] === value) { matched = true; break; } } if (!matched) { throw this.getModel().getErrorMessage("stringMismatch", value); } } return value; } // // for validating numbers // /** * Datatype facet: total digits. If not null, the number of * digits before the decimal point in the data value must not * be greater than this value. */ XModelItem.prototype.totalDigits = null; /** * Datatype facet: fraction digits. If not null, the number of * digits after the decimal point in the data value must not be * greater than this value. */ XModelItem.prototype.fractionDigits = null; /** * Datatype facet: maximum value (inclusive). If not null, the * data value must be less than or equal to this value. */ XModelItem.prototype.maxInclusive = null; /** * Datatype facet: maximum value (exclusive). If not null, the * data value must be less than this value. */ XModelItem.prototype.maxExclusive = null; /** * Datatype facet: minimum value (inclusive). If not null, the * data value must be greater than or equal to this value. */ XModelItem.prototype.minInclusive = null; /** * Datatype facet: minimum value (exclusive). If not null, the * data value must be greater than or equal to this value. */ XModelItem.prototype.minExclusive = null; XModelItem.prototype.getTotalDigits = function() { return this.totalDigits; } XModelItem.prototype.getFractionDigits = function () { return this.fractionDigits; } XModelItem.prototype.getMinInclusive = function () { return this.minInclusive; } XModelItem.prototype.getMinExclusive = function() { return this.minExclusive; } XModelItem.prototype.getMaxInclusive = function () { return this.maxInclusive; } XModelItem.prototype.getMaxExclusive = function() { return this.maxExclusive; } XModel.registerErrorMessage("notANumber", DwtMsg.notANumber); XModel.registerErrorMessage("numberTotalExceeded", DwtMsg.numberTotalExceeded); XModel.registerErrorMessage("numberFractionExceeded", DwtMsg.numberFractionExceeded); XModel.registerErrorMessage("numberMoreThanMax", DwtMsg.numberMoreThanMax); XModel.registerErrorMessage("numberMoreThanEqualMax", DwtMsg.numberMoreThanEqualMax); XModel.registerErrorMessage("numberLessThanMin", DwtMsg.numberLessThanMin); XModel.registerErrorMessage("numberLessThanEqualMin", DwtMsg.numberLessThanEqualMin); XModelItem.prototype.validateNumber = function(value) { value = this._normalizeAndValidate(value); var nvalue = parseFloat(value); if (isNaN(nvalue) || !AjxUtil.FLOAT_RE.test(value)) { throw this.getModel().getErrorMessage("notANumber", value); } var totalDigits = this.getTotalDigits(); if (this.totalDigits !== null) { var wholePart = Math.floor(nvalue); if (wholePart.toString().length > totalDigits) { throw this.getModel().getErrorMessage("numberTotalExceeded", value, totalDigits); } } var fractionDigits = this.getFractionDigits(); if (this.fractionDigits !== null) { var fractionPart = String(nvalue - Math.floor(nvalue)); if (fractionPart.indexOf('.') != -1 && fractionPart.replace(/^\d*\./,"").length > fractionDigits) { throw this.getModel().getErrorMessage("numberFractionExceeded", value, fractionDigits); } } var maxInclusive = this.getMaxInclusive(); if (maxInclusive !== null && nvalue > maxInclusive) { throw this.getModel().getErrorMessage("numberMoreThanMax", value, maxInclusive); } var maxExclusive = this.getMaxExclusive(); if (maxExclusive !== null && nvalue >= maxExclusive) { throw this.getModel().getErrorMessage("numberMoreThanEqualMax", value, maxExclusive); } var minInclusive = this.getMinInclusive(); if (minInclusive !== null && nvalue < minInclusive) { throw this.getModel().getErrorMessage("numberLessThanMin", value, minInclusive); } var minExclusive = this.getMinExclusive(); if (minExclusive !== null && nvalue <= minExclusive) { throw this.getModel().getErrorMessage("numberLessThanEqualMin", value, minExclusive); } return value; } // // for validating dates and times // XModelItem.prototype.msecInOneDay = (1000 * 60 * 60 * 24); XModel.registerErrorMessage("invalidDateString", DwtMsg.invalidDateString); // methods XModelItem.prototype.validateDate = function(value) { if (AjxUtil.isInstance(value, Date)) return value; if (AjxUtil.isString(value)) { value = value.toLowerCase(); var date = new Date(); if (value.indexOf("/") > -1) { value = value.split("/"); if (value.length == 3) { var month = parseInt(value[0]); var day = parseInt(value[1]); var year = parseInt(value[2]); if (!isNaN(month) && !isNaN(day) && !isNaN(year)) { month -= 1; if (year < 1900) { if (year < 50) year += 2000; year += 1900; } date.setFullYear(year, month, day); date.setHours(0,0,0,0); return date; } } } else { // set to midnight today according to local time date.setHours(0,0,0,0); if (value == AjxMsg.today) { return date; } else if (value == AjxMsg.yesterday) { date.setTime(date.getTime() - this.msecInOneDay); return date; } else if (value == AjxMsg.tomorrow) { date.setTiem(date.getTime() + this.msecInOneDay); return date; } } } throw this.getModel().getErrorMessage("invalidDateString", value); return value; } XModel.registerErrorMessage("invalidTimeString", DwtMsg.invalidTimeString); // time is returned as a number of milliseconds since XModelItem.prototype.validateTime = function (value) { if (AjxUtil.isNumber(value)) return value; if (AjxUtil.isInstance(value, Date)) { return ((value.getHours() * 360) + (value.getMinutes() * 60) + value.getSeconds()) * 1000; } if (AjxUtil.isString(value)) { value = value.toLowerCase(); if (value.indexOf(":") > -1) { value = value.split(":"); var isPM = false; var lastPiece = value[value.length - 1]; isPM = (lastPiece.indexOf(AjxMsg.pm.toLowerCase()) > -1); var hour = parseInt(value[0]); var min = parseInt(value[1]); var sec = (value.length == 3 ? parseInt(value[2]) : 0); if (!isNaN(hour) && !isNaN(min) && !isNaN(sec)) { hour -= 1; if (isPM && hour > 11) hour += 12; return ((hour * 360) + (min * 60) + sec) * 1000; } } } throw this.getModel().getErrorMessage("invalidTimeString", value); } XModel.registerErrorMessage("invalidDatetimeString", DwtMsg.invalidDatetimeString); XModelItem.prototype.validateDateTime = function (value) { if (AjxUtil.isInstance(value, Date)) return value; if (AjxUtil.isNumber(value)) return value; if (AjxUtil.isString(value)) { // try to get the value as a date // (this will ignore time fields, and will throw an exeception if we couldn't parse a date) var date = this.validateDate(value); // if it has a time component if (value.indexOf(":") > -1) { var time = value.split(" ")[1]; // this will validate the time string and will throw an exception if it doesn't match time = this.validateTimeString(time); date.setTime(date.getTime() + time); } return date; } // probably should never get here... throw this.getModel().getErrorMessage("invalidDatetimeString", value); } // // XModelItem class: "string" // function String_XModelItem(){} XModelItemFactory.createItemType("_STRING_", "string", String_XModelItem) String_XModelItem.prototype.validateType = XModelItem.prototype.validateString; String_XModelItem.prototype.getDefaultValue = function () { return ""; }; // // XModelItem class: "number" // function Number_XModelItem(){} XModelItemFactory.createItemType("_NUMBER_", "number", Number_XModelItem); Number_XModelItem.prototype.validateType = XModelItem.prototype.validateNumber; Number_XModelItem.prototype.getDefaultValue = function () { return 0; }; // // XModelItem class: "date" // function Date_XModelItem(){} XModelItemFactory.createItemType("_DATE_", "date", Date_XModelItem); Date_XModelItem.prototype.validateType = XModelItem.prototype.validateDate; Date_XModelItem.prototype.getDefaultValue = function () { return new Date(); }; // // XModelItem class: "time" // function Time_XModelItem(){} XModelItemFactory.createItemType("_TIME_", "time", Time_XModelItem); Time_XModelItem.prototype.validateType = XModelItem.prototype.validateTime; Time_XModelItem.prototype.getDefaultValue = function () { return new Date(); }; // // XModelItem class: "datetime" // function Datetime_XModelItem(){} XModelItemFactory.createItemType("_DATETIME_", "datetime", Datetime_XModelItem); Datetime_XModelItem.prototype.validateType = XModelItem.prototype.validateDateTime; Datetime_XModelItem.prototype.getDefaultValue = function () { return new Date(); }; // // XModelItem class: "list" // function List_XModelItem(){} XModelItemFactory.createItemType("_LIST_", "list", List_XModelItem); List_XModelItem.prototype.getDefaultValue = function () { return new Array(); }; // type defaults and accessors List_XModelItem.prototype.outputType = _STRING_; // _STRING_ == convert to a string // _LIST_ == convert to an array List_XModelItem.prototype.itemDelimiter = ","; // delimiter for converting string values to arrays List_XModelItem.prototype.listItem = {type:_UNTYPED_}; List_XModelItem.prototype.getOutputType = function () { return this.outputType; } List_XModelItem.prototype.getItemDelimiter = function() { return this.itemDelimiter } List_XModelItem.prototype.getListItem = function () { return this.listItem; } // methods List_XModelItem.prototype.initializeItems = function () { var listItem = this.listItem; listItem.ref = listItem.id = "#"; this.listItem = XModelItemFactory.createItem(listItem, this, this.getModel()); this.listItem.initializeItems(); } List_XModelItem.prototype.validateType = function (value) { return value; //XXX REWORK THIS TO USE THE listItem MODEL ITEM FOR EACH SUB-ITEM } // // XModelItem class: "enum" // function Enum_XModelItem(){} XModelItemFactory.createItemType("_ENUM_", "enum", Enum_XModelItem); //XXXX Enum_XModelItem.prototype.getDefaultValue = function () { return this.choices[0]; }; Enum_XModelItem.prototype.getChoices = function() { return this.choices; } Enum_XModelItem.prototype.getSelection = function() { return this.selection; } XModel.registerErrorMessage("didNotMatchChoice", DwtMsg.didNotMatchChoice); Enum_XModelItem.prototype.validateType = function (value) { // if the selection is open, they can enter any value they want var selectionIsOpen = this.getSelection() == _OPEN_; if (selectionIsOpen) return value; // selection is not open: it must be one of the supplied choices var choices = this.getChoices(); for (var i = 0; i < choices.length; i++) { var choice = choices[i]; if (AjxUtil.isInstance(choice, Object)) { if (choice.value == value) return value; } else { if (choice == value) return value; } } // if we get here, we didn't match any of the choices throw this.getModel().getErrorMessage("didNotMatchChoice", value); } FileSize_XModelItem = function (){} XModelItemFactory.createItemType("_FILE_SIZE_", "file_size", FileSize_XModelItem); FileSize_XModelItem.prototype.validateType = XModelItem.prototype.validateNumber; FileSize_XModelItem.prototype.getterScope = _MODELITEM_; FileSize_XModelItem.prototype.setterScope = _MODELITEM_; FileSize_XModelItem.prototype.getter = "getValue"; FileSize_XModelItem.prototype.setter = "setValue"; FileSize_XModelItem.prototype.units = AjxUtil.SIZE_MEGABYTES; FileSize_XModelItem.prototype.minInclusive = 0; FileSize_XModelItem.prototype.getValue = function(instance, current, ref) { var value = eval("instance."+ref); return value ? AjxUtil.formatSizeForUnits(value, AjxUtil.SIZE_KILOBYTES, false, 2) : 0; } FileSize_XModelItem.prototype.setValue = function(value, instance, current, ref) { return eval("instance."+ref+" = AjxUtil.parseSize(value, this.units)"); } HostNameOrIp_XModelItem = function() {} XModelItemFactory.createItemType("_HOSTNAME_OR_IP_", "hostname_or_ip", HostNameOrIp_XModelItem); HostNameOrIp_XModelItem.prototype.validateType = XModelItem.prototype.validateString; HostNameOrIp_XModelItem.prototype.maxLength = 256; HostNameOrIp_XModelItem.prototype.pattern = [ AjxUtil.HOST_NAME_RE, AjxUtil.IP_ADDRESS_RE ]; Port_XModelItem = function() {} XModelItemFactory.createItemType("_PORT_", "port", Port_XModelItem); Port_XModelItem.prototype.validateType = XModelItem.prototype.validateNumber; Port_XModelItem.prototype.minInclusive = 0; Port_XModelItem.prototype.maxInclusive = 65535; Percent_XModelItem = function() {} XModelItemFactory.createItemType("_PERCENT_", "percent", Percent_XModelItem); Percent_XModelItem.prototype.validateType = XModelItem.prototype.validateNumber; Percent_XModelItem.prototype.minInclusive = 0; Percent_XModelItem.prototype.maxInclusive = 100;