Move library code into lib folder in preparation for consolidating the lib, CLI, and web projects.

This commit is contained in:
2024-08-04 20:42:26 -05:00
parent 66c00e2473
commit e32c6358e9
15 changed files with 0 additions and 0 deletions

28
lib/build.gradle Normal file
View File

@ -0,0 +1,28 @@
apply plugin: "groovy"
apply plugin: "maven"
group = "com.jdblabs.timestamper"
version = "2.1"
repositories {
mavenLocal()
mavenCentral() }
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.3.6'
compile 'org.pegdown:pegdown:1.4.2'
compile 'commons-codec:commons-codec:1.10'
compile 'commons-beanutils:commons-beanutils:1.9.2'
compile 'org.codehaus.groovy.modules.http-builder:http-builder:0.6'
compile 'org.apache.httpcomponents:httpclient:4.3.6'
compile 'org.slf4j:slf4j-api:1.7.10'
compile 'ch.qos.logback:logback-core:1.1.2'
compile 'ch.qos.logback:logback-classic:1.1.2'
compile 'com.jdbernard:jdb-util:3.4'
}
jar {
manifest {
attributes("Main-Class": "com.jdbernard.remindme.DailyAgenda")
}
}

View File

@ -0,0 +1,9 @@
Add unit tests.
===============
----
========= ==========
Created: 2011-06-28
Resolved: YYYY-MM-DD
========= ==========

1
lib/settings.gradle Normal file
View File

@ -0,0 +1 @@
rootProject.name = "timestamper-lib"

View File

@ -0,0 +1,21 @@
package com.jdblabs.timestamper.core;
import java.io.IOException;
/**
*
* @author Jonathan Bernard ({@literal jdbernard@gmail.com})
*/
public class AuthenticationException extends IOException {
public AuthenticationException() { super(); }
public AuthenticationException(String message) { super(message); }
public AuthenticationException(Throwable t) { super(t); }
public AuthenticationException(String message, Throwable t) {
super(message, t);
}
}

View File

@ -0,0 +1,65 @@
package com.jdblabs.timestamper.core;
import com.jdbernard.util.SmartConfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
/**
*
* @author Jonathan Bernard ({@literal jdbernard@gmail.com})
*/
public class FileTimelineSource extends TimelineSource {
private File file;
public FileTimelineSource(URI uri, SmartConfig config) {
super(uri);
this.file = new File(uri);
}
/** * {@inheritDoc } */
public Timeline read() throws IOException {
FileInputStream fin = new FileInputStream(file);
Timeline t = StreamBasedTimelineSource.readFromStream(fin);
fin.close();
return t;
}
public Timeline readWithComments(OutputStream commentStream)
throws IOException {
FileInputStream fin = new FileInputStream(file);
Timeline t = StreamBasedTimelineSource.readFromStream(fin, commentStream);
fin.close();
return t;
}
/** * {@inheritDoc } */
public void persist(Timeline t) throws IOException {
FileOutputStream fout = new FileOutputStream(file);
StreamBasedTimelineSource.writeToStream(fout, t);
fout.close();
}
public boolean isAuthenticated() {
File dir = file.getParentFile();
return (dir.canRead() && dir.canWrite());
}
public void authenticate() throws AuthenticationException {
File dir = file.getParentFile();
if (!dir.canRead())
throw new AuthenticationException("This FileTimelineSource does not"
+ " have read access to the parent directory for the "
+ "given file (" + file.getAbsolutePath() + ".");
if (!dir.canWrite())
throw new AuthenticationException("This FileTimelineSource does not"
+ " have write access to the parent directory for the "
+ "given file (" + file.getAbsolutePath() + ".");
}
}

View File

