Finished version 2.1.
Updated .gitignore, excluding vim temp filesn and build directories. Promoted version number in application.properties. Switched to SLF4J for logging from LOG4J. Switched to jdb-util SmartConfig for configuration from java.util.Properties Added plugin architecture: * Plugins implement com.jdbernard.timestamper.gui.plugin.TimestamperPlugin * Provides five hooks into the application: - onStartup: called as the aaplication is starting. - onExit: called as the application is exiting. - onTimelineLoad: called when the application loads a timeline. - onNewTask: called when the user creates a new task. - onDeleteTask: called when the user deletes a task. * Plugins must be on the classpath to be enabled. * A new timestamperrc property allows the user to specify a plugin directory. That directory and any JAR files in it will be added to the classpath used to load plugins. This property is 'plugin.dir' and may contain the path, If no directory exists at that path, one will be created. The default path is './plugins'. * A new timestamperrc property allows the user to specify which plugins to load when the application is started: 'plugin.classes'. It expects a comma-seperated list of plugin class names. The default value is ''. * Two default plugins have been created: - 'com.jdbernard.timestamper.gui.plugin.HookLogger'. This plugin logs an info message every time one of the plugin hooks is called. - 'com.jdbernard.timestamper.gui.plugin.XMPPStatusUpdater'. This plugin updates a user's XMPP (Jabber) presence with their current task each time a new task is entered. Various other changes on TimeStamperMainController: * Timeline loading is now broken out into a load() closure. * Plugin hooks described above added at appropriate places. * Added a general wrapPluginCall method to catch any exceptions from a plugin call and sanitize the return value. * The exitGracefully closure now hides the GUI before starting its shutdown sequence so the user and the OS are less likely to assume the app has hung. * Added the functionality for the 'persistOnUpdate' feature (introduced on TimelineProperties, below). Removed the automatically generated logging functions, traceIfEnabled and debugIfEnabled--which were redundant and pointless overhead. Added check box menu option on TimeStamperMainView for the 'persistOnUpdate' feature Changes on TimelineProperties: * Switched to using SmartConfig and renamed several properties: - remote.timeline.<name>.push -> remote.timeline.<name>.push? - remote.timeline.<name>.pull -> remote.timeline.<name>.pull? - remote.timeline.<name>.save-on-exit -> remote.timeline.<name>.syncOnExit? - remote.timeline.<name>.update-interval -> remote.timeline.<name>.updateInterval * Added a feature, 'persistOnUpdate'. If set to true, this signals the application that it should persist the Timeline on each update. * Added a property 'timeline.persistOnUpdate?'.
This commit is contained in:
parent
c964730f03
commit
d08d054fbe
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,5 @@
|
|||||||
release
|
release
|
||||||
|
dist/
|
||||||
|
staging/
|
||||||
|
temp/
|
||||||
|
*.sw?
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#Do not edit app.griffon.* properties, they may change automatically. DO NOT put application configuration in here, it is not the right place!
|
#Do not edit app.griffon.* properties, they may change automatically. DO NOT put application configuration in here, it is not the right place!
|
||||||
#Sun, 11 Apr 2010 16:00:38 +0200
|
#Sun, 11 Apr 2010 16:00:38 +0200
|
||||||
#Thu Apr 01 22:28:40 CDT 2010
|
#Thu Apr 01 22:28:40 CDT 2010
|
||||||
app.version=2.0
|
app.version=2.1
|
||||||
app.griffon.version=0.3
|
app.griffon.version=0.3
|
||||||
app.name=TimeStamper
|
app.name=TimeStamper
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import org.apache.log4j.Logger
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
onNewInstance = { klass, type, instance ->
|
onNewInstance = { klass, type, instance ->
|
||||||
instance.metaClass.logger = Logger.getLogger(klass.name)
|
instance.metaClass.logger = LoggerFactory.getLogger(klass.name)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.jdbernard.timestamper
|
package com.jdbernard.timestamper
|
||||||
|
|
||||||
|
import com.jdbernard.util.SmartConfig
|
||||||
import com.jdbernard.timestamper.core.TimelineMarker
|
import com.jdbernard.timestamper.core.TimelineMarker
|
||||||
import com.jdbernard.timestamper.core.TimelineProperties
|
import com.jdbernard.timestamper.core.TimelineProperties
|
||||||
|
|
||||||
@ -8,15 +9,16 @@ class TimeStamperMainController {
|
|||||||
def model
|
def model
|
||||||
def view
|
def view
|
||||||
|
|
||||||
|
def thisMVC
|
||||||
def syncTimers = [:]
|
def syncTimers = [:]
|
||||||
|
|
||||||
void mvcGroupInit(Map args) {
|
void mvcGroupInit(Map args) {
|
||||||
|
|
||||||
def configFile
|
def configFile
|
||||||
|
|
||||||
logger.traceIfEnabled {"Initializing TimeStamperMain MVC..."}
|
logger.trace("Initializing TimeStamperMain MVC...")
|
||||||
|
|
||||||
def thisMVC = ['model': model, 'view': view, 'controller': this]
|
thisMVC = ['model': model, 'view': view, 'controller': this]
|
||||||
|
|
||||||
model.notesDialogMVC = buildMVCGroup('NotesDialog', 'notesDialog',
|
model.notesDialogMVC = buildMVCGroup('NotesDialog', 'notesDialog',
|
||||||
'mainMVC': thisMVC)
|
'mainMVC': thisMVC)
|
||||||
@ -25,40 +27,74 @@ class TimeStamperMainController {
|
|||||||
'punchcardDialog', 'mainMVC': thisMVC)
|
'punchcardDialog', 'mainMVC': thisMVC)
|
||||||
|
|
||||||
// load application properties
|
// load application properties
|
||||||
Properties prop = new Properties()
|
|
||||||
String userHomeDir = System.getProperty('user.home')
|
String userHomeDir = System.getProperty('user.home')
|
||||||
configFile = new File(userHomeDir, ".timestamperrc")
|
configFile = new File(userHomeDir, ".timestamperrc")
|
||||||
if (!configFile.exists()) configFile.createNewFile()
|
|
||||||
|
|
||||||
logger.traceIfEnabled { "Reading configuration from "
|
logger.trace("Reading configuration from {}", configFile.canonicalPath)
|
||||||
+ "'${configFile.name}'"}
|
|
||||||
|
|
||||||
try { configFile.withInputStream { prop.load(it) } }
|
model.config = new SmartConfig(configFile)
|
||||||
catch (IOException ioe) {
|
|
||||||
logger.error('Unable to load configuration', ioe)
|
|
||||||
}
|
|
||||||
|
|
||||||
model.config = prop
|
|
||||||
|
|
||||||
// load the last used timeline file
|
// load the last used timeline file
|
||||||
String lastUsed = model.config.getProperty('lastUsed', null)
|
String lastUsed = model.config.lastUsed
|
||||||
if (lastUsed == null) {
|
if (lastUsed == "") {
|
||||||
lastUsed = 'timeline.default.properties'
|
lastUsed = 'timeline.default.properties'
|
||||||
model.config.setProperty('lastUsed', lastUsed)
|
model.config.setProperty('lastUsed', lastUsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.traceIfEnabled {"Reading Timeline properties from '${lastUsed}'"}
|
// load the plugin directory
|
||||||
|
File pluginDir = model.config.getProperty("pluginDir", new File("plugins"))
|
||||||
|
if (!pluginDir.exists()) pluginDir.mkdirs()
|
||||||
|
|
||||||
model.timelinePropertiesFile = new File(lastUsed)
|
logger.trace("Adding plugin classpath: '{}'", pluginDir.canonicalPath)
|
||||||
if (!model.timelinePropertiesFile.exists())
|
|
||||||
model.timelinePropertiesFile.createNewFile()
|
|
||||||
|
|
||||||
load(propertyFile)
|
def pluginLoader = new GroovyClassLoader(this.class.classLoader)
|
||||||
|
pluginLoader.addURL(pluginDir.toURI().toURL())
|
||||||
|
pluginDir.eachFileMatch(/.*\.jar/) { jarfile ->
|
||||||
|
pluginLoader.addURL(jarfile.toURI().toURL()) }
|
||||||
|
|
||||||
|
// instantiate plugins
|
||||||
|
model.config."plugin.classes".split(',').each { className -> try {
|
||||||
|
if (className.trim() == "") return
|
||||||
|
def pluginClass = pluginLoader.loadClass(className)
|
||||||
|
model.plugins <<pluginClass.newInstance()
|
||||||
|
|
||||||
|
logger.trace("Loaded plugin: {}", className)
|
||||||
|
} catch (ClassNotFoundException cnfe) {
|
||||||
|
logger.warn("Unable to load plugin ${className}: " +
|
||||||
|
"class not found.", cnfe)
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Unable to load plugin ${className}.", t)
|
||||||
|
} }
|
||||||
|
|
||||||
|
// run plugin startup hookd
|
||||||
|
model.plugins.each { plugin ->
|
||||||
|
wrapPluginCall { plugin.onStartup(thisMVC) } }
|
||||||
|
|
||||||
|
logger.trace("Reading Timeline properties from '{}'", lastUsed)
|
||||||
|
|
||||||
|
load(new File(lastUsed))
|
||||||
|
}
|
||||||
|
|
||||||
|
def wrapPluginCall(Closure c) {
|
||||||
|
try { c() } catch (Throwable t) {
|
||||||
|
logger.warn("Plugin threw an exception in ${functionName} " +
|
||||||
|
"when passed ${param}:", t)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def load = { File propertiesFile ->
|
def load = { File propertiesFile ->
|
||||||
|
// pass through plugins
|
||||||
|
propertiesFile = model.plugins.inject(propertiesFile) { file, plugin ->
|
||||||
|
// call plugin
|
||||||
|
def ret = wrapPluginCall { plugin.onTimelineLoad(thisMVC, file) }
|
||||||
|
// if the plugin call succeeded, pass the result, else pass
|
||||||
|
// the last one
|
||||||
|
ret ? ret : file
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
model.config.setProperty('lastUsed', propertiesFile.canonicalPath)
|
model.config.lastUsed = propertiesFile.canonicalPath
|
||||||
} catch (IOException ioe) { logger.error(ioe) }
|
} catch (IOException ioe) { logger.error(ioe) }
|
||||||
|
|
||||||
// load the properties file
|
// load the properties file
|
||||||
@ -77,34 +113,55 @@ class TimeStamperMainController {
|
|||||||
|
|
||||||
def exitGracefully = { evt = null ->
|
def exitGracefully = { evt = null ->
|
||||||
|
|
||||||
logger.traceIfEnabled {"Exiting gracefully."}
|
// hide the frame immediately
|
||||||
|
view.frame.visible = false
|
||||||
|
|
||||||
|
logger.trace("Exiting gracefully.")
|
||||||
|
|
||||||
// save config
|
// save config
|
||||||
logger.debugIfEnabled("Config: ${model.config}")
|
logger.debug("Config: {}", model.config)
|
||||||
logger.debugIfEnabled("Storing config to file: ${model.configFile.path}")
|
model.config.save()
|
||||||
try { model.configFile.withOutputStream { out ->
|
|
||||||
model.config.store(out, null) } }
|
|
||||||
catch (IOException ioe) {
|
|
||||||
logger.error("Unable to save the configuration file", ioe)
|
|
||||||
}
|
|
||||||
|
|
||||||
// save timeline and properties
|
// save timeline and properties
|
||||||
model.timelineProperties.save()
|
model.timelineProperties.save()
|
||||||
|
|
||||||
logger.traceIfEnabled {"Completed graceful shutdown."}
|
// call plugin exit hooks
|
||||||
|
model.plugins.each { plugin ->
|
||||||
|
wrapPluginCall { plugin.onExit(thisMVC) } }
|
||||||
|
|
||||||
|
logger.trace("Completed graceful shutdown.")
|
||||||
|
|
||||||
app.shutdown()
|
app.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
def newTask = { mark, notes = "No comments.", timestamp = new Date() ->
|
def newTask = { mark, notes = "No comments.", timestamp = new Date() ->
|
||||||
TimelineMarker tm = new TimelineMarker(timestamp, mark, notes)
|
TimelineMarker tm = new TimelineMarker(timestamp, mark, notes)
|
||||||
|
|
||||||
|
// pass through the plugins
|
||||||
|
tm = model.plugins.inject(tm) { marker, plugin ->
|
||||||
|
def ret = wrapPluginCall { plugin.onNewTask(thisMVC, marker) }
|
||||||
|
ret ? ret : marker
|
||||||
|
}
|
||||||
|
|
||||||
model.timeline.addMarker(tm)
|
model.timeline.addMarker(tm)
|
||||||
model.currentMarker = model.timeline.getLastMarker(new Date())
|
model.currentMarker = model.timeline.getLastMarker(new Date())
|
||||||
|
|
||||||
|
// auto-persist if enabled
|
||||||
|
if (model.timelineProperties.persistOnUpdate)
|
||||||
|
model.timelineProperties.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
def deleteTask = { marker ->
|
def deleteTask = { marker ->
|
||||||
|
// pass through the plugins
|
||||||
|
model.plugins.each { plugin ->
|
||||||
|
wrapPluginCall { plugin.onDeleteTask(thisMVC, marker) } }
|
||||||
|
|
||||||
model.timeline.removeMarker(marker)
|
model.timeline.removeMarker(marker)
|
||||||
model.currentMarker = model.timeline.getLastMarker(new Date())
|
model.currentMarker = model.timeline.getLastMarker(new Date())
|
||||||
|
|
||||||
|
// auto-persist if enabled
|
||||||
|
if (model.timelineProperties.persistOnUpdate)
|
||||||
|
model.timelineProperties.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,21 +19,6 @@
|
|||||||
|
|
||||||
import groovy.swing.SwingBuilder
|
import groovy.swing.SwingBuilder
|
||||||
import griffon.util.GriffonPlatformHelper
|
import griffon.util.GriffonPlatformHelper
|
||||||
import org.apache.log4j.Logger
|
|
||||||
|
|
||||||
GriffonPlatformHelper.tweakForNativePlatform(app)
|
GriffonPlatformHelper.tweakForNativePlatform(app)
|
||||||
SwingBuilder.lookAndFeel('system', 'nimbus', ['metal', [boldFonts: false]])
|
SwingBuilder.lookAndFeel('system', 'nimbus', ['metal', [boldFonts: false]])
|
||||||
|
|
||||||
Logger.metaClass.traceIfEnabled = { Closure c, Throwable t = null ->
|
|
||||||
if (delegate.isTraceEnabled()) {
|
|
||||||
if (t != null) delegate.trace(c.call(), t)
|
|
||||||
else delegate.trace(c.call())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.metaClass.debugIfEnabled = { Closure c, Throwable t = null ->
|
|
||||||
if (delegate.isDebugEnabled()) {
|
|
||||||
if (t != null) delegate.debug(c.call(), t)
|
|
||||||
else delegate.debug(c.call())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -4,6 +4,7 @@ import groovy.beans.Bindable
|
|||||||
import java.awt.Point
|
import java.awt.Point
|
||||||
import java.awt.Rectangle
|
import java.awt.Rectangle
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
import com.jdbernard.util.SmartConfig
|
||||||
import com.jdbernard.timestamper.core.Timeline
|
import com.jdbernard.timestamper.core.Timeline
|
||||||
import com.jdbernard.timestamper.core.TimelineMarker
|
import com.jdbernard.timestamper.core.TimelineMarker
|
||||||
import com.jdbernard.timestamper.core.TimelineProperties
|
import com.jdbernard.timestamper.core.TimelineProperties
|
||||||
@ -12,8 +13,9 @@ class TimeStamperMainModel {
|
|||||||
@Bindable TimelineMarker currentMarker
|
@Bindable TimelineMarker currentMarker
|
||||||
@Bindable Timeline timeline
|
@Bindable Timeline timeline
|
||||||
@Bindable TimelineProperties timelineProperties
|
@Bindable TimelineProperties timelineProperties
|
||||||
Properties config
|
SmartConfig config
|
||||||
File timelinePropertiesFile
|
|
||||||
|
List plugins = []
|
||||||
|
|
||||||
def notesDialogMVC
|
def notesDialogMVC
|
||||||
def punchcardDialogMVC
|
def punchcardDialogMVC
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
log4j.rootLogger=info, stdout, fileout
|
|
||||||
|
|
||||||
# set up stdout
|
|
||||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
|
||||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
|
||||||
log4j.appender.stdout.layout.ConversionPattern=[%t] %-5p - %m%n
|
|
||||||
|
|
||||||
# set up fileout
|
|
||||||
log4j.appender.fileout=org.apache.log4j.FileAppender
|
|
||||||
log4j.appender.fileout.File=TimeStamper.log
|
|
||||||
log4j.appender.fileout.layout=org.apache.log4j.PatternLayout
|
|
||||||
log4j.appender.fileout.layout.ConversionPattern=%5p [%t] - %m%n
|
|
20
griffon-app/resources/logback.groovy
Normal file
20
griffon-app/resources/logback.groovy
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
|
||||||
|
import ch.qos.logback.core.ConsoleAppender
|
||||||
|
import ch.qos.logback.core.FileAppender
|
||||||
|
import static ch.qos.logback.classic.Level.*
|
||||||
|
|
||||||
|
appender("CONSOLE", ConsoleAppender) {
|
||||||
|
encoder(PatternLayoutEncoder) {
|
||||||
|
pattern = "%level - %msg%n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appender("FILE", FileAppender) {
|
||||||
|
file="timestamper.log"
|
||||||
|
encoder(PatternLayoutEncoder) {
|
||||||
|
pattern = "%date %level %logger - %msg%n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root(WARN, ["CONSOLE"])
|
||||||
|
logger("com.jdbernard.*", TRACE, ["FILE"], false)
|
@ -6,5 +6,5 @@ dialog = dialog(new JDialog(model.mainMVC.view.frame),
|
|||||||
title: 'Error Messages...',
|
title: 'Error Messages...',
|
||||||
modal: false) {
|
modal: false) {
|
||||||
|
|
||||||
logger.traceIfEnabled { "Building LogDialog view." }
|
logger.trace( "Building LogDialog view." )
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ dialog = dialog(new JDialog(model.mainMVC.view.frame),
|
|||||||
return p
|
return p
|
||||||
} else return dialog.location })
|
} else return dialog.location })
|
||||||
) {
|
) {
|
||||||
logger.traceIfEnabled {'Building NotesDialog GUI'}
|
logger.trace('Building NotesDialog GUI')
|
||||||
panel(
|
panel(
|
||||||
border:lineBorder(color: Color.BLACK, thickness:1, parent:true),
|
border:lineBorder(color: Color.BLACK, thickness:1, parent:true),
|
||||||
layout: new MigLayout('insets 10 10 10 10, fill')
|
layout: new MigLayout('insets 10 10 10 10, fill')
|
||||||
|
@ -51,7 +51,7 @@ dialog = dialog(new JDialog(model.mainMVC.view.frame),
|
|||||||
return p
|
return p
|
||||||
} else return dialog.location })
|
} else return dialog.location })
|
||||||
) {
|
) {
|
||||||
logger.traceIfEnabled {'Building PunchcardDialog GUI'}
|
logger.trace('Building PunchcardDialog GUI')
|
||||||
panel(
|
panel(
|
||||||
border:lineBorder(color: Color.BLACK, thickness:1, parent:true),
|
border:lineBorder(color: Color.BLACK, thickness:1, parent:true),
|
||||||
layout: new MigLayout('fill, insets 10 10 10 10',
|
layout: new MigLayout('fill, insets 10 10 10 10',
|
||||||
|
@ -90,6 +90,11 @@ optionsMenu = popupMenu() {
|
|||||||
if (fileDialog.showOpenDialog(frame) ==
|
if (fileDialog.showOpenDialog(frame) ==
|
||||||
JFileChooser.APPROVE_OPTION)
|
JFileChooser.APPROVE_OPTION)
|
||||||
controller.load(fileDialog.selectedFile) })
|
controller.load(fileDialog.selectedFile) })
|
||||||
|
checkBoxMenuItem(text: 'Save on update?',
|
||||||
|
selected: bind(source: model, sourceProperty: 'timelineProperties',
|
||||||
|
sourceValue: { model.timelineProperties?.persistOnUpdate }),
|
||||||
|
actionPerformed: {
|
||||||
|
model.timelineProperties.persistOnUpdate = it.source.selected })
|
||||||
aboutMenuItem = checkBoxMenuItem(text: 'About...',
|
aboutMenuItem = checkBoxMenuItem(text: 'About...',
|
||||||
actionPerformed: { aboutDialog.visible = aboutMenuItem.selected })
|
actionPerformed: { aboutDialog.visible = aboutMenuItem.selected })
|
||||||
}
|
}
|
||||||
@ -107,7 +112,7 @@ frame = application(title:'TimeStamper',
|
|||||||
imageIcon('/appointment-new-16x16.png').image],
|
imageIcon('/appointment-new-16x16.png').image],
|
||||||
componentMoved: { evt -> model.absoluteLocation = frame.location }
|
componentMoved: { evt -> model.absoluteLocation = frame.location }
|
||||||
) {
|
) {
|
||||||
logger.traceIfEnabled {'Building TimeStamperMain GUI'}
|
logger.trace('Building TimeStamperMain GUI')
|
||||||
panel(
|
panel(
|
||||||
border:lineBorder(color:Color.BLACK, thickness:1, parent:true),
|
border:lineBorder(color:Color.BLACK, thickness:1, parent:true),
|
||||||
layout: new MigLayout('insets 0 5 0 0, fill','', '[]0[]0[]'),
|
layout: new MigLayout('insets 0 5 0 0, fill','', '[]0[]0[]'),
|
||||||
|
BIN
lib/jdb-util-1.1.jar
Normal file
BIN
lib/jdb-util-1.1.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
lib/logback-classic-0.9.26.jar
Normal file
BIN
lib/logback-classic-0.9.26.jar
Normal file
Binary file not shown.
BIN
lib/logback-core-0.9.26.jar
Normal file
BIN
lib/logback-core-0.9.26.jar
Normal file
Binary file not shown.
BIN
lib/slf4j-api-1.6.1.jar
Normal file
BIN
lib/slf4j-api-1.6.1.jar
Normal file
Binary file not shown.
BIN
lib/smack.jar
Normal file
BIN
lib/smack.jar
Normal file
Binary file not shown.
BIN
lib/smackx-jingle.jar
Normal file
BIN
lib/smackx-jingle.jar
Normal file
Binary file not shown.
BIN
lib/smackx.jar
Normal file
BIN
lib/smackx.jar
Normal file
Binary file not shown.
@ -1,20 +1,15 @@
|
|||||||
package com.jdbernard.timestamper.core;
|
package com.jdbernard.timestamper.core;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import com.jdbernard.util.SmartConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Timeline configuration. A configuration has one primary, or
|
* Represents a Timeline configuration. A configuration has one primary, or
|
||||||
@ -25,19 +20,22 @@ import java.util.regex.Pattern;
|
|||||||
* <tr><th>Property</th><th>Description</th></tr>
|
* <tr><th>Property</th><th>Description</th></tr>
|
||||||
* <tr><td><code>timeline.uri</code></td><td>The URI of the primary (local)
|
* <tr><td><code>timeline.uri</code></td><td>The URI of the primary (local)
|
||||||
* timeline</td></tr>
|
* 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>
|
* <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>
|
* <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>
|
* <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>
|
* <td><code>true</code> to enable pushing updates to the <i>name</i>
|
||||||
* remote timeline, <code>false</code> to disable.</td></tr>
|
* remote timeline, <code>false</code> to disable.</td></tr>
|
||||||
* <tr><td><code>remote.timeline.</code><i>name</i><code>.pull</code></td>
|
* <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>
|
* <td><code>true</code> to enable pulling updates from the <i>name</i>
|
||||||
* remote timeline, <code>false</code> to disable.</td></tr>
|
* remote timeline, <code>false</code> to disable.</td></tr>
|
||||||
* <tr><td><code>remote.timeline.</code><i>name</i><code>.save-on-exit</code>
|
* <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
|
* </td><td><code>true</code> to force sync the <i>name</i> remote
|
||||||
* timeline on exit.</td></tr>
|
* timeline on exit.</td></tr>
|
||||||
* <tr><td><code>remote.timeline.</code><i>name</i>
|
* <tr><td><code>remote.timeline.</code><i>name</i>
|
||||||
* <code>.update-interval</code></td><td>The time in milliseconds between
|
* <code>.updateInterval</code></td><td>The time in milliseconds between
|
||||||
* synching the <i>name</i> remote timeline.</td></tr></table>
|
* synching the <i>name</i> remote timeline.</td></tr></table>
|
||||||
* @author Jonathan Bernard ({@literal jonathan.bernard@gemalto.com})
|
* @author Jonathan Bernard ({@literal jonathan.bernard@gemalto.com})
|
||||||
* @see com.jdbernard.timestamper.core.Timeline
|
* @see com.jdbernard.timestamper.core.Timeline
|
||||||
@ -47,14 +45,17 @@ import java.util.regex.Pattern;
|
|||||||
public class TimelineProperties {
|
public class TimelineProperties {
|
||||||
|
|
||||||
public static final String LOCAL_TIMELINE_URI = "timeline.uri";
|
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.";
|
public static final String REMOTE_TIMELINE_BASE = "remote.timeline.";
|
||||||
|
|
||||||
private static final Pattern remoteTimelinePropPattern =
|
private static final Pattern remoteTimelinePropPattern =
|
||||||
Pattern.compile("\\Q" + REMOTE_TIMELINE_BASE + "\\E([^\\s\\.=]+?)[\\.=].*");
|
Pattern.compile("\\Q" + REMOTE_TIMELINE_BASE + "\\E([^\\s\\.=]+?)[\\.=].*");
|
||||||
|
|
||||||
|
private SmartConfig config;
|
||||||
private Timeline timeline;
|
private Timeline timeline;
|
||||||
private TimelineSource timelineSource;
|
private TimelineSource timelineSource;
|
||||||
private LinkedList<SyncTarget> syncTargets = new LinkedList<SyncTarget>();
|
private LinkedList<SyncTarget> syncTargets = new LinkedList<SyncTarget>();
|
||||||
|
private boolean persistOnUpdate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new TimelineProperties, using default values. This will create
|
* Create new TimelineProperties, using default values. This will create
|
||||||
@ -65,43 +66,39 @@ public class TimelineProperties {
|
|||||||
*/
|
*/
|
||||||
public TimelineProperties() {
|
public TimelineProperties() {
|
||||||
File propertyFile = new File("timeline.default.properties");
|
File propertyFile = new File("timeline.default.properties");
|
||||||
Properties config = new Properties();
|
config = new SmartConfig(propertyFile);
|
||||||
|
|
||||||
File timelineFile = new File("timeline.default.txt");
|
File timelineFile = new File("timeline.default.txt");
|
||||||
URI timelineURI = timelineFile.toURI();
|
URI timelineURI = timelineFile.toURI();
|
||||||
|
|
||||||
timeline = new Timeline();
|
timeline = new Timeline();
|
||||||
timelineSource = TimelineSourceFactory.newInstance(timelineURI);
|
timelineSource = TimelineSourceFactory.newInstance(timelineURI);
|
||||||
|
persistOnUpdate = true;
|
||||||
try { timelineSource.persist(timeline); }
|
try { timelineSource.persist(timeline); }
|
||||||
catch (IOException ioe) {
|
catch (IOException ioe) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
config.setProperty(LOCAL_TIMELINE_URI, timelineURI.toString());
|
config.setProperty(LOCAL_TIMELINE_URI, timelineURI);
|
||||||
|
config.setProperty(LOCAL_TIMELINE_PERSIST_ON_UPDATE, persistOnUpdate);
|
||||||
try { config.store(new FileOutputStream(propertyFile), ""); }
|
|
||||||
catch (IOException ioe) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load TimelineProperties from an InputStream.
|
* Load TimelineProperties from an InputStream.
|
||||||
* @param is
|
* @param is
|
||||||
*/
|
*/
|
||||||
public TimelineProperties(InputStream is) throws IOException {
|
public TimelineProperties(File propFile) throws IOException {
|
||||||
String strURI;
|
String strURI;
|
||||||
URI timelineURI;
|
URI timelineURI;
|
||||||
|
|
||||||
Properties config = new Properties();
|
config = new SmartConfig(propFile);
|
||||||
try {
|
|
||||||
config.load(new InputStreamReader(is));
|
// load persist on update information
|
||||||
} catch (IOException ioe) {
|
persistOnUpdate = (Boolean) config.getProperty(
|
||||||
// TODO
|
LOCAL_TIMELINE_PERSIST_ON_UPDATE, true);
|
||||||
}
|
|
||||||
|
|
||||||
// load local timeline
|
// load local timeline
|
||||||
strURI = config.getProperty(LOCAL_TIMELINE_URI, "");
|
strURI = (String) config.getProperty(LOCAL_TIMELINE_URI, "");
|
||||||
if ("".equals(strURI)) {
|
if ("".equals(strURI)) {
|
||||||
File defaultTimelineFile = new File("timeline.default.txt");
|
File defaultTimelineFile = new File("timeline.default.txt");
|
||||||
try {
|
try {
|
||||||
@ -123,6 +120,9 @@ public class TimelineProperties {
|
|||||||
timeline = timelineSource.read();
|
timeline = timelineSource.read();
|
||||||
|
|
||||||
// search keys for remote timeline entries
|
// 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()) {
|
for (Object keyObj : config.keySet()) {
|
||||||
if (!(keyObj instanceof String)) continue;
|
if (!(keyObj instanceof String)) continue;
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ public class TimelineProperties {
|
|||||||
stName = m.group(1);
|
stName = m.group(1);
|
||||||
remoteBase = REMOTE_TIMELINE_BASE + stName;
|
remoteBase = REMOTE_TIMELINE_BASE + stName;
|
||||||
|
|
||||||
strURI = config.getProperty(remoteBase + ".uri", "");
|
strURI = (String) config.getProperty(remoteBase + ".uri", "");
|
||||||
try { timelineURI = new URI(strURI); }
|
try { timelineURI = new URI(strURI); }
|
||||||
catch (URISyntaxException urise) { /* TODO */ }
|
catch (URISyntaxException urise) { /* TODO */ }
|
||||||
|
|
||||||
@ -147,44 +147,32 @@ public class TimelineProperties {
|
|||||||
syncTargets.add(st);
|
syncTargets.add(st);
|
||||||
|
|
||||||
// check for synch options
|
// check for synch options
|
||||||
st.setPullEnabled(Boolean.parseBoolean(
|
st.setPullEnabled((Boolean)config.getProperty(
|
||||||
config.getProperty(remoteBase + ".pull", "true")));
|
remoteBase + ".pull?", true));
|
||||||
st.setPushEnabled(Boolean.parseBoolean(
|
st.setPushEnabled((Boolean)config.getProperty(
|
||||||
config.getProperty(remoteBase + ".push", "true")));
|
remoteBase + ".push?", true));
|
||||||
st.setSyncOnExit(Boolean.parseBoolean(
|
st.setSyncOnExit((Boolean)config.getProperty(
|
||||||
config.getProperty(remoteBase + ".sync-on-exit", "true")));
|
remoteBase + ".syncOnExit?", true));
|
||||||
st.setSyncInterval(Long.parseLong(
|
st.setSyncInterval((Long)config.getProperty(
|
||||||
config.getProperty(remoteBase + ".update-interval",
|
remoteBase + ".updateInterval", 1800000l)); // thirty minutes
|
||||||
"1800000"))); // thirty minutes
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save(OutputStream os) throws IOException {
|
public void save() throws IOException {
|
||||||
Properties config = new Properties();
|
|
||||||
timelineSource.persist(timeline);
|
timelineSource.persist(timeline);
|
||||||
|
|
||||||
config.setProperty(LOCAL_TIMELINE_URI,
|
config.setProperty(LOCAL_TIMELINE_PERSIST_ON_UPDATE, persistOnUpdate);
|
||||||
timelineSource.getURI().toString());
|
config.setProperty(LOCAL_TIMELINE_URI, timelineSource.getURI());
|
||||||
|
|
||||||
for (SyncTarget st : syncTargets) {
|
for (SyncTarget st : syncTargets) {
|
||||||
String remoteBase = REMOTE_TIMELINE_BASE + st.getName();
|
String remoteBase = REMOTE_TIMELINE_BASE + st.getName();
|
||||||
config.setProperty(remoteBase + ".uri",
|
config.setProperty(remoteBase + ".uri", st.getSource().getURI());
|
||||||
st.getSource().getURI().toString());
|
config.setProperty(remoteBase + ".pull?", st.getPullEnabled());
|
||||||
config.setProperty(remoteBase + ".pull",
|
config.setProperty(remoteBase + ".push?", st.getPushEnabled());
|
||||||
Boolean.toString(st.getPullEnabled()));
|
config.setProperty(remoteBase + ".syncOnExit?", st.getSyncOnExit());
|
||||||
config.setProperty(remoteBase + ".push",
|
config.setProperty(remoteBase + ".updateInterval",
|
||||||
Boolean.toString(st.getPushEnabled()));
|
st.getSyncInterval());
|
||||||
config.setProperty(remoteBase + ".sync-on-exit",
|
|
||||||
Boolean.toString(st.getSyncOnExit()));
|
|
||||||
config.setProperty(remoteBase + ".update-interval",
|
|
||||||
Long.toString(st.getSyncInterval()));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
config.store(os, "");
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
// TODO
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,4 +185,11 @@ public class TimelineProperties {
|
|||||||
public TimelineSource getTimelineSource() { return timelineSource; }
|
public TimelineSource getTimelineSource() { return timelineSource; }
|
||||||
|
|
||||||
public Collection<SyncTarget> getSyncTargets() { return syncTargets; }
|
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) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.jdbernard.timestamper.gui.plugin
|
||||||
|
|
||||||
|
import com.jdbernard.timestamper.core.TimelineMarker
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
public class HookLogger implements TimestamperPlugin {
|
||||||
|
|
||||||
|
private Logger logger = LoggerFactory.getLogger(getClass())
|
||||||
|
|
||||||
|
public void onStartup(Map mvc) {
|
||||||
|
logger.info("HookLogger: onStartup() called.")
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onExit(Map mvc) {
|
||||||
|
logger.info("HookLogger: onExit() called.")
|
||||||
|
}
|
||||||
|
|
||||||
|
public File onTimelineLoad(Map mvc, File file) {
|
||||||
|
logger.info("HookLogger: onTimelineLoad() called with ${file.canonicalPath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimelineMarker onNewTask(Map mvc, TimelineMarker tm) {
|
||||||
|
logger.info("HookLogger: onNewTask() called with ${tm}")
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDeleteTask(Map mvc, TimelineMarker tm) {
|
||||||
|
logger.info("HookLogger: onDeleteTask() called with ${tm}")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package com.jdbernard.timestamper.gui.plugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.jdbernard.timestamper.core.TimelineMarker;
|
||||||
|
|
||||||
|
public interface TimestamperPlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before the GUI loads a new timeline properties file. This method
|
||||||
|
* allows you to perform an action before a timeline properties file is
|
||||||
|
* loaded, or to change the file that will be loaded by returning a new
|
||||||
|
* File object.
|
||||||
|
* @param mainMVC Map of the model, view, and controller objects for the
|
||||||
|
* timestamper main GUI.
|
||||||
|
* @param propertyFile The file that will be loaded.
|
||||||
|
* @return The property file to load (often the same as the one passed in)
|
||||||
|
*/
|
||||||
|
File onTimelineLoad(Map<String, Object> mainMVC, File propertyFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the GUI is initialized but before it is displayed.
|
||||||
|
* @param mainMVC map of the model, view, and controller objects for the
|
||||||
|
* timestamper main GUI.
|
||||||
|
*/
|
||||||
|
void onStartup(Map<String, Object> mainMVC);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before the GUI exits. This is a best-attempt call, since the OS
|
||||||
|
* may force-terminate the application before it has a chance to be called.
|
||||||
|
* Long-running operations should not be performed in this call, as some
|
||||||
|
* OSes may impatiently terminate the application if it thinks it has hung.
|
||||||
|
* @param mainMVC map of the model, view, and controller objects for the
|
||||||
|
* timestamper main GUI.
|
||||||
|
*/
|
||||||
|
void onExit(Map<String, Object> mainMVC);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before the GUI adds a new marker to the timeline.
|
||||||
|
* This allows you to perform some action when a new marker is created,
|
||||||
|
* modify the marker, or return an entirely new marker.
|
||||||
|
* @param mainMVC map of the model, view, and controller objects for the
|
||||||
|
* timestamper main GUI.
|
||||||
|
* @param newMarker The new TimelineMarker to be added to the timeline.
|
||||||
|
* @return The timeline marker to add to the timeline (often the same
|
||||||
|
* marker passed in).
|
||||||
|
*/
|
||||||
|
TimelineMarker onNewTask(Map<String, Object> mainMVC, TimelineMarker newMarker);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before the GUI deletes a marker from the timeline.
|
||||||
|
* @param mainMVC map of the model, view, and controller objects for the
|
||||||
|
* timestamper main GUI.
|
||||||
|
* @param marker The TimelineMarker to be deleted.
|
||||||
|
*/
|
||||||
|
void onDeleteTask(Map<String, Object> mainMVC, TimelineMarker marker);
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package com.jdbernard.timestamper.gui.plugin
|
||||||
|
|
||||||
|
import com.jdbernard.timestamper.core.TimelineMarker
|
||||||
|
import org.jivesoftware.smack.ConnectionConfiguration
|
||||||
|
import org.jivesoftware.smack.XMPPConnection
|
||||||
|
import org.jivesoftware.smack.packet.Presence
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
public class XMPPStatusUpdater implements TimestamperPlugin {
|
||||||
|
|
||||||
|
XMPPConnection conn
|
||||||
|
private Logger logger = LoggerFactory.getLogger(getClass())
|
||||||
|
|
||||||
|
public void onStartup(Map mainMVC) {
|
||||||
|
|
||||||
|
logger.trace("Starting XMPPStatusUpdater plugin")
|
||||||
|
|
||||||
|
String server = mainMVC.model.config."xmpp.server"
|
||||||
|
int port = mainMVC.model.config.getProperty("xmpp.port", 5222)
|
||||||
|
String username = mainMVC.model.config."xmpp.username"
|
||||||
|
String password = mainMVC.model.config."xmpp.password"
|
||||||
|
|
||||||
|
try {
|
||||||
|
conn = new XMPPConnection(new ConnectionConfiguration(server, port))
|
||||||
|
conn.connect()
|
||||||
|
conn.login(username, password, "timestamper-xmpp-plugin")
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unable to initialize XMPPUpdater.", e)
|
||||||
|
conn = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onExit(Map mainMVC) {
|
||||||
|
logger.trace("Stopping XMPPStatusUpdater plugin")
|
||||||
|
|
||||||
|
try { conn.disconnect() } catch (Exception e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public File onTimelineLoad(Map mainMVC, File propFile) {}
|
||||||
|
|
||||||
|
public TimelineMarker onNewTask(Map mainMVC, TimelineMarker newTask) {
|
||||||
|
if (!conn) {
|
||||||
|
logger.info("XMPPStatusUpdater is not initialized, " +
|
||||||
|
"skipping onNewTask()")
|
||||||
|
return newTask
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("Setting XMPP presence based on new task: {}", newTask)
|
||||||
|
|
||||||
|
conn.sendPacket(new Presence(Presence.Type.available, newTask.mark,
|
||||||
|
0, Presence.Mode.available))
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDeleteTask(Map mainMVC, TimelineMarker task) {}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user