1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 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) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file contains the search result class.
 27  */
 28 
 29 /**
 30  * Creates the search result
 31  * @class
 32  * This class represents a search result.
 33  * 
 34  * @param	{ZmSearch}	search		the search
 35  */
 36 ZmSearchResult = function(search) {
 37 	if (!search) { return; }
 38 	this._results = {};
 39 	this.search = search;
 40 	this.type = search.searchFor;
 41 };
 42 
 43 ZmSearchResult.prototype.isZmSearchResult = true;
 44 ZmSearchResult.prototype.toString = function() { return "ZmSearchResult"; };
 45 
 46 /**
 47  * Gets the results.
 48  * 
 49  * @param	{constant}	type	the type
 50  * @return	{Array}	an array of results
 51  */
 52 ZmSearchResult.prototype.getResults =
 53 function(type) {
 54 
 55 	type = type || this.type;
 56 	if (!this._results) {
 57 		// probably got an exception - return an empty list
 58 		return this._getResultsList(type);
 59 	} else if (this.search.idsOnly) {
 60 		return this._results;
 61 	} else {
 62 		// if we don't have results for the requested type, the search was probably for the wrong type
 63 		return this._results[type] ? this._results[type] : type && this._getResultsList(type);
 64 	}
 65 };
 66 
 67 /**
 68  * Gets the attribute.
 69  * 
 70  * @param	{String}	name		the attribute name
 71  * @return	{Object}	the attribute
 72  */
 73 ZmSearchResult.prototype.getAttribute = 
 74 function(name) {
 75 	return this._respEl ? this._respEl[name] : null;
 76 };
 77 
 78 /**
 79  * Sets the response.
 80  * 
 81  * @private
 82  */
 83 ZmSearchResult.prototype.set =
 84 function(respEl) {
 85 
 86 	if (!this.search) { return; }
 87 
 88 	this._respEl = respEl;
 89 
 90 	// <match> objects are returned for autocomplete search, not items; let caller handle them
 91 	if (this.search.isAutocompleteSearch) { return; }
 92 
 93 	var foundType = {};
 94 	var numTypes = 0;
 95 	var currentType, defaultType;
 96 	var isGalSearch = this.search.isGalSearch;
 97 	
 98 	var _st = new Date();
 99 	var count = 0;
100 	if (isGalSearch || this.search.isCalResSearch) {
101 		// process JS eval result for SearchGalRequest
102 		currentType = defaultType = isGalSearch ? ZmItem.CONTACT : ZmItem.RESOURCE;
103 		var data = isGalSearch ? respEl.cn : respEl.calresource;
104 		if (data) {
105 			if (!this._results[currentType]) {
106 				// create list as needed - may invoke package load
107 				this._results[currentType] =  this._getResultsList(currentType);
108 			}
109 			for (var j = 0; j < data.length; j++) {
110 				this._results[currentType].addFromDom(data[j]);
111 			}
112 
113 			// manually sort gal results since server won't do it for us :(
114 			if (isGalSearch) {
115 				this._results[currentType].getArray().sort(ZmSearchResult._sortGalResults)
116 			}
117 			count = data.length;
118 		}
119 	} else if (this.search.idsOnly) {
120 		this._results = respEl.hit || [];
121 		return;
122 	} else {
123 		// process JS eval result for SearchResponse
124 		var types = this.search.types.getArray();
125 		defaultType = types[0];
126 
127 		// bug fix #44232 - resolve default type if none provided
128 		if (!defaultType) {
129 			var allTypes = AjxUtil.values(ZmList.NODE);
130 			for (var i = 0; i < allTypes.length; i++) {
131 				var t = allTypes[i];
132 				if (respEl[t]) {
133 					defaultType = ZmList.ITEM_TYPE[t];
134 					if (types && types.length == 0) {
135 						types = [defaultType];
136 					}
137 					break;
138 				}
139 			}
140 		}
141 
142 		if (!defaultType) {
143 			var curApp = appCtxt.getCurrentAppName();
144 			var types = ZmApp.SEARCH_TYPES[curApp];
145 			defaultType = types && types.length && types[0];
146 		}
147 
148 		for (var i = 0; i < types.length; i++) {
149 			var type = types[i];
150 			var data = respEl[ZmList.NODE[type]];
151 
152 			// A chat isa message. Futz with the types to deal with this.
153 			// (Eventually we'll avoid this problem by displying chat history in im app.)
154 			if (!data && (type == ZmItem.MSG)) {
155 				data = respEl["chat"];
156 			}
157 			// Likewise, a Resource is a Contact. Futz with the types to deal with this.
158 			if (!data && (type == ZmItem.RESOURCE)) {
159 				data = respEl[ZmList.NODE[ZmItem.CONTACT]];
160 			}
161 
162 			// do a bunch of sanity checks
163 			if (data && data.length) {
164 				count += data.length;
165 				if (!this._results[type]) {
166 					// create list as needed - may invoke package load
167 					this._results[type] = this._getResultsList(type);
168 				}
169 				for (var j = 0; j < data.length; j++) {
170 					var item = data[j];
171 					item._type = type;
172 					this._results[type].addFromDom(item);
173 				}
174 
175 				if (!foundType[type]) {
176 					foundType[type] = true;
177 					numTypes++;
178 					currentType = type;
179 				}
180 			}
181 		}
182 	}
183 	if (!count && defaultType) {
184 		this._results[defaultType] = this._getResultsList(defaultType);
185 	}
186 	if ((isGalSearch || this.search.isGalAutocompleteSearch) && this._results[ZmItem.CONTACT]) {
187 		this._results[ZmItem.CONTACT].setIsGal(true);
188 	}
189 	if (this.search.isGalAutocompleteSearch) {
190 		this.isTokenized = (this._respEl.tokenizeKey != null);
191 	}
192 	
193 	var _en = new Date();
194 	DBG.println(AjxDebug.DBG1, "TOTAL PARSE TIME for " + count + " NODES: " + (_en.getTime() - _st.getTime()));
195 
196 	currentType = currentType || defaultType;
197 	if (numTypes <= 1) {
198 		this.type = currentType;
199 	}
200 
201 	return this.type;
202 };
203 
204 /**
205  * @private
206  */
207 ZmSearchResult._sortGalResults =
208 function(a, b) {
209 	var af = a.getFileAs && a.getFileAs().toLowerCase();
210 	var bf = b.getFileAs && b.getFileAs().toLowerCase();
211 	return af < bf ? -1 : (af > bf ? 1 : 0);
212 };
213 
214 ZmSearchResult.prototype._getResultsList =
215 function(type) {
216 
217 	if (type && typeof(ZmItem.RESULTS_LIST[type]) === "function") {
218 		return ZmItem.RESULTS_LIST[type](this.search);
219 	} else {
220 		DBG.println(
221 			AjxDebug.DBG1,
222 			AjxMessageFormat.format(
223 				"!type || ZmItem.RESULTS_LIST[type] !== function. Active app: {0}, type: {1}, searchFor: {2}.",
224 				[appCtxt.getCurrentAppName(), type, this.search.searchFor]
225 			)
226 		);
227 		return new ZmList(type, this.search);
228 	}
229 
230 };
231 
232