@ -0,0 +1,195 @@
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.*
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 serverEntryIds
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 + "/ts_api/")
http = new HTTPBuilder(baseUri)
// set some default error handlers
http.handler.'500' = { resp, json ->
throw new IOException("The web timeline reported an internal " +
"error: ${json.error ?: 'no details available'}") }
http.handler.failure = {resp, json ->
throw new IOException("Unable to complete the operation: error " +
"communicating to the web timeline.\n" +
"${resp.statusLine}: ${json}") }
// init our map of known entries
serverEntryIds = [:]
}
public Timeline read() {
def timelineJSON
def entryListJSON
def startDate, endDate
Timeline timeline
// make sure we have a fresh session
authenticate()
// load the timeline information
http.request(GET, JSON) {
uri.path = "timelines/${username}/${timelineId}"
response.'404' = { resp ->
throw new IOException("No timeline '${timelineId}' for user " +
"'${username}'") }
response.success = { resp, json -> timelineJSON = json }
}
timeline = new Timeline()
// create start and end times to use as parameters to the web service
startDate = Calendar.getInstance()
startDate.timeInMillis = 0 // Beginning of the epoch
startDate = isoDateFormat.format(startDate.time)
// Until now
endDate = isoDateFormat.format(Calendar.getInstance().time)
// load the timeline entries
entryListJSON = http.get(
path: "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,
UUID.fromString(entry.uuid));
// add it to the timeline and our map of hashes
timeline.addMarker(marker)
serverEntryIds[marker.uuid] = 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.
// 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 ->
// 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 not present on the server, add to newEntries.
else { newEntries << marker }
}
// Delete all entries that used to be present but are not any longer
deletedEntries.each { uuid, entryId ->
http.request(DELETE) {
uri.path = "entries/${username}/${timelineId}/${entryId}"
response.'404' = { resp -> serverEntryIds.remove(uuid) }
response.success = { resp -> serverEntryIds.remove(uuid) }
}
}
// add all new entries
newEntries.each { entry ->
def entryBody = [
mark: entry.mark,
notes: entry.notes,
uuid: entry.uuid.toString(),
timestamp: isoDateFormat.format(entry.timestamp)
]
http.request(POST, JSON) {
uri.path = "entries/${username}/${timelineId}"
requestContentType = JSON
body = entryBody
response.success = { resp, json ->
serverEntryIds.put(entry.uuid, json?.id ?: 0) }
}
}
}
public boolean isAuthenticated() { return false; }
public void authenticate() throws AuthenticationException {
http.request(POST, JSON) { req ->
uri.path = '/ts_api/login'
requestContentType = JSON
body = [ username: this.username,
password: this.password ]
response.'401' = { resp ->
throw new AuthenticationException(
"Unable to connect to ${baseUri}: " +
"Invalid username/password combination.") }
}
}
protected int fullHash(TimelineMarker tm) {
return 61 * tm.hashCode() + (tm.mark != null ? tm.mark.hashCode() : 0);
}
}

View File

