package org.bbssh.model; import java.io.EOFException; import java.util.Vector; import net.rim.device.api.i18n.Locale; import net.rim.device.api.synchronization.SyncCollection; import net.rim.device.api.synchronization.SyncConverter; import net.rim.device.api.synchronization.SyncObject; import net.rim.device.api.system.CodeModuleManager; import net.rim.device.api.system.CodeSigningKey; import net.rim.device.api.system.ControlledAccess; import net.rim.device.api.system.PersistentObject; import net.rim.device.api.system.PersistentStore; import net.rim.device.api.util.DataBuffer; import org.bbssh.io.SyncBuffer; import org.bbssh.util.Logger; /** * Class that can be registered at app startup as a SyncCollection provider. Does most of the work of maintaining * compatibility for both synchronization and version-proof data serialization. * * @author marc */ abstract public class DefaultSyncCollection implements SyncCollection, SyncConverter { private Vector internalList = new Vector(); private boolean loaded = false; /** * Retrieves the vector used to manage data for this collection. Even internally any access to the wrapped member * must use this accessor in order to ensure that it's loaded. * * @return */ protected final synchronized Vector getDataVector() { if (!loaded) { loadData(); } return internalList; } public final boolean addSyncObject(SyncObject object) { getDataVector().addElement(object); return true; } public final SyncObject getSyncObject(int uid) { Vector dv = getDataVector(); // using the getter ensures that it loads. int max = dv.size(); for (int x = 0; x < max; x++) { if (uid == ((SyncObject) dv.elementAt(x)).getUID()) { return (SyncObject) dv.elementAt(x); } } return null; } public final SyncObject[] getSyncObjects() { Vector dv = getDataVector(); // using the getter ensures that it loads. int len = dv.size(); SyncObject[] ret = new SyncObject[len]; for (int x = 0; x < len; x++) { ret[x] = (SyncObject) dv.elementAt(x); } return ret; } // This seems to be invoked by the framework when restoring a given sync collection from a backup, // prior to performing the restore. public final boolean removeAllSyncObjects() { getDataVector().removeAllElements(); return true; } public final boolean removeSyncObject(SyncObject object) { return getDataVector().removeElement(object); } public final boolean updateSyncObject(SyncObject oldObject, SyncObject newObject) { Vector dv = getDataVector(); // using the getter ensures that it loads. int idx = dv.indexOf(oldObject); if (idx == -1) { return false; } dv.setElementAt(newObject, idx); return true; } public final void beginTransaction() { loadData(); } public final void endTransaction() { commitData(); } public final void clearSyncObjectDirty(SyncObject object) { Vector dv = getDataVector(); // using the getter ensures that it loads. int max = dv.size(); for (int x = 0; x < max; x++) { ((DataObject) dv.elementAt(x)).setSyncStateDirty(false); } } public String getSyncName(Locale locale) { // @todo figure out how to actually implement this with localization return getSyncName(); } public final int getSyncObjectCount() { return getDataVector().size(); } public final boolean isSyncObjectDirty(SyncObject object) { if (object instanceof DataObject) { return ((DataObject) object).isSyncStateDirty(); } return false; } public final void setSyncObjectDirty(SyncObject object) { if (object instanceof DataObject) { ((DataObject) object).setSyncStateDirty(true); } } public final boolean convert(SyncObject object, DataBuffer buffer, int version) { return convertImpl(object, new SyncBuffer(buffer), version); } public final SyncObject convert(DataBuffer data, int version, int UID) { return convertImpl(new SyncBuffer(data), version, UID, false); } protected abstract SyncObject convertImpl(SyncBuffer buffer, int version, int uID, boolean syncDirty); protected abstract boolean convertImpl(SyncObject object, SyncBuffer buffer, int version); /* * (non-Javadoc) * * @see net.rim.device.api.synchronization.SyncCollection#getSyncConverter() */ public final SyncConverter getSyncConverter() { return this; } PersistentObject persistent; public synchronized final PersistentObject getPersistentObject() { if (persistent == null) { persistent = PersistentStore.getPersistentObject(getPersistentStoreId()); } return persistent; } /** * This will be invoked when loadData finishes execution, allowing you to perform any customizations in your * subclass. * * @param numLoaded * number of records loaded */ protected void dataLoadComplete(int numLoaded) { } /** * Loads data from child class's persistent store, relying on implementation to parse the data. This will load * blindly, even if data is already loaded. Invoke this from your constructor */ public void loadData() { loadData(true); } abstract public boolean isSecureStoreRequired(); /** * loads data from persistent store, relying on implementation to parse data. Invoked internally when we on-demand * load data; or when the synchronization framework invokes beginTransaction. * * @param internal */ public synchronized final void loadData(boolean internal) { if (loaded) return; PersistentObject per = getPersistentObject(); Vector v = null; // even if it's required, there's no guarantee that the data was previously saved secured // so we neeed to handle both scenarios. if (isSecureStoreRequired()) { CodeSigningKey key = getSigningKey(); if (key == null) { Logger.error("Could not load data from secured store - failed to obtain signing key."); } else { try { v = (Vector) per.getContents(key); } catch (Throwable t) { Logger.error("Could not load data from secured store: " + t.getMessage() + " " + t); } } } if (v == null) { Logger.info("Attempting unsecure load."); v = (Vector) per.getContents(); } byte[] rawData; if (v == null || v.elementAt(0) == null) { v = new Vector(); DataBuffer b = new DataBuffer(4, true); b.writeInt(0);// length rawData = b.getArray(); v.addElement(rawData); per.setContents(v); per.commit(); } else { rawData = (byte[]) v.elementAt(0); } DataBuffer d = new DataBuffer(rawData, 0, rawData.length, true); internalList.removeAllElements(); try { int count = d.readInt(); // @todo int fieldCount = count >> 16; for (int x = 0; x < count; x++) { // @todo - pull up syncdirty handling // syncDiry = readBoolean internalList.addElement(convertImpl(new SyncBuffer(d), d.readInt(), d.readInt(), d.readBoolean())); // lastObj.setSyncDirty(syncDirty) } } catch (EOFException e) { Logger.error("Reached unexpected EOF while loading connection data."); } loaded = true; dataLoadComplete(internalList.size()); } private CodeSigningKey getSigningKey() { int moduleHandle = CodeModuleManager.getModuleHandle("BBSSH_Common"); return CodeSigningKey.get(moduleHandle, "NOET"); } /** * Invoke this method at any time to save data to persistent store. Override it at your own risk. */ public void commitData() { commitData(true); } /** * Invoked to commit data to eprsistent store or to sync/backup. * * @param internal */ private synchronized final void commitData(boolean internal) { if (internalList == null) { Logger.error("Unexpected null internalList in commitData for " + this.getSyncName()); return; } Vector v = new Vector(); DataBuffer b = new DataBuffer(true); int max = internalList.size(); // @TODO Hm - if we have a failure in writing one of thjese records, // our record count will be wrong... b.writeInt(max);// number of records for (int x = 0; x < max; x++) { SyncObject o = (SyncObject) internalList.elementAt(x); if (internal) { // Additional header when we're writing this data to persistent store. b.writeInt(getSyncVersion()); b.writeInt(o.getUID()); if (o instanceof DataObject) { b.writeBoolean(((DataObject) o).isSyncStateDirty()); } } convertImpl(o, new SyncBuffer(b), getSyncVersion()); } v.addElement(b.getArray()); PersistentObject per = getPersistentObject(); boolean saved = false; if (isSecureStoreRequired()) { CodeSigningKey key = getSigningKey(); if (key == null) { Logger.error("Could not save data in secured store - failed to obtain signing key."); } else { per.setContents(new ControlledAccess(v, getSigningKey())); saved = true; } } if (!saved) { Logger.info("Saving data insecure."); per.setContents(v); } per.commit(); } public boolean isLoaded() { return this.loaded; } public abstract long getPersistentStoreId(); /** * In the event that a purge of data is being performed, this is invoke dby the framework. At ime of invocation, all * persistent values have been removed. Your override must re-set any internal defaults as requierd. Do not override * if you have no defaults to reset. */ public void resetDefaults() { } /** * s This is invoked when the persistent store is being cleared and recreated. Use this to clear any internal state, * but do NOT reset any defaults - this will be invoked prior to repopulating data as well as when a purge occurs. * Do not override if you have no internal state data. */ public void resetState() { } public final void purgePersistentContent() { internalList.removeAllElements(); // Delete and recreate our persistent store PersistentStore.destroyPersistentObject(getPersistentStoreId()); // Force reinitialize of persistent store. persistent = PersistentStore.getPersistentObject(getPersistentStoreId()); // Allow derived classes to do their own cleanup and state reset. resetState(); resetDefaults(); commitData(); } }