// SyncList.java // MIT OKI Filing Demo // S.Fraize July 2002 // ?bug: if remove local entry, ghost created -- if them remove // REMOTE entry, ghost is not removed. However, removing // something else, which triggers a rescan, fixes it, so // we're just not triggering a rescan. BTW, same applies // in reverse direction. // ?BUG 2002-08-20 15:31.50 Tuesday zippy // if you remove a remote entry (ghost created), then remove local entry, remote // ghost is not removed (the reverse procedure is working however:remove local, then remote) import java.util.*; import java.io.*; import javax.swing.event.ChangeListener; import javax.swing.event.ChangeEvent; import org.okip.service.filing.api.*; import org.okip.service.filing.impl.*; import org.okip.service.shared.api.Agent; import org.okip.util.agents.RemoteAgent; public class SyncList implements Runnable { Logger log = new Logger(SyncList.class); public static final int STATUS_UNKNOWN = 0; // unknown status public static final int STATUS_CURRENT = 1; // everything is copacetic public static final int STATUS_NEW_LOCAL = 2; // new local entry, no remote data yet public static final int STATUS_CHANGED_LOCAL = 3; // local version more current than remote version public static final int STATUS_NEW_REMOTE = 4; // new remote entry, no local data yet public static final int STATUS_CHANGED_REMOTE = 5; // remote version more current than local version public static final int STATUS_CONFLICT = 6; // versions in conflict -- both changed since last sync public static final int SYNC_ALL = 0; public static final int SYNC_UPLOADS_ONLY = 1; public static final int SYNC_DOWNLOADS_ONLY = 2; int opposingStatus[] = { STATUS_UNKNOWN, STATUS_CURRENT, STATUS_NEW_REMOTE, STATUS_CHANGED_REMOTE, STATUS_NEW_LOCAL, STATUS_CHANGED_LOCAL, STATUS_CONFLICT }; static final String PropsFileName = ".cabsync"; static final String LockName = ".~cablock~"; static final String IncomingFileExtension = "~incoming~"; HashMap entries = new HashMap(); Cabinet localCabinet; Cabinet remoteCabinet; SyncList remoteList; Properties props = new Properties(); ByteStore propsBS; long bytesIncoming; long bytesOutgoing; boolean isLocalList; boolean importAll = true; boolean takeAction = true; int pollInterval = 0; boolean polling = false; boolean syncRunning = false; boolean refreshRunning = false; long lastRefresh = 0; boolean autoSync = false; /* * are ghosts considered material (visible) list entries? * Effects ONLY change event notifications. */ boolean visibleGhosts = false; public SyncList(Cabinet localCab, Cabinet remoteCab) { this(localCab, remoteCab, null); } /* * create the remote mirror for a given list */ private SyncList(SyncList local) { this.localCabinet = local.remoteCabinet; //this.remoteCabinet = local.localCabinet; this.remoteList = local; this.isLocalList = false; log.copyStates(local.log); try { log.setPrefix(localCabinet.getName()); } catch (FilingException e) { log.errout(e); log.setPrefix(e.getMessage()); } if (log.d) log.debug("new remote " + this); } public SyncList(Cabinet localCab, Cabinet remoteCab, Logger l) { this(localCab, remoteCab, l.action, l.v, l.d); } public SyncList(Cabinet localCab, Cabinet remoteCab, boolean ta, boolean v, boolean d) { setStates(ta, v, d); try { log.setPrefix(localCab.getName()); } catch (FilingException e) { log.errout(e); log.setPrefix(e.getMessage()); } if (log.d) log.debug("new SyncList local=" + localCab + ", remote=" + remoteCab); this.localCabinet = localCab; this.remoteCabinet = remoteCab; this.isLocalList = true; this.remoteList = new SyncList(this); refresh(); } /** * Set a polling interval. This is the minimum * number of seconds that will elapse between * automatic calls to refresh. * * @param seconds seconds between calls to refresh * a value of 0 seconds means stop polling */ public void setPollInterval(int seconds) { this.pollInterval = seconds; polling = pollInterval > 0 ? true : false; if (log.v) log.verbose("poll interval set to " + seconds); if (polling) startPolling(); } public int getPollInterval() { return this.pollInterval; } void startPolling() { new Thread(this, "SyncList refresh poll").start(); } /* * For running the optional polling thread. */ public void run() { if (log.d) log.debug("poll thread started"); long sleepInterval = pollInterval * 1000; while (polling) { try { Thread.sleep(sleepInterval); } catch (InterruptedException e) { polling = false; break; } if (!polling) break; sleepInterval = pollInterval * 1000; if (!syncRunning) { long elapsed = System.currentTimeMillis() - this.lastRefresh; if (elapsed >= sleepInterval) doAutoRefresh(); else sleepInterval -= elapsed; } } if (log.d) log.debug("poll thread exited"); } synchronized void doAutoRefresh() { if (syncRunning || refreshRunning) return; if (log.v) log.verbose("doAutoRefresh"); this.refresh(false); } public void setAutoSync(boolean tv) { this.autoSync = tv; } public boolean isAutoSync() { return this.autoSync; } public boolean isSyncRunning() { return syncRunning; } public void setVisibleGhosts(boolean tv) { visibleGhosts = tv; // Declare that a structural change has taken place so // that our listeners can know to rescan all our data. notifyChangeListeners(new SyncListChangeEvent(this, 1, 1)); remoteList.notifyChangeListeners(new SyncListChangeEvent(this, 1, 1)); } public void setImportAll(boolean tv) { if (importAll == tv) return; importAll = tv; refresh(); notifyChangeListeners(new SyncListChangeEvent(this, 1, 0)); remoteList.notifyChangeListeners(new SyncListChangeEvent(this, 1, 0)); } public boolean isImportAll() { return importAll; } private SyncEntry newGhostEntry(SyncEntry se) { SyncEntry ghost = new SyncEntry(se.getCabinetEntry()); ghost.setIsGhost(true); ghost.setStatus(STATUS_NEW_REMOTE); return ghost; } public Collection values() { return entries.values(); } public Iterator iterator() { return entries.values().iterator(); } void put(String nameKey, SyncEntry se) { entries.put(nameKey, se); } SyncEntry get(String nameKey) { return (SyncEntry) entries.get(nameKey); } Object remove(String nameKey) { return entries.remove(nameKey); } public int size() { return entries.size(); } public void setStates(boolean ta, boolean v, boolean d) { takeAction = ta; log.setVerbose(v); log.setDebug(d); } public void setStates(Logger l) { setStates(l.action, l.v, l.d); } private void setEntrySyncTime(SyncEntry se, String prop) { props.setProperty(se.getName()+".sync", prop); } private String getEntrySyncTime(SyncEntry se) { return props.getProperty(se.getName()+".sync"); } private void setEntryDoSkip(SyncEntry se, boolean doSkip) { String key = se.getName()+".skip"; if (doSkip) props.remove(key); else props.setProperty(key, "t"); } private boolean getEntryDoSkip(SyncEntry se) { String v = props.getProperty(se.getName()+".skip"); return v != null && v.equals("t"); } private void readProperties() { if (!isLocalList) throw new RuntimeException("SyncList: remote list attempted properties access"); this.propsBS = getPropsByteStore(); if (propsBS == null) log.errout("coudn't find a properties store in: " + this); else { try { InputStream in = new JavaInputStreamAdapter(propsBS.getOkiInputStream()); this.props.load(in); in.close(); if (log.d) log.debug("loaded properties from BS " + propsBS); } catch (Exception e) { log.errout(e); log.errout("failed to read properties from BS " + propsBS); } } } public void saveProperties() { if (false) return; // TODO: cull out dead entries? -- if there's no CabinetEntry // for a name, delete it. // todo: save all the sync request bits -- change this // over to actually traversing the sync list each time // instead of relying on setting the prop value at runtime // constantly? String header = " CabSync properties, entries=(" + size() + "). "; header += "\n# Local cabinet is " + getLocalCabinetName(); header += "\n# Remote cabinet is " + getRemoteCabinetName(); try { OutputStream out = new JavaOutputStreamAdapter(propsBS.getOkiOutputStream()); this.props.store(out, header); out.close(); } catch (Exception e) { log.errout(e); log.errout("Couldn't save properties store " + propsBS); } } private ByteStore getPropsByteStore() { ByteStore bs = null; try { bs = (ByteStore) localCabinet.getCabinetEntry(PropsFileName); } catch (NotFoundException e) { try { bs = localCabinet.createByteStore(PropsFileName); if (log.d) log.debug("created properties BS " + bs); } catch (Exception ex) { log.errout("failed to create properties BS " + PropsFileName); log.errout(ex); bs = null; } } catch (Exception e) { log.errout(e, "couldn't get props file"); } return bs; } public Cabinet getLocalCabinet() { return localCabinet; } public Cabinet getRemoteCabinet() { return remoteCabinet; } public String getLocalCabinetName() { if (localCabinet == null) return "<lc>"; else return getCabinetName(localCabinet); } public String getRemoteCabinetName() { if (remoteCabinet == null) return "<rc>"; else return getCabinetName(remoteCabinet); } static String getCabinetName(Cabinet cab) { String s = ""; try { Agent owner = cab.getOwner(); if (owner != null) { RemoteAgent ra = (RemoteAgent) owner.getProxy(RemoteAgent.class); if (ra != null) s += ra.getHost() + ":" + ra.getPort() + "/"; } s += cab.getID().toString(); } catch (Exception e) { s += ceName(cab); } return s; } public SyncList getRemoteList() { return remoteList; } public void setSyncRequested(SyncEntry rse, boolean doRequest) { SyncEntry se = get(rse.getName()); if (se == null) { log.errout("setSyncRequested: not in our list!: " + rse); return; } if (se.isSyncRequested() != doRequest) { se.doSetSyncRequested(doRequest); if (se.isGhost() && doRequest == false) // will end up removing the ghost refresh(); } } /* * Make sure we're set up to sync to a given * entry in a remote list. Create a ghost * in the remote list if we're not. * Only call this on a local list! */ public synchronized SyncEntry addRemote(SyncEntry remoteEntry) { if (!isLocalList) throw new RuntimeException("addRemote on remote list"); int changes = 0; int updates = 0; SyncEntry localMatch = this.get(remoteEntry.getName()); if (localMatch == null) { // if sync-all isn't set, there might not already be a ghost. changes++; localMatch = newGhostEntry(remoteEntry); localMatch.doSetSyncRequested(true); this.put(localMatch.getName(), localMatch); if (log.d) log.debug("addRemote: new ghost " + localMatch); } else if (!localMatch.isSyncRequested()) { updates++; localMatch.doSetSyncRequested(true); } if (updates > 0 || changes > 0) notifyChangeListeners(new SyncListChangeEvent(localMatch, updates, changes)); return localMatch; } public void refresh() { try { refreshRunning = true; doRefresh(false); } finally { refreshRunning = false; } // should this be checking isLocalList also? } public void refresh(boolean force) { /* * this intended to be called on either * local or remote list (e.g., there's a TableModel * somewhere monitoring them) but we only ever * want to refresh the local list. */ try { refreshRunning = true; if (isLocalList) doRefresh(force); else getRemoteList().doRefresh(force); } finally { refreshRunning = false; } } /* * refresh both cabinet listings & compaired status * Do NOT call this for a remote list - this is * a local-list method only. */ // note that the force arg does NOT at moment // refer to making use of the filing.api.Refreshable interface private synchronized void doRefresh(boolean force) { int updates = 0; int changes = 0; int tosync = 0; int remoteUpdates = 0; int remoteChanges = 0; // TODO: if there's a lock (sombodies syncing) // wait a while for it to clear or throw exception. // (we don't changes happening during rescan); if (log.d) log.debug("refresh (force=" + force + ") " + this); if (!isLocalList) { // okay, only place that might ever call this is // our hack when we get an exception in the data model... //todo:clean log.errout("REMOTE REFRESH " + this); return; } if (isLocalList) readProperties(); // just in case somebody else has been syncing here... /* * get a clear picture of the local cabinet */ if (force) entries.clear(); SyncListChangeEvent slev = rescan(); updates += slev.updates; changes += slev.changes; /* * get a clear picture of the REMOTE cabinet, unless * we're already a remote synclist. */ if (isLocalList) { slev = remoteList.rescan(); remoteUpdates += slev.updates; remoteChanges += slev.changes; } /* * Go through local cabinet, finding matches in the remote cabinet, * and compare them. */ bytesIncoming = 0; bytesOutgoing = 0; /* * If the cabinet is really locked, somebody is the in * the middle of updating it and we can't reliably * compute sync status. */ boolean locked = false; if (findLock(localCabinet) != null) { /* * We found a lock: try a couple * more times just in case it's about * to clear. */ locked = true; ByteStore lockBS = null; for (int trys = 0; trys < 3; trys++) { try { Thread.sleep(1000); } catch (InterruptedException e) {} lockBS = findLock(localCabinet); if (lockBS == null) break; } if (lockBS == null) locked = false; else { java.util.Properties lock = readLock(lockBS); //throw new LockException(readLock(lockBS), "cannot safely read locked cabinet"); log.errout(new LockException(lock, "cannot be sure of status in a locked cabinet: lock="+lock)); } } if (log.d) log.debug("refresh COMPAIRING " + this); Iterator i = this.iterator(); while (i.hasNext()) { SyncEntry localEntry = (SyncEntry) i.next(); if (localEntry.isGhost()) { if (importAll || localEntry.isSyncRequested()) { tosync++; continue; } // we're not default to import all, and // this hasn't been specifically requested, // so delete this ghost... i.remove(); if (log.d) log.debug("removed unwanted ghost " + localEntry); changes++; continue; } SyncEntry remoteMatch = (SyncEntry) remoteList.get(localEntry.getName()); if (log.d) log.debug("CMPL: " + localEntry); if (log.d) log.debug("CMPR: " + remoteMatch); int status = STATUS_UNKNOWN; // if (localEntry.isGhost()) { // // ghosts have STATUS_NEW_REMOTE by definition // continue; // } // else // everyones status will remain unknown if we're locked if (!locked) { if (remoteMatch == null) { status = STATUS_NEW_LOCAL; if (importAll) { // create the paired remote entry remoteMatch = newGhostEntry(localEntry); remoteList.put(localEntry.getName(), remoteMatch); remoteChanges++; } else if (log.d) log.debug("no remote ghost for " + localEntry); } // else if (remoteMatch.isGhost()) // continue; else { status = computeStatus(localEntry, remoteMatch); } } if (status != STATUS_CURRENT && status != STATUS_UNKNOWN) tosync++; // todo: compute byte transfers -- do in computeStatus? if (localEntry.setStatus(status)) { updates++; if (log.d) log.debug("\t localStatus=" + localEntry.getStatusName()); } if (remoteMatch != null && remoteMatch.setStatus(opposingStatus[status])) { updates++; if (log.d) log.debug("\tremoteStatus=" + remoteMatch.getStatusName()); } // TODO: keep a running entry of # of bytes to upload / download } /* * Go through REMOTE file list, looking for * any new entries we don't have locally yet. * Create ghosts for any new entries. */ if (importAll) { i = remoteList.iterator(); while (i.hasNext()) { SyncEntry remoteEntry = (SyncEntry) i.next(); if (remoteEntry.isGhost()) continue; SyncEntry localMatch = (SyncEntry) this.get(remoteEntry.getName()); if (localMatch == null) { // no match found, create new sync entry localMatch = newGhostEntry(remoteEntry); this.put(remoteEntry.getName(), localMatch); changes++; if (remoteEntry.setStatus(STATUS_NEW_LOCAL)) updates++; } } } if (updates > 0 || changes > 0 || remoteUpdates > 0 || remoteChanges > 0 || tosync > 0) { // if anything changes on either side, send a change event to // everyone just in case. if (log.d) log.debug(changes + " changes, " + updates + " updates, " + tosync + " tosync, notifying listeners"); notifyChangeListeners(new SyncListChangeEvent(this, updates, changes, tosync)); remoteList.notifyChangeListeners(new SyncListChangeEvent(this, remoteUpdates, remoteChanges, tosync)); } this.lastRefresh = System.currentTimeMillis(); //return changes + updates; } /** * Produce an accurate picture of the current cabinet. * @return the # of significant changes -- meaning * the addition or deletion of an item (as opposed to * just a status change). */ private synchronized SyncListChangeEvent rescan() { SyncListChangeEvent slev = new SyncListChangeEvent(this, 0, 0); if (log.d) log.debug("rescan: " + localCabinet); if (localCabinet instanceof Refreshable) { // If this implemntation supports some kind of // cabinet meta-data caching, trigger a refresh now as we're about // to scan the entire directory. try { ((Refreshable)localCabinet).refresh(); } catch (FilingException e) { log.errout(e); return slev; } } /* * first, detect any deletions (SyncEntry's who's * CabinetEntry no longer exists) */ Iterator i = this.iterator(); while (i.hasNext()) { SyncEntry se = (SyncEntry) i.next(); // don't bother with ghosts -- they don't reflect // a local CabinetEntry to check for (they point // to a remote entry) if (se.isGhost()) continue; if (!se.exists()) { SyncEntry remoteMatch = remoteList.get(se.getName()); if (se.isSyncRequested()) { // an item was deleted out from under us, but // it's sync bit was set, so we still want to honor // that -- so we'll need to turn it into a ghost. slev.updates++; se.makeGhostOf(remoteMatch); } else { slev.changes++; i.remove(); if (log.d) log.debug("Removed " + se); if (remoteMatch != null && remoteMatch.isGhost()) { remoteList.remove(se.getName()); if (log.d) log.debug("cleared remote ghost " + remoteMatch); } } } else if (log.d) log.debug(" exists " + se); } /* * now, try and get the local cabinet list */ try { i = localCabinet.entries(); //todo: if entire directory has been deleted, // we'll start getting NullPointerExceptions out // of LfsCabient... } catch (FilingException ex) { log.errout(ex); return slev; } /* * Search the cabinet for any new entries, and * create new SyncEntry's as appropriate. * First time this is called, all will be new. */ java.text.DateFormat dateFormatter = new java.text.SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US); // This date format is literally taken from Date.toString(), which is // what's being used to generate the output. // todo: when move to java 1.4 Preferences, create a date format that // inclues milliseconds and use it for both input & output. while (i.hasNext()) { CabinetEntry ce = (CabinetEntry) i.next(); String ceName; try { ceName = ce.getName(); } catch (FilingException e) { log.errout(e); continue; } if (ceName.equals(PropsFileName) || ceName.endsWith(IncomingFileExtension)) // ignore special files continue; // ignore windows temp files (~xxx) -- todo: make configurable option // ignore likely unix tmp files (xxx~) // ignore likely config files (.xxx) // on mac: ignore .DS_Store files char ic = ceName.charAt(0); if (ic == '~' || ic == '.' || ceName.endsWith("~")) continue; SyncEntry se = this.get(ceName); if (se != null) { /* * We already have a sync entry for this cabinet entry * Nothing to do in that case, unless something * exceptional happened. */ if (se.isGhost()) { // somehow a ghost was back-filled -- somebody // else must be syncing out from under us. // Convert the ghost to a real SyncEntry. /* * if ghosts always shown, this only an update * otherwise if ghosts not shown, this a structural change. */ if (visibleGhosts) slev.updates++; else slev.changes++; // if (log.d) log.debug("GHOST FILL " + se); se.setCabinetEntry(ce); se.setIsGhost(false); se.setStatus(STATUS_UNKNOWN); } else if (log.d) log.debug("confirm " + se); } else { /* * There is no SyncEntry for this CabinetEntry -- create one */ slev.changes++; se = new SyncEntry(ce); if (this.isLocalList) { String dateStr = getEntrySyncTime(se); if (dateStr != null) //se.setLastSyncTime(Date.parse(dateStr)); try { se.setLastSyncTime(dateFormatter.parse(dateStr).getTime()); } catch (java.text.ParseException e) { log.errout(e, "bad date: [" + dateStr + "]"); } } this.put(se.getName(), se); if (log.d) log.debug(" added " + se); if (se.isCabinet()) if (log.d) log.debug("TODO: new SyncList" + se); //todo: this is where we're going to insert // the recursion -- we need to be able to // create synclists that are syncentry's // (so we'll need to subclass sl from se) // and come up with a sensible status // for a directoriy entry based on status // of sub-entries (unless it's just NEW_REMOTE), // and there are no sub-entries yet, or maybe // we want to scan all sub entries, yeah, that, // and they'll all have NEW_REMOTE status. // may want a new status code of just CHANGED // to reflect directories with varied status children. } } return slev; // do NOT want to notifyChangeListeners here } /** * THIS IS THE MEAT WHERE SYNC SEMANATICS ARE WORKED OUT. * * Compare local & remote entries, returning computed * appropriate status for the local entry. * The remoteEntry is passed as a SyncEntry just for * convenience -- we're really just comparing the CabinetEntry instances. */ protected int computeStatus(SyncEntry localEntry, SyncEntry remoteEntry) { if (localEntry.isGhost()) { if (remoteEntry.getStatus() == STATUS_CURRENT) { // when we do a sync, remote status doesn't get updated, so we do it here localEntry.setStatus(STATUS_CURRENT); return STATUS_CURRENT; } return STATUS_NEW_REMOTE; } if (remoteEntry.isGhost()) { if (localEntry.getStatus() == STATUS_CURRENT) { // when we do a sync, remote status doesn't get updated, so we do it here remoteEntry.setStatus(STATUS_CURRENT); // wait, BOGUS -- we need a new sync entry! return STATUS_CURRENT; } return STATUS_NEW_LOCAL; } try { if (localEntry.isByteStore() && remoteEntry.isByteStore()) { ByteStore localByteStore = localEntry.getByteStore(); ByteStore remoteByteStore = remoteEntry.getByteStore(); long localSyncTime = localEntry.getLastSyncTime(); long localMTime = localByteStore.getLastModifiedTime(); long remoteMTime = remoteByteStore.getLastModifiedTime(); boolean remoteChange = remoteMTime > localSyncTime; boolean localChange = localMTime > localSyncTime; if (remoteChange && localChange) return STATUS_CONFLICT; else if (localChange) return STATUS_CHANGED_LOCAL; else if (remoteChange) return STATUS_CHANGED_REMOTE; else { long ll = localByteStore.length(); long rl = remoteByteStore.length(); if (ll != rl) { log.errout("*SYNC ANOMALY: apparently current, yet sizes different: " + ll + " != " + rl); log.errout("* syncEntryLocal=" + localEntry); log.errout("*syncEntryRemote=" + remoteEntry); log.errout("* ByteStorelocal=" + localByteStore); log.errout("*ByteStoreRemote=" + remoteByteStore); return STATUS_CONFLICT; } else return STATUS_CURRENT; } } // if both are CABINETS, status is going to wind up // unknown at the moment -- eventually cabinet status // will be derived from the sub-components, so unknown // okay for now. TODO - fix return STATUS_UNKNOWN; } catch (Exception e) { log.errout("compairing " + localEntry + " " + e); return STATUS_UNKNOWN; } } protected ArrayList listeners = new ArrayList(); public void addChangeListener(ChangeListener cl) { listeners.add(cl); } public void notifyChangeListeners(Object src) { Iterator i = listeners.iterator(); while (i.hasNext()) { ChangeListener cl = (ChangeListener) i.next(); ChangeEvent event; if (src instanceof ChangeEvent) event = (ChangeEvent) src; else event = new ChangeEvent(src); cl.stateChanged(event); } } public void removeChangeListener(ChangeListener cl) { listeners.remove(listeners.indexOf(cl)); } public void removeAllChangeListeners() { listeners.clear(); } boolean cancelRequested = false; public void requestSyncCancel() { cancelRequested = true; if (log.v) log.verbose("Cancel requested"); } //TODO: okay, we want do be able to do a directional // sync anyway (all upload or all download) so split // this into directional, with less code, but run // it twice, once for each sync list? So for instance, // would only handle NEW_LOCAL & CHANGED_LOCAL. public int doSynchronize() throws FilingException { return doSynchronize(SYNC_ALL); } public synchronized int doSynchronize(int syncType) throws FilingException { int changes = 0; syncRunning = true; try { changes = doSyncType(syncType); } finally { syncRunning = false; cancelRequested = false; //if any locks were obtained, release them. releaseLocks(); } return changes; } private synchronized int doSyncType(int syncType) throws FilingException { if (!isLocalList) throw new RuntimeException("SyncList: remote list attempted sync"); if (log.v) log.verbose("Synchronizing cabinets: type="+syncType); if (log.v) log.verbose("\t (local) " + getLocalCabinet()); if (log.v) log.verbose("\t(remote) " + getRemoteCabinet()); refresh(); // always refresh before a sync int updates = 0; Iterator i = this.iterator(); while (i.hasNext()) { if (cancelRequested) break; SyncEntry localEntry = (SyncEntry) i.next(); if (!importAll && !localEntry.isSyncRequested()) continue; int s = localEntry.getStatus(); if (syncType == SYNC_UPLOADS_ONLY) { if (s == STATUS_NEW_REMOTE || s == STATUS_CHANGED_REMOTE) continue; } else if (syncType == SYNC_DOWNLOADS_ONLY) { if (s == STATUS_NEW_LOCAL || s == STATUS_CHANGED_LOCAL) continue; } else if (s == STATUS_CURRENT) continue; if (doSynchronizeEntry(localEntry)) updates++; } /* * if any locks were obtained, release them. */ releaseLocks(); if (takeAction && updates > 0) { this.saveProperties(); this.readProperties(); } if (updates > 0) refresh(); // if we've created a new entry in remote cabinet, // we'll need to see it. This is because the remote // cabinet may not be using ghosts... (why?) //if (updates > 0) remoteList.rescanNotify(); return updates; } // just in case the VM makes this available to us // on abort (it doesn't on the mac...) protected void finalize() { releaseLocks(); } /** * if any locks are held, release them. */ public void releaseLocks() { releaseCabinetLock(localCabinet); releaseCabinetLock(remoteCabinet); } private HashMap cabinetLocks = new HashMap(); /** * release a lock on cabinet c if we created it * @return true if a lock was released, false */ protected boolean releaseCabinetLock(Cabinet c) { Object lock = cabinetLocks.get(c); if (lock == null) return false; CabinetEntry lockEntry = null; try { lockEntry = c.getCabinetEntry(LockName); } catch (Exception e) { cabinetLocks.remove(c); log.errout(e, "couldn't find our lock: somebody broke it?"); return false; } try { lockEntry.delete(); } catch (FilingException e) { log.errout(e, "failed to delete our own lock!"); return false; } cabinetLocks.remove(c); if (log.v) log.verbose("Released lock on " + ceName(c) + " " + lock); return true; } /* * Cabinet locks are just a file created in the destination * directory during writes. It's currently left to the user to decide * to break an old lock (which means you should present them * the information in the current lock to make that decision). * The lock is actually obtained the first time any write is * attempted, and held to the end of the entire sync, where * any locks that were created are cleared. * * Possible future semantics: to ensure we don't leave a lock * around, all locks considered no-good after certain interval * (e.g., 5 minutes) and it's the responsibility of the * lock-creating instance to keep it current (which means * implementing our own monitored version of copying which checks * how much time has gone by for each bufferful of data sent). * It would also be great if we could make use of File.deleteOnExit * to ensure a lock file was deleted no matter what when the VM * exits. This could be done through Cabinet.createTempByteStore, * which is intended to have these semantics, if we added an * argument to the call which allowed us to specify the name of * the temp file. */ /** * Look for an existing lock on a cabinet. * @return props of lock, or null if none found */ ByteStore findLock(Cabinet c) { ByteStore lockEntry = null; try { lockEntry = (ByteStore) c.getCabinetEntry(LockName); } catch (NotFoundException e) { // no problem } catch (Exception e) { log.errout(e); } return lockEntry; } java.util.Properties readLock(Cabinet c) { return readLock(findLock(c)); } java.util.Properties readLock(ByteStore lockEntry) { java.util.Properties lock = new java.util.Properties(); if (lockEntry == null) { lock.setProperty("message", "false alarm, try again."); return null; } try { InputStream in = new JavaInputStreamAdapter(lockEntry.getOkiInputStream()); lock.load(in); in.close(); } catch (Exception e) { log.errout(e, "error reading lock"); lock.setProperty("exception", e.toString()); } return lock; } boolean obtainCabinetLock(Cabinet c) { if (cabinetLocks.containsKey(c)) { // we already have this lock -- nothing to do return true; } /* * look to see if there's an existing lock */ CabinetEntry lockEntry = findLock(c); if (lockEntry != null) { if (log.v) log.verbose("obtainCabinetLock: already locked: " + lockEntry); return false; } /* * There's no existing lock -- we can create one */ ByteStore lockBS = null; try { lockBS = c.createByteStore(LockName); } catch (FilingException e) { log.errout(e); return false; } Properties lock = new java.util.Properties(); Date now = new Date(); lock.setProperty("now", new Long(now.getTime()).toString()); lock.setProperty("date", now.toString()); try { lock.setProperty("this", c.getPath()); } catch (FilingException e) { log.errout(e); lock.setProperty("this", e.toString()); } lock.setProperty("user", System.getProperty("user.name")); String host = "[unknown host]"; try { host = java.net.InetAddress.getLocalHost().toString(); } catch (java.net.UnknownHostException e) { if (log.d) log.errout(e); } lock.setProperty("host", host); try { OutputStream os = new JavaOutputStreamAdapter(lockBS.getOkiOutputStream()); lock.store(os, "Cabinet lock on " + c); os.close(); } catch (Exception e) { log.errout(e); // if we created the lock file, yet // we failed to write handy information into it, // we still allow the lock to succeed. } cabinetLocks.put(c, lock); if (log.v) log.verbose("Created lock on " + ceName(c) + " " + lock); return true; } public boolean doSynchronizeEntry(SyncEntry localEntry) throws FilingException { SyncEntry remoteEntry = remoteList.get(localEntry.getName()); boolean wasLocalGhost = localEntry.isGhost(); boolean wasRemoteGhost = remoteEntry.isGhost(); boolean change = false; // TODO: how do we handle desired deletions? // would need to flag somehow that something was deliberately // deleted... This implies either a master source, who's // simple non-presence of an entry takes priority, or we // need a special utility to flagging this.. int syncStatus = localEntry.getStatus(); CabinetEntry newEntry; if (log.d) log.debug("doSyncEntry:" + localEntry.toString()); switch (syncStatus) { case STATUS_UNKNOWN: // skip -- print anything? break; case STATUS_CURRENT: // nothing to do. break; case STATUS_NEW_LOCAL: // create new byteStore in remote cabinet // try { newEntry = copyCabinetEntry(getRemoteCabinet(), localEntry.getCabinetEntry(), "create", "remote"); SyncEntry remoteMatch = remoteEntry; if (takeAction) { if (remoteMatch != null) { // must have found a ghost (assert isGhost) // -- bring it to life now that it has it's own data. remoteMatch.setCabinetEntry(newEntry); remoteMatch.setIsGhost(false); } setLastSyncProperty(localEntry); change = true; } // } catch (Exception e) { // log.errout(e); // } break; case STATUS_CHANGED_LOCAL: // ship bytes to remote byteStore // TODO: should do a compare so if all bytes same, don't bother cascading updates // out to everyone who's listenting for changes. Now, should we do the compare // locally or remotely? (This also applies to STATUS_CHANGED_REMOTE) // try { copyCabinetEntry(getRemoteCabinet(), localEntry.getCabinetEntry(), "copy", "remote"); if (takeAction) { setLastSyncProperty(localEntry); change = true; } // } catch (Exception e) { // log.errout(e); // } break; case STATUS_NEW_REMOTE: // in this special case (syncStats == STATUS_NEW_REMOTE) // and syncEntry.isGhost() == true, the // cabinet entry of the SyncEntry was pointed at // the REMOTE cabinet entry temporarily until we // synchronize it. CabinetEntry remoteCabEntry = localEntry.getCabinetEntry(); // create new local entry to match the new remote // try { newEntry = copyCabinetEntry(getLocalCabinet(), remoteCabEntry, "create", "local"); // restore the local SyncEntry to sanity with a real local cabinetEntry if (takeAction) { localEntry.setCabinetEntry(newEntry); localEntry.setIsGhost(false); setLastSyncProperty(localEntry); change = true; } // } catch (Exception e) { /// log.errout(e); // } break; case STATUS_CHANGED_REMOTE: // grab bytes from remote byteStore // try { CabinetEntry remoteCE = getRemoteCabinet().getCabinetEntry(localEntry.getName()); copyCabinetEntry(getLocalCabinet(), remoteCE, "copy", "local"); if (takeAction) { setLastSyncProperty(localEntry); change = true; } // } catch (Exception e) { // log.errout(e); // } break; case STATUS_CONFLICT: // inform the user... break; default: log.errout("doSynchronize: unknown status: " + syncStatus); } if (change) { localEntry.setStatus(STATUS_CURRENT); remoteEntry.setStatus(STATUS_CURRENT); /* * Saving the properties here ensures that even if another * viewer looks at this cabinet, the sync state won't * look all out of whack. */ this.saveProperties(); if (visibleGhosts) { notifyChangeListeners(localEntry); remoteList.notifyChangeListeners(remoteEntry); } else { notifyChangeListeners(new SyncListChangeEvent(localEntry, 1, wasLocalGhost?1:0)); remoteList.notifyChangeListeners(new SyncListChangeEvent(remoteEntry, 1, wasRemoteGhost?1:0)); } } return change; } private void setLastSyncProperty(SyncEntry se) { long time = System.currentTimeMillis()+2000; // we need to add 999ms to the time because we may // lose it -- the properties file doesn't currently // save times with millisecond resolution. // Then we need to add another second because sometimes // on a PC, the file time is set to a time AFTER it's // call to copy the file has returned, which I assume // is due to slow write-back caching or something. se.setLastSyncTime(time); setEntrySyncTime(se, new Date(time).toString()); } /** * @param destCabinet Cabinet we're creating the NEW CabinetEntry in * @param sourceEntry CabinetEntry we're copying * @param loc helpful string used only for verbose output (no function) * TODO: implement copy ourself so that we can progress-monitor it... InputStream in = new BufferedInputStream( new ProgressMonitorInputStream( parentComponent, "Reading " + fileName, new FileInputStream(fileName))); */ private CabinetEntry copyCabinetEntry(Cabinet destCabinet, CabinetEntry sourceEntry, String actionName, String loc) throws FilingException { if (!sourceEntry.canRead()) throw new FilingException("copyCabinetEntry: can't read source " + sourceEntry); CabinetEntry newCabinetEntry = null; // Lfs filing BUG: this test doesn't seem to be working... if (sourceEntry.getParent().equals(destCabinet)) log.errout("ack! incestous copy! cab=" + destCabinet + ", entry=" + sourceEntry); if (sourceEntry.isByteStore()) { actionOut(sourceEntry.getPath() + " -> " + destCabinet.getPath()); if (takeAction) { if (log.d) log.debug(actionName + " ByteStore '" + sourceEntry.getName() + "' in/to " + loc + " cabinet " + destCabinet.getPath()); if (!obtainCabinetLock(destCabinet)) { throw new LockException(readLock(destCabinet), "cannont copy '" + sourceEntry.getName() + "' to " + destCabinet.getPath() + ": destination cabinet is locked"); } ByteStore sourceBS = (ByteStore) sourceEntry; String sourceName = sourceBS.getName(); String tmpName = sourceName + IncomingFileExtension; /* * Go ahead and COPY THE BYTE STORE. Note that createByteStore, * when given a source bytestore argument, actually copies it. */ long srcLen = sourceBS.length(); try { newCabinetEntry = destCabinet.createByteStore(tmpName, sourceBS); /*} catch (NameCollisionException e) {*/ } catch (FilingException e) { destCabinet.getCabinetEntry(tmpName).delete(); newCabinetEntry = destCabinet.createByteStore(tmpName, sourceBS); } /* * Now do a simple verification check -- make sure the sizes * are the same. */ ByteStore newBS = (ByteStore) newCabinetEntry; long destLen = newBS.length(); if (srcLen != destLen) { throw new FilingIOException("copy failed: sizes differ:" + "\n\t" + sourceBS + "=" + srcLen + "\n\t" + newBS + "=" + destLen); } /* * Okay, we've got our bytes, and they look good. * Now rename the temporary file to the real file name. */ // if we like, this is where we could backup the existing item -- // move it out of the way to file.bak or something if (!newCabinetEntry.rename(sourceName)) throw new FilingException("rename of " + newCabinetEntry + " to " + sourceName + " failed"); } } else if (sourceEntry.isCabinet()) { actionOut("copy Cabinet '" + sourceEntry.getName() + "' in " + loc + " cabinet " + destCabinet); if (takeAction) newCabinetEntry = destCabinet.createCabinet(sourceEntry.getName()); // TODO: Need to recurse... somewhere. } else { log.errout(new Exception("copyCabinetEntry: neither ByteStore or Cabinet!")); } return newCabinetEntry; } private static String ceName(CabinetEntry ce) { String s; try { s = ce.getName(); } catch (FilingException e) { s = "<"+e+">"; } return s; } public String toString() { return "SyncList(" + getLocalCabinet() + ")"; } private void actionOut(String s) { if (log.v) log.outln((takeAction ? "" : "(Skipped) ") + s); } //----------------------------------------------------------------------------- // END OF SyncList //----------------------------------------------------------------------------- }