1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 
 25 var _MODEL_ = "model";
 26 var _INSTANCE_ = "instance";
 27 var _INHERIT_ = "inherit";
 28 var _MODELITEM_ = "modelitem";
 29 
 30 
 31 /**
 32  * 
 33  * 
 34  * @private
 35  */
 36 XModel = function(attributes) {
 37 	// get a unique id for this form
 38 	XFG.assignUniqueId(this, "_Model_");
 39 
 40 	// copy any attributes passed in directly into this object
 41 	if (attributes) {
 42 		for (var prop in attributes) {
 43 			this[prop] = attributes[prop];	
 44 		}
 45 	}
 46 	
 47 	if (this.items == null) this.items = [];
 48 	
 49 	this._pathIndex = {};
 50 	this._pathGetters = {};
 51 	this._parentGetters = {};
 52 	this._itemsAreInitialized = false;
 53 	this._errorMessages = {};
 54 
 55 	if (this.getDeferInit() == false) {
 56 		this.initializeItems();
 57 	}
 58 }
 59 XModel.toString = function() {	return "[Class XModel]";	}
 60 XModel.prototype.toString = function() {	return "[XModel " + this.__id + "]";	}
 61 
 62 XModel.prototype.pathDelimiter = "/";
 63 XModel.prototype.getterScope = _INSTANCE_;
 64 XModel.prototype.setterScope = _INSTANCE_;
 65 
 66 // set deferInit to false to initialize all modelItems when the model is created
 67 //	NOTE: this is generally a bad idea, and all XForms are smart enough
 68 //			to tell their models to init before they need them...
 69 XModel.prototype.deferInit = true;
 70 XModel.prototype.getDeferInit = function () {	return this.deferInit	}
 71 
 72 
 73 XModel.prototype.initializeItems = function() {
 74 	if (this._itemsAreInitialized) return;
 75 	
 76 	var t0 = new Date().getTime();
 77 
 78 	this.__nestedItemCount = 0;	//DEBUG
 79 
 80 	// initialize the items for the form
 81 	this.items = this.initItemList(this.items, null);
 82 
 83 	this._itemsAreInitialized = true;
 84 
 85 	var t1 = new Date().getTime();
 86 	//DBG.println(this,".initializeItems(): w/ ", this.__nestedItemCount," items took ", (t1 - t0), " msec");
 87 }
 88 
 89 
 90 
 91 XModel.prototype.initItemList = function(itemAttrs, parentItem) {
 92 	var items = [];
 93 	for (var i = 0; i < itemAttrs.length; i++) {
 94 		items[i] = this.initItem(itemAttrs[i], parentItem);
 95 	}
 96 	this.__nestedItemCount += itemAttrs.length;		//DEBUG
 97 	return items;
 98 }
 99 
