From 65735030942922e88b54b3340a4eccaa22d64cdb Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Thu, 16 Jun 2011 12:09:56 -0500 Subject: [PATCH] Initial commit of the TimeStamper Java core library. This library grew out of the TimeStamper GUI application. Earlier repository history can be found there. --- .gitignore | 2 + build.xml | 7 + jdb-build-1.6.xml | 201 +++++++++++++++++ project.properties | 5 + .../core/AuthenticationException.java | 21 ++ .../timestamper/core/FileTimelineSource.java | 64 ++++++ .../core/StreamBasedTimelineSource.java | 203 ++++++++++++++++++ .../jdblabs/timestamper/core/SyncTarget.java | 132 ++++++++++++ .../jdblabs/timestamper/core/Timeline.java | 128 +++++++++++ .../timestamper/core/TimelineMarker.java | 63 ++++++ .../timestamper/core/TimelineProperties.java | 195 +++++++++++++++++ .../timestamper/core/TimelineSource.java | 50 +++++ .../core/TimelineSourceFactory.java | 38 ++++ .../core/TwitterTimelineSource.java | 34 +++ 14 files changed, 1143 insertions(+) create mode 100644 .gitignore create mode 100644 build.xml create mode 100644 jdb-build-1.6.xml create mode 100644 project.properties create mode 100644 src/main/com/jdblabs/timestamper/core/AuthenticationException.java create mode 100644 src/main/com/jdblabs/timestamper/core/FileTimelineSource.java create mode 100644 src/main/com/jdblabs/timestamper/core/StreamBasedTimelineSource.java create mode 100644 src/main/com/jdblabs/timestamper/core/SyncTarget.java create mode 100644 src/main/com/jdblabs/timestamper/core/Timeline.java create mode 100644 src/main/com/jdblabs/timestamper/core/TimelineMarker.java create mode 100644 src/main/com/jdblabs/timestamper/core/TimelineProperties.java create mode 100644 src/main/com/jdblabs/timestamper/core/TimelineSource.java create mode 100644 src/main/com/jdblabs/timestamper/core/TimelineSourceFactory.java create mode 100644 src/main/com/jdblabs/timestamper/core/TwitterTimelineSource.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1cb804 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.sw? +build/ diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..b2cf664 --- /dev/null +++ b/build.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/jdb-build-1.6.xml b/jdb-build-1.6.xml new file mode 100644 index 0000000..9cc8514 --- /dev/null +++ b/jdb-build-1.6.xml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project.properties b/project.properties new file mode 100644 index 0000000..f5053b3 --- /dev/null +++ b/project.properties @@ -0,0 +1,5 @@ +name=timestamper-lib +version=0.1 +lib.local=true + +build.number=0 diff --git a/src/main/com/jdblabs/timestamper/core/AuthenticationException.java b/src/main/com/jdblabs/timestamper/core/AuthenticationException.java new file mode 100644 index 0000000..8a2a402 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/AuthenticationException.java @@ -0,0 +1,21 @@ +package com.jdbernard.timestamper.core; + +import java.io.IOException; + +/** + * + * @author Jonathan Bernard ({@literal jonathan.bernard@gemalto.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); + } + +} diff --git a/src/main/com/jdblabs/timestamper/core/FileTimelineSource.java b/src/main/com/jdblabs/timestamper/core/FileTimelineSource.java new file mode 100644 index 0000000..61259f6 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/FileTimelineSource.java @@ -0,0 +1,64 @@ +package com.jdbernard.timestamper.core; + +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 jonathan.bernard@gemalto.com}) + */ +public class FileTimelineSource extends TimelineSource { + + private File file; + + public FileTimelineSource(URI uri) { + 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() + "."); + + } +} diff --git a/src/main/com/jdblabs/timestamper/core/StreamBasedTimelineSource.java b/src/main/com/jdblabs/timestamper/core/StreamBasedTimelineSource.java new file mode 100644 index 0000000..cbcdae1 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/StreamBasedTimelineSource.java @@ -0,0 +1,203 @@ +package com.jdbernard.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; + +/** + * + * @author Jonathan Bernard ({@literal jonathan.bernard@gemalto.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 byte[] 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 + out.write(Timeline.longFormat.format(tm.getTimestamp()) + "\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 Timeline instance from a given stream. + * @param stream The stream to read from. + * @return A new Timeline 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 Timeline 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 null, in which case comments are ignored. + * @return A new Timeline 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 d = 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); + continue; // don't parse this line as part of the timeline + } + + switch (readingState) { + + case NewMarker: + try { d = Timeline.longFormat.parse(line); } + 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()); + } + 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'); + timeline.addMarker(d, sMark, sNotes); + readingState = ReadingState.NewMarker; + } + } + + return timeline; + } +} diff --git a/src/main/com/jdblabs/timestamper/core/SyncTarget.java b/src/main/com/jdblabs/timestamper/core/SyncTarget.java new file mode 100644 index 0000000..b7b07c8 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/SyncTarget.java @@ -0,0 +1,132 @@ +package com.jdbernard.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 (jonathan.bernard@gemalto.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 true if the two timelines were out of sync and have + * been put into synch, false 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 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 diffFromLocal = + localTimeline.difference(remoteTimeline); + + if (diffFromLocal.size() != 0) { + // add the difference to the remote timeline + remoteTimeline.addAll(diffFromLocal); + syncPerformed = true; + } + } + + return syncPerformed; + } + + 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; } +} diff --git a/src/main/com/jdblabs/timestamper/core/Timeline.java b/src/main/com/jdblabs/timestamper/core/Timeline.java new file mode 100644 index 0000000..4fd6c93 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/Timeline.java @@ -0,0 +1,128 @@ +package com.jdbernard.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 } + * @see com.jdbernard.timestamper.core.TimelineSource + */ +public class Timeline implements Iterable { + + public static SimpleDateFormat shortFormat = new SimpleDateFormat("HH:mm:ss"); + public static SimpleDateFormat longFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy"); + private TreeSet timelineList; + + /** + * Create a new, empty Timeline. + */ + public Timeline() { + timelineList = new TreeSet(); + } + + /** + * Add a marker to the timeline. + * @param tm The TimelineMarker to add. + * @return true 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 true if this Timeline was modified. + */ + public boolean addMarker(Date timestamp, String name, String notes) { + return timelineList.add(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 true if this Timeline was changed. + */ + public boolean removeMarker(TimelineMarker marker) { + return timelineList.remove(marker); + } + + public Iterator 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 this timeline but not present in the given timeline. + * timeline. + * @param t + * @return A collection representing the TimelineMarkers present in + * this timeline but not in the given timeline. + */ + public Collection difference(Timeline t) { + TreeSet difference = new TreeSet(); + + for (TimelineMarker tm : timelineList) { + if (!t.timelineList.contains(tm)) + difference.add(tm); + } + + return difference; + } + + /** + * Add all TimelineMarkers from t to this + * Timeline, excluding markers already present in this. + * @param t + * @return true if this Timeline was modified. + */ + public boolean addAll(Timeline t) { + boolean modified = false; + for (TimelineMarker tm : t) { + if (!timelineList.contains(tm)) { + timelineList.add(tm); + modified = true; + } + } + + return modified; + } + + /** + * Add all TimelineMarkers from c to this + * Timeline, excluding markers already present in this. + * @param c A Collection of TimelineMarkers + * @return true if this TImeline was modified. + */ + public boolean addAll(Collection c) { + return timelineList.addAll(c); + } +} diff --git a/src/main/com/jdblabs/timestamper/core/TimelineMarker.java b/src/main/com/jdblabs/timestamper/core/TimelineMarker.java new file mode 100644 index 0000000..53378f1 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/TimelineMarker.java @@ -0,0 +1,63 @@ +package com.jdbernard.timestamper.core; + +import java.util.Date; + +/** +* @author Jonathan Bernard {@literal } +* 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 { + + private final Date timestamp; + private final String mark; + private String notes; + + public TimelineMarker(Date timestamp, String mark, String notes) { + if (timestamp == null || mark == null) + throw new IllegalArgumentException("Null timestamp or mark" + + " is not permitted."); + + this.timestamp = timestamp; + this.mark = mark; + this.notes = notes; + } + + public Date getTimestamp() { return timestamp; } + + public String getMark() { return mark; } + + public String getNotes() { return notes; } + + public void setNotes(String notes) { this.notes = notes; } + + @Override + public int compareTo(TimelineMarker that) { + if (that == null) return Integer.MAX_VALUE; + + 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.timestamp.equals(that.timestamp) && + this.mark.equals(that.mark); + } + + @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; + } + +} + diff --git a/src/main/com/jdblabs/timestamper/core/TimelineProperties.java b/src/main/com/jdblabs/timestamper/core/TimelineProperties.java new file mode 100644 index 0000000..23d6acd --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/TimelineProperties.java @@ -0,0 +1,195 @@ +package com.jdbernard.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. + *
+ * + * + * + * + * + * + * + * + * + * + * + *
PropertyDescription
timeline.uriThe URI of the primary (local) + * timeline
timeline.persistOnUpdate?true to + * persist the local timeline when a new event is entered, + * false to disable.
remote.timeline.name.uriThe URI for the name remote timeline.
remote.timeline.name.push?true to enable pushing updates to the name + * remote timeline, false to disable.
remote.timeline.name.pull?true to enable pulling updates from the name + * remote timeline, false to disable.
remote.timeline.name.syncOnExit? + * true to force sync the name remote + * timeline on exit.
remote.timeline.name + * .updateIntervalThe time in milliseconds between + * synching the name remote timeline.
+ * @author Jonathan Bernard ({@literal jonathan.bernard@gemalto.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 syncTargets = new LinkedList(); + private boolean persistOnUpdate; + + /** + * Create new TimelineProperties, using default values. This will create + * a new configuration using a FileTimelineSource pointed at + * 'timeline.default.txt' in the current directory and no + * remote Timelines. It will save this configuration to + * 'timeline.default.properties' 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); + 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 an InputStream. + * @param is + */ + 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); + + // load local timeline + strURI = (String) config.getProperty(LOCAL_TIMELINE_URI, ""); + if ("".equals(strURI)) { + File defaultTimelineFile = new File("timeline.default.txt"); + try { + if (!defaultTimelineFile.exists()) + defaultTimelineFile.createNewFile(); + } catch (IOException ioe) { + // TODO + } + timelineURI = defaultTimelineFile.toURI(); + } else { + try { timelineURI = new URI(strURI); } + catch (URISyntaxException urise) { + throw new IOException("Unable to load the timeline: the timeline " + + "URI is invalid.", urise); + } + } + + timelineSource = TimelineSourceFactory.newInstance(timelineURI); + timeline = timelineSource.read(); + + // search keys for remote timeline entries + // TODO: this code will add a new sync object for every remote target + // property, regardless of whether the SyncTarget for that remote URI + // already exists + 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); + 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), 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 getTimeline() { return timeline; } + + public void setTimelineSource(TimelineSource newSource) { + this.timelineSource = newSource; + } + + public TimelineSource getTimelineSource() { return timelineSource; } + + public Collection getSyncTargets() { return syncTargets; } + + public boolean getPersistOnUpdate() { return persistOnUpdate; } + + public void setPersistOnUpdate(boolean persistOnUpdate) { + this.persistOnUpdate = persistOnUpdate; + if (persistOnUpdate) try { save(); } catch (IOException ioe) {} + } +} diff --git a/src/main/com/jdblabs/timestamper/core/TimelineSource.java b/src/main/com/jdblabs/timestamper/core/TimelineSource.java new file mode 100644 index 0000000..de85445 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/TimelineSource.java @@ -0,0 +1,50 @@ +package com.jdbernard.timestamper.core; + +import java.io.IOException; +import java.net.URI; + +/** + * A means of loading and persisting a Timeline. + * @author Jonathan Bernard (jonathan.bernard@gemalto.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 true 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; + } + +} diff --git a/src/main/com/jdblabs/timestamper/core/TimelineSourceFactory.java b/src/main/com/jdblabs/timestamper/core/TimelineSourceFactory.java new file mode 100644 index 0000000..44ac320 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/TimelineSourceFactory.java @@ -0,0 +1,38 @@ +package com.jdbernard.timestamper.core; + +import java.io.File; +import java.net.URI; + +/** + * + * @author Jonathan Bernard ({@literal jonathan.bernard@gemalto.com}) + */ +public class TimelineSourceFactory { + + public static TimelineSource newInstance(URI uri) { + // File based + if ("file".equalsIgnoreCase(uri.getScheme())) { + return new FileTimelineSource(uri); + } + + // 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."); + } + + // 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."); + } + } +} diff --git a/src/main/com/jdblabs/timestamper/core/TwitterTimelineSource.java b/src/main/com/jdblabs/timestamper/core/TwitterTimelineSource.java new file mode 100644 index 0000000..2544048 --- /dev/null +++ b/src/main/com/jdblabs/timestamper/core/TwitterTimelineSource.java @@ -0,0 +1,34 @@ +package com.jdbernard.timestamper.core; + +import java.io.IOException; +import java.net.URI; + +/** + * + * @author Jonathan Bernard ({@literal jonathan.bernard@gemalto.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."); + } +}