Version 2.0: Added UUID to markers, truncated timestamps to seconds field.

* Added a UUID field to TimelineMarker.
* Updated StreamBasedTimelineSource to read and write UUIDs. The current format
  is compatible with the 1.x format, but support for the 1.x format is planned
  to be deprecated for 3.x.

  The UUID is added on the same line as the timestamp, with a `,` separating
  the timestamp and the UUID value.
* Updated JDBLabsWebTimelineSource to use UUIDs to reconcile the different
  timestamp entries (more reliable that using the timestamp value).
This commit is contained in:
Jonathan Bernard 2013-10-11 19:59:44 +00:00
parent 4c8d8a9f2d
commit d720c6c645
4 changed files with 79 additions and 29 deletions

View File

@ -1,5 +1,5 @@
#Sun, 22 Sep 2013 14:58:43 -0500 #Fri, 11 Oct 2013 18:08:21 +0000
name=timestamper-lib name=timestamper-lib
version=1.5 version=2.0
lib.local=true lib.local=true
build.number=1 build.number=1

View File

@ -3,6 +3,7 @@ package com.jdblabs.timestamper.core
import com.jdbernard.util.SmartConfig import com.jdbernard.util.SmartConfig
import groovyx.net.http.HTTPBuilder import groovyx.net.http.HTTPBuilder
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.UUID;
import static groovyx.net.http.ContentType.* import static groovyx.net.http.ContentType.*
import static groovyx.net.http.Method.* import static groovyx.net.http.Method.*
@ -27,7 +28,7 @@ public class JDBLabsWebTimelineSource extends TimelineSource {
private URI baseUri private URI baseUri
private HTTPBuilder http private HTTPBuilder http
private Map entryHashes private Map serverEntryIds
public JDBLabsWebTimelineSource(URI uri, SmartConfig config) { public JDBLabsWebTimelineSource(URI uri, SmartConfig config) {
super(uri) super(uri)
@ -50,8 +51,8 @@ public class JDBLabsWebTimelineSource extends TimelineSource {
"communicating to the web timeline.\n" + "communicating to the web timeline.\n" +
"${resp.statusLine}: ${json}") } "${resp.statusLine}: ${json}") }
// init our hash of known entries // init our map of known entries
entryHashes = [:] serverEntryIds = [:]
} }
public Timeline read() { public Timeline read() {
@ -76,11 +77,12 @@ public class JDBLabsWebTimelineSource extends TimelineSource {
timeline = new Timeline() 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 = Calendar.getInstance()
startDate.timeInMillis = 0 startDate.timeInMillis = 0 // Beginning of the epoch
startDate = isoDateFormat.format(startDate.time) startDate = isoDateFormat.format(startDate.time)
// Until now
endDate = isoDateFormat.format(Calendar.getInstance().time) endDate = isoDateFormat.format(Calendar.getInstance().time)
// load the timeline entries // load the timeline entries
@ -96,11 +98,12 @@ public class JDBLabsWebTimelineSource extends TimelineSource {
// parse and create the timeline marker // parse and create the timeline marker
def timestamp = isoDateFormat.parse(entry.timestamp) 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 // add it to the timeline and our map of hashes
timeline.addMarker(marker) timeline.addMarker(marker)
entryHashes[fullHash(marker)] = entry.id serverEntryIds[marker.uuid] = entry.id
} }
// return the created timeline // return the created timeline
@ -114,28 +117,38 @@ public class JDBLabsWebTimelineSource extends TimelineSource {
// make sure we have a fresh session // make sure we have a fresh session
authenticate() authenticate()
// find differences since we last persisted // Find differences since we last persisted.
deletedEntries = entryHashes.clone() // shallow copy
// 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 = [] newEntries = []
t.each { marker -> 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 // This marker is not present on the server, add to newEntries.
if (deletedEntries.containsKey(hash)) {
deletedEntries.remove(hash) }
// this marker is not present, add to newEntries
else { newEntries << marker } 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 -> deletedEntries.each { hash, entryId ->
http.request(DELETE) { http.request(DELETE) {
uri.path = "entries/${username}/${timelineId}/${entryId}" uri.path = "entries/${username}/${timelineId}/${entryId}"
response.'404' = { resp -> entryHashes.remove(hash) } response.'404' = { resp -> serverEntryIds.remove(hash) }
response.success = { resp -> entryHashes.remove(hash) } response.success = { resp -> serverEntryIds.remove(hash) }
} }
} }
@ -145,6 +158,7 @@ public class JDBLabsWebTimelineSource extends TimelineSource {
def entryBody = [ def entryBody = [
mark: entry.mark, mark: entry.mark,
notes: entry.notes, notes: entry.notes,
uuid: entry.uuid.toString(),
timestamp: isoDateFormat.format(entry.timestamp) timestamp: isoDateFormat.format(entry.timestamp)
] ]
@ -154,7 +168,7 @@ public class JDBLabsWebTimelineSource extends TimelineSource {
body = entryBody body = entryBody
response.success = { resp, json -> response.success = { resp, json ->
entryHashes.put(fullHash(entry), json?.id ?: 0) } serverEntryIds.put(entry.uuid, json?.id ?: 0) }
} }
} }
} }

