diff --git a/.gitignore b/.gitignore index d702569..e119628 100755 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ release +dist/ +staging/ +temp/ +*.sw? diff --git a/application.properties b/application.properties index 168a320..1f36118 100755 --- a/application.properties +++ b/application.properties @@ -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! #Sun, 11 Apr 2010 16:00:38 +0200 #Thu Apr 01 22:28:40 CDT 2010 -app.version=2.0 +app.version=2.1 app.griffon.version=0.3 app.name=TimeStamper diff --git a/griffon-app/conf/Events.groovy b/griffon-app/conf/Events.groovy index 8f2914c..de9a54e 100644 --- a/griffon-app/conf/Events.groovy +++ b/griffon-app/conf/Events.groovy @@ -1,5 +1,5 @@ -import org.apache.log4j.Logger +import org.slf4j.LoggerFactory onNewInstance = { klass, type, instance -> - instance.metaClass.logger = Logger.getLogger(klass.name) + instance.metaClass.logger = LoggerFactory.getLogger(klass.name) } diff --git a/griffon-app/controllers/com/jdbernard/timestamper/TimeStamperMainController.groovy b/griffon-app/controllers/com/jdbernard/timestamper/TimeStamperMainController.groovy index 221827a..bdfb2fc 100644 --- a/griffon-app/controllers/com/jdbernard/timestamper/TimeStamperMainController.groovy +++ b/griffon-app/controllers/com/jdbernard/timestamper/TimeStamperMainController.groovy @@ -1,5 +1,6 @@ package com.jdbernard.timestamper +import com.jdbernard.util.SmartConfig import com.jdbernard.timestamper.core.TimelineMarker import com.jdbernard.timestamper.core.TimelineProperties @@ -8,15 +9,16 @@ class TimeStamperMainController { def model def view + def thisMVC def syncTimers = [:] void mvcGroupInit(Map args) { 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', 'mainMVC': thisMVC) @@ -25,40 +27,74 @@ class TimeStamperMainController { 'punchcardDialog', 'mainMVC': thisMVC) // load application properties - Properties prop = new Properties() String userHomeDir = System.getProperty('user.home') configFile = new File(userHomeDir, ".timestamperrc") - if (!configFile.exists()) configFile.createNewFile() - logger.traceIfEnabled { "Reading configuration from " - + "'${configFile.name}'"} + logger.trace("Reading configuration from {}", configFile.canonicalPath) - try { configFile.withInputStream { prop.load(it) } } - catch (IOException ioe) { - logger.error('Unable to load configuration', ioe) - } - - model.config = prop + model.config = new SmartConfig(configFile) // load the last used timeline file - String lastUsed = model.config.getProperty('lastUsed', null) - if (lastUsed == null) { + String lastUsed = model.config.lastUsed + if (lastUsed == "") { lastUsed = 'timeline.default.properties' 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) - if (!model.timelinePropertiesFile.exists()) - model.timelinePropertiesFile.createNewFile() + logger.trace("Adding plugin classpath: '{}'", pluginDir.canonicalPath) - 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 < + 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 -> + // 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 { - model.config.setProperty('lastUsed', propertiesFile.canonicalPath) + model.config.lastUsed = propertiesFile.canonicalPath } catch (IOException ioe) { logger.error(ioe) } // load the properties file @@ -77,34 +113,55 @@ class TimeStamperMainController { def exitGracefully = { evt = null -> - logger.traceIfEnabled {"Exiting gracefully."} + // hide the frame immediately + view.frame.visible = false + + logger.trace("Exiting gracefully.") // save config - logger.debugIfEnabled("Config: ${model.config}") - logger.debugIfEnabled("Storing config to file: ${model.configFile.path}") - try { model.configFile.withOutputStream { out -> - model.config.store(out, null) } } - catch (IOException ioe) { - logger.error("Unable to save the configuration file", ioe) - } + logger.debug("Config: {}", model.config) + model.config.save() // save timeline and properties 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() } def newTask = { mark, notes = "No comments.", timestamp = new Date() -> 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.currentMarker = model.timeline.getLastMarker(new Date()) + + // auto-persist if enabled + if (model.timelineProperties.persistOnUpdate) + model.timelineProperties.save() } def deleteTask = { marker -> + // pass through the plugins + model.plugins.each { plugin -> + wrapPluginCall { plugin.onDeleteTask(thisMVC, marker) } } + model.timeline.removeMarker(marker) model.currentMarker = model.timeline.getLastMarker(new Date()) + + // auto-persist if enabled + if (model.timelineProperties.persistOnUpdate) + model.timelineProperties.save() } } diff --git a/griffon-app/lifecycle/Initialize.groovy b/griffon-app/lifecycle/Initialize.groovy index 11ec5e6..b609a8a 100755 --- a/griffon-app/lifecycle/Initialize.groovy +++ b/griffon-app/lifecycle/Initialize.groovy @@ -19,21 +19,6 @@ import groovy.swing.SwingBuilder import griffon.util.GriffonPlatformHelper -import org.apache.log4j.Logger GriffonPlatformHelper.tweakForNativePlatform(app) 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()) - } -} diff --git a/griffon-app/models/com/jdbernard/timestamper/TimeStamperMainModel.groovy b/griffon-app/models/com/jdbernard/timestamper/TimeStamperMainModel.groovy index b3a2f84..c73924a 100644 --- a/griffon-app/models/com/jdbernard/timestamper/TimeStamperMainModel.groovy +++ b/griffon-app/models/com/jdbernard/timestamper/TimeStamperMainModel.groovy @@ -4,6 +4,7 @@ import groovy.beans.Bindable import java.awt.Point import java.awt.Rectangle import java.util.Properties +import com.jdbernard.util.SmartConfig import com.jdbernard.timestamper.core.Timeline import com.jdbernard.timestamper.core.TimelineMarker import com.jdbernard.timestamper.core.TimelineProperties @@ -12,8 +13,9 @@ class TimeStamperMainModel { @Bindable TimelineMarker currentMarker @Bindable Timeline timeline @Bindable TimelineProperties timelineProperties - Properties config - File timelinePropertiesFile + SmartConfig config + + List plugins = [] def notesDialogMVC def punchcardDialogMVC diff --git a/griffon-app/resources/log4j.properties b/griffon-app/resources/log4j.properties deleted file mode 100644 index 43ba9fb..0000000 --- a/griffon-app/resources/log4j.properties +++ /dev/null @@ -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 diff --git a/griffon-app/resources/logback.groovy b/griffon-app/resources/logback.groovy new file mode 100644 index 0000000..d848f4c --- /dev/null +++ b/griffon-app/resources/logback.groovy @@ -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) diff --git a/griffon-app/views/com/jdbernard/timestamper/LogDialogView.groovy b/griffon-app/views/com/jdbernard/timestamper/LogDialogView.groovy index 89de5f3..48c98f7 100644 --- a/griffon-app/views/com/jdbernard/timestamper/LogDialogView.groovy +++ b/griffon-app/views/com/jdbernard/timestamper/LogDialogView.groovy @@ -6,5 +6,5 @@ dialog = dialog(new JDialog(model.mainMVC.view.frame), title: 'Error Messages...', modal: false) { - logger.traceIfEnabled { "Building LogDialog view." } + logger.trace( "Building LogDialog view." ) } diff --git a/griffon-app/views/com/jdbernard/timestamper/NotesDialogView.groovy b/griffon-app/views/com/jdbernard/timestamper/NotesDialogView.groovy index 02f23e7..e632ce5 100644 --- a/griffon-app/views/com/jdbernard/timestamper/NotesDialogView.groovy +++ b/griffon-app/views/com/jdbernard/timestamper/NotesDialogView.groovy @@ -46,7 +46,7 @@ dialog = dialog(new JDialog(model.mainMVC.view.frame), return p } else return dialog.location }) ) { - logger.traceIfEnabled {'Building NotesDialog GUI'} + logger.trace('Building NotesDialog GUI') panel( border:lineBorder(color: Color.BLACK, thickness:1, parent:true), layout: new MigLayout('insets 10 10 10 10, fill') diff --git a/griffon-app/views/com/jdbernard/timestamper/PunchcardDialogView.groovy b/griffon-app/views/com/jdbernard/timestamper/PunchcardDialogView.groovy index 8c06928..18d5ee6 100644 --- a/griffon-app/views/com/jdbernard/timestamper/PunchcardDialogView.groovy +++ b/griffon-app/views/com/jdbernard/timestamper/PunchcardDialogView.groovy @@ -51,7 +51,7 @@ dialog = dialog(new JDialog(model.mainMVC.view.frame), return p } else return dialog.location }) ) { - logger.traceIfEnabled {'Building PunchcardDialog GUI'} + logger.trace('Building PunchcardDialog GUI') panel( border:lineBorder(color: Color.BLACK, thickness:1, parent:true), layout: new MigLayout('fill, insets 10 10 10 10', diff --git a/griffon-app/views/com/jdbernard/timestamper/TimeStamperMainView.groovy b/griffon-app/views/com/jdbernard/timestamper/TimeStamperMainView.groovy index e8dd7a0..2486722 100644 --- a/griffon-app/views/com/jdbernard/timestamper/TimeStamperMainView.groovy +++ b/griffon-app/views/com/jdbernard/timestamper/TimeStamperMainView.groovy @@ -90,6 +90,11 @@ optionsMenu = popupMenu() { if (fileDialog.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) 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...', actionPerformed: { aboutDialog.visible = aboutMenuItem.selected }) } @@ -107,7 +112,7 @@ frame = application(title:'TimeStamper', imageIcon('/appointment-new-16x16.png').image], componentMoved: { evt -> model.absoluteLocation = frame.location } ) { - logger.traceIfEnabled {'Building TimeStamperMain GUI'} + logger.trace('Building TimeStamperMain GUI') panel( border:lineBorder(color:Color.BLACK, thickness:1, parent:true), layout: new MigLayout('insets 0 5 0 0, fill','', '[]0[]0[]'), diff --git a/lib/jdb-util-1.1.jar b/lib/jdb-util-1.1.jar new file mode 100644 index 0000000..94962af Binary files /dev/null and b/lib/jdb-util-1.1.jar differ diff --git a/lib/log4j-1.2.15.jar b/lib/log4j-1.2.15.jar deleted file mode 100644 index c930a6a..0000000 Binary files a/lib/log4j-1.2.15.jar and /dev/null differ diff --git a/lib/logback-classic-0.9.26.jar b/lib/logback-classic-0.9.26.jar new file mode 100644 index 0000000..b900b64 Binary files /dev/null and b/lib/logback-classic-0.9.26.jar differ diff --git a/lib/logback-core-0.9.26.jar b/lib/logback-core-0.9.26.jar new file mode 100644 index 0000000..d50f3cd Binary files /dev/null and b/lib/logback-core-0.9.26.jar differ diff --git a/lib/slf4j-api-1.6.1.jar b/lib/slf4j-api-1.6.1.jar new file mode 100644 index 0000000..42e0ad0 Binary files /dev/null and b/lib/slf4j-api-1.6.1.jar differ diff --git a/lib/smack.jar b/lib/smack.jar new file mode 100644 index 0000000..73d676a Binary files /dev/null and b/lib/smack.jar differ diff --git a/lib/smackx-jingle.jar b/lib/smackx-jingle.jar new file mode 100644 index 0000000..ae044e4 Binary files /dev/null and b/lib/smackx-jingle.jar differ diff --git a/lib/smackx.jar b/lib/smackx.jar new file mode 100644 index 0000000..ad73bed Binary files /dev/null and b/lib/smackx.jar differ diff --git a/src/main/com/jdbernard/timestamper/core/TimelineProperties.java b/src/main/com/jdbernard/timestamper/core/TimelineProperties.java index f396394..23d6acd 100755 --- a/src/main/com/jdbernard/timestamper/core/TimelineProperties.java +++ b/src/main/com/jdbernard/timestamper/core/TimelineProperties.java @@ -1,20 +1,15 @@ package com.jdbernard.timestamper.core; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; import java.util.LinkedList; -import java.util.Properties; 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 @@ -25,19 +20,22 @@ import java.util.regex.Pattern; * 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.uri * The URI for the name remote timeline. - * remote.timeline.name.push + * remote.timeline.name.push? * true to enable pushing updates to the name * remote timeline, false to disable. - * remote.timeline.name.pull + * remote.timeline.name.pull? * true to enable pulling updates from the name * remote timeline, false to disable. - * remote.timeline.name.save-on-exit + * remote.timeline.name.syncOnExit? * true to force sync the name remote * timeline on exit. * remote.timeline.name - * .update-intervalThe time in milliseconds between + * .updateIntervalThe time in milliseconds between * synching the name remote timeline. * @author Jonathan Bernard ({@literal jonathan.bernard@gemalto.com}) * @see com.jdbernard.timestamper.core.Timeline @@ -47,14 +45,17 @@ import java.util.regex.Pattern; 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 @@ -65,43 +66,39 @@ public class TimelineProperties { */ public TimelineProperties() { File propertyFile = new File("timeline.default.properties"); - Properties config = new 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.toString()); - - try { config.store(new FileOutputStream(propertyFile), ""); } - 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(InputStream is) throws IOException { + public TimelineProperties(File propFile) throws IOException { String strURI; URI timelineURI; - Properties config = new Properties(); - try { - config.load(new InputStreamReader(is)); - } catch (IOException ioe) { - // TODO - } + config = new SmartConfig(propFile); + + // load persist on update information + persistOnUpdate = (Boolean) config.getProperty( + LOCAL_TIMELINE_PERSIST_ON_UPDATE, true); // load local timeline - strURI = config.getProperty(LOCAL_TIMELINE_URI, ""); + strURI = (String) config.getProperty(LOCAL_TIMELINE_URI, ""); if ("".equals(strURI)) { File defaultTimelineFile = new File("timeline.default.txt"); try { @@ -123,6 +120,9 @@ public class TimelineProperties { 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; @@ -137,7 +137,7 @@ public class TimelineProperties { stName = m.group(1); remoteBase = REMOTE_TIMELINE_BASE + stName; - strURI = config.getProperty(remoteBase + ".uri", ""); + strURI = (String) config.getProperty(remoteBase + ".uri", ""); try { timelineURI = new URI(strURI); } catch (URISyntaxException urise) { /* TODO */ } @@ -147,44 +147,32 @@ public class TimelineProperties { syncTargets.add(st); // check for synch options - st.setPullEnabled(Boolean.parseBoolean( - config.getProperty(remoteBase + ".pull", "true"))); - st.setPushEnabled(Boolean.parseBoolean( - config.getProperty(remoteBase + ".push", "true"))); - st.setSyncOnExit(Boolean.parseBoolean( - config.getProperty(remoteBase + ".sync-on-exit", "true"))); - st.setSyncInterval(Long.parseLong( - config.getProperty(remoteBase + ".update-interval", - "1800000"))); // thirty minutes + 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(OutputStream os) throws IOException { - Properties config = new Properties(); + public void save() throws IOException { timelineSource.persist(timeline); - config.setProperty(LOCAL_TIMELINE_URI, - timelineSource.getURI().toString()); + 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().toString()); - config.setProperty(remoteBase + ".pull", - Boolean.toString(st.getPullEnabled())); - config.setProperty(remoteBase + ".push", - Boolean.toString(st.getPushEnabled())); - 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 + 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()); } } @@ -197,4 +185,11 @@ public class TimelineProperties { 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/jdbernard/timestamper/gui/plugin/HookLogger.groovy b/src/main/com/jdbernard/timestamper/gui/plugin/HookLogger.groovy new file mode 100644 index 0000000..8720293 --- /dev/null +++ b/src/main/com/jdbernard/timestamper/gui/plugin/HookLogger.groovy @@ -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}") + } +} diff --git a/src/main/com/jdbernard/timestamper/gui/plugin/TimestamperPlugin.java b/src/main/com/jdbernard/timestamper/gui/plugin/TimestamperPlugin.java new file mode 100644 index 0000000..70ade0e --- /dev/null +++ b/src/main/com/jdbernard/timestamper/gui/plugin/TimestamperPlugin.java @@ -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 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 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 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 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 mainMVC, TimelineMarker marker); +} diff --git a/src/main/com/jdbernard/timestamper/gui/plugin/XMPPStatusUpdater.groovy b/src/main/com/jdbernard/timestamper/gui/plugin/XMPPStatusUpdater.groovy new file mode 100644 index 0000000..a1e938c --- /dev/null +++ b/src/main/com/jdbernard/timestamper/gui/plugin/XMPPStatusUpdater.groovy @@ -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) {} +}