1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2011, 2012, 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) 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file defines the progress controller.
 27  *
 28  * it gets a list of folders to work on, uses the search controller to get all the messages in the folders, in chunks. Callbacks the work passed to it to perform the work
 29  * on the message id chunks.
 30  *
 31  */
 32 
 33 /**
 34  *
 35  * @author Eran Yarkon
 36  *
 37  * @param {DwtControl}		container	the containing shell
 38  * @param {ZmApp}		app		the containing application
 39  * 
 40  * @extends		ZmController
 41  */
 42 ZmProgressController = function(container, app) {
 43 	if (arguments.length == 0) { return; }
 44 	ZmController.call(this, container, app);
 45     this._totalNumMsgs = 0; //for determining if run in background is available
 46 };
 47 
 48 ZmProgressController.prototype = new ZmController;
 49 ZmProgressController.prototype.constructor = ZmProgressController;
 50 
 51 // public methods
 52 
 53 /**
 54  * Returns a string representation of the object.
 55  * 
 56  * @return		{String}		a string representation of the object
 57  */
 58 ZmProgressController.prototype.toString =
 59 function() {
 60 	return "ZmProgressController";
 61 };
 62 
 63 
 64 ZmProgressController.prototype._getProgressDialog =
 65 function() {
 66 	if (!this._progressDialog ) {
 67 		var dialog = this._progressDialog = new DwtMessageDialog({parent:this._shell, buttons:[DwtDialog.YES_BUTTON, DwtDialog.CANCEL_BUTTON], id: Dwt.getNextId("ZmProgressControllerDialog_")});
 68 		dialog.registerCallback(DwtDialog.CANCEL_BUTTON, new AjxCallback(this, this._cancelAction));
 69 		dialog.registerCallback(DwtDialog.YES_BUTTON, new AjxCallback(this, this._runInBackgroundAction));
 70         dialog.getButton(DwtDialog.YES_BUTTON).setText(ZmMsg.runInBackground);
 71     }
 72     this._progressDialog.getButton(DwtDialog.YES_BUTTON).setVisible(this._totalNumMsgs <= appCtxt.get(ZmSetting.FILTER_BATCH_SIZE));
 73 	return this._progressDialog;
 74 };
 75 
 76 ZmProgressController.prototype._getFinishedDialog =
 77 function() {
 78 	if (!ZmProgressController._finishedDialog) {
 79 		var dialog = ZmProgressController._finishedDialog = appCtxt.getMsgDialog();
 80 		dialog.reset();
 81 	}
 82 
 83 	return ZmProgressController._finishedDialog;
 84 };
 85 
 86 /**
 87  * start a progres on a folder list and work definition
 88  * @param folderList - list of folders to work on the messages of, in chunks.
 89  * @param work - implements an unwritten interface. See ZmFilterRulesController.ZmFilterWork for the first example of an implementation
 90  */
 91 ZmProgressController.prototype.start =
 92 function(folderList, work) {
 93 	this._currentWork = work;
 94 	this._currentRun = new ZmProgressRun(folderList);
 95     this._totalNumMsgs = this.getNumMsgs(folderList);
 96 	this._nextChunk();
 97 };
 98 
 99 
