Version 1.1: Added support for web timelines.

Web timelines as defined by the JDB Labs Web TimeStamper tool are now supported.

* `TimelineSource` now also takes the timeline config file as constructor input.
* Refactored `Timeline` to use reduce implementation redundencies: internal
  implementations do not duplicate functionality.
* Improved comments in `TimelineProperties`.
* Updated `TimelineSourceFactory` to handle timelines that need to use
  `JDBLabsWebTimelineSource`.
This commit is contained in:
Jonathan Bernard 2011-06-27 17:13:07 -05:00
parent c061ea6c1f
commit 3ca6909b95
39 changed files with 204 additions and 20 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
#Thu, 16 Jun 2011 12:41:01 -0500 #Mon, 27 Jun 2011 17:12:00 -0500
name=timestamper-lib name=timestamper-lib
version=1.0 version=1.1
lib.local=true lib.local=true
build.number=2 build.number=1

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,12 @@
import groovyx.net.http.HTTPBuilder
import com.jdbernard.util.SmartConfig
import com.jdblabs.timestamper.core.*
smartConfig = new SmartConfig("temp.config")
smartConfig."web.username" = "jdbernard"
smartConfig."web.password" = "Y0uthc"
smartConfig."web.timelineId" = "work"
uri = new URI("http://timestamper-local:8000")
wtls = new JDBLabsWebTimelineSource(uri, smartConfig)

View File

