Rename preconfigured beans to '.example.xml' so they don't overwrite configured sync...
[funambol-groupdav-connector.git] / src / main / java / net / bionicmessage / funambol / groupdav / calendar / CalendarSyncSource.java
blob0c6c1e726a10db5f331b0d3b80049b0512a5acf5
1 /* GroupDAV connector for Funambol v6.5
2 * Copyright (C) 2006-2007 Mathew McBride
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation, either version 3 of the
7 * License, or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
14 * You should have received a copy of the GNU Affero General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 package net.bionicmessage.funambol.groupdav.calendar;
19 import com.funambol.framework.engine.SyncItem;
20 import com.funambol.framework.engine.SyncItemKey;
21 import com.funambol.framework.engine.SyncItemState;
22 import com.funambol.framework.engine.source.AbstractSyncSource;
23 import com.funambol.framework.engine.source.SyncContext;
24 import com.funambol.framework.engine.source.SyncSourceException;
25 import com.funambol.framework.tools.beans.*;
26 import java.io.File;
27 import java.io.RandomAccessFile;
28 import java.io.Serializable;
29 import java.nio.channels.FileChannel;
30 import java.nio.channels.FileLock;
31 import java.nio.channels.OverlappingFileLockException;
32 import java.sql.Timestamp;
33 import java.util.ArrayList;
34 import java.util.Hashtable;
35 import java.util.Iterator;
36 import java.util.Map;
37 import java.util.Properties;
38 import net.bionicmessage.utils.HTMLFormatter;
39 import java.util.Properties;
40 import java.util.logging.FileHandler;
41 import java.util.logging.Level;
42 import java.util.logging.Logger;
43 import net.bionicmessage.funambol.framework.Constants;
44 import net.bionicmessage.funambol.framework.Utilities;
45 // </editor-fold>
46 import net.bionicmessage.funambol.groupdav.calendar.plugins.MixedCalendarTaskInbound;
47 import net.bionicmessage.objects.MultipleSourceICalendarObjectStore;
48 import net.bionicmessage.objects.StoreConstants;
49 import net.fortuna.ical4j.model.Calendar;
51 public class CalendarSyncSource extends AbstractSyncSource implements LazyInitBean, Serializable {
53 private Properties connectorProperties = null;
54 private SyncContext ctx = null;
55 private FileHandler fh = null;
56 Logger log = null;
57 private int syncType = 0;
58 private Map itemStates = null;
59 private MultipleSourceICalendarObjectStore so = null;
60 private Map sources = null;
61 private Properties icalStoreProperties = null;
62 private boolean taskMode = false;
63 private Exception startSyncException = null;
64 private FileLock oneSyncLock = null;
65 private FileChannel lockChannel = null;
67 /**
68 * Sets up the calendar sync source instance
69 * @param syncContext the syncContext to use
70 * @throws SyncSourceException in case of any error
71 * @see SyncSource
73 public void beginSync(SyncContext syncContext) throws SyncSourceException {
74 log = Logger.getLogger(Constants.CALENDAR_LOGGER_NAME + "-" + System.currentTimeMillis());
75 log.info("Begin sync for Calendar Sync Source (" + this.hashCode() + ")");
76 this.ctx = syncContext;
77 String principalName = Utilities.cleanFilePath(ctx.getPrincipal().getName());
78 String storeDir = connectorProperties.getProperty(Constants.STOREDIR_PATH);
79 File stdir = new File(storeDir + File.separatorChar + principalName + File.separatorChar);
80 int storeopts = 0;
81 try {
82 /** Set up logging */
83 if (!stdir.exists()) {
84 stdir.mkdirs();
86 // Setup the logger
87 HTMLFormatter hf = new HTMLFormatter();
88 if (connectorProperties.getProperty(Constants.SINGLE_LOG) != null) {
89 fh = new FileHandler(stdir.toString() + File.separatorChar + "connector.html");
90 storeopts += MultipleSourceICalendarObjectStore.OPTION_SINGLELOG;
91 } else {
92 long curTime = new java.util.Date().getTime();
93 fh = new FileHandler(stdir.toString() + File.separatorChar + "connector-" + curTime + ".html");
95 fh.setFormatter(hf);
96 log.addHandler(fh);
97 log.setLevel(Level.ALL);
99 /* Set up a loose lock file to stop concurrent instances for same user, i.e one sync got cancelled
100 * See http://www.exampledepot.com/egs/java.nio/SetFileLock.html
102 String lockFilePath = stdir.toString() + File.separatorChar + "sync.lock";
103 File lockFile = new File(lockFilePath);
104 lockChannel = new RandomAccessFile(lockFile, "rw").getChannel();
106 try {
107 oneSyncLock = lockChannel.tryLock();
108 log.info("File lock created");
109 } catch (OverlappingFileLockException e) {
110 throw new SyncSourceException("More than one sync source for this user! FIXME!!!", e);
113 itemStates = new Hashtable();
114 syncType = ctx.getSyncMode();
115 log.info("Beginning sync for " + principalName + " for mode=" + syncType);
116 if (syncType == 201 || syncType == 205) {
117 storeopts += 64;
119 } catch (Exception e) {
120 log.throwing(this.getClass().getName(), "beginSync", e);
121 throw new SyncSourceException(e);
123 // Perform setup
124 itemStates = new Hashtable();
125 syncType = ctx.getSyncMode();
126 String creds = ctx.getPrincipal().getEncodedCredentials();
127 sources = new Hashtable();
128 icalStoreProperties = new Properties();
129 /** Todo: simply pass connectorProperties into icalStoreProperties */
130 // Process the sources
131 Iterator configKeys = connectorProperties.keySet().iterator();
132 while (configKeys.hasNext()) {
133 String keyName = (String) configKeys.next();
134 if (keyName.contains(Constants.SOURCE_LOCATION_BASE)) {
135 String sourceLocation = connectorProperties.getProperty(keyName);
136 sourceLocation = sourceLocation.replace("%USER%", syncContext.getPrincipal().getUsername());
137 String sourceName = StoreConstants.BASE_PROPERTY_SOURCE.concat(keyName.replace(Constants.SOURCE_LOCATION_BASE, ""));
138 icalStoreProperties.setProperty(sourceName, sourceLocation);
141 String calDavComp = connectorProperties.getProperty(Constants.CALDAV_COMPONENT);
142 String calDavProp = connectorProperties.getProperty(Constants.CALDAV_PROPERTY);
143 String calDavStart = connectorProperties.getProperty(Constants.CALDAV_START);
144 String calDavEnd = connectorProperties.getProperty(Constants.CALDAV_END);
145 if (calDavComp != null) {
146 icalStoreProperties.setProperty(Constants.CALDAV_COMPONENT, calDavComp);
147 icalStoreProperties.setProperty(Constants.CALDAV_PROPERTY, calDavProp);
148 icalStoreProperties.setProperty(Constants.CALDAV_START, calDavStart);
149 icalStoreProperties.setProperty(Constants.CALDAV_END, calDavEnd);
151 String collectionMode = connectorProperties.getProperty(StoreConstants.PROPERTY_SERVER_MODE);
152 if (collectionMode != null)
153 icalStoreProperties.setProperty(StoreConstants.PROPERTY_SERVER_MODE, collectionMode);
154 String cthreads = connectorProperties.getProperty(StoreConstants.PROPERTY_SERVER_CTHREADS);
155 icalStoreProperties.setProperty(StoreConstants.PROPERTY_SERVER_CTHREADS, cthreads);
156 String citems = connectorProperties.getProperty(StoreConstants.PROPERTY_SERVER_ITEMS);
157 icalStoreProperties.setProperty(StoreConstants.PROPERTY_SERVER_ITEMS, citems);
158 // Handle %DOMAIN%
159 String domainString = connectorProperties.getProperty(Constants.SERVER_HOST);
161 // Parse users email address
162 if (domainString.contains(Constants.DOMAIN_REPLACE)) {
163 String emailAddr = ctx.getPrincipal().getUser().getUsername();
164 if (!emailAddr.contains("@")) {
165 emailAddr = ctx.getPrincipal().getUser().getEmail();
167 String[] emailComponenets = emailAddr.split("@");
168 String domain = emailComponenets[1];
169 domainString = domainString.replace(Constants.DOMAIN_REPLACE, domain);
170 int one = 2;
172 icalStoreProperties.setProperty(StoreConstants.PROPERTY_SERVER,
173 domainString);
175 icalStoreProperties.setProperty(StoreConstants.PROPERTY_STORE_LOCATION, storeDir);
176 if (connectorProperties.getProperty(Constants.SOURCE_IS_TASK) != null) {
177 storeopts += MultipleSourceICalendarObjectStore.OPTION_TODO;
179 // Create store
180 if (so == null) {
181 so = new MultipleSourceICalendarObjectStore(stdir.toString(), storeopts);
182 so.setProperties(icalStoreProperties);
183 so.loadSourcesFromProps();
184 } else {
185 log.fine("Reusing existing object store");
187 try {
188 so.connect_Base64(creds);
189 // Sync
190 so.startSync();
191 } catch (Exception e) {
192 log.throwing(this.getClass().getName(), "getSyncItemKeysFromTwin", e);
193 throw new SyncSourceException(e);
198 * It logs the howMany counters. It should then be called by extending
199 * classes before anything else.
200 * @param principal user/device
201 * @throws SyncSourceException in case of any error
202 * @see SyncSource
204 public void endSync() throws SyncSourceException {
205 try {
206 log.info("endSync");
207 so.printDebugReport();
208 so.close();
209 fh.close();
210 oneSyncLock.release();
211 lockChannel.close();
212 } catch (Exception ex) {
213 ex.printStackTrace();
214 log.throwing(this.getClass().getName(), "endSync", ex);
215 throw new SyncSourceException(ex);
220 * -------------------------------------------------------- Abstract methods
222 public SyncItemKey[] getAllSyncItemKeys() {
223 log.fine("getAllSyncItemKeys()");
224 SyncItemKey[] newKeys = this.getNewSyncItemKeys(null, null);
225 SyncItemKey[] updatedKeys = this.getUpdatedSyncItemKeys(null, null);
226 SyncItemKey[] allKnownKeys = new SyncItemKey[newKeys.length + updatedKeys.length];
227 int curKey = 0;
228 for (int i = 0; i < newKeys.length; i++) {
229 allKnownKeys[curKey] = newKeys[i];
230 curKey++;
232 for (int i = 0; i < updatedKeys.length; i++) {
233 allKnownKeys[curKey] = updatedKeys[i];
234 curKey++;
236 return allKnownKeys;
239 /**
240 * Obtain a list of deleted (on GroupDAV Server) objects
241 * @param sinceTs From time, only used for filtering (not in use here)
242 * @param untilTs Until time, only used for filtering (not in use here)
243 * @return
245 public SyncItemKey[] getDeletedSyncItemKeys(java.sql.Timestamp sinceTs, java.sql.Timestamp untilTs) {
246 ArrayList deletedUIDS = so.getDeletedFromStoreUIDS();
247 SyncItemKey[] keys = new SyncItemKey[deletedUIDS.size()];
248 for (int i = 0; i < deletedUIDS.size(); i++) {
249 String key = (String) deletedUIDS.get(i);
250 SyncItemKey sk = new SyncItemKey(key);
251 keys[i] = sk;
253 return keys;
256 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
257 // #[regen=yes,id=DCE.EE4A388C-A94E-B384-EC8A-2E349EACBA40]
258 // </editor-fold>
259 public SyncItemKey[] getNewSyncItemKeys(java.sql.Timestamp sinceTs, java.sql.Timestamp untilTs) {
260 log.info("getNewSyncItemKeys()");
261 ArrayList newUIDS = so.getAddedToStoreUIDS();
262 SyncItemKey[] keys = new SyncItemKey[newUIDS.size()];
263 for (int i = 0; i < newUIDS.size(); i++) {
264 String key = (String) newUIDS.get(i);
265 SyncItemKey sk = new SyncItemKey(key);
266 keys[i] = sk;
267 itemStates.put(sk, "new");
269 return keys;
272 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
273 // #[regen=yes,id=DCE.FF60A355-292D-B905-DF53-B818D850314B]
274 // </editor-fold>
275 public SyncItem getSyncItemFromId(SyncItemKey syncItemKey) throws SyncSourceException {
276 String uid = syncItemKey.getKeyAsString();
277 log.info("getSyncItemFromId(" + uid + ")");
278 try {
279 Calendar object = so.getObjectFromStore(uid);
280 OutboundFunambolCalendarObject ofc = new OutboundFunambolCalendarObject(object);
281 ofc.setConnectorProperties(this.getConnectorProperties());
282 SyncItem si = ofc.generateSyncItem(this);
283 si.setState(getStateForItem(syncItemKey));
284 si.setType(this.getType());
285 return si;
286 } catch (Exception ex) {
287 log.log(Level.SEVERE, "getSyncItemFromId", ex);
288 throw new SyncSourceException(ex);
292 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
293 // #[regen=yes,id=DCE.31A4119B-818A-A6B4-38C1-161C22D749F3]
294 // </editor-fold>
295 public void removeSyncItem(SyncItemKey syncItemKey, Timestamp time, boolean softDelete) throws SyncSourceException {
296 log.info("removeSyncItem(" + syncItemKey.getKeyAsString() + ")");
297 try {
298 so.deleteObject(syncItemKey.getKeyAsString());
299 } catch (Exception ex) {
300 log.throwing(this.getClass().getName(), "removeSyncItem", ex);
301 throw new SyncSourceException(ex);
305 /**
306 * Parse the specified sync item to find any matches on the server.
307 * @param syncItem Sync item to parse and search for
308 * @return An array of SyncItemKeys[] with matching data.
309 * @throws com.funambol.framework.engine.source.SyncSourceException
311 public SyncItemKey[] getSyncItemKeysFromTwin(SyncItem syncItem) throws SyncSourceException {
312 log.info("getSyncItemKeysFromTwin(" + syncItem.getKey().getKeyAsString() + ")");
313 try {
314 InboundFunambolCalendarObject ifcb = initInboundProcessor(syncItem);
315 String uid = (String) syncItem.getKey().getKeyValue();
316 ifcb.setUid(uid);
317 ifcb.parseIntoObjectFromString(null, syncItem.getType());
318 String summary = ifcb.getSummary();
319 long dtstart = ifcb.getDtStart();
320 long dtend = ifcb.getDtEnd();
321 if (dtstart > 0) {
322 dtstart = dtstart / 1000;
324 if (dtend > 0) {
325 dtend = dtend / 1000;
327 String match = so.searchUids(summary, dtstart, dtend);
328 if (match != null) {
329 log.info("Match found: " + match);
330 SyncItemKey[] ma = {new SyncItemKey(match)};
331 return ma;
333 return new SyncItemKey[0];
334 } catch (Exception ex) {
335 log.throwing(this.getClass().getName(), "getSyncItemKeysFromTwin", ex);
336 throw new SyncSourceException(ex);
341 * Called to get the keys of the items updated in the time frame sinceTs - untilTs.
342 * <br><code>sinceTs</code> null means all keys of the items updated until <code>untilTs</code>.
343 * <br><code>untilTs</code> null means all keys of the items updated since <code>sinceTs</code>.
344 * @param sinceTs consider the changes since this point in time.
345 * @param untilTs consider the changes until this point in time.
346 * @return an array of keys containing the <code>SyncItemKey</code>'s key of the updated
347 * items in the given time frame. It MUST NOT return null for
348 * no keys, but instad an empty array.
350 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
351 // #[regen=yes,id=DCE.A80FDB1D-9EC6-0537-6211-5EC359BAAB6D]
352 // </editor-fold>
353 public SyncItemKey[] getUpdatedSyncItemKeys(Timestamp sinceTs, Timestamp untilTs) {
354 log.info("getUpdatedSyncItemKeys()");
355 ArrayList updatedKeys = so.getUpdatedInStoreUIDS();
356 SyncItemKey[] keys = new SyncItemKey[updatedKeys.size()];
357 for (int i = 0; i < updatedKeys.size(); i++) {
358 String key = (String) updatedKeys.get(i);
359 SyncItemKey sk = new SyncItemKey(key);
360 keys[i] = sk;
361 itemStates.put(sk, "updated");
363 return keys;
366 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
367 // #[regen=yes,regenBody=yes,id=DCE.FE19692D-9043-AB64-AA5F-8E43328CD316]
368 // </editor-fold>
369 public Properties getConnectorProperties() {
370 return connectorProperties;
373 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
374 // #[regen=yes,regenBody=yes,id=DCE.F282BB88-7C8F-0167-DA12-B87014A3DD56]
375 // </editor-fold>
376 public void setConnectorProperties(Properties val) {
377 this.connectorProperties = val;
380 public SyncItem updateSyncItem(SyncItem arg0) throws SyncSourceException {
381 log.info("updateSyncItem(" + arg0.getKey().getKeyAsString() + ")");
382 try {
383 InboundFunambolCalendarObject ifcb = initInboundProcessor(arg0);
384 String uid = (String) arg0.getKey().getKeyValue();
385 ifcb.setUid(uid);
386 ifcb.parseIntoObjectFromString(null, arg0.getType());
387 Calendar oldCal = so.getObjectFromStore(uid);
388 /* Some update only operations when attributes need to be
389 * grabbed from the stored calendar */
390 ifcb.copyOrganizerFromCalendar(oldCal);
391 ifcb.copyAttendeesFromCalendar(oldCal);
392 ifcb.copyVAlarmsFromCalendar(oldCal);
393 net.fortuna.ical4j.model.Calendar cal = (net.fortuna.ical4j.model.Calendar) ifcb.getPDIDataObject();
394 int result = so.replaceObject(ifcb.getDestinationStore(), ifcb.getUID(), ifcb.getSummary(), ifcb.getStringRepresentation("text/calendar"), ifcb.getDtStart(), ifcb.getDtEnd());
395 return getSyncItemFromId(arg0.getKey());
396 } catch (Exception ex) {
397 log.throwing(this.getClass().getName(), "updateSyncItem", ex);
398 throw new SyncSourceException(ex);
402 public SyncItem addSyncItem(SyncItem arg0) throws SyncSourceException {
403 log.info("addSyncItem(" + arg0.getKey().getKeyAsString() + ")");
404 try {
405 InboundFunambolCalendarObject ifcb = initInboundProcessor(arg0);
406 String uid = (String) arg0.getKey().getKeyValue();
407 ifcb.setUid(uid);
408 ifcb.parseIntoObjectFromString(null, arg0.getType());
409 net.fortuna.ical4j.model.Calendar cal = (net.fortuna.ical4j.model.Calendar) ifcb.getPDIDataObject();
410 String newUID = so.addObject(ifcb.getDestinationStore(), ifcb.getUID(), ifcb.getSummary(), ifcb.getStringRepresentation("text/calendar"), ifcb.getDtStart(), ifcb.getDtEnd());
411 return getSyncItemFromId(new SyncItemKey(newUID));
412 } catch (Exception ex) {
413 log.throwing(this.getClass().getName(), "addSyncItem", ex);
414 throw new SyncSourceException(ex);
418 public void setOperationStatus(String arg0, int arg1, SyncItemKey[] arg2) {
419 for (int i = 0; i < arg2.length; i++) {
420 SyncItemKey syncItemKey = arg2[i];
421 log.info("setOperationStatus(" + arg0 + "," + arg1 + "," + syncItemKey.getKeyAsString() + ")");
425 private char getStateForItem(SyncItemKey sik) {
426 String state = (String) itemStates.get(sik);
427 if (state == null) {
428 return SyncItemState.SYNCHRONIZED;
429 } else if (state.equals("new")) {
430 return SyncItemState.NEW;
431 } else if (state.equals("updated")) {
432 return SyncItemState.UPDATED;
434 return SyncItemState.UNKNOWN;
437 /** Create the inbound object processing class (default is
438 * InboundFunambolCalendarObject). If the administrator has their own
439 * extension of InboundFunambolCalendarObject loaded, use that instead */
440 private InboundFunambolCalendarObject initInboundProcessor(SyncItem si) throws BeanInstantiationException, BeanInitializationException, BeanException {
441 InboundFunambolCalendarObject ifcb;
442 if (connectorProperties.getProperty(Constants.INBOUND_PROCESSOR) != null) {
443 Object processor = BeanFactory.getBeanInstanceFromConfig(
444 connectorProperties.getProperty(Constants.INBOUND_PROCESSOR));
445 ifcb = (InboundFunambolCalendarObject) processor;
446 ifcb.setItem(si);
447 } else if (connectorProperties.getProperty(Constants.MIXED_MODE) != null) {
448 ifcb = new MixedCalendarTaskInbound();
449 ifcb.setItem(si);
450 } else {
451 ifcb = new InboundFunambolCalendarObject(si);
453 ifcb.setConnectorProperties(connectorProperties);
454 ifcb.setSyncContext(ctx);
455 return ifcb;
458 public void init() throws BeanInitializationException {