@ -0,0 +1,208 @@
package com.jdblabs.timestamper.core;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.ParseException;
import java.util.Date;
import java.util.Scanner;
import java.util.UUID;
/**
*
* @author Jonathan Bernard ({@literal jdbernard@gmail.com})
*/
public class StreamBasedTimelineSource extends TimelineSource {
private static final int lineWrap = 78;
private static enum ReadingState {
NewMarker,
StartMark,
ReadMark,
StartNotes,
ReadNotes,
EndMarker };
private InputStream in;
private OutputStream out;
private ByteArrayOutputStream comments;
public StreamBasedTimelineSource(InputStream inStream,
OutputStream outStream) {
super(null);
this.in = inStream;
this.out = outStream;
this.comments = new ByteArrayOutputStream(); }
/** {@inheritDoc } */
public Timeline read() throws IOException {
return readFromStream(in, comments); }
/** {@inheritDoc } */
public void persist(Timeline t) throws IOException {
writeToStream(out, t); }
public void authenticate() throws AuthenticationException { }
public boolean isAuthenticated() { return true; }
/**
* Allows a user to extract the comments from the last parsed file.
* @return a <code>byte[]</code> representing the portions of the file
* which were comments.
*/
public byte[] getCommentBytes() { return comments.toByteArray(); }
/**
* Write the a representation of the timeline to a stream. This method
* flushes the stream after it finishes writing but does not close the
* stream.
* @param stream An open stream to write the timeline representation to.
* @param timeline The timeline to write.
* @throws java.io.IOException
*/
public static void writeToStream(OutputStream stream, Timeline timeline)
throws IOException {
Writer out = new OutputStreamWriter(stream);
for (TimelineMarker tm : timeline) {
// write timestamp and UUID
out.write(Timeline.longFormat.format(tm.getTimestamp()) + "," +
tm.getUuid().toString() + "\n");
// write mark
String mark = tm.getMark().replace('\n', '\u0000');
if (mark.length() < lineWrap) out.write(mark + "\n");
else {
// wrap lines if neccessary
int i;
for (i = 0; (i + lineWrap) < mark.length(); i+=lineWrap)
out.write(mark.substring(i, i+lineWrap) + "\\\n");
if (i < mark.length())
out.write(mark.substring(i, mark.length()) + "\n"); }
// write notes
String notes = tm.getNotes().replace('\n', '\u0000');
if (notes.length() < lineWrap) out.write(notes + "\n");
else {
// wrap lines if neccessary
int i;
for (i = 0; (i + lineWrap) < notes.length(); i+=lineWrap)
out.write(notes.substring(i, i+lineWrap) + "\\\n");
if (i < notes.length())
out.write(notes.substring(i, notes.length()) + "\n"); }
out.write("\n"); }
out.flush(); }
/**
* Create a <b>Timeline</b> instance from a given stream.
* @param stream The stream to read from.
* @return A new <b>Timeline</b> instance specified by the input stream.
* @throws java.io.IOException
* @throws java.io.FileNotFoundException
*/
public static Timeline readFromStream(InputStream stream)
throws IOException {
return readFromStream(stream, null); }
/**
* Create a <b>Timeline</b> instance from a given stream.
* @param stream The stream to read from.
* @param commentStream A stream to write comments found in the file. This
* parameter may be <b>null</b>, in which case comments are ignored.
* @return A new <b>Timeline</b> instance specified by the input stream.
* @throws java.io.IOException
* @throws java.io.FileNotFoundException
*/
public static Timeline readFromStream(InputStream stream,
OutputStream commentStream) throws IOException {
Scanner in = new Scanner(stream);
Timeline timeline = new Timeline();
PrintWriter commentsWriter = commentStream == null ?
null : new PrintWriter(commentStream);
ReadingState readingState = ReadingState.NewMarker;
Date date = null;
UUID uuid = null;
StringBuilder mark = null;
StringBuilder notes = null;
String line;
int lineNumber = 0;
while (in.hasNextLine()) {
line = in.nextLine();
lineNumber++;
// line is a comment
if (line.startsWith("#")) {
if (commentsWriter != null) commentsWriter.println(line);
// don't parse this line as part of the timeline
continue; }
switch (readingState) {
case NewMarker:
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;
case StartMark:
mark = new StringBuilder();
// fall through to ReadMark
case ReadMark:
if (line.endsWith("\\")) {
readingState = ReadingState.ReadMark;
line = line.substring(0, line.length() - 1); }
else readingState = ReadingState.StartNotes;
mark.append(line);
break;
case StartNotes:
notes = new StringBuilder();
// fall through to ReadNotes
case ReadNotes:
if (line.endsWith("\\")) {
readingState = ReadingState.ReadNotes;
line = line.substring(0, line.length() - 1); }
else readingState = ReadingState.EndMarker;
notes.append(line);
break;
case EndMarker:
String sMark = mark.toString().replace('\u0000', '\n');
String sNotes = notes.toString().replace('\u0000', '\n');
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; }
}

View File

@ -0,0 +1,143 @@
package com.jdblabs.timestamper.core;
import java.io.IOException;
import java.util.Collection;
import java.util.Timer;
import java.util.TimerTask;
/**
* A remote target synchronized against the local timeline.
* @author Jonathan Bernard (jdbernard@gmail.com)
*/
public class SyncTarget {
protected final TimelineSource source;
protected final Timeline localTimeline;
protected final String name;
protected Timer syncTimer;
protected SyncTask syncTask;
protected long syncInterval= 30 * 60 * 1000; // 30 minutes converted to ms
protected boolean pushEnabled = true;
protected boolean pullEnabled = true;
protected boolean syncOnExit = true;
/**
*
*/
protected class SyncTask extends TimerTask {
@Override public void run() {
synchronized(this) {
try { SyncTarget.this.sync(); }
catch (IOException ioe) { /* TODO */ }
}}}
protected SyncTarget(String name, TimelineSource source,
Timeline localTimeline) {
this.name = name;
this.source = source;
this.localTimeline = localTimeline;
syncTimer = new Timer(source.toString() + " sync-timer");
syncTask = new SyncTask();
syncTimer.schedule(syncTask, syncInterval, syncInterval);
}
/**
* Sync the local timeline with the remote timeline represented by this
* SyncTarget object.
* @return <b>true</b> if the two timelines were out of sync and have
* been put into synch, <b>false</b> if the timelines were already in
* sync and no action was requried.
* @throws IOException if there is an error communicating with the remote
* timeline. This includes AuthenticationException.
*/
protected boolean sync() throws IOException {
assert (pullEnabled || pushEnabled);
Timeline remoteTimeline;
boolean syncPerformed = false;
// make sure we're authenticated to whatever source we're using
if (!SyncTarget.this.source.isAuthenticated()) {
SyncTarget.this.source.authenticate();
}
// try to copy the remote timeline locally
remoteTimeline = SyncTarget.this.source.read();
// if we are pulling markers from the remote line
if (SyncTarget.this.pullEnabled) {
// get all markers in the remote timeline not in the local one
Collection<TimelineMarker> diffFromRemote =
remoteTimeline.difference(localTimeline);
if (diffFromRemote.size() != 0) {
// add all markers in the remote tline to the local one
localTimeline.addAll(diffFromRemote);
syncPerformed = true;
}
}
// if we are pushing markers to the remote timeline
if (SyncTarget.this.pushEnabled) {
// get all markers in the local timeline but not in the remote
Collection<TimelineMarker> diffFromLocal =
localTimeline.difference(remoteTimeline);
if (diffFromLocal.size() != 0) {
// add the difference to the remote timeline
remoteTimeline.addAll(diffFromLocal);
syncPerformed = true;
}
// try to persist the updated remote timeline
SyncTarget.this.source.persist(remoteTimeline);
}
return syncPerformed;
}
public void shutdown() throws IOException {
// TODO: move this onto the timer thread?
if (syncOnExit) sync();
syncTimer.cancel();
syncTimer.purge();
}
public String getName() { return name; }
public TimelineSource getSource() { return source; }
public synchronized void setSyncInterval(long syncInterval) {
this.syncInterval= syncInterval;
syncTask.cancel();
syncTask = new SyncTask();
syncTimer.purge();
syncTimer.schedule(syncTask, syncInterval, syncInterval);
}
public long getSyncInterval() { return syncInterval; }
public synchronized void setPushEnabled(boolean pushEnabled) {
this.pullEnabled = pushEnabled;
}
public boolean getPushEnabled() { return pushEnabled; }
public synchronized void setPullEnabled(boolean pullEnabled) {
this.pullEnabled = pullEnabled;
}
public boolean getPullEnabled() { return pullEnabled; }
public synchronized void setSyncOnExit(boolean syncOnExit) {
this.syncOnExit = syncOnExit;
}
public boolean getSyncOnExit() { return syncOnExit; }
}

View File

@ -0,0 +1,124 @@
package com.jdblabs.timestamper.core;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.TreeSet;
/**
* A Timeline object represents a series of markers at specific points in time.
* It represents on logical timeline. The markers have a name or symbol (the
* 'mark') and notes associated with that mark.
* @author Jonathan Bernard {@literal <jdbernard@gmail.com>}
* @see com.jdbernard.timestamper.core.TimelineSource
*/
public class Timeline implements Iterable<TimelineMarker> {
public static SimpleDateFormat shortFormat = new SimpleDateFormat("HH:mm:ss");
public static SimpleDateFormat longFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
protected TreeSet<TimelineMarker> timelineList;
/**
* Create a new, empty Timeline.
*/
public Timeline() {
timelineList = new TreeSet<TimelineMarker>();
}
/**
* Add a marker to the timeline.
* @param tm The TimelineMarker to add.
* @return <code>true</code> if this Timeline was modified.
*/
public boolean addMarker(TimelineMarker tm) { return timelineList.add(tm); }
/**
* Add a marker to the timeline.
* @param timestamp The date and time of the marker.
* @param name The name of the marker (activity, project, etc)
* @param notes Additional notes about this marker.
* @return <code>true</code> if this Timeline was modified.
*/
public boolean addMarker(Date timestamp, String name, String notes) {
return addMarker(new TimelineMarker(timestamp, name, notes));
}
/**
* Get the last marker placed before or on the given timestamp. If you
* think of the markers as demarcating time, then this is effectivly,
* "get the current task."
* @param timestamp The cut-off point for the query.
* @return The latest TimelineMarker placed on or before the given
* timestamp.
*/
public TimelineMarker getLastMarker(Date timestamp) {
TimelineMarker lastMarker = null;
for (TimelineMarker tm : timelineList) {
if (tm.getTimestamp().after(timestamp))
break;
lastMarker = tm;
}
return lastMarker;
}
/**
* Remove a TimelineMarker from this Timelnie.
* @param marker The marker to remove.
* @return <code>true</code> if this Timeline was changed.
*/
public boolean removeMarker(TimelineMarker marker) {
return timelineList.remove(marker);
}
public Iterator<TimelineMarker> iterator() {
return timelineList.iterator();
}
/**
* Return the difference of the this timeline relative to another timeline.
* More specifically, return the set of all
* {@link jdbernard.timestamper.core.TimelineMarker}s that are present in
* the <b>this</b> timeline but not present in the given timeline.
* timeline.
* @param t
* @return A collection representing the TimelineMarkers present in
* <b>this</b> timeline but not in the given timeline.
*/
public Collection<TimelineMarker> difference(Timeline t) {
TreeSet<TimelineMarker> difference = new TreeSet<TimelineMarker>();
for (TimelineMarker tm : timelineList) {
if (!t.timelineList.contains(tm))
difference.add(tm);
}
return difference;
}
/**
* Add all TimelineMarkers from <code>t</code> to <code>this</code>
* Timeline, excluding markers already present in <code>this</code>.
* @param t
* @return <code>true</code> if this Timeline was modified.
*/
public boolean addAll(Timeline t) {
boolean modified = false;
for (TimelineMarker tm : t) {
modified = addMarker(tm) || modified; }
return modified;
}
/**
* Add all TimelineMarkers from <code>c</code> to <code>this</code>
* Timeline, excluding markers already present in <code>this</code>.
* @param c A Collection of TimelineMarkers
* @return <code>true</code> if this TImeline was modified.
*/
public boolean addAll(Collection<TimelineMarker> c) {
return timelineList.addAll(c);
}
}

View File

@ -0,0 +1,77 @@
package com.jdblabs.timestamper.core;
import java.util.Date;
import java.util.UUID;
/**
* @author Jonathan Bernard {@literal <jdbernard@gmail.com>}
* This represents a marker on the timeline.
* The date of the marker and the mark cannot be changed once assigned.
*/
public class TimelineMarker implements Comparable<TimelineMarker> {
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.");
// We truncate milliseconds.
long seconds = 1000l * (timestamp.getTime() / 1000l);
this.timestamp = new Date(seconds);
this.mark = mark;
this.notes = notes;
this.uuid = uuid;
}
public Date getTimestamp() { return timestamp; }
public String getMark() { return mark; }
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);
return val;
}
@Override
public boolean equals(Object o) {
if (o == null) return false;
if (!(o instanceof TimelineMarker)) return false;
TimelineMarker that = (TimelineMarker) o;
return this.uuid.equals(that.uuid);
}
@Override
public int hashCode() {
int hash = 7;
hash = 61 * hash + (this.timestamp != null ? this.timestamp.hashCode() : 0);
hash = 61 * hash + (this.mark != null ? this.mark.hashCode() : 0);
return hash;
}
}

