diff --git a/project.properties b/project.properties index dc32cdc..fb42b0c 100644 --- a/project.properties +++ b/project.properties @@ -1,5 +1,5 @@ -#Sun, 22 Sep 2013 14:58:43 -0500 +#Fri, 11 Oct 2013 18:08:21 +0000 name=timestamper-lib -version=1.5 +version=2.0 lib.local=true build.number=1 diff --git a/src/main/com/jdblabs/timestamper/core/JDBLabsWebTimelineSource.groovy b/src/main/com/jdblabs/timestamper/core/JDBLabsWebTimelineSource.groovy index 30eba3f..a70b306 100644 --- a/src/main/com/jdblabs/timestamper/core/JDBLabsWebTimelineSource.groovy +++ b/src/main/com/jdblabs/timestamper/core/JDBLabsWebTimelineSource.groovy @@ -3,6 +3,7 @@ package com.jdblabs.timestamper.core import com.jdbernard.util.SmartConfig import groovyx.net.http.HTTPBuilder import java.text.SimpleDateFormat +import java.util.UUID; import static groovyx.net.http.ContentType.* import static groovyx.net.http.Method.* @@ -27,7 +28,7 @@ public class JDBLabsWebTimelineSource extends TimelineSource { private URI baseUri private HTTPBuilder http - private Map entryHashes + private Map serverEntryIds public JDBLabsWebTimelineSource(URI uri, SmartConfig config) { super(uri) @@ -50,8 +51,8 @@ public class JDBLabsWebTimelineSource extends TimelineSource { "communicating to the web timeline.\n" + "${resp.statusLine}: ${json}") } - // init our hash of known entries - entryHashes = [:] + // init our map of known entries + serverEntryIds = [:] } public Timeline read() { @@ -76,11 +77,12 @@ public class JDBLabsWebTimelineSource extends TimelineSource { timeline = new Timeline() - // create start and end times + // create start and end times to use as parameters to the web service startDate = Calendar.getInstance() - startDate.timeInMillis = 0 + startDate.timeInMillis = 0 // Beginning of the epoch startDate = isoDateFormat.format(startDate.time) + // Until now endDate = isoDateFormat.format(Calendar.getInstance().time) // load the timeline entries @@ -96,11 +98,12 @@ public class JDBLabsWebTimelineSource extends TimelineSource { // parse and create the timeline marker def timestamp = isoDateFormat.parse(entry.timestamp) - def marker = new TimelineMarker(timestamp, entry.mark, entry.notes) + def marker = new TimelineMarker(timestamp, entry.mark, entry.notes, + UUID.fromString(entry.uuid)); // add it to the timeline and our map of hashes timeline.addMarker(marker) - entryHashes[fullHash(marker)] = entry.id + serverEntryIds[marker.uuid] = entry.id } // return the created timeline @@ -114,28 +117,38 @@ public class JDBLabsWebTimelineSource extends TimelineSource { // make sure we have a fresh session authenticate() - // find differences since we last persisted - deletedEntries = entryHashes.clone() // shallow copy + // Find differences since we last persisted. + + // We want to identify the set of entries that were previously present + // on the server but are no longer present locally and delete them from + // the server. We start with the set of all entries last seen on the + // server. We look through our local entries and if we find the entry + // locally we remove it from our set of deletion candidates. + + // We will also be constructing a set of entries that we have locally + // which were not present on the server. This set of entries will be + // added to the server. + + deletedEntries = serverEntryIds.clone() // shallow copy newEntries = [] t.each { marker -> - def hash = fullHash(marker) + // This marker is present on the server and is still present + // locally, remove from deletedEntries + if (deletedEntries.containsKey(marker.uuid)) { + deletedEntries.remove(marker.uuid) } - // this marker is present, remove from deletedEntries - if (deletedEntries.containsKey(hash)) { - deletedEntries.remove(hash) } - - // this marker is not present, add to newEntries + // This marker is not present on the server, add to newEntries. else { newEntries << marker } } - // delete all entries that used to be present but are not any longer + // Delete all entries that used to be present but are not any longer deletedEntries.each { hash, entryId -> http.request(DELETE) { uri.path = "entries/${username}/${timelineId}/${entryId}" - response.'404' = { resp -> entryHashes.remove(hash) } - response.success = { resp -> entryHashes.remove(hash) } + response.'404' = { resp -> serverEntryIds.remove(hash) } + response.success = { resp -> serverEntryIds.remove(hash) } } } @@ -145,6 +158,7 @@ public class JDBLabsWebTimelineSource extends TimelineSource { def entryBody = [ mark: entry.mark, notes: entry.notes, + uuid: entry.uuid.toString(), timestamp: isoDateFormat.format(entry.timestamp) ] @@ -154,7 +168,7 @@ public class JDBLabsWebTimelineSource extends TimelineSource { body = entryBody response.success = { resp, json -> - entryHashes.put(fullHash(entry), json?.id ?: 0) } + serverEntryIds.put(entry.uuid, json?.id ?: 0) } } } } diff --git a/src/main/com/jdblabs/timestamper/core/StreamBasedTimelineSource.java b/src/main/com/jdblabs/timestamper/core/StreamBasedTimelineSource.java index c12ea17..886aa04 100644 --- a/src/main/com/jdblabs/timestamper/core/StreamBasedTimelineSource.java +++ b/src/main/com/jdblabs/timestamper/core/StreamBasedTimelineSource.java @@ -11,6 +11,7 @@ import java.io.Writer; import java.text.ParseException; import java.util.Date; import java.util.Scanner; +import java.util.UUID; /** * @@ -71,8 +72,9 @@ public class StreamBasedTimelineSource extends TimelineSource { Writer out = new OutputStreamWriter(stream); for (TimelineMarker tm : timeline) { - // write timestamp - out.write(Timeline.longFormat.format(tm.getTimestamp()) + "\n"); + // write timestamp and UUID + out.write(Timeline.longFormat.format(tm.getTimestamp()) + "," + + tm.getUuid().toString() + "\n"); // write mark String mark = tm.getMark().replace('\n', '\u0000'); @@ -127,7 +129,8 @@ public class StreamBasedTimelineSource extends TimelineSource { null : new PrintWriter(commentStream); ReadingState readingState = ReadingState.NewMarker; - Date d = null; + Date date = null; + UUID uuid = null; StringBuilder mark = null; StringBuilder notes = null; String line; @@ -146,11 +149,24 @@ public class StreamBasedTimelineSource extends TimelineSource { switch (readingState) { case NewMarker: - try { d = Timeline.longFormat.parse(line); } + try { + String[] parts = line.split(","); + date = Timeline.longFormat.parse(parts[0]); + + // If there is no UUID, we will ignore it. This allows us + // to support timeline files generated by 1.x versions of + // this library. + // TODO: Remove this check in version 3.x + if (parts.length > 1) uuid = UUID.fromString(parts[1]); + else uuid = null; } catch (ParseException pe) { throw new IOException("Error parsing timeline file at line " + lineNumber + ": expected a new marker date but could not parse" + " the date. Error: " + pe.getLocalizedMessage()); } + catch (IllegalArgumentException iae) { + throw new IOException("Error parsing timeline file at line " + + lineNumber + ": Expected a UUID but could not parse " + + "the value. Details: " + iae.getLocalizedMessage()); } readingState = ReadingState.StartMark; break; @@ -179,7 +195,13 @@ public class StreamBasedTimelineSource extends TimelineSource { case EndMarker: String sMark = mark.toString().replace('\u0000', '\n'); String sNotes = notes.toString().replace('\u0000', '\n'); - timeline.addMarker(d, sMark, sNotes); + TimelineMarker marker; + + // Support a missing UUID until version 3.x + if (uuid == null) + marker = new TimelineMarker(date, sMark, sNotes); + else marker = new TimelineMarker(date, sMark, sNotes, uuid); + timeline.addMarker(marker); readingState = ReadingState.NewMarker; } } return timeline; } diff --git a/src/main/com/jdblabs/timestamper/core/TimelineMarker.java b/src/main/com/jdblabs/timestamper/core/TimelineMarker.java index 7448910..8cd09fa 100644 --- a/src/main/com/jdblabs/timestamper/core/TimelineMarker.java +++ b/src/main/com/jdblabs/timestamper/core/TimelineMarker.java @@ -1,6 +1,7 @@ package com.jdblabs.timestamper.core; import java.util.Date; +import java.util.UUID; /** * @author Jonathan Bernard {@literal } @@ -11,16 +12,23 @@ public class TimelineMarker implements Comparable { private final Date timestamp; private final String mark; + private final UUID uuid; private String notes; public TimelineMarker(Date timestamp, String mark, String notes) { + this(timestamp, mark, notes, UUID.randomUUID()); } + + public TimelineMarker(Date timestamp, String mark, String notes, UUID uuid) { if (timestamp == null || mark == null) throw new IllegalArgumentException("Null timestamp or mark" + " is not permitted."); - this.timestamp = timestamp; + // We truncate milliseconds. + this.timestamp = 1000 * (timestamp / 1000); + this.mark = mark; this.notes = notes; + this.uuid = uuid; } public Date getTimestamp() { return timestamp; } @@ -29,12 +37,19 @@ public class TimelineMarker implements Comparable { public String getNotes() { return notes; } + public UUID getUuid() { return uuid; } + public void setNotes(String notes) { this.notes = notes; } @Override public int compareTo(TimelineMarker that) { + // Always greater than null if (that == null) return Integer.MAX_VALUE; + // Always equal to other instances of itself (same UUID). + if (this.uuid.equals(that.uuid)) return 0; + + // Check the timestamp, then the mark if the timestamps are equal. int val = this.timestamp.compareTo(that.timestamp); if (val == 0) val = this.mark.compareTo(that.mark); @@ -47,8 +62,7 @@ public class TimelineMarker implements Comparable { if (!(o instanceof TimelineMarker)) return false; TimelineMarker that = (TimelineMarker) o; - return this.timestamp.equals(that.timestamp) && - this.mark.equals(that.mark); + return this.uuid.equals(that.uuid); } @Override