100 /**
101  * next chunk of work. Get the next chunk of ids from the search
102  */
103 ZmProgressController.prototype._nextChunk =
104 function() {
105 	var run = this._currentRun;
106 	var work = this._currentWork;
107 	if (run._runInBackground) {
108 		//don't get Ids
109 		this._handleRunInBackground(this._getFolderQuery(run._folderList));	
110 	}
111 	else {
112 		var searchParams = {
113 			query:		this._getFolderQuery(run._folderList),
114 			types:		ZmItem.MSG,
115 			forceTypes:	true,
116 			limit:		ZmProgressController.CHUNK_SIZE,
117 			idsOnly:	true,
118 			noBusyOverlay: true
119 		};
120 	
121 		if (run._lastItem) {
122 			//this is not the first chunk - supply the last id and sort val to the search.
123 			searchParams.lastId = run._lastItem.id;
124 			searchParams.lastSortVal = run._lastItem.sf;
125 			AjxDebug.println(AjxDebug.PROGRESS, "***** progress search: " + searchParams.query + " --- " + [run._lastItem.id, run._lastItem.sf].join("/"));
126 		}
127 	
128 		var search = new ZmSearch(searchParams);
129 		var respCallback = new AjxCallback(this, this._handleSearchResults);
130 		appCtxt.getSearchController().redoSearch(search, true, null, respCallback);
131 	}
132 };
133 
134 ZmProgressController.prototype._handleRunInBackground = 
135 function(query) {
136 	var run = this._currentRun;
137 	if (run._cancelled) {
138 		return;
139 	}
140 	run._finished = true; //running all at once
141 	var afterWorkCallback = new AjxCallback(this, this._afterChunk);
142 	this._currentWork.doWork(null, query, afterWorkCallback); //callback the work to do it's job on the message ids	
143 };
144 
145 /**
146  * process the returned message ids and
147  * @param result
148  */
149 ZmProgressController.prototype._handleSearchResults =
150 function(result) {
151 	var run = this._currentRun;
152 	if (run._cancelled) {
153 		return;
154 	}
155 
156 	var response = result.getResponse();
157 	var items = response.getResults();
158 
159 	AjxDebug.println(AjxDebug.PROGRESS, "progress search results: " + items.length);
160 	if (!items.length) {
161 		AjxDebug.println(AjxDebug.PROGRESS, "progress with empty search results!");
162 		return;
163 	}
164 
165 	run._lastItem = items[items.length - 1];
166 	run._totalMessagesProcessed += items.length;
167 	var hasMore = response.getAttribute("more");
168 	run._finished = !hasMore;
169 
170 	items = this._getIds(items);
171 
172 	var afterWorkCallback = new AjxCallback(this, this._afterChunk);
173 	this._currentWork.doWork(items, null, afterWorkCallback); //callback the work to do it's job on the message ids
174 
175 };
176 
177 /**
178  * returns here after the work is done on the chunk
179  */
180 ZmProgressController.prototype._afterChunk =
181 function() {
182 	var work = this._currentWork;
183 	var run = this._currentRun;
184 	if (run._cancelled) {
185 		return;
186 	}
187 
188 	var progDialog = this._getProgressDialog();
189 	if (run._finished) {
190 		//search is over, show summary messsage
191 		if (progDialog.isPoppedUp()) {
192 			progDialog.popdown();
193 		}
194 		var messagesProcessed = run._runInBackground ? false : run._totalMessagesProcessed;
195 		var finishedMessage = work.getFinishedMessage(messagesProcessed);
196 		var finishDialog = this._getFinishedDialog();
197 		finishDialog.setMessage(finishedMessage, DwtMessageDialog.INFO_STYLE, work.getFinishedTitle());
198 		finishDialog.popup();
199 		return;
200 	}
201 
202 	if (!run._runInBackground) {
203 		var workMessage = work.getProgressMessage(run._totalMessagesProcessed);
204 		progDialog.setMessage(workMessage, DwtMessageDialog.INFO_STYLE, work.getProgressTitle());
205 		if (!progDialog.isPoppedUp()) {
206 			progDialog.popup();
207 		}
208 	}
209 
210 	this._nextChunk();
211 };
212 
213 
214 /**
215  * extract just the ids from the item objects. (they include also the search value)
216  * @param items
217  */
218 ZmProgressController.prototype._getIds =
219 function(items) {
220 	var ids = [];
221 	if (!items.length) { //not sure if this could happen but I've seen it elsewhere.
222 		items = [items];
223 	}
224 
225 	for (var i = 0; i < items.length; i++) {
226 		ids.push(items[i].id);
227 	}
228 	return ids;
229 };
230 
231 ZmProgressController.prototype._getFolderQuery =
232 function(folderList) {
233 	if (!(folderList instanceof Array)) {
234 		folderList = [folderList];
235 	}
236 	var query = [];
237 	for (var j = 0; j < folderList.length; j++) {
238 		query.push(folderList[j].createQuery());
239 	}
240 	return query.join(" OR ");
241 
242 };
243 
244 /**
245  * Determine total number of messages filters are being applied to.
246  * @param folderList {ZmOrganizer[]} array of folders
247  * @return {int} number of messages
248  */
249 ZmProgressController.prototype.getNumMsgs = 
250 function(folderList) {
251     var numMsgs = 0;
252     if (!(folderList instanceof Array)) {
253         folderList = [folderList];
254     }
255 
256     for (var j = 0; j < folderList.length; j++) {
257         numMsgs += folderList[j].numTotal;
258     }
259     return numMsgs;
260 };
261 
262 ZmProgressController.prototype._cancelAction =
263 function() {
264 	this._currentRun._cancelled = true;
265 	var dialog = this._getProgressDialog();
266 	if (dialog && dialog.isPoppedUp()) {
267 		dialog.popdown();
268 	}
269 };
270 
271 ZmProgressController.prototype._runInBackgroundAction = 
272 function() {
273 	this._currentRun._runInBackground = true;
274 	var dialog = this._getProgressDialog();
275 	if (dialog && dialog.isPoppedUp()) {
276 		dialog.popdown();
277 	}
278 	AjxDebug.println(AjxDebug.PROGRESS, "set to run in background");
279 };
280 
281 /**
282  * internal class to keep track of progress, with last item processed, total messages processed, and the folder list we work on
283  * @param folderList
284  */
285 ZmProgressRun = function(folderList) {
286 	this._lastItem = null;
287 	this._totalMessagesProcessed = 0;
288 	this._folderList = folderList;
289 };
290 
291 ZmProgressRun.CHUNK_SIZE = 100;