/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2003 - 2007 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.syncclient.sps;
import java.util.Enumeration;
import java.util.Vector;
import javax.microedition.rms.RecordStore;
import javax.microedition.pim.Event;
import javax.microedition.pim.EventList;
import javax.microedition.pim.PIMList;//CHANGE 15-03-2006 line added
import javax.microedition.pim.PIM;
import javax.microedition.pim.PIMException;
import com.funambol.syncclient.blackberry.EventCache;
import com.funambol.syncclient.blackberry.parser.EventParser;
import com.funambol.syncclient.blackberry.parser.ParserFactory;
import com.funambol.syncclient.util.PagedVector;
import com.funambol.syncclient.util.StaticDataHelper;
import net.rim.blackberry.api.pdap.BlackBerryEvent;
import net.rim.device.api.system.PersistentObject;
import net.rim.device.api.system.PersistentStore;
import net.rim.device.api.ui.component.Dialog;
import net.rim.device.api.util.StringMatch;
/**
* This class provide methods
* to access device database and Event List
*
*/
public class EventDataStore extends DataStore
{
protected static String TIMESTAMP_RECORDSTORE = "TimeStampEventRS";
protected static String EVENT_UID_RECORDSTORE = "EventsUIDRS";
private static long EVENT_KEY = 0xafd852c254393341L;
protected static PersistentObject store;
protected static Vector changes;
private PagedVector modifiedItems;
static
{
store = PersistentStore.getPersistentObject(EVENT_KEY);
if (store.getContents() == null)
{
store.setContents(new Vector());
store.commit();
}
changes = (Vector)store.getContents();
}
public EventDataStore()
{
super();
modifiedItems = null;
}
/**
* Set last timestamp in dedicate recordStore
* @param lastTimestamp
* @throws DataAccessException
*/
public void setLastTimestamp(long lastTimestamp) throws DataAccessException
{
RecordStore recordStore = null;
try
{
recordStore = RecordStore.openRecordStore(TIMESTAMP_RECORDSTORE, true);
int numRecords = recordStore.getNumRecords();
String recordValue = String.valueOf(lastTimestamp);
if (numRecords == 1)
{
recordStore.setRecord(1, recordValue.getBytes(), 0, (recordValue.getBytes()).length);
}
else
{
recordStore.addRecord(recordValue.getBytes(), 0, (recordValue.getBytes()).length);
}
}
catch (Exception e)
{
StaticDataHelper.log("error:" + e.getMessage());
throw new DataAccessException(e.getMessage());
}
finally
{
if (recordStore != null)
{
try
{
recordStore.closeRecordStore();
recordStore = null;
}
catch (Exception e)
{
throw new DataAccessException(e.getMessage());
}
}
}
}
/**
* @return last timestamp from dedicate recordstore
* @throws DataAccessException
*/
public long getLastTimestamp() throws DataAccessException
{
RecordStore recordStore = null;
long lastTimestamp = 0;
try
{
recordStore = RecordStore.openRecordStore(TIMESTAMP_RECORDSTORE, true);
int numRecords = recordStore.getNumRecords();
if (numRecords == 0)
{
lastTimestamp = 0;
}
else
{
lastTimestamp = Long.parseLong(new String(recordStore.getRecord(1)));
}
}
catch (Exception e)
{
StaticDataHelper.log("error:" + e.getMessage());
throw new DataAccessException(e.getMessage());
}
finally
{
if (recordStore != null)
{
try
{
recordStore.closeRecordStore();
recordStore = null;
}
catch (Exception e)
{
throw new DataAccessException(e.getMessage());
}
}
}
return lastTimestamp;
}
/**
* If a record exists in the store, update it;
* if the record doesn't exist in the store, add it
*
* @param record Record to be added or updated
* @throws DataAccessException
*/
public Record setRecord(Record record, boolean modify) throws DataAccessException {
try {
EventList list = (EventList)PIM.getInstance().openPIMList(PIM.EVENT_LIST, PIM.READ_WRITE);
Event event = getEvent(record.getKey(), list, modify);
if (event == null) {
Dialog.inform("Event is null.");
return null;
}
String content = fixTag(record.getUid());
EventParser parser = ParserFactory.getParserInstance(list, event, modify);
parser.parseEvent(content);
event.commit();
String uid = event.getString(Event.UID, 0);
// Save the event data in cache
EventCache cache = new EventCache();
cache.put(uid, parser.getCacheData());
cache.save();
// Set the record key
record.setKey(uid);
list.close();
}
catch (Exception e) {
StaticDataHelper.log("[DEBUG]Exception in EventDataStore.setRecord(): " + e.toString());
//e.printStackTrace();
throw new DataAccessException(e.getMessage());
}
return record;
}
/**
* Obtains or creates an Event object depending on the passed modify option
*
* @param key A string representing the event UID
* @param list The events list of the device
* @param modify Boolean flag that determines if the event needs to be created
* or searched for and obtained from the device's events list
*
* @return A new event object if the modify flag was set to false, otherwise
* searches for an event from the events list with a key matching
* an existing UID and returns that
*
* @throws Exception FIXME
*/
private Event getEvent(String key, EventList list, boolean modify) throws Exception {
if (!modify) {
return list.createEvent();
} else {
Enumeration enumeration = list.items(key);
while (enumeration.hasMoreElements()) {
Event event = (Event)enumeration.nextElement();
if (event.getString(Event.UID, 0).equals(key))
return event;
}
return null;
}
}
/**
* Sync4j escapes <. This method replaces.
* @param content
* @return
*/
private String fixTag(String content)
{
StringBuffer contentBuffer = new StringBuffer(content);
StringMatch matcher = new StringMatch("<");
int matchOffset = matcher.indexOf(contentBuffer, 0);
while (matchOffset != -1)
{
contentBuffer.delete(matchOffset,matchOffset+4);
contentBuffer.insert(matchOffset,"<");
matchOffset = matcher.indexOf(contentBuffer, 0);
}
return contentBuffer.toString();
}
/**
* Delete a record from the event database.
* @param record
* @throws DataAccessException
*/
public void deleteRecord(Record record) throws DataAccessException
{
try
{
EventList list = (EventList)PIM.getInstance()
.openPIMList(PIM.EVENT_LIST, PIM.READ_WRITE);
Event event = getEvent(record.getKey(), list, true);
if(event != null)
list.removeEvent(event);
list.close();
}
catch(Exception expn)
{
throw new DataAccessException(expn);
}
}
/**
* Rules to add to the datastore
* a) If there is already a record with state 'N' and 'U' is received, don't update.
* b) If there is already a record with state 'N' and 'D' is received, remove the entry
* c) If there is a record with state 'U' and 'U' is got, ignore it
* d) If there is a record with state 'U' and 'D' is got, replace its state with 'D'
*/
public void addRecord(String uid, char state, Event event) throws Exception
{
String value = uid + "|" + state;
/**
* In case of a deleted record, the information won't be
* available during the synchronization.
* ??? hence it is store along with the update information ???
* In case of an updated record, this is available
* in the event database, so that it is not stored to save space
*/
if (state == RECORD_STATE_DELETED)
{
EventList list = (EventList)PIM.getInstance()
.openPIMList(PIM.EVENT_LIST, PIM.READ_WRITE);
String data = getEventString(list, event);
list.close();
value += "|" + data;
}
int size = changes.size();//the number of components in this vector
String data = "";
boolean dataSet = false;
if (changes.contains(value))
{
return;
}
for (int i = size-1; i >= 0; --i)
{
data = (String)changes.elementAt(i);
String dataUID = data.substring(0, data.indexOf("|"));
char dataState = data.substring(data.indexOf("|")+1).charAt(0);
if(!dataUID.equals(uid)) {
continue;
} else if(dataState == RECORD_STATE_NEW && state == RECORD_STATE_UPDATED) {
dataSet = true;
} else if (dataState == RECORD_STATE_NEW && state == RECORD_STATE_DELETED) {
changes.removeElement(data);
i = i-1;
dataSet = true;
}
else if(dataState == RECORD_STATE_UPDATED && state == RECORD_STATE_DELETED) {
changes.setElementAt(value,i);
changes.removeElement(data);
dataSet = true;
}
}
if(!dataSet) {
changes.addElement(value);
}
store.commit();
}
/**
* return no deleted records from device recordstore
*
* @return find records
* @throws DataAccessException
*/
public Vector getNoDeletedRecords() throws DataAccessException
{
//CHANGE 06-04-2006
return null;
/*
Enumeration enumeration = null ;
EventList list = null ;
Vector noDeletedRecords = null ;
Event event = null ;
Record record = null ;
try
{
noDeletedRecords = new Vector();
list = (EventList)PIM.getInstance().openPIMList(PIM.EVENT_LIST, PIM.READ_WRITE);
enumeration = list.items();
while (enumeration.hasMoreElements())
{
event = (Event)enumeration.nextElement();
record = new Record(event.getString(event.UID,0), RECORD_STATE_UNSIGNED, getEventString(list, event));
noDeletedRecords.addElement(record);
}
list.close();
return noDeletedRecords;
}
catch(Exception e)
{
e.printStackTrace();
throw new DataAccessException(e);
}
*/
}
/**
* returns a given event as an xml string
* @param uid
* @return
* @throws DataAccessException
*/
private String getEventAsString(String uid) throws DataAccessException
{
try
{
String contanctAsString = null;
EventList list = (EventList)PIM.getInstance()
.openPIMList(PIM.EVENT_LIST, PIM.READ_WRITE);
Event event = getEvent(uid, list, true);
contanctAsString = getEventString(list, event);
list.close();
return contanctAsString;
}
catch (Throwable e)//Exception
{
StaticDataHelper.log(">>> EXCEPTION in EventDataStore.getEventAsString(String) --> " + e.toString());
e.printStackTrace();
throw new DataAccessException("Error in getting event");
}
}
/**
* Converts one event information from a
* BlackBerry calendar event list into a String
*
* @param String: uid [event UID]
* @param EventList: event list
* @param Event: event.
* @return String: returns null id event object is null else the method returns
* event information parsed to string.
* @throws Exception,PIMException
*/
private String getEventString(EventList list, Event event) throws Exception,
PIMException
{
if (event == null) {
Dialog.inform("Event is null.");
return null;
}
EventParser parser = ParserFactory.getParserInstance(list, event, true);
// Retrieve the event from cache and pass its data
// to the parser.
EventCache cache = new EventCache();
String uid = event.getString(Event.UID, 0);
parser.setCacheData(cache.get(uid));
// Format the event.
String data = parser.toString(event);
return data;
}
/**
* execute init recordstore operations
*/
public void startDSOperations()
{
if (pimList != null)
{
try
{
pimList.close();
}
catch (Exception e)
{
StaticDataHelper.log("PIM database error: " + e.getMessage());
e.printStackTrace();
return;
}
items = null;
}
try
{
pimList = (EventList)PIM.getInstance()
.openPIMList(PIM.EVENT_LIST, PIM.READ_WRITE);
items = pimList.items();
}
catch (Exception e)
{
StaticDataHelper.log("PIM database error: " + e.getMessage());
e.printStackTrace();
return;
}
}
/**
* execute commit recordstore operations
* remove records signed as DELETED 'D'
* mark UNSIGNED ' ' records signed as NEW 'N' and UPDATED 'U'
*
* @throws DataAccessException
*
*/
public void commitDSOperations() throws DataAccessException {
changes.removeAllElements();
store.commit();
}
public long getNextKey()
{
//found original comment: "da implementare"
return 0;
}
/**
* return no deleted records from device recordstore
*
* @return records found
*
* @throws DataAccessException
**/
public boolean getNextRecords(Vector v) throws DataAccessException
{
boolean moreElements = true;
Event event = null;
Record record = null;
try
{
for (int i = 0; i < page; i++)
{
moreElements = items.hasMoreElements();
if (!moreElements)
{
break;
}
event = (Event)items.nextElement();
record = new Record(event.getString(event.UID,0), RECORD_STATE_UNSIGNED, getEventString((EventList)pimList, event));
v.addElement(record);
}
return moreElements;
}
catch (Throwable e)//Exception
{
StaticDataHelper.log("Exception in EventDataStore.getNextRecords(Vector) -->\n" + e.toString());
e.printStackTrace();
throw new DataAccessException(e);
}
}
/**
* Prepares a vector of records representing the
* items modified on the device starting from
* the calendar events in the device's store. Former
* these records were prepared sorted by modification
* state ('N', 'R', 'D'), but now they are added
* all together to the vector passed. The 'D'
* status is used to differentiate between Replaced
* and New status, to avoid to create a record
* consisting of the information payload too where
* you don't need such information to simply give
* the command to delete an item
*
* @param v The reference to a Vector to be modified
* by adding Record objects to it
*
* @param sep A separation character ('|') preceding
* the character representing the state
* of the modification ('N'ew, 'R'eplaced,
* 'D'eleted). Former the status character
* instead of the separator was passed, but
* now you need a placeholder to determine
* the exact position of the status character
* in the record string coming from the calendar
* events store.
* Starting from the information contained in
* a {@code String} record in the calendar events
* store, a new {@code Record} record for each item
* modified on the device is created
*
* @return {@code true} if {@code <Final/>} command from
* server not reached (there are more elements to be
* processed)
*
* @throws DataAccessException
*/
public boolean getNextRecords(Vector v, char sep) throws DataAccessException {
boolean moreElements = true;
if (modifiedItems == null) {
/*
* the persistent object of the device's store containing
* the Vector with the changes occurred on the device
*/
store = PersistentStore.getPersistentObject(EVENT_KEY);
if (store.getContents() == null) {
store.setContents(new Vector(MAX_ITEM_NUMBER));
store.commit();
}
/*
* the Vector containing the changes occurred on the device.
* It is treated in predefined-size pages to avoid sending
* to the server in the SyncML message too many items to be
* modified
*/
modifiedItems = new PagedVector((Vector)store.getContents(), MAX_ITEM_NUMBER);
}
Vector ids = new Vector();
moreElements = modifiedItems.getNextPage(ids);
int size = ids.size();
Record rec = null;
for (int i = 0; i < size; i++) {
String record = (String)ids.elementAt(i);
//sep is always the '|' character passed to this method
int idx = record.indexOf(sep);
if (idx != -1) {
String uid = record.substring(0, idx);
/**
* The character ('N', 'D', 'R')
* immediately following the separator '|'
*/
char state = record.charAt(idx+1);
try {
if (state == RECORD_STATE_DELETED)
rec = new Record(record);
else
rec = new Record(uid, state, getEventAsString(uid));
v.addElement(rec);
}
catch (DataAccessException e) {
StaticDataHelper.log(">>> EXCEPTION in EventDataStore.getNextRecords(Vector, char) --> " + e.toString());
throw new DataAccessException("Error getting next record" + e.getMessage());
}
}
else {
// Should never happen
StaticDataHelper.log("Invalid record:" + record);
}
}
return moreElements;
}
/**
* Resets the internal cursor of the paged vector
*
* @see PagedVector#reset()
*/
public void resetModificationCursor() {
modifiedItems.reset();
}
public void appendData(String data, long key)
{ }
}