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;