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:
		@@ -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)  
 | 
			
		||||
}  
 | 
			
		||||
 
 | 
			
		||||
@@ -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 <<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 ->
 | 
			
		||||
        // 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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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...',
 | 
			
		||||
    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
 | 
			
		||||
            } 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')
 | 
			
		||||
 
 | 
			
		||||
@@ -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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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[]'),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user