View File

@ -11,6 +11,7 @@ import java.io.Writer;
import java.text.ParseException; import java.text.ParseException;
import java.util.Date; import java.util.Date;
import java.util.Scanner; import java.util.Scanner;
import java.util.UUID;
/** /**
* *
@ -71,8 +72,9 @@ public class StreamBasedTimelineSource extends TimelineSource {
Writer out = new OutputStreamWriter(stream); Writer out = new OutputStreamWriter(stream);
for (TimelineMarker tm : timeline) { for (TimelineMarker tm : timeline) {
// write timestamp // write timestamp and UUID
out.write(Timeline.longFormat.format(tm.getTimestamp()) + "\n"); out.write(Timeline.longFormat.format(tm.getTimestamp()) + "," +
tm.getUuid().toString() + "\n");
// write mark // write mark
String mark = tm.getMark().replace('\n', '\u0000'); String mark = tm.getMark().replace('\n', '\u0000');
@ -127,7 +129,8 @@ public class StreamBasedTimelineSource extends TimelineSource {
null : new PrintWriter(commentStream); null : new PrintWriter(commentStream);
ReadingState readingState = ReadingState.NewMarker; ReadingState readingState = ReadingState.NewMarker;
Date d = null; Date date = null;
UUID uuid = null;
StringBuilder mark = null; StringBuilder mark = null;
StringBuilder notes = null; StringBuilder notes = null;
String line; String line;
@ -146,11 +149,24 @@ public class StreamBasedTimelineSource extends TimelineSource {
switch (readingState) { switch (readingState) {
case NewMarker: 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) { catch (ParseException pe) {
throw new IOException("Error parsing timeline file at line " throw new IOException("Error parsing timeline file at line "
+ lineNumber + ": expected a new marker date but could not parse" + lineNumber + ": expected a new marker date but could not parse"
+ " the date. Error: " + pe.getLocalizedMessage()); } + " 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; readingState = ReadingState.StartMark;
break; break;
@ -179,7 +195,13 @@ public class StreamBasedTimelineSource extends TimelineSource {
case EndMarker: case EndMarker:
String sMark = mark.toString().replace('\u0000', '\n'); String sMark = mark.toString().replace('\u0000', '\n');
String sNotes = notes.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; } } readingState = ReadingState.NewMarker; } }
return timeline; } return timeline; }

View File

@ -1,6 +1,7 @@
package com.jdblabs.timestamper.core; package com.jdblabs.timestamper.core;
import java.util.Date; import java.util.Date;
import java.util.UUID;
/** /**
* @author Jonathan Bernard {@literal <jdbernard@gmail.com>} * @author Jonathan Bernard {@literal <jdbernard@gmail.com>}
@ -11,16 +12,23 @@ public class TimelineMarker implements Comparable<TimelineMarker> {
private final Date timestamp; private final Date timestamp;
private final String mark; private final String mark;
private final UUID uuid;
private String notes; private String notes;
public TimelineMarker(Date timestamp, String mark, 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) if (timestamp == null || mark == null)
throw new IllegalArgumentException("Null timestamp or mark" throw new IllegalArgumentException("Null timestamp or mark"
+ " is not permitted."); + " is not permitted.");
this.timestamp = timestamp; // We truncate milliseconds.
this.timestamp = 1000 * (timestamp / 1000);
this.mark = mark; this.mark = mark;
this.notes = notes; this.notes = notes;
this.uuid = uuid;
} }
public Date getTimestamp() { return timestamp; } public Date getTimestamp() { return timestamp; }
@ -29,12 +37,19 @@ public class TimelineMarker implements Comparable<TimelineMarker> {
public String getNotes() { return notes; } public String getNotes() { return notes; }
public UUID getUuid() { return uuid; }
public void setNotes(String notes) { this.notes = notes; } public void setNotes(String notes) { this.notes = notes; }
@Override @Override
public int compareTo(TimelineMarker that) { public int compareTo(TimelineMarker that) {
// Always greater than null
if (that == null) return Integer.MAX_VALUE; 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); int val = this.timestamp.compareTo(that.timestamp);
if (val == 0) val = this.mark.compareTo(that.mark); if (val == 0) val = this.mark.compareTo(that.mark);
@ -47,8 +62,7 @@ public class TimelineMarker implements Comparable<TimelineMarker> {
if (!(o instanceof TimelineMarker)) return false; if (!(o instanceof TimelineMarker)) return false;
TimelineMarker that = (TimelineMarker) o; TimelineMarker that = (TimelineMarker) o;
return this.timestamp.equals(that.timestamp) && return this.uuid.equals(that.uuid);
this.mark.equals(that.mark);
} }
@Override @Override