100 
101 XModel.prototype.initItem = function(itemAttr, parentItem) {
102 	// if we already have a form item, assume it's been initialized already!
103 	if (itemAttr.__isXModelItem) return itemAttr;
104 
105 	// create the XFormItem subclass from the item attributes passed in
106 	//	(also links to the model)
107 	var item = XModelItemFactory.createItem(itemAttr, parentItem, this);
108 	
109 	
110 	
111 	// have the item initialize it's sub-items, if necessary (may be recursive)
112 	item.initializeItems();
113 
114 	return item;
115 }
116 
117 XModel.prototype.addItem = function(item, parentItem) {
118 	if (!item.__isXModelItem) item = this.initItem(item, parentItem);
119 	if (parentItem == null) {
120 		this.items.push(item);
121 	} else {
122 		parentItem.addItem(item);
123 	}
124 }
125 
126 // add an item to our index, so we can find it easily later
127 XModel.prototype.indexItem = function(item, path) {
128 	this._pathIndex[path] = item;
129 }
130 
131 
132 
133 
134 
135 
136 //
137 // getting modelItems, parent items, their paths, etc
138 //
139 
140 XModel.prototype.getItem = function(path, createIfNecessary) {
141 	// try to find the item by the path, return if we found it
142 	var item = this._pathIndex[path];
143 	if (item != null) return this._pathIndex[path];
144 
145 	// if we didn't find it, try normalizing the path
146 	var normalizedPath = this.normalizePath(path);
147 	//	convert any "#1", etc to just "#"
148 	for (var i = 0; i < normalizedPath.length; i++) {
149 		if (normalizedPath[i].charAt(0) == "#") normalizedPath[i] = "#";
150 	}
151 	// and if we find it, save that item under the original path and return it
152 	item = this._pathIndex[normalizedPath.join(this.pathDelimiter)];
153 	if (item != null) {
154 		this._pathIndex[path] = item;
155 		return item;
156 	}
157 
158 	if (createIfNecessary != true) return null;
159 
160 	// get each parent item (creating if necessary) until we get to the end
161 	var parentItem = null;
162 	for (var p = 0; p < normalizedPath.length; p++) {
163 		var itemPath = normalizedPath.slice(0, p+1).join(this.pathDelimiter);
164 		var item = this.getItem(itemPath, false);
165 		if (item == null) {
166 			//DBG.println("making modelItem for ", itemPath);
167 			item = XModelItemFactory.createItem({id:normalizedPath[p]}, parentItem, this);
168 		}
169 		parentItem = item;
170 	}
171 	return item;
172 }
173 
174 
175 
176 // "normalize" a path and return it split on the itemDelimiter for this model
177 XModel.prototype.normalizePath = function (path) {
178 	if (path.indexOf("[") > -1) {
179 		path = path.split("[").join("/#");
180 		path = path.split("]").join("");
181 	}
182 	if (path.indexOf(".") > -1) {
183 		path = path.split(/[\/\.]+/);
184 		var outputPath = [];
185 		for (var i = 0; i < path.length; i++) {
186 			var step = path[i];
187 			if (step == "..") {
188 				outputPath.pop();
189 			} else if (step != ".") {
190 				outputPath.push(step);
191 			}
192 		}
193 		return outputPath;
194 	}
195 	return path.split(this.pathDelimiter);
196 }
197 
198 
199 XModel.prototype.getParentPath = function (path) {
200 	path = this.normalizePath(path);
201 	return path.slice(0, path.length - 1);
202 }
203 
204 XModel.prototype.getLeafPath = function (path) {
205 	path = this.normalizePath(path);
206 	return path[path.length - 1];
207 }
208 
209 
210 
211 
212 
213 
214 XModel.prototype.getInstanceValue = function (instance, path) {
215 	var getter = this._getPathGetter(path);
216 	return getter.call(this, instance);
217 }
218 
219 XModel.prototype.getParentInstanceValue = function (instance, path) {
220 	var getter = this._getParentPathGetter(path);
221 	return getter.call(this, instance);
222 }
223 
224 XModel.prototype.setInstanceValue = function (instance, path, value) {
225 //DBG.println("setInstanceValue(",path,"): ", value, " (",typeof value,")");
226 	var parentValue = this.getParentInstanceValue(instance, path);
227 	if (parentValue == null) {
228 		parentValue = this.setParentInstanceValues(instance, path);
229 	}
230 	var modelItem = this.getItem(path, true);
231 	var leafPath = this.getLeafPath(path);
232 	var ref = modelItem.ref;
233 	if (leafPath.charAt(0) == "#") ref = parseInt(leafPath.substr(1));
234 	
235 	if (modelItem.setter) {
236 		// convert "/" to "." in the ref
237 		if (ref.indexOf(this.pathDelimiter) > -1) ref = ref.split(this.pathDelimiter).join(".");
238 
239 		var setter = modelItem.setter;
240 		var scope = modelItem.setterScope;
241 		if (scope == _INHERIT_) scope = this.setterScope;
242 		if (scope == _INSTANCE_) {
243 			instance[setter](value, parentValue, ref);
244 		} else if (scope == _MODEL_) {
245 			this[setter](value, instance, parentValue, ref);		
246 		} else {
247 			modelItem[setter](value, instance, parentValue, ref);
248 		}
249 	} else {
250 		if (typeof ref == "string" && ref.indexOf(this.pathDelimiter) > -1) {
251 			ref = ref.split(this.pathDelimiter);
252 			for (var i = 0; i < ref.length - 1; i++) {
253 				parentValue = parentValue[ref[i]];
254 			}
255 			ref = ref.pop();
256 		}
257 		parentValue[ref] = value;
258 		var parentItem = modelItem.getParentItem();
259 		if(parentItem) {
260 			var parentPath = this.getParentPath(path).join(this.pathDelimiter);
261 			 if(parentItem.setter) {
262 				XModel.prototype.setInstanceValue.call(this,instance, parentPath, parentValue);
263 			 } else {
264 				var event = new DwtXModelEvent(instance, parentItem, parentPath, parentValue);
265 				parentItem.notifyListeners(DwtEvent.XFORMS_VALUE_CHANGED, event);
266 			 }
267 		}
268 	}
269 	
270 	//notify listeners that my value has changed
271 	var event = new DwtXModelEvent(instance, modelItem, path, value);
272 	modelItem.notifyListeners(DwtEvent.XFORMS_VALUE_CHANGED, event);
273 	return value;
274 }
275 
276 
277 XModel.prototype.setParentInstanceValues = function (instance, path) {
278 	var pathList = this.getParentPath(path);
279 	for (var i = 0; i < pathList.length; i++) {
280 		var itemPath = pathList.slice(0, i+1).join(this.pathDelimiter);
281 		var itemValue = this.getInstanceValue(instance, itemPath);
282 		if (itemValue == null) {
283 			var modelItem = this.getItem(itemPath, true);
284 			var defaultValue = modelItem.getDefaultValue();
285 			itemValue = this.setInstanceValue(instance, itemPath, defaultValue);
286 		}
287 	}
288 	return itemValue;
289 }
290 
291 
292 
293 
294 
295 
296 
297 //NOTE: model.getInstance() gets count of PARENT
298 // "modelItem" is a pointer to a modelItem, or an path as a string
299 XModel.prototype.getInstanceCount = function (instance, path) {
300 	var list = this.getParentInstanceValue(instance, path);
301 	if (list != null && list.length) return list.length;
302 	return 0;
303 }
304 
305 
306 // "path" is a path of id's
307 XModel.prototype.addRowAfter = function (instance, path, afterRow) {
308 	var newInstance = null;	
309 	
310 	var modelItem = this.getItem(path);
311 	if (modelItem) {
312 		newInstance = this.getNewListItemInstance(modelItem);
313 	} else {
314 		newInstance = "";
315 	}
316 	var list = this.getInstanceValue(instance, path);
317 	if (list == null) {
318 		list = [];
319 	}
320 
321 	list.splice(afterRow+1, 0, newInstance);
322 	this.setInstanceValue(instance, path, list);
323 }
324 
325 
326 XModel.prototype.getNewListItemInstance = function (modelItem) {
327 	var listItem = modelItem.listItem;
328 	if (listItem == null) return "";
329 	return this.getNewInstance(listItem);
330 }
331 
332 XModel.prototype.getNewInstance = function (modelItem) {
333 	if (modelItem.defaultValue != null) return modelItem.defaultValue;
334 	
335 	var type = modelItem.type;
336 	switch (type) {
337 		case _STRING_:
338 			return "";
339 
340 		case _NUMBER_:
341 			return 0;
342 			
343 		case _OBJECT_:
344 			var output = {};
345 			if (modelItem.items) {
346 				for (var i = 0; i < modelItem.items.length; i++) {
347 					var subItem = modelItem.items[i];
348 					if (subItem.ref) {
349 						output[subItem.ref] = this.getNewInstance(subItem);
350 					} else if (subItem.id) {
351 						output[subItem.id] = this.getNewInstance(subItem);
352 					}
353 				}
354 			
355 			}
356 			return output;
357 			
358 		case _LIST_:
359 			return [];
360 
361 		case _DATE_:
362 		case _TIME_:
363 		case _DATETIME_:
364 			return new Date();
365 			
366 		default:
367 			return "";
368 	}
369 }
370 
371 
372 
373 // "modelItem" is a pointer to a modelItem, or an path as a string
374 XModel.prototype.removeRow = function (instance, path, instanceNum) {
375 	var list = this.getInstanceValue(instance, path);
376 	if (list == null) return;
377 	
378 	var isString = false;
379 	if(list instanceof String || typeof(list) == "string")
380 		isString=true;
381 	
382 	var tmpList = isString ? list.split(",") : list;
383 	var newList = [];
384 	var cnt = tmpList.length;
385 	for (var i=0;i<cnt;i++) {
386 		if(i != instanceNum) {
387 			newList.push(tmpList[i]);
388 		}
389 	}
390 
391 	this.setInstanceValue(instance, path, newList);
392 }
393 
394 
395 
396 
397 
398 
399 // for speed, we create optimized functions to traverse paths in the instance
400 //	to actually return values for an instance.  Make them here.
401 //
402 XModel.prototype._getPathGetter = function (path) {
403 //DBG.println("_getPathGetter(",path,")");
404 	var getter = this._pathGetters[path];
405 	if (getter != null) return getter;
406 
407 	getter = this._makePathGetter(path);
408 //DBG.println("assigning path getter for ", path, " to ", getter);
409 	this._pathGetters[path] = getter;
410 	return getter;
411 }
412 
413 XModel.prototype._getParentPathGetter = function (path) {
414 	var getter = this._parentGetters[path];
415 	if (getter != null) return getter;
416 	
417 	var parentPath = this.getParentPath(path).join(this.pathDelimiter);
418 	getter = this._getPathGetter(parentPath);
419 	this._parentGetters[path] = getter;
420 	this._pathGetters[parentPath] = getter;
421 	return getter;
422 }
423 
424 XModel.prototype._makePathGetter = function (path) {
425 	if (path == null) return new Function("return null");
426 	
427 	// normalizePath() converts to an array, fixes all "." and ".." items, and changes [x]  to #x
428 	var pathList = this.normalizePath(path);
429 
430 	// forget any leading slashes
431 	if (pathList[0] == "") pathList = pathList.slice(1);
432 	
433 //	DBG.println("_makePathGetter(", path, "): ", pathList);
434 	var methodSteps = [];
435 	var pathToStep = "";
436 	
437 	for (var i = 0; i < pathList.length; i++) {
438 		var	pathStep = pathList[0, i];
439 		
440 		if (pathStep.charAt(0) == "#") {
441 			pathStep = pathStep.substr(1);
442 			pathToStep = pathToStep + "#";
443 		} else {
444 			pathToStep = pathToStep + pathStep;
445 
446 		}
447 		var	modelItem = this.getItem(pathToStep, true);
448 		var ref = modelItem.ref;
449 
450 		// convert "/" to "." in the ref
451 		if (ref.indexOf(this.pathDelimiter) > -1) ref = ref.split(this.pathDelimiter).join(".");
452 
453 		if (modelItem.getter) {
454 			var getter = modelItem.getter;
455 			var scope = modelItem.getterScope;
456 			if (scope == _INHERIT_) {
457 				scope = this.getterScope;
458 			}
459 			
460 			if (scope == _INSTANCE_) {
461 				methodSteps.push("if(instance) {");	
462 				methodSteps.push("	current = instance."+ getter+ "(current, '"+ref+"');");
463 				methodSteps.push("}");							
464 			} else if (scope == _MODEL_) {
465 				methodSteps.push("current = this."+ getter+ "(instance, current, '"+ref+"');");
466 			} else {
467 				methodSteps.push("current = this.getItem(\""+ pathToStep+ "\")."+ getter+ "(instance, current, '"+ref+"');");			
468 			}
469 			
470 		} else if (ref == "#") {
471 			methodSteps.push("if(current) {");	
472 			methodSteps.push("	current = current[" + pathStep + "];");
473 			methodSteps.push("}");					
474 		} else {
475 			methodSteps.push("if(current) {");		
476 			methodSteps.push("	current = current." + ref + ";");
477 			methodSteps.push("}");					
478 		}
479 		pathToStep += this.pathDelimiter;
480 	}
481 
482 	var methodBody = AjxBuffer.concat(
483 			"try {\r",
484 			"var current = instance;\r",
485 			"\t", methodSteps.join("\r\t"), "\r",
486 			"} catch (e) {\r ",
487 			"	DBG.println('Error in getting path for \"", path, "\": ' + e);\r",
488 			"	current = null;\r",
489 			"}\r",
490 			"return current;\r"
491 		);
492 	//DBG.println(path,"\r\t", methodSteps.join("\r\t"), "\r");
493 	var method = new Function("instance", methodBody);
494 
495 	return method;
496 }
497 
498 
499 
500 
501 
502 
503 
504 
505 // error messages
506 //	NOTE: every call to XModel.prototype.registerError() should be translated!
507 
508 XModel._errorMessages = {};
509 XModel.registerErrorMessage = XModel.prototype.registerErrorMessage = function (id, message) {
510 	this._errorMessages[id] = message;
511 }
512 XModel.registerErrorMessage("unknownError", "Unknown error.");
513 XModel.prototype.defaultErrorMessage = "unknownError";
514 
515 
516 // set the default error message for the model (it's not a bad idea to override this in your models!)
517 XModel.prototype.getDefaultErrorMessage = function (modelItem) {
518 	if (modelItem && modelItem.errorMessage) {
519 		return modelItem.getDefaultErrorMessage();
520 	}
521 
522 	return this.defaultErrorMessage;
523 }
524 
525 XModel.prototype.getErrorMessage = function (id, arg0, arg1, arg2, arg3, arg4) {
526 	var msg = this._errorMessages[id];
527 	if (msg == null) msg = XModel._errorMessages[id];
528 	
529 	if (msg == null) {
530 		DBG.println("getErrorMessage('", id, "'): message not found.  If this is an actual error message, add it to the XModel error messages so it can be translated.");
531 		return id;
532 	}
533 	if (arg0 !== null) msg = msg.split("{0}").join(arg0);
534 	if (arg1 !== null) msg = msg.split("{1}").join(arg1);
535 	if (arg2 !== null) msg = msg.split("{2}").join(arg2);
536 	if (arg3 !== null) msg = msg.split("{3}").join(arg3);
537 	if (arg4 !== null) msg = msg.split("{4}").join(arg4);
538 	return msg;
539 }
540