Here's a more detailed explanation of each directory within the application's structure
griffon-app
- top level directory for Groovy sources.
conf
- Configuration sources.
webstart
- Webstart resources.
keys
- Jar signing keys.
dist
- Package specific files.
shared
- Common files to all packaging targets (like LICENSE.txt)
metainf
- Files that should go in META-INF inside the application/addon jar.
models
- Models.
views
- Views.
controllers
- Controllers.
services
- Services.
resources
- Images, properties files, etc.
i18n
- Support for internationalization (i18n).
scripts
- Gant scripts.
src
- Supporting sources.
main
- Other Groovy/Java sources.
test
- Unit and integration tests.
unit
- Directory for unit tests.
integration
- Directory for integration tests.
cli
- Directory for command line tests (Scripts).
All Griffon applications operate with a basic unit called the MVC group. An MVC group is comprised of 3 member parts: Models, Views and Controllers. However it is possible to add (or even remove) members from an MVC group by carefully choosing a suitable configuration.MVC groups configuration is setup in Application.groovy
located inside griffon-app/conf
. This file holds an entry for every MVC group that the application has (not counting those provided by plugins/addons).Here's how a typical MVC group configuration looks likemvcGroups {
// MVC Group for "sample"
'sample' {
model = 'sample.SampleModel'
view = 'sample.SampleView'
controller = 'sample.SampleController'
}
}
The order of the members is very important, it determines the order in which each member will be initialized. In the previous example both model
and view
will be initialized before the controller
. Do not mistake initialization for instantiation, as initialization relies on calling mvcGroupInit on the group member.MVC group configurations accept a special key that defines additional configuration for that group, as it can be seen in the following snippetmvcGroups {
// MVC Group for "sample"
'sample' {
model = 'sample.SampleModel'
view = 'sample.SampleView'
controller = 'sample.SampleController'
} // MVC Group for "foo"
'foo' {
model = 'sample.FooModel'
view = 'sample.FooView'
controller = 'sample.FooController'
config {
key = 'bar'
}
}
}
Values placed under this key become available to MVC members during the call to mvcGroupInit, as part of the arguments sent to that method. Here's how the FooController
can reach the key defined in the configurationclass FooController {
void mvcGroupInit(Map args) {
println args.configuration.config.key
}
}
While being able to set additional values under this key is a certainly an advantage it would probably be better if those values could be mutated or tweaked, probably treating them as variables, effectively making a group configuration work as a template. For that we'll have to discuss the mvcGroupManager first.This class is responsible for holding the configuration of all MVC groups no matter how they were defined, which can be either in Application.groovy
or in an addon descriptor.During the startup sequence an instance of MVCGroupManager
will be created and initialized. Later the application will instruct this instance to create all startup groups as required. MVCGroupManager
has a handful set of methods that deal with MVC group configuration alone; however those that deal with group instantiation come in 3 versions, with 2 flavors each (one Groovy, the other Java friendly).Locating a group configuration is easily done by specifying the name you're interested in findingdef configuration = app.mvcGroupManager.findConfiguration('foo')
Once you have a configuration reference you can instantiate a group with it by calling any of the variants of the create
methoddef configuration = app.mvcGroupManager.findConfiguration('foo')
def group1 = configuration.create('foo1')
def group2 = configuration.create('foo2', [someKey: 'someValue'])
// the following will make the group's id match its name
def group3 = configuration.create()
def group4 = configuration.create(someKey: 'someValue')
Be careful that creating groups with the same name is usually not a good idea. The default MVCGroupManager will complain when this happens and will automatically spit out an exception. This behavior may be changed by setting a configuration key in Config.groovy
griffon.mvcid.collision = 'warning' // accepted values are 'warning', 'exception' (default)
The manager will log a warning and destroy the previously existing group before instantiating the new one when 'warning' is the preferred strategyNow, even though you can create group instances based on their configurations the preferred way is to call any of createMVCGroup, buildMVCGroup or withMVCGroup methods. These methods are available to the app property every GriffonArtifact has, which points to the currently running application. Griffon artifacts also inherit these methods as part of their default contract. Finally, any class annotated with the MVCAware AST transformation will also gain access to these methods.Groups will be available by id regardless of how they were instantiated. You can ask the mvcGroupManager for a particular group at any time, for exampledef g1 = app.mvcGroupManager.groups.foo1
def g2 = app.mvcGroupManager.findGroup('foo1')
def g3 = app.mvcGroupManager.foo1
assert g1 == g2
assert g1 == g3
It's also possible to query all models, views, controllers and builders on their own. Say you'd want to inspect all currently instantiated models, this is how it can be doneapp.mvcGroupManager.models.each { model ->
// do something with model
}
Now that you know there are several ways to instantiate MVC groups we can go back to customizing them.The simples way is to pass in new values as part of the arguments map that mvcGroupInit receives, for exampledef group = app.mvcGroupManager.buildMVCGroup('foo', [key: 'foo'])
However is you wish to use the special config
key that every MVC group configuration may have then you must instantiate the group in the following waydef configuration = app.mvcGroupManager.cloneMVCConfiguration('foo', [key: 'someValue'])
def group = configuration.create()
Note that you can still send custom arguments to the create()
method.
The following options are available to all MVC groups as long as you use the config
key.Disabling Lifecycle Events
Every MVC group triggers a few events during the span of its lifetime. These events will be sent to the event bus even if no component is interested in handling them. There may be times when you don't want these events to be placed in the event bus in order to speed up group creation/destruction. Use the following configuration to gain this effect:mvcGroups {
// MVC Group for "sample"
'sample' {
model = 'sample.SampleModel'
view = 'sample.SampleView'
controller = 'sample.SampleController'
config {
events {
lifecycle = false
}
}
}
}
The following events will be disabled with this setting:
Disabling Instantiation Events
The Griffon runtime will trigger an event for every artifact it manages. As with the previous events this one will be sent to the event bus even if no component handles it. Skipping publication of this event may result in a slight increase of speed during group instantiation. Use the following configuration to gain this effect:mvcGroups {
// MVC Group for "sample"
'sample' {
model = 'sample.SampleModel'
view = 'sample.SampleView'
controller = 'sample.SampleController'
config {
events {
instantiation = false
}
}
}
}
The following events will be disabled with this setting:
Disabling Controllers as Application Event Listeners
Controllers are registered as application event handlers by default when a group in instantiated. This makes it very convenient to have them react to events placed in the application's event bus. However you may want to avoid this automatic registration altogether, as it can lead to performance improvements. You can disable this feature with the following configuration:mvcGroups {
// MVC Group for "sample"
'sample' {
model = 'sample.SampleModel'
view = 'sample.SampleView'
controller = 'sample.SampleController'
config {
events {
listener = false
}
}
}
}
You can still manually register a controller as an application event handler at any time, with the caveat that it's now your responsibility to unregister it when the time is appropriate, most typically during the group's destroy sequence.Every Griffon application goes through the same life cycle phases no matter in which mode they are running, with the exception of applet mode where there is an additional phase due to the intrinsic nature of applets. The application's lifecycle has been inspired by JSR-296, the Swing Application Framework.Every phase has an associated life cycle script that will be invoked at the appropriate time. These scripts are guaranteed to be invoked inside the UI thread (the Event Dispatch Thread in Swing). The script names match each phase name; you'll find them inside griffon-app/lifecycle
.
The initialization phase is the first to be called by the application's life cycle. The application instance has just been created and its configuration has been read. No other artifact has been created at this point, which means that event publishing and the ArtifactManager
are not yet available to the script's binding.This phase is typically used to tweak the application for the current platform, including its Look & Feel.Addons will be initialized during this phase.
The Initialize
script will be called right after the configuration has been read but before addons are initialized. You wont have access to addon contributions.
This phase is responsible for instantiating all MVC groups that have been defined in the application's configuration (Application.groovy
) and that also have been marked as startup groups in the same configuration file.
The Startup
script will be called after all MVC groups have been initialized.
This phase will be called right after Startup with the condition that no pending events are available in the UI queue. The application's main frame will be displayed at the end of this phase.
Called when the application is about to close. Any artifact can invoke the shutdown sequence by calling shutdown()
on the app
instance.
The Shutdown
script will be called before any ShutdownHandler
or event handler interested in the ShutdownStart
event.
This phase is only available when running on applet mode. It will be called when the applet container invokes destroy()
on the applet instance.Applications have the ability to publish events from time to time to communicate that something of interest has happened at runtime. Events will be triggered by the application during each of its life cycle phases, also when MVC groups are created and destroyed.
All application event handlers are guaranteed to be called in the same thread that originated the event.
Any artifact or class can trigger an application event, by routing it through the reference to the current running application instance. All artifacts posses an instance variable that points to that reference. All other classes can use ApplicationHolder to gain access to the current application's instance.Publishing an event can be done synchronously on the current thread or asynchronously relative to the UI thread. For example, the following snippet will trigger an event that will be handled in the same thread, which could be the UI thread itselfapp.event('MyEventName', ['arg0', 'arg1'])
Whereas the following snippet guarantees that all event handlers that are interested in an event of type MyEventName
will be called outside of the UI threadapp.eventOutside('MyEventName', ['arg0', 'arg1'])
Finally, if you'd want event notification to be handed in a thread that is not the current one (regardless if the current one is the UI thread or not) then use the following methodapp.eventAsync('MyEventName', ['arg0', 'arg1'])
There may be times when event publishing must be stopped for a while. If that's the case then you can instruct the application to stop delivering events by invoking the following codeapp.eventPublishingEnabled = false
Any events sent through the application's event bus will be discarded after that call; there's no way to get them back or replay them. When it's time to enable the event bus again simply callapp.eventPublishingEnabled = true
The following events will be triggered by the application during each one of its phases
Log4jConfigStart[config]
- during the Initialize phase.
BootstrapStart[app]
- after logging configuration has been setup, during the Initialize phase.
BootstrapEnd[app]
- at the end of the Initialize phase.
StartupStart[app]
- at the beginning of the Startup phase.
StartupEnd[app]
- at the end of the Startup phase.
ReadyStart[app]
- at the beginning of the Startup phase.
ReadyEnd[app]
- at the end of the Startup phase.
ShutdownRequested[app]
- before the Shutdown begins.
ShutdownAborted[app]
- if a Shutdown Handler prevented the application from entering the Shutdown phase.
ShutdownStart[app]
- at the beginning of the Shutdown phase.
The following events will be triggered by the application when dealing with artifacts
NewInstance[klass, type, instance]
- when a new artifact is created.
LoadAddonsStart[app]
- before any addons are initialized, during the Initialize phase.
LoadAddonsEnd[app, addons]
- after all addons have been initialized, during the Initialize phase. addons
is a Map of <name, instance> pairs.
LoadAddonStart[name, addon, app]
- before an addon is initialized, during the Initialize phase.
LoadAddonEnd[name, addon, app]
- after an addon has been initialized, during the Initialize phase.
These events will be triggered when dealing with MVC groups
InitializeMVCGroup[configuration, group]
- when a new MVC group is initialized. configuration
is of type MVCGroupConfiguration; group
is of type MVCGroup.
CreateMVCGroup[group]
- when a new MVC group is created. configuration
is of type MVCGroupConfiguration; group
is of type MVCGroup.
DestroyMVCGroup[group]
- when an MVC group is destroyed. group
is of type MVCGroup.
These events will be triggered when a specific condition is reached
UncaughtExceptionThrown[exception]
- when an uncaught exception bubbles up to GriffonExceptionHandler.
WindowShown[window]
- triggered by the WindowManager when a Window is shown.
WindowHidden[window]
- triggered by the WindowManager when a Window is hidden.
Any artifact that holds a reference to the current application may trigger events at its leisure by calling the event()
or eventAsync
methods on the application instance. The following example demonstrates how a Controller triggers a "Done" event after an action has finishedclass MyController {
def action = { evt = null ->
// do some work
app.event('Done')
}
}
There are two versions of the event()
method. The first takes just the name of the event to be published; the second accepts an additional argument which should be a List of parameters to be sent to every event handler. Event handlers notified by this method are guaranteed to process the event in the same thread that published it. However, if what you need is to post a new event and return immediately then use the eventAsync
variants. If you want the event to be handled outside of the UI thread then use the eventOutsideUI()
variants.
Any artifact or class that abides to the following conventions can be registered as an application listener, those conventions are:
- it is a Script, class, Map, RunnableWithArgs or closure.
- in the case of scripts or classes, they must define an event handler whose name matches on<EventName>, this handler can be a method, RunnableWithArgs or a closure property.
- in the case of a Map, each key maps to <EventName>, the value must be a RunnableWithArgs or a closure.
- scripts, classes and maps can be registered/unregistered by calling
addApplicationListener
/ removeApplicationListener
on the app instance.
- RunnableWithArgs and closure event handlers must be registered with an overloaded version of
addApplicationListener
/removeApplicationListener
that takes <EventName> as the first parameter, and the runnable/closure itself as the second parameter.
There is a general, per application, script that can provide event handlers. If you want to take advantage of this feature you must define a script named Events.groovy
inside griffon-app/conf
. Lastly both Controller and Service instances are automatically registered as application event listeners. This is the only way to declare event listeners for Log4jConfigStart
and BootstrapStart
events.
You can also write a class named Events.java
in src/main
as an alternative to griffon-app/conf/Events.groovy
, but not both!
These are some examples of event handlers:
- Display a message right before default MVC groups are instantiated
File: griffon-app/conf/Events.groovy
onBootstrapEnd = { app ->
println """Application configuration has finished loading.
MVC Groups will be initialized now."""
}
- Quit the application when an uncaught exception is thrown
File: src/main/Events.java
import griffon.util.ApplicationHolder;public class Events {
public void onUncaughtExceptionThrown(Exception e) {
ApplicationHolder.getApplication().shutdown();
}
}
- Print the name of the application plus a short message when the application is about to shut down.
File: griffon-app/controller/MyController.groovy
class MyController {
def onShutdownStart = { app ->
println "${app.config.application.title} is shutting down"
}
}
- Print a message every time the event "Foo" is published
File: griffon-app/controller/MyController.groovy
class MyController {
void mvcGroupInit(Map args) {
app.addApplicationListener([
Foo: {-> println 'got foo!' }
])
} def fooAction = { evt = null ->
// do something
app.event('Foo')
}
}
- An alternative to the previous example using a closure event handler
File: griffon-app/controller/MyController.groovy
class MyController {
void mvcGroupInit(Map args) {
app.addApplicationListener('Foo'){-> println 'got foo!' }
} def fooAction = { evt = null ->
// do something
app.event('Foo')
}
}
- Second alternative to the previous example using a RunnableWithArgs event handler
File: griffon-app/controller/MyController.java
import java.util.Map;
import griffon.util.RunnableWithArgs;
import org.codehaus.griffon.runtime.core.AbstractGriffonController;public class MyController extends AbstractGriffonController {
public void mvcGroupInit(Map<String, Object> params) {
getApp().addApplicationListener("Foo", new RunnableWithArgs() {
public void run(Object[] args) {
System.out.println("got foo!");
}
});
} public void fooAction() {
// do something
getApp().event("Foo");
}
}
As the name implies application events are sent system wide. However there is an option to create localized event publishers. Griffon provides a @griffon.transform.EventPublisher AST transformation that you can apply to any class that wishes to be an event publisher.This AST transformation will inject the following methods to the annotated classes:
- addEventListener(Object)
- addEventListener(String, Closure)
- addEventListener(String, RunnableWithArgs)
- removeEventListener(Object)
- removeEventListener(String, Closure)
- removeEventListener(String, RunnableWithArgs)
- publishEvent(String)
- publishEvent(String,List)
- publishEventOutsideUI(String)
- publishEventOutsideUI(String,List)
- publishEventAsync(String)
- publishEventAsync(String,List)
- isEventPublishingEnabled()
- setEventPublishingEnabled(boolean)
Event listeners registered with these classes should follow the same rules as application event handlers (they can be Scripts, classes, maps or closures, and so on).The following example shows a trivial usage of this feature@griffon.transform.EventPublisher
class Publisher {
void doit(String name) {
publishEvent('arg', [name])
} void doit() {
publishEvent('empty')
}
}class Consumer {
String value void onArg(String arg) { value = 'arg = ' + arg }
void onEmpty() { value = 'empty' }
}p = new Publisher()
c = new Consumer()
p.addEventListener(c)
assert !c.value
p.doit()
assert c.value == 'empty'
p.doit('Groovy')
assert c.value == 'arg = Groovy'
The GriffonApplication interface defines the base contract for all Griffon applications. However there are some meta enhancements done at runtime to all applications. The following methods become available before the Initialize phase is executed:
The application's runtime configuration is available through the config
property of the application instance. This is a ConfigObject
whose contents are obtained by merging Application.groovy
and Config.groovy
. Builder configuration is available through the builderConfig
property and reflects the contents of Builder.groovy
.However starting with Griffon 0.9.2 there's an alternative for defining the application's configuration. You can now use properties files instead of Groovy scripts. If both properties files and Groovy scripts are available in the classpath then the settings of the scripts will be overriden by those set in the properties file. Each properties file must match the name of the configuration script. The following table shows the conventionsScript File | Properties File |
---|
Application.groovy | Application.properties |
Config.groovy | Config.properties |
Builder.groovy | Builder.properties |
An application can change the name of the configuration script but it can not change the name of the configuration properties file.
Access to the application's metadata file (application.properties
) is available by querying the griffon.util.Metadata
singleton. Here's a snippet of code that shows how to setup a welcome message that displays the application's name and version, along with its Griffon versionimport griffon.util.Metadatadef meta = Metadata.current
application(title: "Some app", package: true) {
gridLayout cols: 1, rows: 2
label "Hello, I'm ${meta['app.name']}-${meta['app.version']}"
label "Built with Griffon ${meta['app.griffon.version']}"
}
There are also a few helpful methods on this class
getApplicationName()
- same result as meta['app.name']
getApplicationVersion()
- same result as meta['app.version']
getGriffonVersion()
- same result as meta['app.griffon.version']
getGriffonStartDir()
- returns the value of 'griffon.start.dir'
from the System properties
getGriffonWorkingDir()
- returns a File that points to 'griffon.start.dir'
if the value is set and the file is writable, otherwise returns a File pointing to the current location if it is writable; if that fails then attempts to return a File pointing to 'user.dir'
; if all fail it will return the location to a temporal file, typically '/tmp/${griffonAppName}'
.
A Griffon application can run in several environments, default ones being DEVELOPMENT, TEST and PRODUCTION. An application can inspect its current running environment by means of the griifon.util.Environment
enum.The following example enhances the previous one by displaying the current running environmentimport griffon.util.Metadata
import griffon.util.Environmentdef meta = Metadata.current
application(title: "Some app", package: true) {
gridLayout cols: 1, rows: 3
label "Hello, I'm ${meta['app.name']}-${meta['app.version']}"
label "Built with Griffon ${meta['app.griffon.version']}"
label "Current environment is ${Environment.current}"
}
Applications can run in any of the following modes: STANDALONE, WEBSTART or APPLET. The griffon.util.RunMode
enum allows access to the current running mode.This example extends the previous one by adding information on the current running modeimport griffon.util.Metadata
import griffon.util.Environment
import griffon.util.RunModedef meta = Metadata.current
application(title: "Some app", package: true) {
gridLayout cols: 1, rows: 3
label "Hello, I'm ${meta['app.name']}-${meta['app.version']}"
label "Built with Griffon ${meta['app.griffon.version']}"
label "Current environment is ${Environment.current}"
label "Current running mode is ${RunMode.current}"
}
Applications have the option to let particular artifacts abort the shutdown sequence and/or perform a task while the shutdown sequence is in process. Artifacts that desire to be part of the shutdown sequence should implement the griffon.core.ShutdownHandler
interface and register themselves with the application instance.The contract of a ShutdownHandler
is very simple
boolean canShutdown(GriffonApplication app)
- return false
to abort the shutdown sequence.
void onShutdown(GriffonApplication app)
- called if the shutdown sequence was not aborted.
There are no default ShutdownHandlers registered with an application.
All applications have the same life-cycle phases. You can inspect in which phase the application is currently on by calling the getPhase()
method on an application instance. Valid values are defined by the ApplicationPhase enum : INITIALIZE
, STARTUP
, READY
, MAIN
and SHUTDOWN
.
Starting with Griffon 0.9 applications have a bound locale
property that is initialized to the default Locale. Components can listen to Locale changes by registering themselves as PropertyChangeListeners on the application instance.
Since Griffon 0.9.1 default imports per artifacts are supported. All Groovy based artifacts will resolve classes from the griffon.core
and griffon.util
packages automatically, there is no longer a need to define imports on those classes unless you require an static import or define an alias. An example of this feature would be as follows.class MyController {
void mvcGroupInit(Map args) {
println Metadata.current.'app.name'
}
}
The Metadata
class is defined in package griffon.util
.
There are additional imports per artifact type, here's the list of default definitions
- Model
- groovy.beans -> @Bindable, @Vetoable
- java.beans -> useful for all PropertyChange* classes
- View (when using Swing)
- java.awt
- java.awt.event
- javax.swing
- javax.swing.event
- javax.swing.table
- javax.swing.text
- javax.swing.tree
The list of imports per artifacts can be tweaked or changed completely at will. You only need to specify a file named META-INF/griffon-default-imports.properties
with the following format<artifact_type> = <comma_separated_package_list>
These are the contents of the default fileviews = javax.swing., javax.swing.event., javax.swing.table., javax.swing.text., javax.swing.tree., java.awt., java.awt.event.
models = groovy.beans., java.beans.
Imports are cumulative, this means you a package can't be removed from the default list provided by Griffon.
Command line arguments can be passed to the application and be accessed by calling getStartupArgs()
on the application instance. This will return a copy of the args (if any) defined at the command line.Here's a typical example of this feature in development modegriffon run-app arg0 arg1 argn
Here's another example demonstrating that the feature can be used once the application has been packaged, in this case as a single jargriffon dev package jar
java -jar dist/jars/app.jar arg0 arg1 argn
Resources can be loaded form the classpath using the standard mechanism provided by the Java runtime, that is, ask a ClassLoader
instance to load a resource URL
or obtain an InputStream
that points to the resource.But the code can get quite verbose, take for example the following view code that locates a text file and displays it on a text componentscrollPane {
textArea(columns: 40, rows: 20,
text: this.class.classLoader.getResource('someTextFile.txt').text)
}
In order to reduce visual clutter, also to provide an abstraction over resource location, both GriffonApplication
and GriffonArtifact
have a new pair of methods that simply working with resources. Those methods are provided by ResourceHandler
:
URL getResourceAsURL(String resourceName)
InputStream getResourceAsStream(String resourceName)
List<URL> getResources(String resourceName)
Thus, the previous example can be rewritten this wayscrollPane {
textArea(columns: 40, rows: 20,
text: getResourceAsURL('someTextFile.txt').text)
}
In the future Griffon may switch to a modularized runtime, this abstraction will shield you from any problems when the underlying mechanism changes.These methods can be attached to any non-artifact class at compile time if you apply the @griffon.transform.ResourcesAware AST transformation.
There are times when an exception catches you off guard. The JVM provides a mechanism for handling these kind of exceptions: Thread.UncaughtExceptionHandler. You can register an instance that implements this interface with a Thread or ThreadGroup, however it's very likely that exceptions thrown inside the EDT will not be caught by such instance. Furthermore, it might be the case that other components would like to be notified when an uncaught exception is thrown. This is precisely what GriffonExceptionHandler does.Stack traces will be sanitized by default, in other words, you won't see a long list containing Groovy internals. However you can bypass the filtering process and instruct Griffon to leave the stack traces untouched by specifying the following flag either in the command line with -D
switch or in Config.groovy
griffon.full.stacktrace = true
Exceptions will be automatically logged using the error
level. Should you choose to disable logging but still have some output when an exception occurs then configure the following flaggriffon.exception.output = true
Any exception caught by GriffonExceptionHandler will trigger a pair of events. The event names match the following convention
- Uncaught<exception.class.simpleName>
- UncaughtExceptionThrown
The exception is sent as the sole argument of these events. As an example, assume that a service throws an IllegalArgumentException
during the invocation of a service method. This method was called from within a Controller which defines a handler for this exception, like thisclass SampleService {
void work() {
throw new IllegalArgumentException('boom!')
}
}class SampleController {
def sampleService def someAction = {
sampleService.work()
} def onUncaughtIllegalArgumentException = { iae ->
// process exception
}
}
As mentioned before, the name of an event handler matches the type of the exception it will handle, polymorphism is not supported. In other words, you wont be able to handle an IllegalArgumentException
if you declare a handler for RuntimeException
. You can however, rely on the second event triggered by this mechanism. Be aware that every single exception will trigger 2 events each time it is caught. It is best to use a synchronization approach to keep track of the last exception caught, like soimport groovy.transform.Synchronizedclass SampleController {
private lastCaughtException @Synchronized
void onUncaughtRuntimeException(RuntimeException e) {
lastCaughtException = e
// handle runtime exception only
} @Synchronized
void onUncaughtExceptionThrown(e) {
if(lastCaughtException == e) return
lastCaughtException = e
// handle any other exception types
}
}
As a final remark, any exceptions that might occur during the handling of the events wont trigger GriffonExceptionHandler
again, they will simply be logged and discarded instead.The following features are available to Swing based applications only.
The WindowManager
class is responsible for keeping track of all the windows managed by the application. It also controls how these windows are displayed (via a pair of methods: show, hide). WindowManager relies on an instance of WindowDisplayHandler
to actually show or hide a window. The default implementation simple shows and hide windows directly, however you can change this behavior by setting a different implementation of WindowDisplayHandler
on the application instance.WindowManager DSL
Starting with Griffon 0.9.2 there's a new DSL for configuring show/hide behavior per window. This configuration can be set in griffon-app/conf/Config.groovy
, and here is how it looksswing {
windowManager {
myWindowName = [
show: {window, app -> … },
hide: {window, app -> … }
]
myOtherWindowName = [
show: {window, app -> … }
]
}
}
The name of each entry must match the value of the Window's name: property. Each entry may have the following options
- show - used to show the window to the screen. It must be a closure that takes two parameters: the window to display and the current application.
- hide - used to hide the window from the screen. It must be a closure that takes two parameters: the window to hide and the current application.
- handler - a custom
WindowDisplayHandler
.
The first two options have priority over the third one. If one is missing then the WindowManager will invoke the default behavior. There is one last option that can be used to override the default behavior provided to all windowsswing {
windowManager {
defaultHandler = new MyCustomWindowDisplayHandler()
}
}
You can go a bit further by specifying a global show or hide behavior as shown in the following exampleswing {
windowManager {
defaultShow = {window, app -> … }
// defaultHide = {window, app -> … }
someWindowName = [
hide: {window, app -> … }
]
}
}
Custom WindowDisplayHandlers
The following example shows how you can animate all managed windows using a dropIn effect for show() and a dropOut effect for hide(). This code assumes you have installed the Effects plugin.In src/main/Dropper.groovy
import java.awt.Window
import griffon.swing.SwingUtils
import griffon.swing.DefaultWindowDisplayHandler
import griffon.core.GriffonApplication
import griffon.effects.Effectsclass Dropper extends DefaultWindowDisplayHandler {
void show(Window window, GriffonApplication app) {
SwingUtils.centerOnScreen(window)
app.execOutsideUI {
Effects.dropIn(window, wait: true)
}
} void hide(Window window, GriffonApplication app) {
app.execOutsideUI {
Effects.dropOut(window, wait: true)
}
}
}
Notice that the effects are executed outside of the UI thread because we need to wait for them to finish before continuing, otherwise we'll hog the UI thread.The second step to get this example to work is to inform the application it should use Dropper to display/hide windows. This a task that can be easily achieved by adding an application event handler, for example in griffon-app/conf/Events.groovy
// No windows have been created before this step
onBootstrapEnd = { app ->
app.windowDisplayHandler = new Dropper()
}
Custom WindowDisplayHandler
implementations set in this manner will be called for all managed windows. You'll loose the ability of using the WindowManager DSL.
Alternatively, you could specify an instance of Dropper
as the default handler by changing the WindowManager
's configuration toswing {
windowManager {
defaultHandler = new Dropper()
}
}
The WindowDisplayHandler
interface also defines show/hide methods that can manage JInternalFrame
instances.Starting Window
Previous to Griffon 0.9.2 the first window to be displayed during the Ready phase was determined by a simple algorithm: picking the first available window from the managed windows list. With 0.9.2 however, it's now possible to configure this behavior by means of the WindowManager DSL. Simply specify a value for swing.windowManager.startingWindow
, like thisswing {
windowManager {
startingWindow = 'primary'
}
}
This configuration flag accepts two types of values:
- a String that defines the name of the Window. You must make sure the Window has a matching name property.
- a Number that defines the index of the Window in the list of managed windows.
If no match is found then the default behavior will be executed.
The Artifact API provides introspection capabilities on the conventions set on each artifact type. The following sections explain further what you can do with this API.
Every Griffon application exposes all information about its artifacts and addons via a pair of helper classes
AddonManager
- used for all installed addons
ArtifactManager
- used for all remaining artifacts
ArtifactManager
The ArtifactManager
class provides methods to evaluate the conventions within the project and internally stores references to all classes within a GriffonApplication using subclasses of GriffonClass class.A GriffonClass
represents a physical Griffon resources such as a controller or a service. For example to get all GriffonClass
instances you can call:app.artifactManager.allClasses.each { println it.name }
There are a few "magic" properties that the ArtifactManager
instance possesses that allow you to narrow the type of artifact you are interested in. For example if you only need to deal with controllers you can do:app.artifactManager.controllerClasses.each { println it.name }
Dynamic method conventions are as follows:
get*Classes
- Retrieves all the classes for a particular artifact type. Example app.artifactManager.getControllerClasses()
.
*Classes
- Retrieves all the classes for a particular artifact type. Example app.artifactManager.controllerClasses
.
is*Class
- Returns true if the given class is of the given artifact type. Example app.artifactManager.isControllerClass(ExampleController)
The GriffonClass
interface itself provides a number of useful methods that allow you to further evaluate and work with the conventions. These include:
newInstance
- Creates a new instance of the enclosed class.
getName
- Returns the logical name of the class in the application without the trailing convention part if applicable
getClazz
- Returns the artifact class
getType
- Returns the type of the artifact, i.e "controller"
getTrailing
- Returns the suffix (if any) of the artifact, i.e "Controller"
For a full reference refer to the javadoc API.AddonManager
The AddonManager
class is responsible for holding references to all addons (which are of type griffon.core.GriffonAddon), as well as providing metainformation on each addon via an addon descriptor. The latter can be used to know at runtime the name and version of a particular addon, useful for building a dynamic About dialog for example.All addons have the same behavior which is explained in detail in section 12.6 Addons.
For Griffon managed classes like controllers, models and so forth you can add methods, constructors etc. using the ExpandoMetaClass mechanism by accessing each controller's MetaClass:class ExampleAddon {
def addonPostInit(app) {
app.artifactManager.controllerClasses.each { controllerClass ->
controllerClass.metaClass.myNewMethod = {-> println "hello world" }
}
}
}
In this case we use the app.artifactManager
object to get a reference to all of the controller classes' MetaClass instances and then add a new method called myNewMethod
to each controller.
Alternatively, if you know before hand the class you wish add a method to you can simple reference that classes metaClass
property:class ExampleAddon {
def addonPostInit(app) {
String.metaClass.swapCase = {->
def sb = new StringBuffer()
delegate.each {
sb << (Character.isUpperCase(it as char) ?
Character.toLowerCase(it as char) :
Character.toUpperCase(it as char))
}
sb.toString()
} assert "UpAndDown" == "uPaNDdOWN".swapCase()
}
}
In this example we add a new method swapCase
to java.lang.String
directly by accessing its metaClass
.All Griffon artifacts share common behavior. This behavior is captured by an interface named griffon.core.GriffonArtifact. Additional interfaces with more explicit behavior exist per each artifact type. The following is a list of the basic types and their corresponding interface
Starting with Griffon 0.9.1 the compiler will make sure that each artifact implements its corresponding interface via AST injection. This feature can be very useful when accessing artifacts from languages other than Groovy (see section 13.1 Dealing with Non-Groovy Artifacts to learn more about this kind of artifacts).
AST injection is always enabled unless you disable it as explained in section 4.7.2 Disable AST Injection.
Additionally to each artifact type you will find a companion GriffonClass that is specialized for each type. These specialized classes can be used to discover metadata about a particular artifact. The following is a list of the companion GriffonClass for each of the basic artifacts found in core
Be aware that additional artifacts provided by plugins (such as Charts and Wizards) may provide their own interface and companion GriffonClass. These too will be available when querying the ArtifactManager
.
While it's true that artifact templates can be provided by plugins it simply was not possible to configure how an application is created. Application Archetypes fill this gap by providing a hook into the application creation process. Archetypes can do the following:
- provide new versions of existing templates, like Model, Controller and so forth
- create new directories and files
- most importantly perhaps, install a preset of plugins
So, if your company requires all applications to be built following the same template and basic behavior then you can create an archetype that enforces those constraints. Archetypes are simple zip files with an application descriptor and templates. Despite this, Griffon provides a few scripts that let you manage archetypes
Archetypes are installed per Griffon location under $USER_HOME/.griffon/<version>/archetypes
. Archetypes are registered with an application's metadata when creating an application. You can either manually modify the value of 'app.archetype' to a known archetype name or specify an -archetype=<archetypeName>
flag when creating a new application.If no valid archetype is found then the default archetype will be used. Following is the default template for an application archetypeimport griffon.util.MetadataincludeTargets << griffonScript('CreateMvc')target(name: 'createApplicationProject',
description: 'Creates a new application project',
prehook: null, posthook: null) {
createProjectWithDefaults()
createMVC() // to install plugins do the following
// Metadata md = Metadata.getInstance(new File("${basedir}/application.properties"))
//
// for a single plugin
// installPluginExternal md, pluginName, pluginVersion
// ** pluginVersion is optional **
//
// for multiple plugins where the latest version is preferred
// installPluginsLatest md, [pluginName1, pluginName2]
//
// for multiple plugins with an specific version
// installPlugins md, [pluginName1: pluginVersion1]
}
setDefaultTarget(createApplicationProject)
This section demonstrates how an archetype can be created and put to good use for building applications.#1 Create the archetype
The first step is to create the archetype project and its descriptor, which can be done by executing the following commandgriffon create-archetype fancy
cd fancy
#2 Tweak the archetype descriptor
Locate the archetype descriptor (application.groovy
) and open it in your favorite editor, paste the following snippetimport griffon.util.MetadataincludeTargets << griffonScript('CreateMvc')target(name: 'createApplicationProject',
description: 'Creates a new application project',
prehook: null, posthook: null) {
createProjectWithDefaults() argsMap.model = 'MainModel'
argsMap.view = 'MainView'
argsMap.controller = 'MainController'
createMVC() createArtifact(
name: mvcFullQualifiedClassName,
suffix: 'Actions',
type: 'MainActions',
path: 'griffon-app/views') createArtifact(
name: mvcFullQualifiedClassName,
suffix: 'MenuBar',
type: 'MainMenuBar',
path: 'griffon-app/views') createArtifact(
name: mvcFullQualifiedClassName,
suffix: 'StatusBar',
type: 'MainStatusBar',
path: 'griffon-app/views') createArtifact(
name: mvcFullQualifiedClassName,
suffix: 'Content',
type: 'MainContent',
path: 'griffon-app/views') Metadata md = Metadata.getInstance(new File("${basedir}/application.properties"))
installPluginExternal md, 'miglayout'
}setDefaultTarget(createApplicationProject)
This archetype overrides the default templates for Model, View and Controller that will be used for the initial MVC group. It also creates 4 additional files inside griffon-app/views
. Finally it installs the latest version of the MigLayout plugin.#3 Create the artifact templates
According to the conventions laid out in the archetype descriptor there must exist a file under templates/artifacts
that matches each one of the specified artifact types, in other words we need the following filesMainModel.groovy
@artifact.package@import groovy.beans.Bindable
import griffon.util.GriffonNameUtilsclass @artifact.name@ {
@Bindable String status void mvcGroupInit(Map args) {
status = "Welcome to ${GriffonNameUtils.capitalize(app.config.application.title)}"
}
}
MainController.groovy
@artifact.package@class @artifact.name@ {
def model
def view // void mvcGroupInit(Map args) {
// // this method is called after model and view are injected
// } // void mvcGroupDestroy() {
// // this method is called when the group is destroyed
// } def newAction = { evt = null ->
model.status = 'New action'
} def openAction = { evt = null ->
model.status = 'Open action'
} def saveAction = { evt = null ->
model.status = 'Save action'
} def saveAsAction = { evt = null ->
model.status = 'Save As action'
} def aboutAction = { evt = null ->
model.status = 'About action'
} def quitAction = { evt = null ->
model.status = 'Quit action'
} def cutAction = { evt = null ->
model.status = 'Cut action'
} def copyAction = { evt = null ->
model.status = 'Copy action'
} def pasteAction = { evt = null ->
model.status = 'Paste action'
}
}
MainView.groovy
@artifact.package@build(@artifact.name.plain@Actions)
application(title: GriffonNameUtils.capitalize(app.config.application.title),
pack: true,
locationByPlatform:true,
iconImage: imageIcon('/griffon-icon-48x48.png').image,
iconImages: [imageIcon('/griffon-icon-48x48.png').image,
imageIcon('/griffon-icon-32x32.png').image,
imageIcon('/griffon-icon-16x16.png').image]) {
menuBar(build(@artifact.name.plain@MenuBar))
migLayout(layoutConstraints: 'fill')
widget(build(@artifact.name.plain@Content), constraints: 'center, grow')
widget(build(@artifact.name.plain@StatusBar), constraints: 'south, grow')
}
MainActions.groovy
@artifact.package@import groovy.ui.Consoleactions {
action( id: 'newAction',
name: 'New',
closure: controller.newAction,
mnemonic: 'N',
accelerator: shortcut('N'),
smallIcon: imageIcon(resource:"icons/page.png", class: Console),
shortDescription: 'New'
)
action( id: 'openAction',
name: 'Open...',
closure: controller.openAction,
mnemonic: 'O',
accelerator: shortcut('O'),
smallIcon: imageIcon(resource:"icons/folder_page.png", class: Console),
shortDescription: 'Open'
)
action( id: 'quitAction',
name: 'Quit',
closure: controller.quitAction,
mnemonic: 'Q',
accelerator: shortcut('Q'),
)
action( id: 'aboutAction',
name: 'About',
closure: controller.aboutAction,
mnemonic: 'B',
accelerator: shortcut('B')
) action( id: 'saveAction',
name: 'Save',
closure: controller.saveAction,
mnemonic: 'S',
accelerator: shortcut('S'),
smallIcon: imageIcon(resource:"icons/disk.png", class: Console),
shortDescription: 'Save'
)
action( id: 'saveAsAction',
name: 'Save as...',
closure: controller.saveAsAction,
accelerator: shortcut('shift S')
) action(id: 'cutAction',
name: 'Cut',
closure: controller.cutAction,
mnemonic: 'T',
accelerator: shortcut('X'),
smallIcon: imageIcon(resource:"icons/cut.png", class: Console),
shortDescription: 'Cut'
)
action(id: 'copyAction',
name: 'Copy',
closure: controller.copyAction,
mnemonic: 'C',
accelerator: shortcut('C'),
smallIcon: imageIcon(resource:"icons/page_copy.png", class: Console),
shortDescription: 'Copy'
)
action(id: 'pasteAction',
name: 'Paste',
closure: controller.pasteAction,
mnemonic: 'P',
accelerator: shortcut('V'),
smallIcon: imageIcon(resource:"icons/page_paste.png", class: Console),
shortDescription: 'Paste'
)
}
MainMenuBar.groovy
@artifact.package@import static griffon.util.GriffonApplicationUtils.*menuBar = menuBar {
menu(text: 'File', mnemonic: 'F') {
menuItem(newAction)
menuItem(openAction)
separator()
menuItem(saveAction)
menuItem(saveAsAction)
if( !isMacOSX ) {
separator()
menuItem(quitAction)
}
} menu(text: 'Edit', mnemonic: 'E') {
menuItem(cutAction)
menuItem(copyAction)
menuItem(pasteAction)
} if(!isMacOSX) {
glue()
menu(text: 'Help', mnemonic: 'H') {
menuItem(aboutAction)
}
}
}
MainContent.groovy
@artifact.package@label('Main content')
MainStatusBar.groovy
@artifact.package@label(id: 'status', text: bind { model.status })
#4 Package and install the archetype
This step is easily done with a pair of command invocationsgriffon package-archetype
griffon install-archetype target/package/griffon-fancy-0.1.zip
#5 Use the archetype
Now that the archetype has been installed all that is left is put it to good use, like thisgriffon create-app sample --archetype=fancy
This command should generate the following files in the application directory
- griffon-app
- controllers
- models
- views
- sample
SampleActions
SampleContent
SampleMenuBar
SampleStatusBar
SampleView
If you inspect the application.properties
file you'll notice that the miglayout plugin has been installed too.Archetypes can be versioned, installed and released in the same way plugins are.The following sections outline specific tweaks and options available for a particular platform.
Griffon will automatically apply tweaks to the application depending on the current platform. However you have the option to specify a different set of tweaks.
For example, the following configuration in Config.groovy
specifies a different handler for macosx
:platform {
handler = [
macosx: 'com.acme.MyMacOSXPlatformHandler'
]
}
Now you only need to create such handler, like this:package com.acmeimport griffon.core.GriffonApplication
import griffon.util.PlatformHandlerclass MyMacOSXPlatformHandler implements PlatformHandler {
void handle(GriffonApplication app) {
System.setProperty('apple.laf.useScreenMenuBar', 'true')
…
}
}
The following platform keys are recognized by the application in order to locate a particular handler: linux
, macosx
, solaris
and windows
.
Applications that run in Apple's MacOSX must adhere to an strict set of rules. We recommend you to have a look at Apple's (Human Interface Guidelines).Griffon makes it easier to integrate with MacOSX by automatically registering a couple of System properties that make the applicaiton behave like a native one
apple.laf.useScreenMenuBar
- if set to true will force the application's menu bar to appear at the top. Griffon sets its value to true.
com.apple.mrj.application.apple.menu.about.name
- sets the name that will appear next to the About
menu option.
Java applications running on MacOSX also have the option to register handlers for About
, Preferences
and Quit
menu options. The default handlers will trigger an specific application event each. These events can be disabled with a command flag set in griffon-app/conf/Config.groovy
. The following table outlines the events, flags and the default behavior when the flags are enabledEvent | Fired when | Flag | Default behavior |
---|
OSXAbout | user activates About menu | osx.noabout | Default about dialog is displayed |
OSXPrefs | user activates Preferences menu | osx.noprefs | No Preferences menu is available |
OSXQuit | user activates Quit menu | osx.noquit | Application shutdowns immediately |