@ -1,5 +1,6 @@
package com.jdblabs.timestamper.core; package com.jdblabs.timestamper.core;
import com.jdbernard.util.SmartConfig;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -15,7 +16,7 @@ public class FileTimelineSource extends TimelineSource {
private File file; private File file;
public FileTimelineSource(URI uri) { public FileTimelineSource(URI uri, SmartConfig config) {
super(uri); super(uri);
this.file = new File(uri); this.file = new File(uri);
} }

View File

@ -0,0 +1,165 @@
package com.jdblabs.timestamper.core
import com.jdbernard.util.SmartConfig
import groovyx.net.http.HTTPBuilder
import java.text.SimpleDateFormat
import static groovyx.net.http.ContentType.*
import static groovyx.net.http.Method.*
public class JDBLabsWebTimelineSource extends TimelineSource {
private static final String CONFIG_UNAME = "web.username"
private static final String CONFIG_PWD = "web.password"
private static final String CONFIG_TIMELINE = "web.timelineId"
private static isoDateFormat
static {
isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
isoDateFormat.timeZone = TimeZone.getTimeZone("UTC")
}
private String username
private String password
private String timelineId
private URI baseUri
private HTTPBuilder http
private Map entryHashes
public JDBLabsWebTimelineSource(URI uri, SmartConfig config) {
super(uri)
// load web-timeline specific properties
username = config.getProperty(CONFIG_UNAME, "")
password = config.getProperty(CONFIG_PWD, "")
timelineId = config.getProperty(CONFIG_TIMELINE, "")
baseUri = new URI(uri.scheme + "://" + uri.authority)
http = new HTTPBuilder(baseUri)
entryHashes = [:]
}
public Timeline read() {
def timelineJSON
def entryListJSON
def startDate, endDate
Timeline timeline
// make sure we have a fresh session
authenticate()
// load the timeline information
timelineJSON = http.get(
path: "/ts_api/timelines/${username}/${timelineId}",
contentType: JSON) { resp, json -> json }
timeline = new Timeline()
// create start and end times
startDate = Calendar.getInstance()
startDate.timeInMillis = 0
startDate = isoDateFormat.format(startDate.time)
endDate = isoDateFormat.format(Calendar.getInstance().time)
// load the timeline entries
entryListJSON = http.get(
path: "/ts_api/entries/${username}/${timelineId}",
contentType: JSON,
query: [
byDate: true,
startDate: startDate,
endDate: endDate ] ) { resp, json -> json }
entryListJSON.each { entry ->
// parse and create the timeline marker
def timestamp = isoDateFormat.parse(entry.timestamp)
def marker = new TimelineMarker(timestamp, entry.mark, entry.notes)
// add it to the timeline and our map of hashes
timeline.addMarker(marker)
entryHashes[fullHash(marker)] = entry.id
}
// return the created timeline
return timeline
}
public void persist(Timeline t) {
Map deletedEntries
List newEntries
// make sure we have a fresh session
authenticate()
// find differences since we last persisted
deletedEntries = entryHashes.clone() // shallow copy
newEntries = []
t.each { marker ->
def hash = fullHash(marker)
// this marker is present, remove from deletedEntries
if (deletedEntries.containsKey(hash)) {
deletedEntries.remove(hash) }
// this marker is not present, add to newEntries
else { newEntries << marker }
}
// delete all entries that used to be present but are not any longer
deletedEntries.each { hash, entryId ->
http.request(DELETE) {
uri.path = "/ts_api/entries/${username}/${timelineId}/${entryId}"
}
// TODO: error handling, make sure this only happens on success
entryHashes.remove(hash)
}
// add all new entries
newEntries.each { entry ->
def entryBody = [
mark: entry.mark,
notes: entry.notes,
timestamp: isoDateFormat.format(entry.timestamp)
]
// TODO: error handling
http.post(
path: "/ts_api/entries/${username}/${timelineId}",
contentType: JSON,
requestContentType: JSON,
body: entryBody) { resp, json ->
entryHashes.put(fullHash(entry), entry)
json
}
}
}
public boolean isAuthenticated() {
// TODO
}
public void authenticate() {
// TODO: error detection
http.post(
path: '/ts_api/login',
contentType: JSON,
requestContentType: JSON,
body: [ username: this.username,
password: this.password ]) { resp, json -> json }
}
protected int fullHash(TimelineMarker tm) {
return 61 * tm.hashCode() + (tm.mark != null ? tm.mark.hashCode() : 0);
}
}

View File

@ -18,7 +18,7 @@ public class Timeline implements Iterable<TimelineMarker> {
public static SimpleDateFormat shortFormat = new SimpleDateFormat("HH:mm:ss"); public static SimpleDateFormat shortFormat = new SimpleDateFormat("HH:mm:ss");
public static SimpleDateFormat longFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy"); public static SimpleDateFormat longFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
private TreeSet<TimelineMarker> timelineList; protected TreeSet<TimelineMarker> timelineList;
/** /**
* Create a new, empty Timeline. * Create a new, empty Timeline.
@ -42,7 +42,7 @@ public class Timeline implements Iterable<TimelineMarker> {
* @return <code>true</code> if this Timeline was modified. * @return <code>true</code> if this Timeline was modified.
*/ */
public boolean addMarker(Date timestamp, String name, String notes) { public boolean addMarker(Date timestamp, String name, String notes) {
return timelineList.add(new TimelineMarker(timestamp, name, notes)); return addMarker(new TimelineMarker(timestamp, name, notes));
} }
/** /**
@ -107,11 +107,7 @@ public class Timeline implements Iterable<TimelineMarker> {
public boolean addAll(Timeline t) { public boolean addAll(Timeline t) {
boolean modified = false; boolean modified = false;
for (TimelineMarker tm : t) { for (TimelineMarker tm : t) {
if (!timelineList.contains(tm)) { modified = addMarker(tm) || modified; }
timelineList.add(tm);
modified = true;
}
}
return modified; return modified;
} }

View File

@ -72,7 +72,7 @@ public class TimelineProperties {
URI timelineURI = timelineFile.toURI(); URI timelineURI = timelineFile.toURI();
timeline = new Timeline(); timeline = new Timeline();
timelineSource = TimelineSourceFactory.newInstance(timelineURI); timelineSource = TimelineSourceFactory.newInstance(timelineURI, config);
persistOnUpdate = true; persistOnUpdate = true;
try { timelineSource.persist(timeline); } try { timelineSource.persist(timeline); }
catch (IOException ioe) { catch (IOException ioe) {
@ -84,8 +84,8 @@ public class TimelineProperties {
} }
/** /**
* Load TimelineProperties from an InputStream. * Load TimelineProperties from a config file.
* @param is * @param propFile
*/ */
public TimelineProperties(File propFile) throws IOException { public TimelineProperties(File propFile) throws IOException {
String strURI; String strURI;
@ -97,8 +97,10 @@ public class TimelineProperties {
persistOnUpdate = (Boolean) config.getProperty( persistOnUpdate = (Boolean) config.getProperty(
LOCAL_TIMELINE_PERSIST_ON_UPDATE, true); LOCAL_TIMELINE_PERSIST_ON_UPDATE, true);
// load local timeline // get the URI for the primary timeline
strURI = (String) config.getProperty(LOCAL_TIMELINE_URI, ""); strURI = (String) config.getProperty(LOCAL_TIMELINE_URI, "");
// no primary timeline, default to file-based timeline
if ("".equals(strURI)) { if ("".equals(strURI)) {
File defaultTimelineFile = new File("timeline.default.txt"); File defaultTimelineFile = new File("timeline.default.txt");
try { try {
@ -108,7 +110,7 @@ public class TimelineProperties {
// TODO // TODO
} }
timelineURI = defaultTimelineFile.toURI(); timelineURI = defaultTimelineFile.toURI();
} else { } else { // we do have a URI
try { timelineURI = new URI(strURI); } try { timelineURI = new URI(strURI); }
catch (URISyntaxException urise) { catch (URISyntaxException urise) {
throw new IOException("Unable to load the timeline: the timeline " throw new IOException("Unable to load the timeline: the timeline "
@ -116,7 +118,8 @@ public class TimelineProperties {
} }
} }
timelineSource = TimelineSourceFactory.newInstance(timelineURI); // create our timeline source and read the timeline
timelineSource = TimelineSourceFactory.newInstance(timelineURI, config);
timeline = timelineSource.read(); timeline = timelineSource.read();
// search keys for remote timeline entries // search keys for remote timeline entries
@ -143,7 +146,7 @@ public class TimelineProperties {
// add a new SyncTarget to the list // add a new SyncTarget to the list
st = new SyncTarget(stName, TimelineSourceFactory st = new SyncTarget(stName, TimelineSourceFactory
.newInstance(timelineURI), timeline); .newInstance(timelineURI, config), timeline);
syncTargets.add(st); syncTargets.add(st);
// check for synch options // check for synch options

View File

@ -1,5 +1,6 @@
package com.jdblabs.timestamper.core; package com.jdblabs.timestamper.core;
import com.jdbernard.util.SmartConfig;
import java.io.File; import java.io.File;
import java.net.URI; import java.net.URI;
@ -9,10 +10,10 @@ import java.net.URI;
*/ */
public class TimelineSourceFactory { public class TimelineSourceFactory {
public static TimelineSource newInstance(URI uri) { public static TimelineSource newInstance(URI uri, SmartConfig config) {
// File based // File based
if ("file".equalsIgnoreCase(uri.getScheme())) { if ("file".equalsIgnoreCase(uri.getScheme())) {
return new FileTimelineSource(uri); return new FileTimelineSource(uri, config);
} }
// Twitter // Twitter
@ -23,6 +24,11 @@ public class TimelineSourceFactory {
+ "sources are not yet supported."); + "sources are not yet supported.");
} }
// Other HTTP, assume JDB Labs web app
else if ("http".equalsIgnoreCase(uri.getScheme())) {
return new JDBLabsWebTimelineSource(uri, config);
}
// SSH // SSH
else if ("ssh".equalsIgnoreCase(uri.getScheme())) { else if ("ssh".equalsIgnoreCase(uri.getScheme())) {
throw new UnsupportedOperationException("SSH based timeline sources" throw new UnsupportedOperationException("SSH based timeline sources"

View File

@ -1,5 +1,6 @@
package com.jdblabs.timestamper.core; package com.jdblabs.timestamper.core;
import com.jdbernard.util.SmartConfig;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;