View File

@ -0,0 +1,208 @@
package com.jdblabs.timestamper.core;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.jdbernard.util.SmartConfig;
/**
* Represents a Timeline configuration. A configuration has one primary, or
* local, Timeline with an associated TimelineSource, and a list of remote
* Timelines synched to the local Timeline.
* <br>
* <table>
* <tr><th>Property</th><th>Description</th></tr>
* <tr><td><code>timeline.uri</code></td><td>The URI of the primary (local)
* timeline</td></tr>
* <tr><td><code>timeline.persistOnUpdate?</code></td><td><code>true</code> to
* persist the local timeline when a new event is entered,
* <code>false</code> to disable.</td></tr>
* <tr><td><code>remote.timeline.</code><i>name</i><code>.uri</code></td>
* <td>The URI for the <i>name</i> remote timeline.</td></tr>
* <tr><td><code>remote.timeline.</code><i>name</i><code>.push?</code></td>
* <td><code>true</code> to enable pushing updates to the <i>name</i>
* remote timeline, <code>false</code> to disable.</td></tr>
* <tr><td><code>remote.timeline.</code><i>name</i><code>.pull?</code></td>
* <td><code>true</code> to enable pulling updates from the <i>name</i>
* remote timeline, <code>false</code> to disable.</td></tr>
* <tr><td><code>remote.timeline.</code><i>name</i><code>.syncOnExit?</code>
* </td><td><code>true</code> to force sync the <i>name</i> remote
* timeline on exit.</td></tr>
* <tr><td><code>remote.timeline.</code><i>name</i>
* <code>.updateInterval</code></td><td>The time in milliseconds between
* synching the <i>name</i> remote timeline.</td></tr></table>
* @author Jonathan Bernard ({@literal jdbernard@gmail.com})
* @see com.jdbernard.timestamper.core.Timeline
* @see com.jdbernard.timestamper.core.TimelineSource
* @see com.jdbernard.timestamper.core.SyncTarget
*/
public class TimelineProperties {
public static final String LOCAL_TIMELINE_URI = "timeline.uri";
public static final String LOCAL_TIMELINE_PERSIST_ON_UPDATE = "timeline.persistOnUpdate?";
public static final String REMOTE_TIMELINE_BASE = "remote.timeline.";
private static final Pattern remoteTimelinePropPattern =
Pattern.compile("\\Q" + REMOTE_TIMELINE_BASE + "\\E([^\\s\\.=]+?)[\\.=].*");
private SmartConfig config;
private Timeline timeline;
private TimelineSource timelineSource;
private LinkedList<SyncTarget> syncTargets = new LinkedList<SyncTarget>();
private boolean persistOnUpdate;
/**
* Create new TimelineProperties, using default values. This will create
* a new configuration using a FileTimelineSource pointed at
* <code>'timeline.default.txt'</code> in the current directory and no
* remote Timelines. It will save this configuration to
* <code>'timeline.default.properties'</code> in the current directory.
*/
public TimelineProperties() {
File propertyFile = new File("timeline.default.properties");
config = new SmartConfig(propertyFile);
File timelineFile = new File("timeline.default.txt");
URI timelineURI = timelineFile.toURI();
timeline = new Timeline();
timelineSource = TimelineSourceFactory.newInstance(timelineURI, config);
persistOnUpdate = true;
try { timelineSource.persist(timeline); }
catch (IOException ioe) {
// TODO
}
config.setProperty(LOCAL_TIMELINE_URI, timelineURI);
config.setProperty(LOCAL_TIMELINE_PERSIST_ON_UPDATE, persistOnUpdate);
}
/**
* Load TimelineProperties from a config file.
* @param propFile
*/
public TimelineProperties(File propFile) throws IOException {
String strURI;
URI timelineURI;
config = new SmartConfig(propFile);
// load persist on update information
persistOnUpdate = (Boolean) config.getProperty(
LOCAL_TIMELINE_PERSIST_ON_UPDATE, true);
// get the URI for the primary timeline
strURI = (String) config.getProperty(LOCAL_TIMELINE_URI, "");
// no primary timeline, default to file-based timeline
if ("".equals(strURI)) {
File defaultTimelineFile = new File("timeline.default.txt");
try {
if (!defaultTimelineFile.exists())
defaultTimelineFile.createNewFile();
} catch (IOException ioe) {
// TODO
}
timelineURI = defaultTimelineFile.toURI();
} else { // we do have a URI
try { timelineURI = new URI(strURI); }
catch (URISyntaxException urise) {
throw new IOException("Unable to load the timeline: the timeline "
+ "URI is invalid.", urise);
}
}
// create our timeline source and read the timeline
timelineSource = TimelineSourceFactory.newInstance(timelineURI, config);
timeline = timelineSource.read();
// search keys for remote timeline entries
for (Object keyObj : config.keySet()) {
if (!(keyObj instanceof String)) continue;
String key = (String) keyObj;
String stName;
String remoteBase;
SyncTarget st;
Matcher m = remoteTimelinePropPattern.matcher(key);
if (!m.matches()) continue;
stName = m.group(1);
// skip if we have already setup this remote sync
boolean stExists = false;
for (SyncTarget target : syncTargets)
if (target.getName().equals(stName))
stExists = true;
if (stExists) continue;
remoteBase = REMOTE_TIMELINE_BASE + stName;
strURI = (String) config.getProperty(remoteBase + ".uri", "");
try { timelineURI = new URI(strURI); }
catch (URISyntaxException urise) { /* TODO */ }
// add a new SyncTarget to the list
st = new SyncTarget(stName, TimelineSourceFactory
.newInstance(timelineURI, config), timeline);
syncTargets.add(st);
// check for synch options
st.setPullEnabled((Boolean)config.getProperty(
remoteBase + ".pull?", true));
st.setPushEnabled((Boolean)config.getProperty(
remoteBase + ".push?", true));
st.setSyncOnExit((Boolean)config.getProperty(
remoteBase + ".syncOnExit?", true));
st.setSyncInterval((Long)config.getProperty(
remoteBase + ".updateInterval", 1800000l)); // thirty minutes
}
}
public void save() throws IOException {
timelineSource.persist(timeline);
config.setProperty(LOCAL_TIMELINE_PERSIST_ON_UPDATE, persistOnUpdate);
config.setProperty(LOCAL_TIMELINE_URI, timelineSource.getURI());
for (SyncTarget st : syncTargets) {
String remoteBase = REMOTE_TIMELINE_BASE + st.getName();
config.setProperty(remoteBase + ".uri", st.getSource().getURI());
config.setProperty(remoteBase + ".pull?", st.getPullEnabled());
config.setProperty(remoteBase + ".push?", st.getPushEnabled());
config.setProperty(remoteBase + ".syncOnExit?", st.getSyncOnExit());
config.setProperty(remoteBase + ".updateInterval",
st.getSyncInterval());
}
}
public Timeline reloadTimeline() throws IOException {
timeline = timelineSource.read();
return timeline;
}
public Timeline getTimeline() { return timeline; }
public void setTimelineSource(TimelineSource newSource) {
this.timelineSource = newSource;
}
public TimelineSource getTimelineSource() { return timelineSource; }
public Collection<SyncTarget> getSyncTargets() { return syncTargets; }
public boolean getPersistOnUpdate() { return persistOnUpdate; }
public void setPersistOnUpdate(boolean persistOnUpdate) {
this.persistOnUpdate = persistOnUpdate;
if (persistOnUpdate) try { save(); } catch (IOException ioe) {}
}
}

