/** * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2009 Funambol, Inc. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by * the Free Software Foundation with the addition of the following permission * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA. * * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License * version 3, these Appropriate Legal Notices must retain the display of the * "Powered by Funambol" logo. If the display of the logo is not reasonably * feasible for technical reasons, the Appropriate Legal Notices must display * the words "Powered by Funambol". */ package com.funambol.push; import java.util.Vector; import java.util.TimerTask; import java.util.Timer; import java.util.Date; import com.funambol.util.Log; /** * This class queues sync requests and perform basic requests merging that can * be used for client to server push. * The class provides the ability to manage a queue of requests and merge * requests that are "equivalent", giving the user the ability to implement its * own equivalence concept. * * A request is described by a SyncRequest which is a general interface that * clients must implement. Each request is stored in the queue with an * associated time at which the request shall be served. When a new request * comes in, the scheduler checks if the this requests "contains" other requests * already in the queue. In such a case the requests are combined. When requests * are combined, the scheduler removes what was already in the queue, and * store the new request in the proper position (depending only on its * interval). * */ public class SyncScheduler { private static final String TAG_LOG = "SyncScheduler"; private Vector requestQueue; private SyncSchedulerListener ssListener; private Timer scheduleTimer = null; private RequestTimer rq = null; /** * Construct a scheduler with the given listener. * * @param ssListener the listener. This cannot be null otherwise NPE * exceptions will be generated when trying to fire a sync */ public SyncScheduler(SyncSchedulerListener ssListener) { this.ssListener = ssListener; requestQueue = new Vector(); } /** * Add a sync request to the SyncScheduler. * @param syncRequest object to be added * */ public void addRequest(SyncRequest syncRequest){ if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "addRequest"); } if (syncRequest.getInterval() == 0){ doSync(syncRequest); }else{ SchedulerRequest sr = new SchedulerRequest(syncRequest); checkQueue(sr); } } /** * Set a Listener to the SyncScheduler. * @param listenet to be added */ public void setListener(SyncSchedulerListener ssListener){ this.ssListener = ssListener; } /** * Manage the request queue, adding, replacing or merge the new request * @param syncRequest */ private void checkQueue(SchedulerRequest schedulReq){ if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "checkQueue"); } int queueSize = requestQueue.size(); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "requests in queue: " + queueSize); } boolean setTimer = false; boolean requestCombined = false; boolean insertedRequest = false; if(queueSize > 0){ // Check if it's possible merge or replace a request, putting the new return request in the // ordered position for (int i=0; i< queueSize; i++){ SchedulerRequest srCombined = schedulReq.combine((SchedulerRequest)requestQueue.elementAt(i)); if(srCombined != null){ if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Request was merged"); } requestCombined = true; requestQueue.removeElementAt(i); if(!requestQueue.isEmpty()){ queueSize = requestQueue.size(); for (int pos = 0; pos<queueSize; pos++) { long expTime = ((SchedulerRequest)requestQueue.elementAt(pos)).getExpirationTime(); if (srCombined.getExpirationTime() < expTime) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Putting combined request in "+pos+"position"); } requestQueue.insertElementAt(srCombined, pos); insertedRequest = true; if(pos==0){ setTimer = true; } break; }else if (pos == queueSize-1) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Putting new request in last position"); } requestQueue.addElement(schedulReq); insertedRequest = true; } } } else { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "The queue is empty... adding the combined request"); } requestQueue.addElement(srCombined); setTimer = true; insertedRequest = true; } } if (insertedRequest) { break; } } // It's not possible to merge o replace the request than put // the request on the rigth position if (!requestCombined) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Request cannot be merged, enqueue a new one"); } for (int i=0; i< queueSize; i++) { long expTime = ((SchedulerRequest)requestQueue.elementAt(i)).getExpirationTime(); if (schedulReq.getExpirationTime() < expTime) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "putting new request in "+i+" position"); } requestQueue.insertElementAt(schedulReq, i); if (i==0) { setTimer = true; } break; } else if(i == queueSize-1) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "putting new request in last position"); } requestQueue.addElement(schedulReq); } } } } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "first element into the queue"); } requestQueue.addElement(schedulReq); setTimer = true; } if (setTimer) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "needs to set a timer"); } setTimer(((SchedulerRequest)requestQueue.elementAt(0)).getExpirationTime()); } } private void resetQueue(){ requestQueue.removeAllElements(); } /** * This method is invoked when syncRequest is requested immediately * (interval=0). This request cleans all other equivalent requests in queue * (aka requests that are can be merged with this one) */ private void doSync(SyncRequest syncRequest) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "doSync"); } //Check if it's present the request in the queue to remove it if(requestQueue.size() > 0) { for (int i=0; i< requestQueue.size(); i++) { SyncRequest queuedReq = ((SchedulerRequest)requestQueue.elementAt(i)).getSyncRequest(); SyncRequest merged = syncRequest.merge(queuedReq); if (merged != null) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "removing old request"); } SchedulerRequest or = (SchedulerRequest)requestQueue.elementAt(i); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "was scheduled at: " + new Date(or.getExpirationTime())); } requestQueue.removeElementAt(i); if(i==0) { if(rq != null) { rq.cancel(); scheduleTimer.cancel(); } } // What we sync is the merged version syncRequest = merged; } } } if(scheduleTimer != null){ scheduleTimer.cancel(); } callListener(syncRequest); } private void doScheludedSync(){ if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "doScheludedSync"); } SyncRequest requestToSync = ((SchedulerRequest)requestQueue.elementAt(0)).getSyncRequest(); requestQueue.removeElementAt(0); callListener(requestToSync); } private void setTimer(long startTime){ if(rq!=null){ if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Cancelling previous timer"); } rq.cancel(); scheduleTimer.cancel(); } scheduleTimer = new Timer(); rq = new RequestTimer(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "timer set at: "+new Date(startTime)); } try{ scheduleTimer.schedule(rq, new Date(startTime)); }catch(Exception ex){ Log.error(TAG_LOG, "ex scheduling timer: ", ex); } } private void callListener(SyncRequest syncRequest){ //restart timer if in the queue is present an other request if(!requestQueue.isEmpty()){ long nextTimer = ((SchedulerRequest)requestQueue.elementAt(0)).getExpirationTime(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "starting sync... reschedule timer for next request at: " + new Date(nextTimer)); } setTimer(nextTimer); } if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "starting sync at : "+new Date(System.currentTimeMillis())); } ssListener.sync(syncRequest.getRequestContent()); } //------------------------ SchedulerRequest ---------------------------------// // used to incapsule the request with the expiration time private class SchedulerRequest{ private SyncRequest mySyncRequest; private long expirationTime; public SchedulerRequest(SyncRequest synReq){ mySyncRequest = synReq; setExpTime(mySyncRequest.getInterval()); } private void setExpTime(long expTime){ expirationTime = System.currentTimeMillis() + expTime; } long getExpirationTime(){ return expirationTime; } SyncRequest getSyncRequest(){ return mySyncRequest; } void setSyncRequest(SyncRequest syncRequest){ mySyncRequest = syncRequest; } /** * Check if it is possible to merge or to replace the new request with * the existing one * * @param queuedRequest * @return SchedulerRequest if it's possible to combine two or more * requests, or null in it's not possible. */ SchedulerRequest combine(SchedulerRequest queuedRequest) { SyncRequest sr = queuedRequest.getSyncRequest(); //mySyncRequest is the new request SyncRequest merged = mySyncRequest.merge(sr); if (merged != null) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Requests can be combined and scheduled at: " + new Date(System.currentTimeMillis() + mySyncRequest.getInterval())); } queuedRequest.setExpTime(mySyncRequest.getInterval()); queuedRequest.setSyncRequest(merged); return queuedRequest; } return null; } } //------------------------ RequestTimer ---------------------------------// private class RequestTimer extends TimerTask{ RequestTimer() { } public void run() { doScheludedSync(); } } }