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
.*;
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
;
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
;
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;
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;
68 * Sets up the calendar sync source instance
69 * @param syncContext the syncContext to use
70 * @throws SyncSourceException in case of any error
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
);
83 if (!stdir
.exists()) {
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
;
92 long curTime
= new java
.util
.Date().getTime();
93 fh
= new FileHandler(stdir
.toString() + File
.separatorChar
+ "connector-" + curTime
+ ".html");
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();
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) {
119 } catch (Exception e
) {
120 log
.throwing(this.getClass().getName(), "beginSync", e
);
121 throw new SyncSourceException(e
);
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
);
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
);
172 icalStoreProperties
.setProperty(StoreConstants
.PROPERTY_SERVER
,
175 icalStoreProperties
.setProperty(StoreConstants
.PROPERTY_STORE_LOCATION
, storeDir
);
176 if (connectorProperties
.getProperty(Constants
.SOURCE_IS_TASK
) != null) {
177 storeopts
+= MultipleSourceICalendarObjectStore
.OPTION_TODO
;
181 so
= new MultipleSourceICalendarObjectStore(stdir
.toString(), storeopts
);
182 so
.setProperties(icalStoreProperties
);
183 so
.loadSourcesFromProps();
185 log
.fine("Reusing existing object store");
188 so
.connect_Base64(creds
);
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
204 public void endSync() throws SyncSourceException
{
207 so
.printDebugReport();
210 oneSyncLock
.release();
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
];
228 for (int i
= 0; i
< newKeys
.length
; i
++) {
229 allKnownKeys
[curKey
] = newKeys
[i
];
232 for (int i
= 0; i
< updatedKeys
.length
; i
++) {
233 allKnownKeys
[curKey
] = updatedKeys
[i
];
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)
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
);
256 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
257 // #[regen=yes,id=DCE.EE4A388C-A94E-B384-EC8A-2E349EACBA40]
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
);
267 itemStates
.put(sk
, "new");
272 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
273 // #[regen=yes,id=DCE.FF60A355-292D-B905-DF53-B818D850314B]
275 public SyncItem
getSyncItemFromId(SyncItemKey syncItemKey
) throws SyncSourceException
{
276 String uid
= syncItemKey
.getKeyAsString();
277 log
.info("getSyncItemFromId(" + uid
+ ")");
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());
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]
295 public void removeSyncItem(SyncItemKey syncItemKey
, Timestamp time
, boolean softDelete
) throws SyncSourceException
{
296 log
.info("removeSyncItem(" + syncItemKey
.getKeyAsString() + ")");
298 so
.deleteObject(syncItemKey
.getKeyAsString());
299 } catch (Exception ex
) {
300 log
.throwing(this.getClass().getName(), "removeSyncItem", ex
);
301 throw new SyncSourceException(ex
);
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() + ")");
314 InboundFunambolCalendarObject ifcb
= initInboundProcessor(syncItem
);
315 String uid
= (String
) syncItem
.getKey().getKeyValue();
317 ifcb
.parseIntoObjectFromString(null, syncItem
.getType());
318 String summary
= ifcb
.getSummary();
319 long dtstart
= ifcb
.getDtStart();
320 long dtend
= ifcb
.getDtEnd();
322 dtstart
= dtstart
/ 1000;
325 dtend
= dtend
/ 1000;
327 String match
= so
.searchUids(summary
, dtstart
, dtend
);
329 log
.info("Match found: " + match
);
330 SyncItemKey
[] ma
= {new SyncItemKey(match
)};
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]
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
);
361 itemStates
.put(sk
, "updated");
366 // <editor-fold defaultstate="collapsed" desc=" UML Marker ">
367 // #[regen=yes,regenBody=yes,id=DCE.FE19692D-9043-AB64-AA5F-8E43328CD316]
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]
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() + ")");
383 InboundFunambolCalendarObject ifcb
= initInboundProcessor(arg0
);
384 String uid
= (String
) arg0
.getKey().getKeyValue();
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() + ")");
405 InboundFunambolCalendarObject ifcb
= initInboundProcessor(arg0
);
406 String uid
= (String
) arg0
.getKey().getKeyValue();
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
);
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
;
447 } else if (connectorProperties
.getProperty(Constants
.MIXED_MODE
) != null) {
448 ifcb
= new MixedCalendarTaskInbound();
451 ifcb
= new InboundFunambolCalendarObject(si
);
453 ifcb
.setConnectorProperties(connectorProperties
);
454 ifcb
.setSyncContext(ctx
);
458 public void init() throws BeanInitializationException
{