View File

@ -0,0 +1,50 @@
package com.jdblabs.timestamper.core;
import java.io.IOException;
import java.net.URI;
/**
* A means of loading and persisting a Timeline.
* @author Jonathan Bernard (jdbernard@gmail.com)
*/
public abstract class TimelineSource {
protected final URI uri;
public TimelineSource(URI uri) { this.uri = uri; }
/**
* Read the Timeline from the source.
* @throws IOException
*/
public abstract Timeline read() throws IOException;
/**
* Persist a give timeline to this source.
* @param t
* @throws IOException
*/
public abstract void persist(Timeline t) throws IOException;
/**
* Is this source authenticated and ready for IO.
* @return <code>true</code> if the source is authenticated (or if no
* authentication is required).
*/
public abstract boolean isAuthenticated();
/**
* Authenticate the client to this source.
* @throws AuthenticationException
*/
public abstract void authenticate() throws AuthenticationException;
/**
* Get the URI representing this source.
* @return The {@link java.net.URI} representing this source.
*/
public URI getURI() {
return uri;
}
}

View File

@ -0,0 +1,44 @@
package com.jdblabs.timestamper.core;
import com.jdbernard.util.SmartConfig;
import java.io.File;
import java.net.URI;
/**
*
* @author Jonathan Bernard ({@literal jdbernard@gmail.com})
*/
public class TimelineSourceFactory {
public static TimelineSource newInstance(URI uri, SmartConfig config) {
// File based
if ("file".equalsIgnoreCase(uri.getScheme())) {
return new FileTimelineSource(uri, config);
}
// Twitter
else if ("http".equalsIgnoreCase(uri.getScheme()) &&
("twitter.com".equalsIgnoreCase(uri.getHost())
|| "www.twitter.com".equalsIgnoreCase(uri.getHost()))) {
throw new UnsupportedOperationException("Twitter based timeline "
+ "sources are not yet supported.");
}
// Other HTTP, assume JDB Labs web app
else if ("http".equalsIgnoreCase(uri.getScheme())) {
return new JDBLabsWebTimelineSource(uri, config);
}
// SSH
else if ("ssh".equalsIgnoreCase(uri.getScheme())) {
throw new UnsupportedOperationException("SSH based timeline sources"
+ " are not yet supported.");
}
// unknown
else {
throw new UnsupportedOperationException("Timeline sources for the"
+ " " + uri.getScheme() + " are not currently supported.");
}
}
}

View File

@ -0,0 +1,35 @@
package com.jdblabs.timestamper.core;
import com.jdbernard.util.SmartConfig;
import java.io.IOException;
import java.net.URI;
/**
*
* @author Jonathan Bernard ({@literal jdbernard@gmail.com})
*/
public class TwitterTimelineSource extends TimelineSource {
private String username;
private char[] password;
public TwitterTimelineSource(URI uri) {
super(uri);
}
public Timeline read() throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
public void persist(Timeline t) throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
public boolean isAuthenticated() {
throw new UnsupportedOperationException("Not supported yet.");
}
public void authenticate() throws AuthenticationException {
throw new UnsupportedOperationException("Not supported yet.");
}
}

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)