1. Introduction
Developing desktop/RIA applications on the JVM is a hard task. You have to make choices up front during application design that might complicate the implementation, compromising the user experience, not to mention the amount of configuration needed.
RCP solutions like Eclipse RCP and NetBeans RCP are great for developing desktop applications, but not so much for RIAs and applets. Griffon is a framework inspired by Grails, whose aim is to overcome the problems you may encounter while developing all these types of applications. It brings along popular concepts like
-
Convention over Configuration
-
Don’t Repeat Yourself (DRY)
-
Pervasive MVC
-
Testing supported "out of the box"
This documentation will take you through getting started with Griffon and building desktop/RIA applications with the Griffon framework.
Credits and Acknowledgements
This guide is heavily influenced by the Grails Guide. It simply would not have been possible without the great efforts made by: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown and their sponsor: SpringSource. The Griffon team would like to thank them all (and the Grails community too!) for making such a great framework and bringing the fun back to programming applications.
Background
Griffon’s lineage can be traced back to Grails; the first release of the framework was posted on September 10th 2008. At the time the team concentrated their efforts in making Griffon a lightweight addition to what SwingBuilder offered. JavaFX was not in the radar at it was still very young and not ready for prime time as opposed to Swing. Groovy’s dynamic features made it very easy for people to write applications with a concise syntax and a few lines of code.
JavaFX became a viable option as time passed by. Also, Griffon 2.x was revamped to support Java from the core, not just as an after thought, same for for JavaFX and Dependency Injection. This design goal enabled the framework to be more flexible in terms of language and UI toolkit choices. It can also target a broader audience as there’s a sizable number of developers that prefer writing Java code rather than Groovy (or Kotlin).
Many of the examples posted since 2.x came out showcase the usage of Java to entice and convince Java developers to give it a try. Seasoned Groovy developers know they can adapt the code using idiomatic Groovy based on their preferences. This Guide continues to show Groovy code for brevity, opting for Java when it makes sense for a particular feature. The documentation of most Griffon plugins also show both Groovy and Java examples that can be Copy & Pasted as a starting point.
2. Getting Started
This chapter will help you get started with Griffon by explaining how to download and install the project, how to use Griffon from within your favorite IDE, and how to write your first application.
2.1. Environment Setup
The following section outlines the minimum environment requirements to get started with Griffon.
2.1.1. JDK
JDK8 is the lowest JVM version supported by Griffon. We strongly suggest to use JDK8 update 60 as a minimum if you’re planning to build JavaFX based applications.
2.1.3. Maven
Alternatively you may use Maven instead of Gradle as your build tool of choice. Maven is a popular choice amongst Java developers; however, it’s our firm belief that Gradle delivers a much better development and user experience.
2.2. Console Example
2.2.1. Creating a Project (Gradle)
The first step is to get Lazybones installed on your system. The easiest way to achieve this goal is to install SDKMAN first:
$ curl -s http://get.sdkman.io | bash
Install the latest version of Lazybones with the following command:
$ sdk install lazybones
Next, add the official Griffon Lazybones templates repository to your Lazybones
configuration. Edit $USER_HOME/.lazybones/config.groovy
and paste the following
content:
1
2
3
4
bintrayRepositories = [
"griffon/griffon-lazybones-templates",
"pledbrook/lazybones-templates"
]
We’re now ready to create the project. You can list all available templates with the following command:
$ lazybones list
Available templates in griffon/griffon-lazybones-templates:
griffon-javafx-groovy
griffon-javafx-java
griffon-javafx-kotlin
griffon-lanterna-java
griffon-lanterna-groovy
griffon-pivot-java
griffon-pivot-groovy
griffon-swing-java
griffon-swing-groovy
griffon-plugin
Notice that template names follow a naming convention identifying the main UI toolkit and main programming language. All right, let’s create a simple project using Swing as main UI toolkit, and Groovy as main language:
$ lazybones create griffon-swing-groovy console
Creating project from template griffon-swing-groovy (latest) in 'console'
Define value for 'group' [org.example]: console
Define value for 'version' [0.1.0-SNAPSHOT]:
Define value for 'package' [console]:
Define value for 'griffonVersion' [2.15.0]:
2.2.2. Creating a Project (Maven)
The first step is to get Maven installed on your system. The easiest way to achieve this goal is to install SDKMAN first:
$ curl -s http://get.sdkman.io | bash
Install the latest version of Maven with the following command:
$ sdk install maven
Or if your prefer you may install Maven using other means as explained in the official Maven documentation (Download and Install).
We’re now ready to create the project. You can select any of the Maven archetypes available from https://bintray.com/griffon/griffon-archetypes/ which currently resolve to the following list
griffon-javafx-groovy-archetype
griffon-javafx-java-archetype
griffon-lanterna-java-archetype
griffon-lanterna-groovy-archetype
griffon-pivot-java-archetype
griffon-pivot-groovy-archetype
griffon-swing-java-archetype
griffon-swing-groovy-archetype
Notice that archetypes names follow a naming convention identifying the main UI toolkit and main programming language. All right, let’s create a simple project using Swing as main UI toolkit, and Groovy as main language:
$ mvn archetype:generate \
-DarchetypeGroupId=org.codehaus.griffon.maven \
-DarchetypeArtifactId=griffon-swing-groovy-archetype \
-DarchetypeVersion=2.15.0 \
-DgroupId=org.example \
-DartifactId=console \
-Dversion=1.0.0-SNAPSHOT
2.2.3. Project Layout
Take a moment to familiarize yourself with the standard Griffon project layout. Every Griffon project shares the same layout, making it easy to dive in as artifacts are located in specific directories according to their responsibilities and behavior.
console
├── griffon-app
│ ├── conf
│ ├── controllers
│ ├── i18n
│ ├── lifecycle
│ ├── models
│ ├── resources
│ ├── services
│ └── views
└── src
├── integration-test
│ └── groovy
├── main
│ ├── groovy
│ └── resources
└── test
├── groovy
└── resources
Griffon uses a "convention over configuration" approach to enable a fast development pace. This typically means that the name and location of files are used instead of explicit configuration, hence you need to familiarize yourself with the directory structure provided by Griffon.
Here is a breakdown and links to relevant sections:
-
griffon-app
- top level directory for Groovy sources.-
conf
- Configuration sources. -
models
- Models. -
views
- Views. -
controllers
- Controllers. -
services
- Services. -
resources
- Images, properties files, etc.
-
-
src
- Supporting sources.-
main
- Other Groovy/Java sources.
-
-
test
- Unit tests. -
integration-test
- Integration tests.
These conventions can be omitted provided you reconfigure the default build files provided by the chosen project template.
Here’s a screenshot of the finished, running application to give you an idea of what we’re aiming at with this example. A small Groovy script has been executed; you can see the result on the bottom right side:
Gradle
The following listing shows the Gradle build file generated by the Lazybones template:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
buildscript {
repositories {
jcenter()
maven { url 'https://plugins.gradle.org/m2/' }
}
dependencies {
classpath 'org.codehaus.griffon:gradle-griffon-plugin:2.15.0'
classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2'
classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.11.0'
classpath 'org.gradle.api.plugins:gradle-izpack-plugin:0.2.3'
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
classpath 'com.github.cr0:gradle-macappbundle-plugin:3.1.0'
classpath 'org.kordamp.gradle:stats-gradle-plugin:0.2.2'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
classpath 'de.gliderpilot.gradle.jnlp:gradle-jnlp-plugin:1.2.5'
classpath 'net.nemerosa:versioning:2.6.1'
}
}
apply plugin: 'groovy'
apply plugin: 'org.codehaus.griffon.griffon'
apply plugin: 'net.nemerosa.versioning'
Date buildTimeAndDate = new Date()
ext {
buildDate = new SimpleDateFormat('yyyy-MM-dd').format(buildTimeAndDate)
buildTime = new SimpleDateFormat('HH:mm:ss.SSSZ').format(buildTimeAndDate)
macosx = System.getProperty('os.name').contains('Mac OS')
}
griffon {
disableDependencyResolution = false
includeGroovyDependencies = true
version = '2.15.0'
toolkit = 'swing'
applicationProperties = [
'build_date' : buildDate,
'build_time' : buildTime,
'build_revision': versioning.info.commit
]
}
mainClassName = 'console.Launcher'
apply from: 'gradle/publishing.gradle'
apply from: 'gradle/code-coverage.gradle'
apply from: 'gradle/code-quality.gradle'
apply from: 'gradle/integration-test.gradle'
apply from: 'gradle/package.gradle'
apply from: 'gradle/docs.gradle'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'org.kordamp.gradle.stats'
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'com.github.kt3k.coveralls'
dependencies {
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
runtime "org.slf4j:slf4j-simple:1.7.25"
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
testCompile "org.codehaus.groovy:groovy-all:2.4.15"
testCompile "org.spockframework:spock-core:1.1-groovy-2.4"
}
task sourceJar(type: Jar) {
group 'Build'
description 'An archive of the source code'
classifier 'sources'
from sourceSets.main.allSource
}
tasks.withType(JavaCompile) {
sourceCompatibility = project.sourceCompatibility
targetCompatibility = project.targetCompatibility
}
tasks.withType(GroovyCompile) {
sourceCompatibility = project.sourceCompatibility
targetCompatibility = project.targetCompatibility
}
import com.github.jengelman.gradle.plugins.shadow.transformers.*
import java.text.SimpleDateFormat
shadowJar {
transform(ServiceFileTransformer)
transform(ServiceFileTransformer) {
path = 'META-INF/griffon'
}
transform(ServiceFileTransformer) {
path = 'META-INF/types'
}
transform(PropertiesFileTransformer) {
paths = [
'META-INF/editors/java.beans.PropertyEditor'
]
}
}
startScripts {
doLast {
if (!macosx) unixScript.text = unixScript.text.replaceAll('"(-Xdock:(name|icon)=)([^"]*?)(")', ' ')
windowsScript.text = windowsScript.text.replaceAll('"(-Xdock:(name|icon)=)([^"]*?)(")', ' ')
}
}
if (hasProperty('debugRun') && ((project.debugRun as boolean))) {
run {
jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
}
}
task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport, jacocoFunctionalTestReport]) {
executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData, jacocoFunctionalTestReport.executionData)
destinationFile = file("${buildDir}/jacoco/root.exec")
}
task jacocoRootReport(dependsOn: jacocoRootMerge, type: JacocoReport) {
group = 'Reporting'
description = 'Generate Jacoco coverage reports after running all tests.'
executionData file("${buildDir}/jacoco/root.exec")
sourceDirectories = files(sourceSets.main.allSource.srcDirs)
classDirectories = files(sourceSets.main.output)
reports {
csv.enabled = false
xml.enabled = true
html.enabled = true
html.destination = "${buildDir}/reports/jacoco/root/html"
xml.destination = "${buildDir}/reports/jacoco/root/root.xml"
}
}
Maven
The following listing shows the Maven build file generated by the archetype:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>console</groupId>
<artifactId>console</artifactId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<parent>
<groupId>org.codehaus.griffon</groupId>
<artifactId>application-master-pom</artifactId>
<version>1.15.0</version>
</parent>
<properties>
<griffon.version>2.15.0</griffon.version>
<application.main.class>console.Launcher</application.main.class>
<application_name>${project.name}</application_name>
<application_version>${project.version}</application_version>
<build_date>${git.build.time}</build_date>
<build_time>${git.build.time}</build_time>
<build_revision>${git.commit.id}</build_revision>
</properties>
<build>
<!-- Uncomment if project is versioned using Git
<filters>
<filter>${project.build.outputDirectory}/git.properties</filter>
</filters>
-->
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<!-- compile -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-groovy-compile</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-groovy</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-swing</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-swing-groovy</artifactId>
</dependency>
<!-- runtime -->
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-guice</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>runtime</scope>
</dependency>
<!-- test -->
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-core-test</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-fest-test</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>jcenter</id>
<name>jcenter</name>
<url>http://jcenter.bintray.com/</url>
</repository>
<repository>
<id>griffon-plugins</id>
<name>griffon-plugins</name>
<url>http://dl.bintray.com/griffon/griffon-plugins</url>
</repository>
</repositories>
</project>
All right, let’s get started with the code. We’ll visit the Model first.
Model
The model for this application is simple: it contains properties that hold the script to be
evaluated and the results of the evaluation. Make sure you paste the following code into
griffon-app/models/console/ConsoleModel.groovy
.
1
2
3
4
5
6
7
8
9
10
11
12
package console
import griffon.core.artifact.GriffonModel
import griffon.metadata.ArtifactProviderFor
import griffon.transform.Observable
@ArtifactProviderFor(GriffonModel)
class ConsoleModel {
String scriptSource (1)
@Observable Object scriptResult (2)
@Observable boolean enabled = true (3)
}
1 | Holds the script’s text |
2 | Holds the result of the script’s execution |
3 | Enable/disable flag |
Griffon Models are not domain classes like the ones you find in Grails; they’re more akin to presentation models, and as such, they’re used to transfer data between Views and Controllers.
Notice the usage of the @ArtifactProviderFor
annotation. This annotation serves as an additional hint to the
compiler, letting it know it must generate or update a metadata file in an specific location.
The file is named after the argument set on the annotation, in this case, griffon.core.artifact.GriffonModel
. This file
is automatically placed under META-INF/griffon
. It’s contents are fully qualified class names of types that implement
the argument set on the annotation. This results in the following file being automatically created or updated with every
compilation session
1
console.ConsoleModel
You will see this annotation being used by other artifacts too albeit with different values, which will produce different
files, such as META-INF/griffon/griffon.core.artifact.GriffonController
, META-INF/griffon/griffon.core.artifact.GriffonView
,
and META-INF/griffon/griffon.core.artifact.GriffonService
.
It’s worth mentioning that you may skip applying this annotation, in which case you’ll be responsible for creating and updating the files mentioned earlier. These files are very important to the Griffon runtime, as they are used to locate and configure all artifacts in an application.
Controller
The controller is also trivial: throw the contents of the script from the model at an Evaluator
,
then store the result back into the model. Make sure you paste the following code into
griffon-app/controllers/console/ConsoleController.groovy
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package console
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonController)
class ConsoleController {
@MVCMember @Nonnull
ConsoleModel model (1)
@Inject
Evaluator evaluator (2)
@ControllerAction
void executeScript() { (3)
model.enabled = false
def result
try {
result = evaluator.evaluate(model.scriptSource) (4)
} finally {
model.enabled = true
model.scriptResult = result (5)
}
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Controller action; automatically executed off the UI thread |
4 | Evaluate the script |
5 | Write back result to Model |
The Griffon framework will inject references to the other portions of the MVC triad if fields
named model
, view
, and controller
are present in the Model, Controller or View. This allows
us to access the view widgets and the model data if needed. These properties are annotated with
@griffon.inject.MVCMember
and @javax.annotation.Nonnull
as hints to the Griffon runtime, enabling
additional checks. Any other class members annotated with @javax.inject.Inject
participate in
dependency injection as laid out by JSR 330, in this case the controller will get an instance of
Evaluator
if a suitable implementation is bound.
The executeScript
3 method will be used later in the View in combination
with a button. You may notice that there’s no explicit threading management. All Swing developers
know they must obey the Swing Rule: long running computations must run outside of the EDT (Swing’s
Event Dispatch Thread); all UI components should be queried/modified inside the EDT. It
turns out Griffon is aware of this rule, making sure an action is called outside of the EDT by
default; all bindings made to UI components via the model will be updated inside the EDT
5. We’ll setup the bindings in the next listing.
We must create a Module
in order to bind Evaluator
.
These are the required class definitions:
1
2
3
4
5
package console
interface Evaluator {
Object evaluate(String input)
}
1
2
3
4
5
6
7
8
9
10
package console
class GroovyShellEvaluator implements Evaluator {
private GroovyShell shell = new GroovyShell()
@Override
Object evaluate(String input) {
shell.evaluate(input)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package console
import griffon.core.injection.Module
import griffon.inject.DependsOn
import griffon.swing.SwingWindowDisplayHandler
import org.codehaus.griffon.runtime.core.injection.AbstractModule
import org.kordamp.jipsy.ServiceProviderFor
import static griffon.util.AnnotationUtils.named
@DependsOn('swing') (3)
@ServiceProviderFor(Module) (4)
class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(Evaluator) (1)
.to(GroovyShellEvaluator)
.asSingleton()
bind(SwingWindowDisplayHandler) (2)
.withClassifier(named('defaultWindowDisplayHandler'))
.to(CenteringWindowDisplayHandler)
.asSingleton()
}
}
1 | Binding definition |
2 | Overriding an existing binding |
3 | Loaded after swing module |
4 | Generate metadata file automatically |
Modules can define several bindings, even override existing bindings. In our particular
case, we defined a binding 1 for Evaluator
and overrode a
binding 2 for SwingWindowDisplayHandler
. The latter is
supplied by the swing
module; thus, we must mark it as a dependency 3
in our module definition. Modules must be listed in a metadata file named META-INF/services/griffon.core.injection.Module
;
this is exactly what the @ServiceProviderFor
annotation does, by instructing the compiler that it must create or update
this particular file. This annotation is handled by the Annotation Processing tool facilities in Java via Jipsy or
by AST Transformations in Groovy via Gipsy. What’s important to remember is that this annotation will keep the metadata
files up to date everytime a compilation session is executed.
The implementation of our custom SwingWindowDisplayHandler
is quite trivial, as shown by the following snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package console
import org.codehaus.griffon.runtime.swing.DefaultSwingWindowDisplayHandler
import javax.annotation.Nonnull
import java.awt.Window
import static griffon.swing.support.SwingUtils.centerOnScreen
class CenteringWindowDisplayHandler extends DefaultSwingWindowDisplayHandler {
@Override
void show(@Nonnull String name, @Nonnull Window window) {
centerOnScreen(window)
super.show(name, window)
}
}
This handler is only concerned with centering the window on the screen before showing it.
View
The view classes contain the visual components for your application. Please paste the following code
into griffon-app/views/console/ConsoleView.groovy
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package console
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class ConsoleView {
@MVCMember @Nonnull
FactoryBuilderSupport builder (1)
@MVCMember @Nonnull
ConsoleModel model (1)
void initUI() {
builder.with {
actions {
action(executeScriptAction, (2)
enabled: bind { model.enabled })
}
application(title: application.configuration['application.title'],
pack: true, locationByPlatform: true, id: 'mainWindow',
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]) {
panel(border: emptyBorder(6)) {
borderLayout()
scrollPane(constraints: CENTER) {
textArea(text: bind(target: model, 'scriptSource'), (3)
enabled: bind { model.enabled }, (2)
columns: 40, rows: 10)
}
hbox(constraints: SOUTH) {
button(executeScriptAction) (4)
hstrut(5)
label('Result:')
hstrut(5)
textField(editable: false,
text: bind { model.scriptResult }) (5)
}
}
}
}
}
}
1 | MVC member injected by MVCGroupManager |
2 | Bind enabled state from model |
3 | Bind script source to model |
4 | Apply controller action by convention |
5 | Bind script result from model |
The View contains a fairly straightforward SwingBuilder script. Griffon will execute these groovy
scripts in context of its CompositeBuilder
.
2.2.4. Running the application
Running the application requires you to execute the run
task if using Gradle:
$ ./gradlew run
Or the exec:java
plugin goal if using Maven. Take special note that this goal assumes classes
have been compiled already, so it’s best to pair it up with compile
for safe measure. Or better
yet, use the pre-configured run
profile, like so
$ mvn -Prun
Now that we know the basic structure of a Griffon application and how to run it, we turn to testing.
2.2.5. Testing
It’s always a good idea to test out the code we write. It’s pretty easy to write tests for
regular components such as Evaluator
and GroovyShellEvaluator
, as they require little
or no external dependencies. Testing Griffon artifacts such as Controllers and Models on the
other hand requires a bit more of effort, but not much, as shown by ConsoleControllerTest
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package console
import griffon.core.artifact.ArtifactManager
import griffon.core.injection.Module
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import griffon.inject.DependsOn
import org.codehaus.griffon.runtime.core.injection.AbstractTestingModule
import org.junit.Rule
import org.junit.Test
import javax.annotation.Nonnull
import javax.inject.Inject
import static java.util.concurrent.TimeUnit.SECONDS
import static org.awaitility.Awaitility.await
import static org.awaitility.Awaitility.fieldIn
import static org.hamcrest.Matchers.notNullValue
@TestFor(ConsoleController) (1)
class ConsoleControllerTest {
private ConsoleController controller (2)
@Inject
private ArtifactManager artifactManager (3)
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule() (4)
@Test
void testExecuteScriptAction() {
// given: (5)
ConsoleModel model = artifactManager.newInstance(ConsoleModel.class)
controller.model = model
// when: (6)
String input = 'var = "Griffon"'
model.scriptSource = input
controller.invokeAction('executeScript')
// then: (7)
await().atMost(2, SECONDS)
.until(fieldIn(model)
.ofType(Object)
.andWithName('scriptResult'),
notNullValue())
assert input == model.scriptResult
}
private static class EchoEvaluator implements Evaluator { (8)
@Override
Object evaluate(String input) {
input
}
}
@DependsOn('application')
private static class TestModule extends AbstractTestingModule {
@Override
protected void doConfigure() {
bind(Evaluator)
.to(EchoEvaluator)
.asSingleton()
}
}
@Nonnull
private List<Module> moduleOverrides() {
[new TestModule()]
}
}
1 | Indicate class under test |
2 | Injected by GriffonUnitRule given 1 |
3 | Injected by GriffonUnitRule via JSR 330 |
4 | Instantiates and configures a GriffonAplication for testing |
5 | Setup collaborators |
6 | Stimulus |
7 | Validate after waiting 2 seconds at most |
At the heart, we have the @TestFor
1 annotation and a JUnit4 rule:
@GriffonUnitRule
4. These two key elements are responsible for
injecting the required behavior into the test case. @TestFor
identifies the type of component,
a Controller in this case, which is under test; it assumes a suitable private field 2
to be defined in the testcase. This field is used to inject the instance under test.
The GriffonUnitRule
is responsible for bootstrapping a barebones application and putting
together all the required bindings. Notice that the test case can participate in dependency
injection too 3.
Running tests requires executing the test
task:
$ ./gradlew test
A similar command can be invoked with Maven:
$ mvn test
These are the basics for getting started with a Griffon project.
3. Application Overview
3.1. Configuration
It may seem odd in a framework that embraces "convention-over-configuration" that we tackle this topic now, but since what configuration there is is typically a one-off, it is best to get it out the way.
3.1.1. Basic Configuration
For general configuration, Griffon provides a file called griffon-app/conf/Config.groovy
.
This file uses Groovy’s ConfigSlurper, which is very similar to Java properties files
except it is pure Groovy, hence you can re-use variables and use proper Java types!
Here’s a typical configuration file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package sample.swing.groovy
application {
title = 'Swing + Groovy'
startupGroups = ['sample']
autoShutdown = true
}
mvcGroups {
// MVC Group for "sample"
'sample' {
model = 'sample.swing.groovy.SampleModel'
view = 'sample.swing.groovy.SampleView'
controller = 'sample.swing.groovy.SampleController'
}
}
You can define this file using Java too:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package sample.swing.java;
import griffon.util.AbstractMapResourceBundle;
import griffon.util.CollectionUtils;
import javax.annotation.Nonnull;
import java.util.Map;
import static java.util.Collections.singletonList;
public class Config extends AbstractMapResourceBundle {
@Override
protected void initialize(@Nonnull Map<String, Object> entries) {
CollectionUtils.map(entries)
.e("application", CollectionUtils.map()
.e("title", "Swing + Java")
.e("startupGroups", singletonList("sample"))
.e("autoShutdown", true)
)
.e("mvcGroups", CollectionUtils.map()
.e("sample", CollectionUtils.map()
.e("model", "sample.swing.java.SampleModel")
.e("view", "sample.swing.java.SampleView")
.e("controller", "sample.swing.java.SampleController")
)
);
}
}
Or if you prefer properties files, then do the following:
1
2
3
4
5
6
application.title = Swing + Groovy
application.startupGroups = sample
application.autoShutdown = true
mvcGroups.sample.model = sample.swing.groovy.SampleModel
mvcGroups.sample.view = sample.swing.groovy.SampleView
mvcGroups.sample.controller = sample.swing.groovy.SampleController
Finally, you can also use XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0"?>
<configuration>
<application>
<title>Swing + Groovy</title>
<startupGroups>sample</startupGroups>
<autoShutdown>true</autoShutdown>
</application>
<mvcGroups>
<sample>
<model>sample.swing.groovy.SampleModel</model>
<view>sample.swing.groovy.SampleView</view>
<controller>sample.swing.groovy.SampleController</controller>
</sample>
</mvcGroups>
</configuration>
Take special note that these last two files must be placed under griffon-app/resources
instead.
The application’s runtime configuration is available through the configuration
property
of the application instance. This configuration instance is read-only; you can’t modify
its contents in any way.
There are 3 configuration keys that control the application’s behavior:
application.title |
Defines the default title for the application. |
application.startupGroups |
Defines a list of MVC groups to be initialized during the Startup phase. Refer to the MVC chapter to know more about MVC groups. |
application.autoShutdown |
Controls if the application should shutdown when no windows are visible. Set to `true`by default. |
Additional configuration keys may be added as you deem fit. Take note that the mvcGroups
prefix has special meaning.
Refer to the MVC chapter to know more about the different options available to you.
3.1.2. Internationalization Support
Configuration files are i18n aware, which means you can append
locale specific strings to a configuration file; for example, Config_de_CH.groovy
.
Locale suffixes are resolved from least to most specific; for a locale with
language = 'de'
, country = 'CH'
and variant = 'Basel'
, the following files are loaded in order:
-
Config.groovy
-
Config.properties
-
Config_de.groovy
-
Config_de.properties
-
Config_de_CH.groovy
-
Config_de_CH.properties
-
Config_de_CH_Basel.groovy
-
Config_de_CH_Basel.properties
The current java.util.Locale
is used to determine values for language, country and variant.
3.1.3. Mutable Configuration
As mentioned before, the default application configuration is made read-only, however there’s a way to make it
mutable; you simply must wrap the Configuration
instance with a MutableConfiguration
. You can accomplish
this by registering an appropriate implementation of ConfigurationDecoratorFactory
, for example
MutableConfigurationDecoratorFactory
. You’ll have to register this class with a module:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.acme
import griffon.core.injection.Module;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import org.codehaus.griffon.runtime.core.configuration.ConfigurationDecoratorFactory;
import org.codehaus.griffon.runtime.core.configuration.MutableConfigurationDecoratorFactory;
import static griffon.util.AnnotationUtils.named;
@ServiceProviderFor(Module.class)
public class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(ConfigurationDecoratorFactory.class)
.to(MutableConfigurationDecoratorFactory.class)
.asSingleton();
}
}
3.1.4. Conditional Blocks
There are times where a configuration value may dependent on an enviromental setting such as the current Application Environment. All types of configuration files (properties, Groovy scripts, class based resource bundles) support the notion of conditional blocks. Take for example the following datasource configuration in Groovy script format:
dataSource.driverClassName = 'org.h2.Driver'
environments {
development {
dataSource.url = 'jdbc:h2:mem:${application_name}-dev'
}
test {
dataSource.url = 'jdbc:h2:mem:${application_name}-test'
}
production {
dataSource.url = 'jdbc:h2:file:/opt/${application_name}/data/db'
}
}
The same configuration in properties format:
dataSource.driverClassName = org.h2.Driver
environments.development.dataSource.url = jdbc:h2:mem:${application_name}-dev
environments.test.dataSource.url = jdbc:h2:mem:${application_name}-test
environments.production.dataSource.url = jdbc:h2:file:/opt/${application_name}/data/db
Or in XML format:
<?xml version="1.0"?>
<configuration>
<dataSource>
<driverClassName>org.h2.Driver</driverClassName>
</dataSource>
<environments>
<development>
<dataSource>
<url>jdbc:h2:mem:${application_name}-dev</url>
</dataSource>
</development>
<test>
<dataSource>
<url>jdbc:h2:mem:${application_name}-test</url>
</dataSource>
</test>
<production>
<dataSource>
<url>jdbc:h2:mem:${application_name}-prod</url>
</dataSource>
</production>
</environments>
</configuration>
Or as a class based ResourceBundle
subclass:
import java.util.Map;
import griffon.util.AbstractMapResourceBundle;
import static griffon.util.CollectionUtils.map;
public class DataSource extends AbstractMapResourceBundle {
@Override
protected void initialize(Map<String, Object> entries) {
map(entries)
.e("dataSource", map()
.e("driverClassName", "org.h2.Driver")
)
.e("environments", map()
.e("development", map()
.e("dataSource", map()
.e("url", "jdbc:h2:mem:sample-dev")
)
)
.e("test", map()
.e("dataSource", map()
.e("url", "jdbc:h2:mem:sample-test")
)
)
.e("production", map()
.e("dataSource", map()
.e("url", "jdbc:h2:file:/opt/sample/data/db")
)
)
);
}
}
The conditional block in this case is environments
; the value of Environment
will dictate the actual value
associated with the dataSource.url
key. There are 3 conditional blocks defined by default:
Name |
Default value |
environments |
Environment.getName() |
projects |
Metadata.getApplicationName() |
platforms |
GriffonApplicationUtils.getPlatform() |
You may define additional conditional blocks as needed, but you must do it by defining appropriate binding overrides for the following types in a custom Module of your choice:
-
griffon.util.PropertiesReader
for properties files. -
griffon.util.ResourceBundleReader
for class basedResourceBundle
and XML. -
griffon.util.ConfigReader
for Groovy scripts.
3.1.5. Configuration Injection
Configuration values may be injected into beans if the @Configured annotation is applied to a field or setter method of a type that’s bound to the application’s dependency container. Take for example the following configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
application {
title = 'JavaFX + Groovy'
startupGroups = ['sample']
autoShutdown = true
}
props {
string = 'string'
number = 42
date = '1970-12-24'
}
mvcGroups {
'sample' {
model = 'sample.javafx.groovy.SampleModel'
view = 'sample.javafx.groovy.SampleView'
controller = 'sample.javafx.groovy.SampleController'
}
}
Any of those configuration values may be set on a managed bean, such as the SampleModel
class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package sample.javafx.groovy
import griffon.core.artifact.GriffonModel
import griffon.metadata.ArtifactProviderFor
import griffon.transform.FXObservable
import griffon.core.configuration.Configured
@ArtifactProviderFor(GriffonModel)
class SampleModel {
@FXObservable String input
@FXObservable String output
@Configured('application.title')
String title
@Configured('props.string')
String string
@Configured('props.number')
int number
@Configured(value = 'props.date', format = 'YYYY-MM-dd')
Date date
@Configured(value = 'props.undefined', defaultValue = 'undefined')
String undefined
}
Values will be injected before @PostConstruct
is triggered on the managed bean. Type conversion will be carried out
using property editors.
3.2. Metadata
The Griffon runtime keeps track of useful metadata that can be consumed by applications. The following sections describe them with more detail.
3.2.1. Application Metadata
Access to the application’s metadata file (application.properties
) is available by
querying the Metadata
singleton. Here’s a snippet of code that shows how to
setup a welcome message that displays the application’s name and version:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package sample
import griffon.core.artifact.GriffonView
import griffon.core.env.Metadata
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull FactoryBuilderSupport builder
@Inject Metadata metadata
void initUI() {
builder.with {
application(pack: true,
title: application.configuration['application.title']) {
label "Hello, I'm ${metadata['application.name']}-${metadata['application.version']}"
}
}
}
}
There are also a few helpful methods found in Metadata
:
-
getApplicationName()
- same result asmeta['application.name']
-
getApplicationVersion()
- same result asmeta['application.version']
The following table shows the default properties and values found in application.properties
. You may edit this
file and either set a fixed value of an existing property or add new key/value pairs
Key | Value |
---|---|
application.name |
${application_name} |
application.version |
${application_version} |
build.date |
${build_date} |
build.time |
${build_time} |
build.revision |
${build_revision} |
Values surrounded by ${}
are treated as placeholder tokens; their values are determined at build time. Look
for the griffon
section in the Gradle build file to change these values, which looks like this by default
griffon {
disableDependencyResolution = false
includeGroovyDependencies = false
version = '2.15.0'
toolkit = 'javafx'
applicationProperties = [ (1)
'build_date' : buildDate,
'build_time' : buildTime,
'build_revision': versioning.info.commit
]
}
1 | placeholder tokens. |
3.2.2. Feature
A Feature
is a boolean flag that determines if a capability is available to
the application at runtime. Feature
s are nothing more than a System
property. Here’s an example of a module that decides if a specific binding
should be applied over another:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.opendolphin.demo
import griffon.core.env.Feature
import griffon.core.injection.Module
import org.codehaus.griffon.runtime.core.injection.AbstractModule
import org.kordamp.jipsy.ServiceProviderFor
import org.opendolphin.core.client.comm.ClientConnector
import org.opendolphin.demo.injection.HttpClientConnectorProvider
import org.opendolphin.demo.injection.InMemoryClientConnectorProvider
@ServiceProviderFor(Module)
class DolphinModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(ClientConnector)
.toProvider(InMemoryClientConnectorProvider)
.asSingleton()
Feature.withFeature('dolphin.remote') {
bind(ClientConnector)
.toProvider(HttpClientConnectorProvider)
.asSingleton()
}
}
}
The remote option can be enabled by running the application with -Ddolphin.remote=true
,
or by adding the following entry to griffon-app/resources/application.properties
:
1
dolphin.remote=true
3.2.3. Application Environment
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 Environment
enum.
The following example enhances the previous one by displaying the current running environment:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package sample
import griffon.core.artifact.GriffonView
import griffon.core.env.Metadata
import griffon.core.env.Environment
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull FactoryBuilderSupport builder
@Inject Metadata metadata
@Inject Environment environment
void initUI() {
builder.with {
application(pack: true,
title: application.configuration['application.title']) {
gridLayout cols: 1, rows: 2
label "Hello, I'm ${metadata['application.name']}-${metadata['application.version']}"
label "Current environment is ${environment}"
}
}
}
}
The default environment is DEVELOPMENT. A different value can be specified by setting
a proper value for the griffon.env
System property. The Environment
class
recognizes the following aliases:
-
dev
- short fordevelopment
. -
prod
- short forproduction
.
You have the following options to change the environment value if using Gradle as build tool:
-
specify the value as a project property named
griffonEnv
. -
specify the value in the
jvmArgs
property of therun
task.
The value for this type is determined by the griffon.env
System property.
3.2.4. Griffon Environment
The GriffonEnvironment
gives you access to the following values:
-
Griffon version
-
Griffon build date & time (ISO 8601)
-
JVM version
-
OS version
Here’s an example displaying all values:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package sample
import griffon.core.artifact.GriffonView
import griffon.core.env.Metadata
import griffon.core.env.Environment
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import static griffon.core.env.GriffonEnvironment.*
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull FactoryBuilderSupport builder
@Inject Metadata metadata
@Inject Environment environment
void initUI() {
builder.with {
application(pack: true,
title: application.configuration['application.title']) {
gridLayout cols: 1, rows: 6
label "Hello, I'm ${metadata['application.name']}-${metadata['application.version']}"
label "Current environment is ${environment}"
label "Griffon version is ${getGriffonVersion()}"
label "Build date/time is ${getBuildDateTime()}"
label "JVM version is ${getJvmVersion()}"
label "OS version is ${getOsVersion()}"
}
}
}
}
3.3. Lifecycle
Every Griffon application goes through the same lifecycle phases no matter in which mode it is running, with the exception of applet mode where there is an additional phase due to the intrinsic nature of applets. The application’s lifecycle was inspired by JSR-296, the Swing Application Framework.
Every phase has an associated lifecycle class that will be invoked at the appropriate
time. Class names match each phase name; you’ll find them inside griffon-app/lifecycle
.
The GriffonApplication
exposes an observable property of type ApplicationPhase
;
this property reflects the phase the application currently has. Any component can register a
listener on this property in order to be notified when the application changes phase.
3.3.1. Initialize
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.
This phase is typically used to tweak the application for the current platform, including its Look & Feel.
The Initialize lifecycle handler will be called immediately after the configuration
has been read, but before addons and managers are initialized.
|
3.3.2. Startup
This phase is responsible for instantiating all MVC groups that have been defined in the application’s configuration and that also have been marked as startup groups in the same configuration file.
The Startup lifecycle handler will be called after all MVC groups have been
initialized.
|
3.3.3. Ready
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 window will be displayed
at the end of this phase.
3.3.4. Shutdown
Called when the application is about to close. Any artifact can invoke the shutdown
sequence by calling shutdown()
on the GriffonApplication
instance.
The Shutdown lifecycle handler will be called after all ShutdownHandler s and
event handlers interested in the ShutdownStart event.
|
3.4. Locale
The GriffonApplication
exposes an observable property of type java.util.Locale
.
This property reflects the locale the application currently has. Any component can register a
listener on this property in order to be notified when the application changes locale.
You may either change the application’s locale by setting a value of type Locale
or a literal
that can be parsed as a locale. The format is
language[_country[_variant]]
where
-
language is an ISO 639 alpha-2 or alpha-3 language code. Example: "en" (English), "ja" (Japanese), "kok" (Konkani).
-
country is an ISO 3166 alpha-2 country code or UN M.49 numeric-3 area code. Example: "US" (United States), "FR" (France), "029" (Caribbean).
-
variant is any arbitrary value used to indicate a variation of a Locale. Example: "polyton" (Polytonic Greek), "POSIX".
Both country
and variant
are optional however you must specify a value for country
before defining
a value for variant
.
3.5. Modules
Module
s define components that can be injected using the JSR-330 API.
Module definitions are highly influenced by the Guice 3.x API; however, they do not
impose a strict dependency on Guice.
3.5.1. Module Definition
All modules must implement the Module
interface shown next:
1
2
3
4
5
6
public interface Module {
@Nonnull
List<Binding<?>> getBindings();
void configure();
}
They also require a well defined name. This can be achieved by annotating the module
class with @javax.inject.Named
. If a value for @javax.inject.Named
is not defined, then
one will be automatically calculated using the fully qualified class name. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;
import griffon.core.injection.Module;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import javax.inject.Named;
@ServiceProviderFor(Module.class)
@Named
public class CustomModule extends AbstractModule {
@Override
protected void doConfigure() {
// bindings
}
}
Results in a module named org.example.custom
; whereas the following definition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;
import griffon.core.injection.Module;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import javax.inject.Named;
@ServiceProviderFor(Module.class)
@Named("custom")
public class CustomModule extends AbstractModule {
@Override
protected void doConfigure() {
// bindings
}
}
Results in a module simply called custom
. A module may depend on another module; when
that’s the case, then the bindings of this module are processed after their dependencies. This
allows dependent modules to override bindings made by their dependencies. Module dependencies
are specified using the @griffon.inject.DependsOn
annotation, like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import griffon.core.injection.Module;
import griffon.inject.DependsOn;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import javax.inject.Named;
@ServiceProviderFor(Module.class)
@DependsOn("parent-module")
@Named
public class CustomModule extends AbstractModule {
@Override
protected void doConfigure() {
// bindings
}
}
3.5.2. Module Configuration
Modules perform their duty by defining bindings, of which there are 4 kinds:
Bindings have a scope of either singleton
or prototype
. InstanceBinding
is the only
binding that has singleton
as implicit scope; this setting can’t be changed.
All bindings accept a classifier
or an classifier_type
. Classifiers are annotations that
have been annotated with @javax.inject.Qualifier
. You must specify one or the other but
not both; classifier
has precedence over classifier_type
.
Instance Binding
This binding is used to define explicit and eager singletons, for example:
bind(Calculator.class)
.toInstance(new GroovyShellCalculator());
Consumers of this binding will typically define the following:
@Inject private Calculator calculator;
Notice that the field type matches the source type defined in the binding.
Target Binding
This binding enables you to specify the source type and target type. Typically the source is an interface while the target is a concrete implementation. If the target is omitted, then the binding assumes the source to be a concrete type.
bind(Calculator.class)
.to(GroovyShellCalculator.class);
Consumers of this binding will use the following form:
@Inject private Calculator calculator;
On the other hand, if the binding where to be defined as
bind(GroovyShellCalculator.class);
then the consumers must use the same matching source/target type, that is
@Inject private GroovyShellCalculator calculator;
Attempting to use the base type Calculator
by the consumer will result in an unresolved binding.
Provider Binding
This binding behaves like a factory, giving you the power to decide with extreme precision
how the instance should be created. Bindings of this type require an eager instance of
type javax.inject.Provider
.
bind(Calculator.class)
.toProvider(new CalculatorProvider());
Consumers of this binding will use the following form:
@Inject private Calculator calculator;
Provider Type Binding
Finally we have the most flexible binding as it lets you specify a javax.injectProvider
type that will be used to lazily instantiate the provider before obtaining the type instance.
bind(Calculator.class)
.toProvider(CalculatorProvider.class);
Consumers of this binding will use the following form:
@Inject private Calculator calculator;
Constant Value Binding
You may use a variation of Instance Binding to bind a constant value, for example an instance
of java.lang.String
. You must make sure to always use a qualifier. For example, the following constant value
bind(String.class)
.withClassifier(named(GithubAPI.GITHUB_API_URL_KEY))
.toInstance("https://api.github.com");
Can be injected into a target instance as
public class GithubBean {
@Inject @Named(GithubAPI.GITHUB_API_URL_KEY)
private String githubApiUrl;
}
3.5.3. Additional Settings
All bindings (with one particular exception) accept the following additional settings:
This method is used to define an additional annotation type that’s been marked as a @Qualifier
(such as @javax.inject.Named
)
that should go along with the declared binding. Any qualifiers found in the target type (or Provider
) will be overridden
by this setting.
Similar to the previous setting, this one allows you to supply an annotation instance, enabling the setting of a custom
annotation property, such as the value
of @javax.inject.Named
. Here’s an excerpt of the default bindings provided
by the Griffon runtime:
bind(Context.class)
.withClassifier(griffon.util.AnnotationUtils.named("applicationContext"))
.toProvider(DefaultContextProvider.class)
.asSingleton();
Any qualifiers found in the target (or Provider
) will be overridden by this setting.
If omitted, the binding will be placed in prototype
scope. The only binding that does not conform to this rule is InstanceBinding,
as it will be automatically placed in singleton
scope.
3.5.4. Binding Equivalencies
Any binding can be overridden by an equivalent binding. The following table describes the equivalencies between bindings:
Binding | Equivalence |
---|---|
|
|
|
|
|
|
Equivalent bindings must match qualifiers too. |
3.5.5. Module Evictions
Given binding equivalences a module may override bindings supplied by other modules, but you can’t avoid the original bindings from being added in the first case. With module evictions is possible for a module to evict another one, effectively barring the evicted module from contributing its bindings. Let’s say there’s a module named "foo" defined as follows
import griffon.core.injection.Module;
import griffon.inject.DependsOn;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import javax.inject.Named;
@Named("foo")
@ServiceProviderFor(Module.class)
public class FooModule extends AbstractModule {
@Override
protected void doConfigure() {
// binding definitions
}
}
You may evict this module by defining another module annotated with @griffon.inject.Evicts
, such as
import griffon.core.injection.Module;
import griffon.inject.DependsOn;
import griffon.inject.Evicts;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import javax.inject.Named;
@Named("foo")
@Evicts("foo")
@ServiceProviderFor(Module.class)
public class FooEvictorModule extends AbstractModule {
@Override
protected void doConfigure() {
// binding definitions
}
}
The evicting module (FooEvictorModule
) has to have the same name as the evicted module (FooModule
) hence why both
modules are annotated with @Named("foo")
.
3.5.6. The Guice Injector
The dependency griffon-guice-2.15.0
uses Google Guice to implement the JSR-330 API
that griffon-core-2.15.0
requires. As an additional feature, this dependency can automatically
load Guice modules during startup. The only requirement is a metadata file that defines the module class
that should be loaded.
Here’s an example that reuses the Quartz Scheduler from Apache Onami.
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.acme;
import org.apache.onami.scheduler.QuartzModule;
import com.google.inject.Module;
import org.kordamp.jipsy.ServiceProviderFor;
@ServiceProviderFor(Module.class)
public class MyQuartzModule extends QuartzModule {
@Override
protected void schedule() {
scheduleJob(MyJobClass.class);
}
}
When compiled, this class generates a metadata file at META-INF/services/com.google.inject.Module
with
contents similar to
1
2
3
# Generated by org.kordamp.jipsy.processor.service.ServiceProviderProcessor (0.4.0)
# Thu, 7 Aug 2014 21:02:58 +0200
com.acme.MyQuartzModule
Of course the usage of @ServiceProviderFor
is just a convenience; you can generate the metadata file
by other means. The important thing is that the file must be available in the classpath.
Guice modules are added after all Griffon modules; this means they have the chance to override any bindings set by the Griffon modules.
3.6. Shutdown Handlers
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 ShutdownHandler
interface and register themselves with the application instance.
The contract of a ShutdownHandler
is very simple:
-
boolean canShutdown(GriffonApplication application)
- returnfalse
to abort the shutdown sequence. -
void onShutdown(GriffonApplication application)
- called if the shutdown sequence was not aborted.
ShutdownHandler
s will be called on the same order as they were registered.
3.7. Startup Arguments
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 mode:
1
2
3
4
5
6
7
8
9
10
package sample
import griffon.core.GriffonApplication
import griffon.core.event.EventHandler
class ApplicationEventHandler implements EventHandler {
void onBootstrapStart(GriffonApplication application) {
println application.startupArgs
}
}
Arguments must be defined in the build file if using Gradle:
run {
args = ['one', 'two']
}
Running the application with run
command results in an output similar to the following:
$ gradle run
:compileJava
:compileGroovy
:processResources
:classes
:run
// logging statements elided
[one, two]
3.8. Context
A Context
is like a Map
; it stores key/value pairs that can be used to keep track of any
kind of data. However, unlike regular Map
s, Context
s are hierarchical. Keys found in a child
Context
have precedence over keys existing in its parent, e.g, a child Context
has the ability
to shadow keys that may be defined up the chain.
The GriffonApplication
has a default Context
whose parent is set to null
. This is the only
case where this property will be null, as the runtime makes sure that a child context will have the right value
set in its parent property when instantiated.
The Context
plays an integral role in MVC Groups and
controller actions.
3.9. Exception Handler
Griffon provides a centralized exception management facility: ExceptionHandler
. An instance of this type
is automatically registered with the application’s runtime to react to exceptions that are not caught by the
application’s code. In a sense it works like an uncuaght exception handler that’s attached to all threads, including
the UI thread.
An ExceptionHandler
has three responsibilities:
-
Filter out stacktraces to reduce the amount of noise.
-
Output the exception to a log file and/or the console.
-
Pump events to the application’s
EventRouter
.
Filtering out stacktraces can be configured by setting a System property named griffon.full.stacktrace
whose value
defaults to false
. This means stacktraces will be filtered by default if no configuration changes are made. Uncaught
exceptions are automatically logged with an ERROR
level. You cat set the System property griffon.exception.output
to
true
if you’d like to see the stacktraces forwarded to the console’s output. Finally, the System property griffon.sanitized.stacktraces
defines the package patterns used to filter out stractrace lines. The default package patterns used are:
-
org.codehaus.groovy.
-
org.codehaus.griffon.
-
groovy.
-
java.
-
javax.
-
sun.
-
com.sun.
The ExceptionHandler
will trigger the followoing events when an exception occurs.
Uncaught<ShortName>(Throwable t) |
Triggered after the |
UncaughtExceptionThrown(Throwable t) |
Triggered after the previous event. |
For the first event, <ShortName>
refers to the Throwable’s short class name. If an exception of type
java.lang.IllegalArgumentException
was caught by the ExceptionHandler
then the event name would become
UncaughtIllegalArgumentException
.
Please refer to the Events chapter in order to find out how these events may be handled.
4. The MVC Pattern
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 Config.groovy
located inside griffon-app/conf
(or
Config.java
if using Java as main language). This file holds an entry for every MVC group
that the application has (not counting those provided by Addons).
Here’s how a typical MVC group configuration looks:
1
2
3
4
5
6
7
8
mvcGroups {
// MVC Group for "sample"
sample {
model = 'sample.SampleModel'
view = 'sample.SampleView'
controller = 'sample.SampleController'
}
}
The definition order 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. The name of each member is
also defined by convention, you will typically find model
, view
, and controller
entries
in an MVC group configuration. These names are used to identify each member by their responsibility.
They are also used to inject member references into a particular MVC member. For example, if a
controller
needs access to its model
member (both defined in the same MVC group) then it simply
must define a field with matching type and name, such as
1
2
3
4
5
6
7
8
9
import griffon.core.artifact.GriffonController
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull SampleModel model
}
MVC group configurations accept a special key that defines additional configuration for that group, as it can be seen in the following snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mvcGroups {
// 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 configuration:
1
2
3
4
5
6
@ArtifactProviderFor(GriffonController)
class FooController {
void mvcGroupInit(Map<String, Object> args) {
println mvcGroup.configuration.config.key
}
}
While being able to set additional values under this key is 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.
4.1. The MVCGroupManager
This class is responsible for holding the configuration of all MVC groups no matter
how they were defined, which can be either in Config.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 finding:
MVCGroupConfiguration configuration = application.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
method:
MVCGroupConfiguration configuration = application.mvcGroupManager.findConfiguration('foo')
MVCGroup group1 = configuration.create('foo1')
MVCGroup group2 = configuration.create('foo2', [someKey: 'someValue'])
// the following will make the group's id match its name
MVCGroup group3 = configuration.create()
MVCGroup group4 = configuration.create(someKey: 'someValue')
Be aware 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 strategy.
Now, even though you can create group instances based on their configurations, the preferred
way is to call any of createMVC()
,
createMVCGroup()
, withMVCGroup()
or
withMVC()
methods. Any class annotated with the
@griffon.transform.MVCAware 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 example:
def g1 = application.mvcGroupManager.groups.foo1
def g2 = application.mvcGroupManager.findGroup('foo1')
def g3 = application.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 done:
application.mvcGroupManager.models.each { model ->
// do something with model
}
4.2. MVC Groups
Now that we know the different ways to instantiate MVC groups, we can go back to customizing them.
The simplest way is to pass in new values as part of the arguments map that
mvcGroupInit()
receives, for example:
MVCGroup group = application.mvcGroupManager.createMVCGroup('foo', [key: 'foo'])
However, if you wish to use the special config
key that every MVC group configuration
may have, then you must instantiate the group in the following way:
MVCGroupConfiguration configuration = application.mvcGroupManager
.cloneMVCConfiguration('foo', [key: 'someValue'])
MVCGroup group = configuration.create()
Note that you can still send custom arguments to the create()
method.
4.2.1. Configuring MVC Groups
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 obtain this effect:
1
2
3
4
5
6
7
8
9
10
11
12
13
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 obtain this effect:
1
2
3
4
5
6
7
8
9
10
11
12
13
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 Destruction Events
This is the counterpart to the NewInstance
event. Skipping publication of this event
may result in a slight increase of speed when a group or any artifact instance is destroyed.
Use the following configuration to obtain this effect:
1
2
3
4
5
6
7
8
9
10
11
12
13
mvcGroups {
// MVC Group for "sample"
sample {
model = 'sample.SampleModel'
view = 'sample.SampleView'
controller = 'sample.SampleController'
config {
events {
destruction = 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 is 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
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 when mvcGroupDestroy()
is invoked.
4.2.2. MVC Group Relationships
Instances of MVCGroup can be created at any time by any other instance. If an MVCGroup instance is created explicitly by another MVCGroup instance or by an MVC member (such as a Controller) then a special link is established: the newly created MVCGroup will have access to its parent MVCGroup.
Here’s an example. Assuming the following MVCGroup configuration is in place:
1
2
3
4
5
6
7
8
9
10
11
12
mvcGroups {
app {
model = 'org.example.AppModel'
view = 'org.example.AppView'
controller = 'org.example.AppController'
},
tab {
model = 'org.example.TabModel'
view = 'org.example.TabView'
controller = 'org.example.TabController'
}
}
An instance of the app
MVCGroup can be used to instantiate tab
in this way:
MVCGroup appGroup = createMVCGroup('app')
MVCGroup tabGroup = appGroup.createMVCGroup('tab')
assert appGroup == tabGroup.parentGroup
Parent-child relationships are established right after MVC members have been instantiated and can be accessed immediately
inside life-cycle methods such as mvcGroupInit()
; this comes in handy when a child group adds new UI content to
the parent’s, for example:
class TabView {
private JComponent tab
void initUI() {
tab = ... // initialize
}
void mvcGroupInit(Map<String, Object> args) {
group.parentGroup.view.tabContainer.addTab(group.mvcId, tab)
}
}
As a shortcut you may specify additional MVC members as properties using a parent
prefix; when this happens the matching
parent MVC members will be injected into the child MVC member. The previous example can be rewritten as
class TabView {
private JComponent tab
AppView parentView
void initUI() {
tab = ... // initialize
}
void mvcGroupInit(Map<String, Object> args) {
parentView.tabContainer.addTab(group.mvcId, tab)
}
}
as with the default model , view and controller MVC properties, the parent prefix can only be combined to
form parentModel , parentView and parentController .
|
4.2.3. MVC Group Context
An MVCGroup
has its own Context
. The parent of this context is set to the context of the owner of this
MVCGroup
; thus the parent of all startup MVCGroup
s is the application’s Context
. In the
previous examples, the context of the app
group is set as the parent of the context of the tab
group.
The Context
of an MVCGroup
has the same lifetime of its owning group, that is, once the owning
MVCGroup
is destroyed so is the Context
.
MVC members can have some of their properties injected from the group’s Context
. Either annotate a field or a
property setter with @Contextual
. If the @Contextual
field or argument does not have @Named
qualifier
then the fully qualified class name of the field’s or argument’s type will be used as a key.
It’s worth noting that failure to resolve a @Contextual
injection does not result in an immediate exception; if
the key could not be found in the Context
then a null
value will be set as value. You may annotate the field
or argument with @Nonnull
, in which case contextual injection will fail if the named key was not found in the context
or if its value is null.
4.3. Typed MVCGroups
Typed MVCGroups gives you type safe access to each MVC member defined by the group. This feature assumes
the MVC group has the required Model
, View
, and Controller
members; it won’t work for groups that
define less than these 3 members. You may define additional typed accessors for other members too. For example
1
2
3
4
5
6
7
8
9
10
11
12
package org.example;
import javax.inject.Named;
import griffon.core.mvc.MVCGroup;
import org.codehaus.griffon.runtime.core.mvc.AbstractTypedMVCGroup;
@Named("foo")
public class FooMVCGroup extends AbstractTypedMVCGroup<FooModel, FooView, FooController> {
public FooMVCGroup(MVCGroup delegate) {
super(delegate);
}
}
Instances of this group can be created as follows
1
2
FooMVCGroup fooGroup1 = createMVCGroup(FooMVCGroup.class);
FooMVCGroup fooGroup2 = createMVCGroup(FooMVCGroup.class, "foo2");
You may now refer to the exact types of each MVC member, for example
1
2
3
4
5
6
FooMVCGroup fooGroup = createMVCGroup(FooMVCGroup.class);
fooGroup.model().setSomeProperty("value"); // returned model type is FooModel
// the following won't even compile
MVCGroup mvcGroup = createMVCGroup("foo");
mvcGroup.getModel().setSomeProperty("value"); // returned model type is GriffonModel !!
The name of the group is determined by the value of the @Named
annotation. If the annotation is not present
then the simple class name (FooMVCGroup
in this case) will be used instead.
4.4. MVC Lifecycle
All subclasses of GriffonMvcArtifact
(such as controllers, models, and views) have a basic lifecycle that complements
the lifecycle offered by the Depenendency Injection container. The following rules apply (in order):
-
The artifact’s constructor is invoked by the DI container.
-
If a method is annotated with
@javax.annotation.PostConstruct
then it’s invoked by the DI container. -
All members (fields and setters) annotated with
@Contextual
are resolved and injected. -
The
MVCGroupManager
invokesmvcGroupInit()
on the artifact.
-
The
MVCGroupManager
invokesmvcGroupDestroy()
on the artifact. -
All members (fields and setters) annotated with
@Contextual
are set tonull
. -
If a method is annotated with
@javax.annotation.PreDestroy
then it’s invoked by the DI container.
4.5. The @MVCAware Transformation
Any component may gain the ability to create and destroy MVC groups through an MVCGroupManager
instance. You only need annotate the class with @griffon.transform.MVCAware
and it will automatically gain all methods exposed by MVCHandler
.
This feature is just a shortcut to avoid reaching for the application instance from objects that do not hold a reference to it.
Here’s an example of a custom bean that’s able to work with MVC groups:
1
2
3
@griffon.transform.MVCAware
class Bean {
}
This class can be used in the following way
1
2
3
4
5
6
7
8
class SampleService {
@Inject Bean bean
void buildSecondary(String groupName) {
def (m, v, c) = bean.createMVC(groupName)
// do something with m, v and c
}
}
5. Models
This chapter describe models and all binding options.
Models are very simple in nature. Their responsibility is to hold data that can be used by both Controller and View to communicate with each other. In other words, Models are not equivalent to domain classes.
5.1. Swing Binding
Binding in Griffon is achieved by leveraging Java Beans' java.beans.PropertyChangeEvent
and their related classes; thus binding will work with any class that fires this type of
event, regardless of its usage of @griffon.transform.Observable
or not.
5.1.1. Groovy Bindings
The following options are available for writing bindings using the bind
call when Groovy is the source language:
The most complete of all three. You must specify both ends of the binding explicitly. The following snippet sets a
unidirectional binding from bean1.prop1
to bean2.prop2
:
bind(source: bean1, sourceProperty: 'prop1',
target: bean2, targetProperty: 'prop2')
This type of binding can assume either the sources or the targets depending on the context. The following snippets
set an unidirectional binding from bean1.prop1
to bean2.prop2
bean(bean1, prop1: bind(target: bean2, targetProperty: 'prop2'))
bean(bean2, prop2: bind(source: bean1, sourceProperty: 'prop1'))
When used in this way, either sourceProperty:
or targetProperty:
can be omitted; the bind node’s value will become
the property name, in other words
bean(bean1, prop1: bind('prop2', target: bean2))
This type of binding is only useful for setting implicit targets. It expects a closure as the definition of the binding value:
bean(bean2, prop2: bind{ bean1.prop1 })
The following properties can be used with either the long or contextual binding syntax:
Bindings are usually setup in one direction. If this property is specified with a value of true
then a bidirectional
binding will be created instead.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import griffon.transform.Observable
import groovy.swing.SwingBuilder
class MyModel {
@Observable String value
}
def model = new MyModel()
def swing = new SwingBuilder()
swing.edt {
frame(title: 'Binding', pack: true, visible: true) {
gridLayout(cols: 2, rows: 3)
label 'Normal'
textField(columns: 20, text: bind('value', target: model))
label 'Bidirectional'
textField(columns: 20, text: bind('value', target: model, mutual: true))
label 'Model'
textField(columns: 20, text: bind('value', source: model))
}
}
Typing text on textfield #2 pushes the value to model, which in turns updates textfield #2 and #3, demonstrating that textfield #2 listens top model updates. Typing text on textfield #2 pushes the value to textfield #3 but not #1, demonstrating that textfield #1 is not a bidirectional binding.
Transforms the value before it is sent to event listeners.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import griffon.transform.Observable
import groovy.swing.SwingBuilder
class MyModel {
@Observable String value
}
def convertValue = { val ->
'*' * val?.size()
}
def model = new MyModel()
def swing = new SwingBuilder()
swing.edt {
frame(title: 'Binding', pack: true, visible: true) {
gridLayout(cols: 2, rows: 3)
label 'Normal'
textField(columns: 20, text: bind('value', target: model))
label 'Converter'
textField(columns: 20, text: bind('value', target: model, converter: convertValue))
label 'Model'
textField(columns: 20, text: bind('value', source: model))
}
}
Typing text on textfield #1 pushes the value to the model as expected, which you can inspect by looking at textfield #3. Typing text on textfield #2 however transform’s every keystroke into an '*' character.
Transforms the value from the target to the source.
Guards the trigger. Prevents the event from being sent if the return value is false
or null
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import griffon.transform.Observable
import groovy.swing.SwingBuilder
class MyModel {
@Observable String value
}
def isNumber = { val ->
if(!val) return true
try {
Double.parseDouble(val)
} catch(NumberFormatException e) {
false
}
}
def model = new MyModel()
def swing = new SwingBuilder()
swing.edt {
frame(title: 'Binding', pack: true, visible: true) {
gridLayout(cols: 2, rows: 3)
label 'Normal'
textField(columns: 20, text: bind('value', target: model))
label 'Converter'
textField(columns: 20, text: bind('value', target: model, validator: isNumber))
label 'Model'
textField(columns: 20, text: bind('value', source: model))
}
}
You can type any characters on textfield #1 and see the result in textfield #3. You can only type numbers on textfield #2 and see the result in textfield #3.
This type of validation is not suitable for semantic validation (a.k.a. constraints in domain classes). |
Maps a different event type, instead of PropertyChangeEvent
.
Specify a value that may come from a different source. Usually found in partnership with sourceEvent
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import griffon.transform.Observable
import groovy.swing.SwingBuilder
class MyModel {
@Observable String value
}
def model = new MyModel()
def swing = new SwingBuilder()
swing.edt {
frame(title: 'Binding', pack: true, visible: true) {
gridLayout(cols: 2, rows: 3)
label 'Text'
textField(columns: 20, id: 'tf1')
label 'Trigger'
button('Copy Text', id: 'bt1')
bind(source: bt1,
sourceEvent: 'actionPerformed',
sourceValue: {tf1.text},
target: model,
targetProperty: 'value')
label 'Model'
textField(columns: 20, text: bind('value', source: model))
}
}
A contrived way to copy text from one textfield to another. The copy is performed by listening to `ActionEvent`s pumped by the button.
These examples made use of the @griffon.transform.Observable AST transformation. This transformation is a carbon
copy of @groovy.beans.Bindable with one addition: the owner class will also implement the `Observable interface.
Both transformations are functionally equivalent and can be used interchangeably.
|
5.2. JavaFX Binding
JavaFX provides its own binding mechanism, based on Observable
and related types, such as
javafx.beans.property.Property
and javafx.beans.binding.Binding
.
5.2.1. Groovy Bindings
GroovyFX brings Groovy support for JavaFX in a similar way as standard Groovy does for Swing, which was discussed in a previous section. However there are some slight differences.
The griffon-javafx-groovy-2.15.0.jar
delivers a new AST Transformation: @FXObservable,
which by all means and purposes is functionally equivalent to GroovyFX’s @FXBindable
annotation. However @FXObservable
delivers better integration with the Griffon runtime, for example hooking into ThreadingManager
instead of calling
into Platform.runLater
.
Using the @FXObservable
AST transformation results in shorter, more expressive code. For example, the following code
1
2
3
class MyModel {
@FXObservable String value
}
Is transformed into byte code as if you had written the following source code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
class MyModel {
private StringProperty value
String getValue() { valueProperty().get() }
void setValue(String s) { valueProperty().set(s) }
StringProperty getValueProperty() { valueProperty() }
StringProperty valueProperty() {
if (value == null) {
value = new SimpleStringProperty(this, 'value')
}
value
}
}
This annotation serves then as a shortcut for writing observable properties and nothing more.
5.3. The @Observable AST Transformation
The @griffon.transform.Observable
transformation will inject the behavior of Observable
into the annotated class. It basically injects an instance of java.beans.PropertyChangeSupport
and all methods required to make the model an observable class. It will also ensure that
a java.beans.PropertyChangeEvent
is fired for each observable property whenever said
property changes value.
The following is a list of all methods added by @griffon.transform.Observable
:
-
void addPropertyChangeListener(PropertyChangeListener listener)
-
void addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
-
void removePropertyChangeListener(PropertyChangeListener listener)
-
void removePropertyChangeListener(String propertyName, PropertyChangeListener listener)
-
PropertyChangeListener[] getPropertyChangeListeners()
-
PropertyChangeListener[] getPropertyChangeListeners(String propertyName)
-
void firePropertyChange(String propertyName, Object oldValue, Object newValue)
5.4. The @Vetoable AST Transformation
The @griffon.transform.Vetoable
transformation will inject the behavior of Vetoable
into the annotated class. It basically injects an instance of java.beans.VetoableChangeSupport
and all methods required to make the model a vetoable class. It will also ensure that
a java.beans.PropertyChangeEvent
is fired for each vetoable property whenever said
property changes value.
The following is a list of all methods added by @griffon.transform.Vetoable
:
-
void addVetoableChangeListener(VetoableChangeListener listener)
-
void addVetoableChangeListener(String propertyName, VetoableChangeListener listener)
-
void removeVetoableChangeListener(VetoableChangeListener listener)
-
void removeVetoableChangeListener(String propertyName, VetoableChangeListener listener)
-
VetoableChangeListener[] getVetoableChangeListeners()
-
VetoableChangeListener[] getVetoableChangeListeners(String propertyName)
-
void fireVetoableChange(String propertyName, Object oldValue, Object newValue)
5.5. The @PropertyListener AST Transformation
The @griffon.transform.PropertyListener
helps you to register PropertyChangeListener
s
without so much effort. The following code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import griffon.transform.PropertyListener
import griffon.transform.Observable
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
@PropertyListener(snoopAll)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@Observable String name
@Observable
@PropertyListener({controller.someAction(it)})
String lastname
def snoopAll = { evt -> ... }
}
is equivalent to this one:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.beans.PropertyChangeListener
import griffon.transform.Observable
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
@PropertyListener(snoopAll)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@Observable String name
@Observable String lastname
def snoopAll = { evt -> ... }
SampleModel() {
addPropertyChangeListener(snoopAll as PropertyChangeListener)
addPropertyChangeListener('lastname', {
controller.someAction(it)
} as PropertyChangeListener)
}
}
@griffon.transform.PropertyListener
accepts the following values:
-
in-place definition of a closure
-
reference of a closure property defined in the same class
-
a List of any of the previous two
5.6. The @ChangeListener AST Transformation
The @griffon.transform.ChangeListener
helps you to register ChangeListener
s
without so much effort. The following code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import griffon.transform.ChangeListener
import griffon.transform.FXObservable
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@FXObservable
@ChangeListener(snoopAll)
String name
@FXObservable
@ChangeListener({ ob, ov, nv -> controller.someAction(nv)})
String lastname
def snoopAll = { ob, ov, nv -> ... }
}
is equivalent to this one:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javafx.beans.value.ChangeListener
import griffon.transform.FXObservable
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@FXObservable String name
@FXObservable String lastname
def snoopAll = { ob, ov, nv -> ... }
SampleModel() {
nameProperty().addListener(snoopAll as ChangeListener)
lastnameProperty().addListener({ ob, ov, nv ->
controller.someAction(nv)
} as ChangeListener)
}
}
@griffon.transform.ChangeListener
accepts the following values:
-
in-place definition of a closure
-
reference of a closure property defined in the same class
-
a List of any of the previous two
@griffon.transform.ChangeListener
has an additional member named weak
. When set to true
the generated ChangeListener
will
be wrapped with a WeakChangeListener
.
5.7. The @ListChangeListener AST Transformation
The @griffon.transform.ListChangeListener
helps you to register ListChangeListener
s
without so much effort. The following code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import griffon.transform.ListChangeListener
import griffon.transform.FXObservable
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@FXObservable
@ListChangeListener(snoop)
ObservableList list = FXCollections.observableArrayList()
def snoop = { change -> ... }
}
is equivalent to this one:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import javafx.collections.ListChangeListener
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@FXObservable ObservableList list = FXCollections.observableArrayList()
def snoop = { change -> ... }
SampleModel() {
listProperty().addListener(snoopAll as ListChangeListener)
}
}
@griffon.transform.ListChangeListener
accepts the following values:
-
in-place definition of a closure
-
reference of a closure property defined in the same class
-
a List of any of the previous two
@griffon.transform.ListChangeListener
has an additional member named weak
. When set to true
the generated ListChangeListener
will
be wrapped with a WeakListChangeListener
.
5.8. The @MapChangeListener AST Transformation
The @griffon.transform.MapChangeListener
helps you to register MapChangeListener
s
without so much effort. The following code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import griffon.transform.MapChangeListener
import griffon.transform.FXObservable
import javafx.collections.FXCollections
import javafx.collections.ObservableMap
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@FXObservable
@MapChangeListener(snoop)
ObservableMap map = FXCollections.observableHashMap()
def snoop = { change -> ... }
}
is equivalent to this one:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import javafx.collections.MapChangeListener
import javafx.collections.FXCollections
import javafx.collections.ObservableMap
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@FXObservable ObservableMap map = FXCollections.observableHashMap()
def snoop = { change -> ... }
SampleModel() {
listProperty().addListener(snoop as MapChangeListener)
}
}
@griffon.transform.MapChangeListener
accepts the following values:
-
in-place definition of a closure
-
reference of a closure property defined in the same class
-
a List of any of the previous two
@griffon.transform.MapChangeListener
has an additional member named weak
. When set to true
the generated MapChangeListener
will
be wrapped with a WeakMapChangeListener
.
5.9. The @InvalidationListener AST Transformation
The @griffon.transform.InvalidationListener
helps you to register InvalidationListener
s
without so much effort. The following code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import griffon.transform.InvalidationListener
import griffon.transform.FXObservable
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@FXObservable
@InvalidationListener(snoopAll)
String name
@FXObservable
@InvalidationListener({ controller.someAction(it)})
String lastname
def snoopAll = { ... }
}
is equivalent to this one:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javafx.beans.InvalidationListener
import griffon.transform.FXObservable
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@MVCMember @Nonnull SampleController controller
@FXObservable String name
@FXObservable String lastname
def snoopAll = { ... }
SampleModel() {
nameProperty().addListener(snoopAll as InvalidationListener)
lastnameProperty().addListener({
controller.someAction(it)
} as InvalidationListener)
}
}
@griffon.transform.InvalidationListener
accepts the following values:
-
in-place definition of a closure
-
reference of a closure property defined in the same class
-
a List of any of the previous two
@griffon.transform.InvalidationListener
has an additional member named weak
. When set to true
the generated InvalidationListener
will
be wrapped with a WeakInvalidationListener
.
5.10. The @FXObservable AST Transformation
The @griffon.transform.FXObservable
transformation modifies a class in such a way that plain fields become
JavaFX bindable properties. Each field generates mutators and accessors that accept the plain type
and the JavaFX property type. For example,
1
2
3
4
5
6
7
8
9
10
package org.example
import griffon.core.artifact.GriffonModel
import griffon.metadata.ArtifactProviderFor
import griffon.transform.FXObservable
@ArtifactProviderFor(GriffonModel)
class SampleModel {
@FXObservable String input = 'foo'
}
is functionally equivalent to
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.example
import griffon.core.artifact.GriffonModel
import griffon.metadata.ArtifactProviderFor
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
@ArtifactProviderFor(GriffonModel)
class SampleModel {
private StringProperty input
final StringProperty getInputProperty() {
inputProperty()
}
final StringProperty inputProperty() {
if (input == null) {
input = new SimpleStringProperty(this, 'foo')
}
input
}
void setInput(String input) {
inputProperty().set(input)
}
String getInput() {
return input == null ? null : inputProperty().get()
}
}
The following is a list of field types that @griffon.transform.FXObservable
can transform:
Type | JavaFX Property |
---|---|
Boolean.class |
javafx.beans.property.BooleanProperty |
Boolean.TYPE |
javafx.beans.property.BooleanProperty |
Double.class |
javafx.beans.property. DoubleProperty |
Double.TYPE |
javafx.beans.property.DoubleProperty |
Float.class |
javafx.beans.property.FloatProperty |
Float.TYPE |
javafx.beans.property.FloatProperty |
Integer.class |
javafx.beans.property.IntProperty |
Integer.TYPE |
javafx.beans.property.IntProperty |
Long.class |
javafx.beans.property.LongProperty |
Long.TYPE |
javafx.beans.property.LongProperty |
Short.class |
javafx.beans.property.IntProperty |
Short.TYPE |
javafx.beans.property.IntProperty |
Byte.class |
javafx.beans.property.IntProperty |
Byte.TYPE |
javafx.beans.property.IntProperty |
String.class |
javafx.beans.property.StringProperty |
List.class |
javafx.beans.property.ListProperty |
Map.class |
javafx.beans.property.MapProperty |
Set.class |
javafx.beans.property.SetProperty |
6. Controllers
Controllers are the entry point for your application’s logic. Each controller has
access to their model
and view
instances from their respective MVC group.
Controller actions are defined as public methods on a controller class. For example, in Groovy you’d write
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package sample
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull SampleModel model
@ControllerAction
void click() {
model.clickCount++
}
}
The corresponding code for Java 8 would be
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package sample;
import griffon.core.artifact.GriffonController;
import griffon.core.controller.ControllerAction;
import griffon.inject.MVCMember;
import javax.annotation.Nonnull;
@griffon.metadata.ArtifactProviderFor(GriffonController.class)
public class SampleController {
private SampleModel model;
@MVCMember
public void setModel(@Nonnull SampleModel model) {
this.model = model;
}
@ControllerAction
public void click() {
runInsideUIAsync(() -> {
model.setClickCount(model.getClickCount() + 1);
});
}
}
Actions must follow these rules in order to be considered as such:
-
must have public visibility modifier.
-
name does not match an event handler, i.e, it does not begin with
on
. -
must pass
GriffonClassUtils.isPlainMethod()
. -
must be annotated with
@ControllerAction
or havevoid
as return type.
The application’s ActionManager
will automatically configure an Action
instance for each matching controller action. These Action
instances can be later used
within Views to link them to UI components. The following example shows a Swing View
making use of the configured clickAction
from SampleController
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package sample
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull FactoryBuilderSupport builder
@MVCMember @Nonnull SampleModel model
void initUI() {
builder.with {
application(title: 'Clicker', pack: true)) {
button(clickAction, label: bind { model.clickCount })
}
}
}
}
Controllers can perform other tasks too:
-
listen to application events.
-
react to MVC initialization/destruction via a pair of methods (
mvcGroupInit()
,mvcGroupDestroy()
). -
hold references to services.
-
participate in dependency injection (members must be annotated with
@javax.inject.Inject
).
6.1. Actions and Threads
A key aspect that you must always keep in mind is proper threading. Often, controller actions will be bound in response to an event driven by the UI. Those actions will usually be invoked in the same thread that triggered the event, which would be the UI thread. When that happens, you must make sure that the executed code is short and that it quickly returns control to the UI thread. Failure to do so may result in unresponsive applications.
The following example is the typical usage:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package sample
import groovy.sql.Sql
import java.awt.event.ActionEvent
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonController)
class BadController {
@MVCMember @Nonnull def model
@ControllerAction
void badAction(ActionEvent evt = null) {
def sql = Sql.newInstance(
application.configuration.datasource.url,
model.username,
model.password,
application.configuration.datasource.driver
)
model.products.clear()
sql.eachRow("select * from products") { product ->
model.products << [product.id, product.name, product.price]
}
sql.close()
}
}
What’s wrong with this code? It’s very likely that this action is triggered by clicking on a button, in which case its body will be executed inside the UI thread. This means the database query will be executed on the UI thread too. The model is also updated; one could assume the model is bound to an UI component. This update should happen inside the UI thread, but clearly that’s not what’s happening here.
In order to simplify things, the Griffon runtime (via the ActionManager
) assumes
by default that all actions will be invoked outside of the UI thread. This solves the
first problem, that of performing a database operation on the wrong thread. The second
problem, updating the model, can be solved in the following manner:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package sample
import groovy.sql.Sql
import java.awt.event.ActionEvent
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonController)
class GoodController {
@MVCMember @Nonnull def model
@ControllerAction
void goodAction(ActionEvent evt = null) { (1)
def sql = Sql.newInstance(
application.configuration.datasource.url,
model.username,
model.password,
application.configuration.datasource.driver
)
try {
List results = []
sql.eachRow("select * from products") { product ->
results << [product.id, product.name, product.price]
}
runInsideUIAsync { (2)
model.products.clear()
model.products.addAll(results)
}
} finally {
sql.close()
}
}
}
1 | Executed outside the UI thread |
2 | Go back inside the UI thread |
There are other options at your disposal to make sure the code behaves properly according to the specific threading rules of a particular UI toolkit. These options are covered in the threading chapter.
The default behavior of invoking controller actions outside of the UI thread can be altered in different ways, here listed from most specific to most generic:
-
annotating the action method with
@Threading
. -
annotating the controller class with
@Threading
. All controller actions belonging to the annotated controller will use this setting. -
specify a value under key
controller.threading.default
in the application’s configuration. Accepted values and their equivalentThreading.Policy
value are listed below:
Threading.Policy.INSIDE_UITHREAD_SYNC |
sync, inside sync, inside uithread sync, inside_uithread_sync. |
Threading.Policy.INSIDE_UITHREAD_ASYNC |
async, inside async, inside uithread async, inside_uithread_async. |
Threading.Policy.OUTSIDE_UITHREAD |
outside, outside uithread, outside_uithread. |
Threading.Policy.SKIP |
skip. |
You may use lower case and/or upper case values. You may also use a real value of the Threading.Policy
enum if the
application’s configuration is defined using Java or Groovy code.
It’s also possible to completely disable automatic UI threading management for a particular action, controller, package,
or even the whole application. Just specify a value in the application’s configuration with the prefix controller.threading
.
Here are some examples:
controller.threading.com.acme.SampleController.click = false (1)
controller.threading.org.example.SimpleController = false (2)
controller.threading.org.another = false (3)
griffon.disable.threading.injection = true (4)
1 | targeted action |
2 | targeted controller |
3 | targeted package |
4 | application wide |
The 1 setting disables threading management for a single action only. The 2
disables threading management for all actions belonging to a single controller. 3 disables threading
management for all controllers inside the org.another
package. Finally, 4 disables threading
management altogether, for the whole application.
6.2. The ActionManager
Controller actions may automatically be wrapped and exposed as toolkit specific actions; this greatly simplifies how actions can be configured based on i18n concerns.
At the heart of this feature lies the ActionManager
. This component is responsible
for instantiating, configuring and maintaining references to all actions per controller.
It will automatically harvest all action candidates from a Controller once it has been
instantiated. Each action has all of its properties configured following this strategy:
-
match
<controller.class.name>
.action.<action.name>
.<key>
-
match application.action.
<action.name>
.<key>
<action.name>
should be properly capitalized. In other words, you can configure action
properties specifically per Controller or application wide. Available keys are
Key | Type | Default Value |
---|---|---|
name |
String |
Natural name minus the |
accelerator |
String |
undefined |
long_description |
String |
undefined |
short_description |
String |
undefined |
mnemonic |
String |
undefined |
small_icon |
String |
undefined |
large_icon |
String |
undefined |
enabled |
boolean |
true |
selected |
boolean |
false |
Key | Type | Default Value |
---|---|---|
name |
String |
Natural name minus the |
accelerator |
String |
undefined |
description |
String |
undefined |
mnemonic |
String |
undefined |
icon |
String |
undefined |
image |
String |
undefined |
enabled |
boolean |
true |
selected |
boolean |
false |
visible |
boolean |
true |
style |
String |
undefined |
styleclass |
String |
undefined |
graphic |
String |
undefined |
graphic_style |
String |
undefined |
graphic_styleclass |
String |
undefined |
Key | Type | Default Value |
---|---|---|
name |
String |
Natural name minus the |
description |
String |
undefined |
enabled |
boolean |
true |
Key | Type | Default Value |
---|---|---|
name |
String |
Natural name minus the |
Icon keys should point to a URL available in the classpath, or they may use the following notation:
iconClassName|constructorArg
Here’s an example using the Ikonli library with the FontAwesome icon pack installed:
org.kordamp.griffon.addressbook.AddresbookController.action.New.icon=org.kordamp.ikonli.javafx.FontIcon|fa-flash
Values must be placed in resources files following the internationalization guidelines.
It’s worth mentioning that all of action properties will react to changes made to the application’s
Locale
.
6.2.1. Configuration Examples
The following Controller defines 2 actions; one of them uses the Action
suffix because its name clashes
with a known Java keyword.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package sample
import java.awt.event.ActionEvent
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
@griffon.metadata.ArtifactProviderFor(GriffonController)
class SampleController {
@ControllerAction
void close(ActionEvent evt) { ... }
@ControllerAction
void newAction(ActionEvent evt) { ... }
}
The ActionManager
will generate and configure the following actions:
-
newAction
-
closeAction
The following keys are expected to be available in the application’s i18n resources (i.e. griffon-app/i18n/messages.properties
):
1
2
3
4
5
sample.SampleController.action.New.name = New
sample.SampleController.action.Open.name = Open
sample.SampleController.action.Close.name = Close
sample.SampleController.action.Delete.name = Delete
# additional keys per action elided
In the case that you’d like the close action to be customized for all controllers, say using
the Spanish language, then you’ll have to provide a file named griffon-app/i18n/messages_es.properties
with the following keys:
1
application.action.Close.name = Cerrar
Make sure to remove any controller specific keys when reaching for application wide configuration.
6.3. Action Handlers
ActionHandler
s open a new set of possibilities by allowing developers
and addon authors to define code that should be executed before and after
any controller action is invoked by the framework. For example, you may want to protect
the execution of a particular action given specific permissions; the shiro
plugin uses
annotations that are handled by an ActionHandler
, like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.metadata.ArtifactProviderFor
import griffon.plugins.shiro.annotation.*
@ArtifactProviderFor(GriffonController)
@RequiresAuthentication
class PrinterController {
@RequiresPermission('printer:print')
@ControllerAction
void print () { ... }
@RequiresRoles('administrator')
@ControllerAction
void configure() { ... }
}
The scaffolding
plugin on the other hand modifies the arguments sent to the action.
Take the following snippet for example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.metadata.ArtifactProviderFor
import griffon.plugins.shiro.annotation.*
import org.apache.shiro.authc.UsernamePasswordToken
impprt org.apache.shiro.subject.Subject
import javax.swing.JOptionPane
import javax.inject.Inject
@ArtifactProviderFor(GriffonController)
class StrutsController {
@Inject
private Subject subject
@RequiresGuest
@ControllerAction
void login(LoginCommandObject cmd) {
try {
subject.login(new UsernamePasswordToken(cmd.username, cmd.password))
} catch(Exception e) {
JOptionPane.showMessageDialog(
app.windowManager.findWindow('mainWindow'),
'Invalid username and/or password',
'Security Failure', JOptionPane.ERROR_MESSAGE)
}
}
@RequiresAuthentication
@ControllerAction
void logout() {
subject.logout()
}
}
Note that the login
action requires an instance of LoginCommandObject
. The scaffolding
plugin is aware of this fact; it will create an instance of said class, wire up a scaffolded
view in a dialog and present it to the user. The LoginCommandObject
instance will be set
as the action’s arguments if it validates successfully, otherwise action execution is aborted.
6.3.1. Implementing an Action Handler
Action handlers must implement the ActionHandler
interface. There’s a
handy base class (org.codehaus.griffon.runtime.core.controller.AbstractActionHandler
)
that provides sensible defaults. Say you’d want to know how much time it took for an action
to be executed, also if an exception occurred during its execution. This handler could
be implemented as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.acme
import griffon.core.controller.Action
import griffon.core.controller.ActionExecutionStatus
import org.codehaus.griffon.runtime.core.controller.AbstractActionHandler
import javax.inject.Named
@Named('tracer')
class TracerActionHandler extends AbstractActionHandler {
private final Map<String, Long> TIMES = [:]
Object[] before(Action action, Object[] args) {
TIMES[action.fullyQuallifiedName] = System.currentTimeMillis()
return super.before(controller, actionName, args)
}
Object after(ActionExecutionStatus status, Action action, Object[] args, Object result) {
long time = System.currentTimeMillis() - TIMES[action.fullyQuallifiedName]
println("Action ${action.fullyQuallifiedName} took ${time} ms [${status}]")
result
}
}
The ActionHandler
interface defines a handful of methods that are invoked
by the ActionManager
at very specific points during the lifetime and execution
of controller actions.
- void configure(Action action, Method method)
-
The
configure()
method is called during the configuration phase, when theActionManager
creates the actions. This method is called once in the lifetime of an action. - void update(Action action)
-
The
update()
method can be called at any time. Its responsibility is to update the action’s properties, such as the enabled state, given the current state of the application. - Object[] before(Action action, Object[] args)
-
The
before()
method is executed every time an action is about to be invoked. This method is responsible for adjusting the arguments (if needed) or aborting the action execution altogether. Any exception thrown by an handler in this method will halt action execution however onlyAbortActionExecution
is interpreted as a graceful abort. - boolean exception(Exception exception, Action action, Object[] args)
-
The
exception()
method is invoked only when an exception occurred during the action’s execution. Implementors must returntrue
if the exception was handled successfully. The exception will be rethrown by theActionManager
if no handler handled the exception. This happens as the last step of the action interception procedure. - Object after(ActionExecutionStatus status, Action axtion, Object[] args, Object result)
-
The
after()
method is called after an action has been executed. Any exceptions occurred during the action’s execution should have been handled byexception()
. Thestatus
argument specifies if the action was successfully executed (OK
), if it was aborted by an handler (ABORTERD
) or if an exception occurred during its execution (EXCEPTION
).
Action handlers can participate in Dependency Injection.
6.3.2. Configuration
Action Handlers must be registered within a Module
in order to be picked
up by the ActionManager
. The following example shows how the previous TracerActionHandler
can be registered in a Module
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.acme
import griffon.core.injection.Module
import griffon.core.controller.ActionHandler
import org.codehaus.griffon.runtime.core.injection.AbstractModule
import org.kordamp.jipsy.ServiceProviderFor
import javax.inject.Named
@ServiceProviderFor(Module)
@Named('application')
public class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(ActionHandler)
.to(TracerActionHandler)
.asSingleton()
}
}
A Handler may define a dependency on another handler; use the @griffon.inject.DependsOn
annotation to express the relationship.
It’s also possible to globally override the order of execution of handlers, or
define an order when handlers are orthogonal. Take for example the security
handler provided by the shiro
plugin and the scaffolding
handler provided by
scaffolding
plugin. These handlers know nothing about each other; however, security
should be called before scaffolding
. This can be accomplished by adding the following
snippet to Config.groovy
:
griffon.controller.action.handler.order = ['security', 'scaffolding']
6.4. Actions and Contexts
Actions have direct access to their controller’s Context
which means they can retrieve and store information
at any time. It’s also possible to specify that the arguments of an action should be matched against keys that may
exist in a Context
; this enables ActionHandlers
to decorate the value
without directly affecting the Context
itself.
The following example shows a different implementation of a controller whose login action expects an instance of
org.apache.shiro.authc.UsernamePasswordToken
. This instance has been created somewhere else and stored in the group’s
context using the "credentials" key.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.metadata.ArtifactProviderFor
import griffon.plugins.shiro.annotation.*
import org.apache.shiro.authc.UsernamePasswordToken
impprt org.apache.shiro.subject.Subject
import javax.swing.JOptionPane
import javax.inject.Inject
import javax.inject.Named
import griffon.inject.Contextual
@ArtifactProviderFor(GriffonController)
class StrutsController {
@Inject
private Subject subject
@RequiresGuest
@ControllerAction
void login(@Contextual @Named('credentials') UsernamePasswordToken token) {
try {
subject.login(token)
} catch(Exception e) {
JOptionPane.showMessageDialog(
app.windowManager.findWindow('mainWindow'),
'Invalid username and/or password',
'Security Failure', JOptionPane.ERROR_MESSAGE)
}
}
@RequiresAuthentication
@ControllerAction
void logout() {
subject.logout()
}
}
Because the org.apache.shiro.authc.UsernamePasswordToken
resides in the context, it can now be shared with other
actions that need it, something that could not be done in the previous example because the LoginCommandObject
existed
only for the action that declared it as an argument.
Another interesting thing is that the instance of org.apache.shiro.authc.UsernamePasswordToken
could have been added
by the parent Context
, or the application’s Context
(remember that contexts are hierarchical); thus
the value can be shared with more than one controller or with the whole application, as needed.
If the @Contextual
argument does not have @Named
qualifier, then the fully qualified class name of the argument’s
type will be used as a key. This means that the key "org.apache.shiro.authc.UsernamePasswordToken" would be used to
search for the argument if @Named('credentials')
were to be omitted.
It’s worth noting that failure to resolve a @Contextual
argument does not result in an immediate exception; if the key
could not be found in the Context
, then a null
value will be set as the argument’s value. It’s the action’s
job to ensure that it received the correct arguments. An alternative would be to annotate the parameter with @Nonnull
,
in which case the ActionManager
will abort the execution if the named parameter was not found in the context, or if
its value is null.
7. Services
Services are responsible for the application logic that does not belong to a single
controller. They are meant to be treated as singletons, and are injected to MVC members by
following a naming convention. Services must be located inside the griffon-app/services
.
Let’s say you want to create a Math service. A trivial implementation of an addition
operation performed by the MathService
would look like the following snippet:
1
2
3
4
5
6
7
8
9
package sample
import griffon.core.artifact.GriffonService
@javax.inject.Singleton
@griffon.metadata.ArtifactProviderFor(GriffonService)
class MathService {
def addition(a, b) { a + b }
}
Using this service from a Controller is a straightforward task; you just have to
define an injection point and annotate it with @javax.inject.Inject
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package sample
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull SampleModel model
@javax.inject.Inject
private MathService mathService
@ControllerAction
void calculate(evt = null) {
model.result = mathService.addition model.a, model.b
}
}
Given that services are inherently treated as singletons, they are also automatically
registered as application event listeners. Be aware that services will be instantiated
lazily, which means that some events might not reach a particular service if it has not
been instantiated by the framework by the time of event publication.
Using Groovy’s @Singleton
annotation on a Service class is also discouraged, as it will cause trouble with
the automatic singleton management Griffon has in place.
7.1. Life Cycle
Services do not have a well-defined lifecycle like other MVC artifacts, because they do not
implement the GriffonMvcArtifact
interface. However, you may annotate service
methods with @javax.annotation.PostConstruct
and @javax.annotation.PreDestroy
.
The Griffon runtime guarantees that methods annotated with @javax.annotation.PostConstruct
will be invoked right after the instance has been created by the Injector
.
Likewise, it will invoke all methods annotated with @javax.annotation.PreDestroy
when the
Injector
is closed.
Only one method of the same class can be annotated with @javax.annotation.PostConstruct
or @javax.annotation.PreDestroy .
|
8. Views
8.1. The WindowManager
Although the following API refers directly to Swing
in all examples, it’s possible to use
the WindowManager
with other toolkits such as JavaFX
, Pivot
and Lanterna
, as
there are specific WindowManager
implementations plus helper classes for those
UI toolkits too.
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
simply shows and hides windows directly; however, you can change this behavior by setting
a different implementation of WindowDisplayHandler
on the application instance.
8.1.1. WindowManager DSL
This configuration can be set in griffon-app/conf/Config.groovy
file; here is how it looks:
1
2
3
4
5
6
7
8
9
windowManager {
myWindowName = [
show: {name, window -> ... },
hide: {name, window -> ... }
]
myOtherWindowName = [
show: {name, window -> ... }
]
}
The name of each entry must match the value of the Window’s name:
property (if supported)
or the name used to register the Window with the WindowManager
. Each entry may
have any of the following options:
show |
Used to show the window to the screen. It must be a closure that takes two parameters: the name of the window and the window to be displayed. |
hide |
Used to hide the window from the screen. It must be a closure that takes two parameters: the name of the window and the window to be hidden. |
handler |
A custom |
You must use CallableWithArgs instead of closures if using the Java version
of the Config file.
|
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 windows:
1
2
3
windowManager {
defaultHandler = new MyCustomWindowDisplayHandler()
}
You can go a bit further by specifying a global show or hide behavior, as shown in the following example:
1
2
3
4
5
6
7
8
9
10
windowManager {
defaultShow: {name, window -> ... },
myWindowName = [
show: {name, window -> ... },
hide: {name, window -> ... }
]
myOtherWindowName = [
show: {name, window -> ... }
]
}
8.1.2. Starting Window
By default, the WindowManager
picks the first available window from the managed
windows list to be the starting window. However, this behavior can be configured
by means of the WindowManager DSL. Simply specify a value for windowManager.startingWindow
,
like this:
1
2
3
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 or was attached to the
WindowManager
with the same name. -
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.
8.1.3. Custom WindowDisplayHandlers
The following example shows how you can center on screen all managed windows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package sample
import org.codehaus.griffon.runtime.swing.DefaultSwingWindowDisplayHandler
import javax.annotation.Nonnull
import java.awt.Window
import static griffon.swing.support.SwingUtils.centerOnScreen
class CenteringWindowDisplayHandler extends DefaultSwingWindowDisplayHandler {
@Override
void show(@Nonnull String name, @Nonnull Window window) {
centerOnScreen(window)
super.show(name, window)
}
}
You can register CenteringWindowDisplayHandler
using the WindowManager DSL. Alternatively,
you may use a Module to register the class/instance.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package sample
import griffon.core.injection.Module
import griffon.inject.DependsOn
import griffon.swing.SwingWindowDisplayHandler
import org.codehaus.griffon.runtime.core.injection.AbstractModule
import org.kordamp.jipsy.ServiceProviderFor
import static griffon.util.AnnotationUtils.named
@DependsOn('swing')
@ServiceProviderFor(Module)
class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(SwingWindowDisplayHandler)
.withClassifier(named('defaultWindowDisplayHandler'))
.to(CenteringWindowDisplayHandler)
.asSingleton()
}
}
This example is equivalent to defining a WindowDisplayHandler
for all windows.
You may target specific windows, by define multiple bindings, making sure that the name
of the classifier matches the window name. Notice the explicit dependency on the swing
module.
If this dependency is left out, it’s very likely that the WindowManager
will fail
to pick the correct WindowDisplayHandler
.
8.2. BuilderCustomizers
Recall from the ConsoleView sample application that you can use a Groovy DSL for building View components when the main language of the application is Groovy. This DSL is configured with default nodes that are tightly coupled with the chosen UI toolkit.
8.2.1. Extending the UI DSL
The UI DSL can be extended by registering new BuilderCustomizer
s with the application
using a Module
. The BuilderCustomizer
instance defines methods
that can be used to configure every aspect of groovy.factory.FactoryBuilderSupport
.
Here’s an example of the miglayout-swing
extension:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package griffon.builder.swing;
import griffon.inject.DependsOn;
import groovy.swing.factory.LayoutFactory;
import net.miginfocom.swing.MigLayout;
import groovy.util.Factory;
import org.codehaus.griffon.runtime.groovy.view.AbstractBuilderCustomizer;
import javax.inject.Named;
import java.util.LinkedHashMap;
import java.util.Map;
@DependsOn("swing")
@Named("miglayout-swing")
public class MiglayoutSwingBuilderCustomizer extends AbstractBuilderCustomizer {
public MiglayoutSwingBuilderCustomizer() {
Map<String, Factory> factories = new LinkedHashMap<>();
factories.put("migLayout", new LayoutFactory(MigLayout.class));
setFactories(factories);
}
}
This customizer registers a single factory, miglayout
. It also defines a dependency
on the swing
customizer in order for its customizations to be applied after swing
's.
The customizer is registered using the following Module.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.codehaus.griffon.runtime.miglayout;
import griffon.builder.swing.MiglayoutSwingBuilderCustomizer;
import griffon.core.injection.Module;
import griffon.inject.DependsOn;
import griffon.util.BuilderCustomizer;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import javax.inject.Named;
@DependsOn("swing-groovy")
@Named("miglayout-swing-groovy")
@ServiceProviderFor(Module.class)
public class MiglayoutSwingGroovyModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(BuilderCustomizer.class)
.to(MiglayoutSwingBuilderCustomizer.class)
.asSingleton();
}
}
This module is loaded before swing-groovy
, as per the @griffon.inject.DependsOn
definition,
thus ensuring that the customizations from this customizer are applied afterwards.
8.2.2. Default DSL Nodes
The griffon-groovy
dependency delivers the following nodes:
MetaComponent
Enables the usage of a meta-component as a View node. Meta-components are MVC groups that contain additional configuration, for example:
1
2
3
4
5
6
7
8
9
10
11
mvcGroups {
'custom' {
model = 'sample.CustomModel'
view = 'sample.CustomView'
controller = 'sample.CustomController'
config {
component = true
title = 'My Default Title'
}
}
}
The metaComponent()
node instantiates the MVC group and attaches the top node from
the groups' View member into the current hierarchy. Using the previous group definition
in a View is straightforward:
metaComponent('custom', title: 'Another Title')
8.2.3. Default Builder Delegates
The griffon-groovy
dependency delivers the following delegates:
Root
Identifies the root node of a View. Views may identify the root node. The root node is the first node call invoked on the builder member.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull FactoryBuilderSupport builder
void initUI() {
builder.with {
button(id: 'clickActionTarget', clickAction)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.acme
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class PrimaryView {
@MVCMember @Nonnull FactoryBuilderSupport builder
void initUI() {
builder.with {
application(title: 'Sample') {
borderLayout()
label 'Options', constraints: NORTH
node createMVCGroup('sample').rootNode
}
}
}
}
You may also define an explicit rootNode
attribute with a boolean value on the target node that should be
treated as the root.
8.3. Swing Specific
Refer to the list of nodes which become available when
the griffon-swing-groovy-2.15.0.jar
is added as a dependency.
8.4. JavaFX Specific
Refer to the list of nodes which become available when
the griffon-javafx-groovy-2.15.0.jar
is added as a dependency.
8.4.1. I18n Support
Nodes of type Labeled
can react to changes made to the application’s Locale
provided you
supply enough information to resolve the target message. JavaFXUtils
exposes 3 synthetic
properties and 1 connect method to make this feature work. You may use these properties with
FXML, for example
<Label JavaFXUtils.i18nKey="key.label"
JavaFXUtils.i18nArgs="one, two"
JavaFXUtils.i18nDefaultValue="No value supplied"/>
These values are used in combination with MessageSource
to resolve a message and set it
as the value of the node’s text
property. The message will be resolved again anytime the application’s
Locale
changes value. You must use the connect method on the View
class too, by invoking the
following method during UI construction
connectMessageSource(node);
Where node
is the root of the hierarchy.
8.4.2. Action Support
Actions can be attached to JavaFX nodes defined in two ways:
-
By defining a node id that follows the
<action_name>ActionTarget
naming convention. -
By invoking
JavaFXUtils.setGriffonActionId()
with the target node and action id as arguments.
Both methods work whether you create the UI using the JavaFX API directly or with FXML. The advantage
of the second option is that you can link mutliple nodes to the same action. Once action ids have been
set on the target nodes you must still establish a connection between the configured nodes and the
controller; this task is attained by invoking the following method during UI construction in the view
connectActions(node, controller);
Where node
is the root of the hierarchy. Here’s an example of 3 actions being used by two different
controls each:
<?import griffon.javafx.support.JavaFXUtils?>
...
<VBox>
<MenuBar>
<Menu text="File">
<MenuItem JavaFXUtils.griffonActionId="cut"/>
<MenuItem JavaFXUtils.griffonActionId="copy"/>
<MenuItem JavaFXUtils.griffonActionId="paste"/>
</Menu>
</MenuBar>
<ToolBar>
<Button JavaFXUtils.griffonActionId="cut"/>
<Button JavaFXUtils.griffonActionId="copy"/>
<Button JavaFXUtils.griffonActionId="paste"/>
</ToolBar>
</VBox>
In contrast this is how the FXML would look if node ids would be set instead
...
<VBox>
<ToolBar>
<Button fx:id="cutActionTarget"/>
<Button fx:id="copyActionTarget"/>
<Button fx:id="pasteActionTarget"/>
</ToolBar>
</VBox>
In this case we can’t assign the same action to a different node because the node ids would clash. It’s possible to override
this naming convention by defining your own strategy. Simply implement ActionMatcher
, making sure to bind your
custom strategy with module binding, such as
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;
import griffon.core.injection.Module;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import griffon.javafx.support.ActionMatcher;
import griffon.inject.DependsOn;
@DependsOn("javafx")
@ServiceProviderFor(Module.class)
public class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(ActionMatcher.class)
.to(MyActionMatcher.class)
.asSingleton();
}
}
Be aware that using the standard FXML notation (#actionName ) for event handlers is not compatible with Griffon actions!
|
FXML conventions dictate that the argument specified in fx:controller
defines the type on which node injections via
@FXML
will take place. It’s also the source for any event handler defined using the format #<actionName>
where <actionName>
matches a public method that takes the required event type as argument. In othwer words, the following code shows a simple
FXML controller
package com.acme;
import javafx.scene.control.Label;
import javafx.event.ActionEvent;
public class SampleView {
@FXML private void myLabel;
@FXML
public void initialize() {
myLabel.setText("random text");
}
public void click(ActionEvent ignored) { ... }
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8"
fx:controller="com.acme.SampleView">
<Label fx:id="myLabel"/>
<Button text="click" onAction="#click"/>
</VBox>
Notice that the name SampleView
was chosen on purpose. This demonstrates one of the problems with the concept behind
fx:controller
, as it defines both the place where widgets may be configured and the source of event handlers. The first
is the sole responsibility of a View component while the latter is the responsibility of a Controller. Griffon decided to
split these responsibilities into their proper MVC members; thus the previous example can be rewritten as follows:
package com.acme;
import griffon.core.artifact.GriffonController;
import griffon.core.controller.ControllerAction;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javafx.event.ActionEvent;
@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
@ControllerAction
public void click(ActionEvent ignored) { ... }
}
package com.acme;
import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import javax.annotation.Nonnull;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
@MVCMember @Nonnull
private SampleController controller;
@FXML private void myLabel;
@Override
public void initUI() {
VBox content = loadFromFXML();
myLabel.setText("random text");
connectActions(content, controller);
connectMessageSource(content);
// add content to SceneGraph ...
}
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.VBox?>
<?import griffon.javafx.support.JavaFXUtils?>
<VBox xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8"
fx:controller="com.acme.SampleView">
<Label fx:id="myLabel"/>
<Button JavaFXUtils.griffonActionId="click"/>
<!--
As an alternative you may define the Button like this too
<Button fx:id="clickActionTarget"/>
-->
</VBox>
There may be cases where you would still like to use the standar FXML event handlers, for exmaple where an event handler is UI specific such a focus handler or a scroll handler. In this case you would define the event handler as a public method on the View class.
8.4.3. Binding Support
Collections
CollectionBindings
class provides binding factories on ObservableList
/ObservableSet
/ObservableMap
-
Join source observable collection to
StringBinding
. -
Calculate
min
,max
,average
, andsum
on source observable collection.
Filtering
FilteringBindings
class provides filtering capabilities on ObservableList
/ObservableSet
/ObservableMap
-
Filter
ObservableList
/ObservableSet
/ObservableMap
and find first match, creating aObjectBinding
. -
Filter
ObservableList
/ObservableSet
/ObservableMap
then map and find first match to X; where X may be a wrapper type, String or a typeR
. -
Map elements of
ObservableList
/ObservableSet
/ObservableMap
to X then filter and find first match; where X may be a wrapper type, String or a typeR
.
Matching
MatchingBindings
class provides matching capabilities on ObservableList
/ObservableSet
/ObservableMap
-
Apply
allMatch
,anyMatch
, andnoneMatch
predicates.
Mapping
MappingBindings
class provides lots of useful binding and property factories
-
Convert
ObservableValue<X>
to is correspondingXBinding
. -
Convert
ObservableXValue
toObjectBinding<X>
.
Reducing
ReducingBindings
class provides reduce capabilities on ObservableList
/ObservableSet
/ObservableMap
-
Reduce
ObservableList
/ObservableSet
/ObservableMap
toObjectBinding
. -
Reduce
ObservableList
/ObservableSet
/ObservableMap
then map to X; where X may be a wrapper type, String or a typeR
. -
Map elements of
ObservableList
/ObservableSet
/ObservableMap
to X then reduce; where X may be a wrapper type, String or a typeR
.
UI Thread Specific
It’s very important to obey the basic rules of UI programming in the Java platform. Basically everything
related to UI must be performed inside the UI thread. Everything that’s not UI related must be executed
outside the UI thread. Griffon provides the means to wrap ObservableList
, ObservableSet
,
and ObservableMap
with versions that guarantee to notify its listeners inside the UI thread.
This lets you write the following code in a View
artifact:
import griffon.javafx.collections.GriffonFXCollections;
...
ObservableList<String> items = model.getItems();
ObservableList<String> uiItems = GriffonFXCollections.uiThreadAwareObservableList(items);
listView.setItems(uiItems);
Now every time the items
list that belongs to the Model
gets updated so will be the listView
widget
with the guarantee that it doesn’t matter which thread pushes the original changes the listView
will
be updated inside the UI thread.
You may also create UI thread aware versions of ChangeListener
, InvalidationListener
, ListChangeListener
,
SetChangeListener
, MapChangeListener
, all combinations of Property<X>
and their specializations,
XProperty
; where X stands for Boolean
, Integer
, Long
, Float
, Double
, String
, Map
, List
,
Set
. The class UIThreadAwareBindings
provides the means to create these type of bindings.
8.4.4. MetaComponent Support
You can use MVCGroups as components inside an FXML file. Let’s assume there’s an mvcGroup named form
with a matching
FormView
. This view defines the following content in form.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import griffon.javafx.support.MetaComponent?>
<?import griffon.javafx.support.MetaComponent.MvcArg?>
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="com/acme/ContainerView">
<MetaComponent mvcType="formItem">
<MetaComponent.MvcArg name="key" value="name"/>
</MetaComponent>
<MetaComponent mvcType="formItem">
<MetaComponent.MvcArg name="propertyKey" value="lastname"/>
</MetaComponent>
</VBox>
The formItem
MVC group defines a Label, a TextField, and handles validation for its input. FormView
must identify
the root node that can be added to its parent view; the convention is to use the group’s id plus "-rootNode"
, for example
package org.example;
import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import java.util.Collections;
import javax.annotation.Nonnull;
@ArtifactProviderFor(GriffonView.class)
public class FormItemView extends AbstractJavaFXGriffonView {
@MVCMember private FormItemController controller;
@MVCMember private FormItemModel model;
@MVCMember private String propertyKey;
@FXML private Label propertyLabel;
@FXML private TextField propertyValue;
@Override
public void initUI() {
Node content = loadFromFXML();
propertyLabel.setText(propertyKey);
model.valueProperty().bindBidirectional(propertyValue.textProperty());
connectActions(content, controller);
connectMessageSource(content);
getMvcGroup().getContext().put(getMvcGroup().getMvcId() + "-rootNode", content); (1)
}
}
1 | Naming convention |
The current naming convention to identify the root node of a View is to use the MVCGroup’s id with the -rootNode
prefix
as a key in the MVCGroup’s context.
8.5. Lanterna Specific
Lanterna is a Java library allowing you to easily write semi-graphical user interfaces in a text-only environment, very similar to the C library curses, but with more functionality. Lanterna supports xterm compatible terminals and terminal emulators such as konsole, gnome-terminal, putty, xterm and many more. One of the main benefits of lanterna is that it’s not dependent on any native library, but runs 100% in pure Java.
Refer to the list of nodes which become available when
the griffon-lanterna-groovy-2.15.0.jar
is added as a dependency.
8.6. Pivot Specific
Apache Pivot is an open-source platform for building installable Internet applications (IIAs). It combines the enhanced productivity and usability features of a modern user interface toolkit with the robustness of the Java platform.
Pivot has a deep listener hierarchy as opposed to the simple one found in Swing.
This listener hierarchy does not follow the conventions set forth by the JavaBeans
Conventions, thus making it difficult to extrapolate synthetic properties based
on event methods when using Groovy builders, as happens with Swing classes.
However this plugin applies a convention for wiring up listeners. Take for example
Button
and ButtonPressListener
; the following example shows how to wire up a
buttonPressed
event handler.
button('Click me!') {
buttonPressListener {
buttonPressed = { source -> println "You pressed on button ${source}!" }
}
}
For each listener in the Pivot listener list, there’s a corresponding node matching its name. For each method of such listener interface, there’s a variable matching its name that may have a closure assigned to it. The closure must match the same arguments as the method.
Refer to the list of nodes which become available when
the griffon-pivot-groovy-2.15.0.jar
is added as a dependency.
9. Threading
Building a well-behaved multi-threaded desktop application has been a hard task for many years; however, it does not have to be that way anymore. The following sections explain the threading facilities supplied by the Griffon framework.
9.1. Synchronous Calls
Synchronous calls inside the UI thread are made by invoking the runInsideUISync
method.
This method results in the same behavior as calling SwingUtilities.invokeAndWait()
when
using Swing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package sample
import java.awt.event.ActionEvent
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull SampleModel model
@ControllerAction
void work(evt = null) {
// will be invoked outside of the UI thread by default
def value = model.value
// do some calculations
runInsideUISync {
// back inside the UI thread
model.result = ...
}
}
}
9.2. Asynchronous Calls
Similarly to synchronous calls, asynchronous calls inside the UI thread are made by
invoking the runInsideUIAsync
method. This method results in the same behavior as
calling SwingUtilities.invokeLater()
when using Swing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package sample
import java.awt.event.ActionEvent
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull SampleModel model
@ControllerAction
void work(evt = null) {
// will be invoked outside of the UI thread by default
def value = model.value
// do some calculations
runInsideUIAsync {
// back inside the UI Thread
model.result = ...
}
}
}
9.3. Outside Calls
Making sure a block of code is executed outside the UI thread is accomplished by invoking
the runOutsideUI
method. This method is smart enough to figure out if the unit
of work is already outside of the UI thread; otherwise it instructs the Griffon
runtime to run the unit in a different thread. This is usually performed by a
helper java.util.concurrent.ExecutorService
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package sample
import java.awt.event.ActionEvent
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull SampleModel model
@ControllerAction
void work(evt = null) {
// will be invoked outside of the UI thread by default
def value = model.value
// do some calculations
runInsideUIAsync {
// back inside the UI thread
model.result = ...
runOutsideUI {
// do more calculations
}
}
}
}
9.4. Background Calls
Making sure a block of code is executed on a background thread is accomplished by invoking
the runOutsideUIAsync
method. This method always runs the code on a bakcground thread regardless
of the caller / invoking thread. This is usually performed by a helper java.util.concurrent.ExecutorService
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package sample
import java.awt.event.ActionEvent
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@griffon.metadata.ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull SampleModel model
@ControllerAction
void work(evt = null) {
// will be invoked outside of the UI thread by default
def value = model.value
// do some calculations
runInsideUIAsync {
// back inside the UI thread
model.result = ...
runOutsideUIAsync {
// do more calculations
}
}
}
}
9.5. Additional Threading Methods
There are two additional methods that complement the generic threading facilities which Griffon exposes to the application and its artifacts:
- isUIThread()
-
Returns
true
if the current thread is the UI thread,false
otherwise. Functionally equivalent to callingSwingUtilities.isEventDispatchThread()
in Swing. - runFuture(ExecutorService s, Callable c)
-
schedules a callable on the target
ExecutorService
. The executor service can be left unspecified; if so, a default Thread pool executor will be used.
9.6. The @Threading Annotation
The @griffon.transform.Threading
annotation can be used to alter the default behavior of
executing a controller action outside of the UI thread. There are 4 possible values
that can be specified for this annotation:
INSIDE_UITHREAD_SYNC |
Executes the code in a synchronous call inside the UI thread.
Equivalent to wrapping the code with |
INSIDE_UITHREAD_ASYNC |
Executes the code in an asynchronous call inside the UI thread.
Equivalent to wrapping the code with |
OUTSIDE_UITHREAD |
Executes the code outside of the UI thread. Equivalent to wrapping
the code with |
OUTSIDE_UITHREAD_ASYNC |
Executes the code on a background thread regardless of the invoking
thread. Equivalent to wrapping the code with |
SKIP |
Executes the code in the same thread as the invoker, whatever it may be. |
This annotation can be used as an AST transformation on any other component that’s not a controller. Any component may gain the ability to execute code in a particular thread, following the selected UI toolkit’s execution rules.
Here’s an example of a custom component that’s able to call its methods on different threads:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package sample
import griffon.transform.Threading
class Sample {
@Threading
void doStuff() {
// executed outside of the UI thread
}
@Threading(Threading.Policy.INSIDE_UITHREAD_SYNC)
void moreStuff() {
// executed synchronously inside the UI thread
}
}
You must annotate a method with @griffon.transform.Threading
. Annotated methods must conform to these rules:
-
must be public.
-
name does not match an event handler.
-
must pass
GriffonClassUtils.isPlainMethod()
. -
must have
void
as return type.
9.7. The @ThreadingAware AST Transformation
Any component may gain the ability to execute code in a particular thread, following
the selected UI toolkit’s execution rules. It injects the behavior of ThreadingHandler
into the annotated class.
This feature is just a shortcut to avoid obtaining the UIThreadManager
instance
by objects which do not hold a reference to it.
Here’s an example of a custom component which is able to call its methods in different threads:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package sample
@griffon.transform.ThreadingAware
class Sample {
void doStuff() {
runOutsideUI {
// executed outside of the UI thread
}
}
void moreStuff() {
runInsideUIAsync {
// executed asynchronously inside the UI thread
}
}
}
10. Events
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, and 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. |
10.1. Publishing Events
Any instance that obtains a reference to an EventRouter
can publish events.
GriffonApplication
exposes the application wide EventRouter
via a read-only
property. You may use Dependency Injection to inject an EventRouter
to any class too.
Publishing an event can be done synchronously on the current thread, asynchronously to 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 itself:
application.eventRouter.publishEvent('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 thread:
application.eventRouter.publishEventOutsideUI('MyEventName', ['arg0', 'arg1'])
Finally, if you’d like event notification to be handled in a thread that is not the current one (regardless if the current one is the UI thread or not), then use the following method:
application.eventRouter.publishEventAsync('MyEventName', ['arg0', 'arg1'])
Alternatively, you may specify an instance of a subclass of Event
as the sole
argument to any of these methods. The event instance will be the single argument sent
to the event handlers when the event
methods are invoked in this way.
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 code:
application.eventRouter.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 call
application.eventRouter.eventPublishingEnabled = true
10.2. Consuming events
Any artifact or class that abides by the following conventions can be registered as an application listener. These conventions are:
-
it is a Map, a
RunnableWithArgs
or an Object. -
in the case of a Map, each key maps to
<EventName>
, the value must be a RunnableWithArgs. -
in the case of object, public methods whose name matches
on<EventName>
will be used as event handlers. -
Objects and maps can be registered/unregistered by calling
addApplicationListener()
/removeApplicationListener()
on theEventRouter
instance. -
RunnableWithArgs event handlers must be registered with an overloaded version of
addApplicationListener()
/removeApplicationListener()
that takes<EventName>
as the first parameter, and the runnable itself as the second parameter.
There is a global, per-application event handler that can be registered. If you want
to take advantage of this feature, you must define a class that implements the
EventHandler
interface. This class must be registered with a Module
.
Lastly, both Controller and Service instances are automatically registered as application
event listeners.
10.2.1. Examples
These are some examples of event handlers.
Display a message right before default MVC groups are instantiated:
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.acme
import griffon.core.GriffonApplication
import griffon.core.event.EventHandler
class ApplicationEventHandler implements EventHandler {
void onBootstrapEnd(GriffonApplication application) {
println """
Application configuration has finished loading.
MVC Groups will be initialized now.
""".stripIndent(12)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.acme
import griffon.core.event.EventHandler
import griffon.core.injection.Module
import org.codehaus.griffon.runtime.core.injection.AbstractModule
import org.kordamp.jipsy.ServiceProviderFor
@ServiceProviderFor(Module)
class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(EventHandler)
.to(ApplicationEventHandler)
.asSingleton()
}
}
Print the name of the application plus a short message when the application is about to shut down:
1
2
3
4
5
6
7
8
9
10
package com.acme
import griffon.core.artifact.GriffonController
@griffon.metadata.ArtifactProviderFor(GriffonController)
class MyController {
void onShutdownStart(application)
println "${application.configuration['application.title']} is shutting down"
}
}
Print a message every time the event "Foo" is published:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.acme
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
@griffon.metadata.ArtifactProviderFor(GriffonController)
class MyController {
void mvcGroupInit(Map<String, Object> args) {
application.eventRouter.addEventListener([
Foo: { println 'got foo!' } as RunnableWithArgs
])
}
@ControllerAction
void fooAction() {
// do something
application.eventRouter.publishEvent('Foo')
}
}
An alternative to the previous example using a RunnableWithArgs
event handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.acme
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
@griffon.metadata.ArtifactProviderFor(GriffonController)
class MyController {
void mvcGroupInit(Map<String, Object> args) {
application.eventRouter.addEventListener('Foo',
{ println 'got foo!' } as RunnableWithArgs
])
}
@ControllerAction
void fooAction() {
// do something
application.eventRouter.publishEvent('Foo')
}
}
An alternative to the previous example using a custom event class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.acme
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
@griffon.metadata.ArtifactProviderFor(GriffonController)
class MyController {
void mvcGroupInit(Map<String, Object> args) {
application.eventRouter.addListener(Foo) { e -> assert e instanceof Foo }
}
@ControllerAction
void fooAction() {
// do something
application.eventRouter.publishEvent(new MyController.Foo(this))
}
static class Foo extends griffon.core.Event {
Foo(Object source) { super(source) }
}
}
10.3. Application Events
The following events will be triggered by the application when dealing with artifacts:
NewInstance(Class klass, Object instance) |
When a new artifact is created. |
DestroyInstance(Class klass, Object instance) |
When an artifact instance is destroyed. |
LoadAddonsStart(GriffonApplication application) |
Before any addons are initialized, during the Initialize phase. |
LoadAddonsEnd(GriffonApplication application, Map<String, GriffonAddon> addons) |
After all addons have been initialized, during the Initialize phase. |
LoadAddonStart(String name, GriffonAddon addon, GriffonApplication application) |
Before an addon is initialized, during the Initialize phase. |
LoadAddonEnd(String name, GriffonAddon addon, GriffonApplication application) |
After an addon has been initialized, during the Initialize phase. |
These events will be triggered when dealing with MVC groups:
InitializeMVCGroup(MVCGroupConfiguration configuration, MVCGroup group) |
When a new MVC group is initialized. |
CreateMVCGroup(MVCGroup group) |
When a new MVC group is created. |
DestroyMVCGroup(MVCGroup group) |
When an MVC group is destroyed. |
10.4. Lifecycle Events
The following events will be triggered by the application during each one of its phases:
BootstrapStart(GriffonApplication application) |
After logging configuration has been setup, during the Initialize phase. |
BootstrapEnd(GriffonApplication application) |
At the end of the Initialize phase. |
StartupStart(GriffonApplication application) |
At the beginning of the Startup phase. |
StartupEnd(GriffonApplication application) |
At the end of the Startup phase. |
ReadyStart(GriffonApplication application) |
At the beginning of the Ready phase. |
ReadyEnd(GriffonApplication application) |
At the end of the ready phase. |
ShutdownRequested(GriffonApplication application) |
Before the Shutdown begins. |
ShutdownAborted(GriffonApplication application) |
If a |
ShutdownStart(GriffonApplication application) |
At the beginning of the Shutdown phase. |
10.5. Miscellaneous Events
These events will be triggered when a specific condition is reached:
WindowShown(String name, W window) |
Triggered by the |
WindowHidden(String name, W window) |
Triggered by the |
WindowAttached(String name, W window) |
Triggered by the |
WindowDetached(String name, W window) |
Triggered by the |
10.6. The @EventPublisher AST Transformation
Any component may gain the ability to publish events through an EventRouter
instance. You only need annotate the class with @griffon.transform.EventPublisher
and it will automatically gain all methods exposed by EventPublisher
.
The following example shows a trivial usage of this feature:
1
2
3
4
5
6
7
8
9
10
@griffon.transform.EventPublisher
class Publisher {
void doit(String name) {
publishEvent('arg', [name])
}
void doit() {
publishEvent('empty')
}
}
The application’s event router will be used by default. If you’d like your custom
event publisher to use a private EventRouter
, then you must define a binding
for it using a specific name, like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import griffon.core.injection.Module
import griffon.core.event.EventRouter
import org.kordamp.jipsy.ServiceProviderFor
import org.codehaus.griffon.runtime.core.injection.AbstractModule
import org.codehaus.griffon.runtime.core.event.DefaultEventRouter
import javax.inject.Named
import static griffon.util.AnnotationUtils.named
@ServiceProviderFor(Module)
@Named
class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(EventRouter)
.withClassifier(named('my-private-event-router'))
.to(DefaultEventRouter)
.asSingleton()
}
}
Next, specify the named EventRouter
as a parameter on the @griffon.transform.EventPublisher
transformation:
1
2
3
4
5
6
7
8
9
10
@griffon.transform.EventPublisher('my-private-event-router')
class Publisher {
void doit(String name) {
publishEvent('arg', [name])
}
void doit() {
publishEvent('empty')
}
}
11. Internationalization
This chapter describes the Internationalization (I18N
) features available to all applications.
11.1. MessageSource
Applications have the ability to resolve internationalizable messages by leveraging
the behavior exposed by MessageSource
. This interface exposes the following methods:
-
String getMessage(String key)
-
String getMessage(String key, Locale locale)
-
String getMessage(String key, Object[] args)
-
String getMessage(String key, Object[] args, Locale locale)
-
String getMessage(String key, List args)
-
String getMessage(String key, List args, Locale locale)
-
String getMessage(String key, Map args)
-
String getMessage(String key, Map args, Locale locale)
-
Object resolveMessageValue(String key, Locale locale)
The first set throws a NoSuchMessageException
if a message could not be resolved given
the key sent as argument. The following methods take an additional defaultMessage
parameter that may be used if no configured message is found. If this optional parameter
is null, then the key
is used as the message; in other words, these methods
never throw NoSuchMessageException
nor return null
unless the passed in key
is null.
-
String getMessage(String key, String defaultMessage)
-
String getMessage(String key, Locale locale, String defaultMessage)
-
String getMessage(String key, Object[] args, String defaultMessage)
-
String getMessage(String key, Object[] args, Locale locale, String defaultMessage)
-
String getMessage(String key, List args, String defaultMessage)
-
String getMessage(String key, List args, Locale locale, String defaultMessage)
-
String getMessage(String key, Map args, String defaultMessage)
-
String getMessage(String key, Map args, Locale locale, String defaultMessage)
The simplest way to resolve a message is as follows:
getApplication().getMessageSource().getMessage('some.key')
The set of methods that take a List
as arguments are meant to be used from Groovy
code, whereas those that take an Object[]
are meant for Java code; this leads to
better idiomatic code, as the following examples reveal:
getApplication().getMessageSource()
.getMessage('groovy.message', ['apples', 'bananas'])
getApplication().getMessageSource()
.getMessage("java.message", new Object[]{"unicorns", "rainbows"});
Of course you may also use List
versions in Java, like this:
getApplication().getMessageSource()
.getMessage("hybrid.message", Arrays.asList("bells", "whistles"));
11.1.1. Message Formats
There are three types of message formats supported by default. Additional formats may be supported if the right plugins are installed. Resources may be configured using either properties files or Groovy scripts; please refer to the configuration section.
Standard Format
The first set of message formats are those supported by the JDK’s
MessageFormat
facilities. These formats work with all versions of the getMessage()
method that
take a List
or an Object[]
as arguments. Examples follow. First, the messages may be
stored in a properties file:
1
2
healthy.proverb = An {0} a day keeps the {1} away
yoda.says = {0} is the path to the dark side. {0} leads to {1}. {1} leads to {2}. {2} leads to suffering.
Then the code used to resolve them is:
String quote = getApplication().getMessageSource()
.getMessage('healthy.proverb', ['apple', 'doctor'])
assert quote == 'An apple a day keeps the doctor away'
String quote = getApplication().getMessageSource()
.getMessage("yoday.says", new Object[]{"Fear", "Anger", "Hate"});
assertEquals(quote, "Fear is the path to the dark side. Fear leads to Anger. Anger leads to Hate. Hate leads to suffering");
Map Format
The following format is non-standard (i.e, not supported by MessageFormat
) and can
only be resolved by Griffon. This format uses symbols instead of numbers as placeholders
for arguments. Thus the previous messages can be rewritten as follows:
1
2
healthy.proverb = An {:fruit} a day keeps the {:occupation} away
yoda.says = {:foo} is the path to the dark side. {:foo} leads to {:bar}. {:bar} leads to {:foobar}. {:foobar} leads to suffering.
Which may be resolved in this manner:
String quote = getApplication().getMessageSource()
.getMessage('healthy.proverb', [fruit: 'apple', occupation: 'doctor'])
assert quote == 'An apple a day keeps the doctor away
import static griffon.util.CollectionUtils.map;
String quote = getApplication().getMessageSource()
.getMessage("yoday.says", map().e("foo", "Fear")
.e("bar", "Anger")
.e("foobar","Hate"));
assertEquals(quote, "Fear is the path to the dark side. Fear leads to Anger. Anger leads to Hate. Hate leads to suffering");
Groovy format
Groovy scripts have one advantage over properties files: you can embed custom logic that may conditionally resolve a message based on environmental values or generate a message on the fly. In order to accomplish this feat, messages must be defined as closures and must return a String value; if they do not, then their return value will be translated to a String. The following message uses the value of the current running environment to determine the text of a warning to be displayed on a label:
1
2
3
4
5
6
7
8
9
import griffon.util.Environment
warning.label = { args ->
if (Environment.current == Environment.PRODUCTION) {
"The application has encountered an error: $args"
} else {
"Somebody sent us a bomb! $args"
}
}
11.1.2. Reference Keys
There may be times where you would want to have 2 keys reference the same value,
as if one key were an alias for the other. MessageSource
supports the notion of
referenced keys for this matter. In order to achieve this, the value of the alias
key must define the aliased key with a special format, for example
1
2
famous.quote = This is {0}!
hello.world = @[famous.quote]
Resolving those keys results in
assert getApplication()
.getMessageSource()
.getMessage('famous.quote', ['Sparta']) == 'This is Sparta!'
assert getApplication()
.getMessageSource()
.getMessage('hello.world', ['Griffon']) == 'This is Griffon!'
11.2. MessageSource Configuration
Messages may be configured in either properties files or Groovy scripts.
Groovy scripts have precedence over properties files should there be two files that
match the same basename
. The default configured basename
is “messages”, thus
the application will search for the following resources in the classpath.
-
messages.properties
-
messages.groovy
Of course Groovy scripts are only enabled if you add a dependency to the griffon-groovy
module to your project. The default basename
may be changed to some other value,
or additional basenames may be specified too; it’s just a matter of configuring a
Module override:
1
2
3
4
5
6
7
8
9
10
11
12
@ServiceProviderFor(Module.class)
@Named("application")
@DependsOn("core")
public class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(MessageSource.class)
.withClassifier(named("applicationMessageSource"))
.toProvider(new MessageSourceProvider("custom_basename"))
.asSingleton();
}
}
Both properties files and Groovy scripts are subject to a locale-aware loading mechanism
described next. For a Locale set to de_CH_Basel
the following resources will be
searched for and loaded:
-
messages.properties
-
messages.groovy
-
messages_de.properties
-
messages_de.groovy
-
messages_de_CH.properties
-
messages_de_CH.groovy
-
messages_de_CH_Basel.properties
-
messages_de_CH_Basel.groovy
Properties files and Groovy scripts used for internationalization purposes are usually
placed under griffon-app/i18n
. The default messages.properties
file is placed in
this directory upon creating an application using the standard project templates.
11.3. The @MessageSourceAware AST Transformation
Any component may gain the ability to resolve messages through a MessageSource
instance. You only need annotate the class with @griffon.transform.MessageSourceAware
and it will automatically gain all methods exposed by MessageSource
.
This feature is just a shortcut to avoid reaching for the application instance from objects that do not hold a reference to it.
Here’s an example of a custom bean that’s able to resolve messages:
1
2
3
4
@griffon.transform.MessageSourceAware
class Bean {
String name
}
This class can be used in the following way:
1
2
3
4
5
6
7
8
class SampleService {
@Inject Bean bean
String lookupValues(String arg) {
bean.name = arg
bean.getMessage('some.message.key', [bean.name])
}
}
The application’s MessageSource
will be injected to annotated beans if no name is
specified as an argument to MessageSourceAware
. You may define multiple MessageSource
bindings as long as you qualify them with a distinct name, such as
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ServiceProviderFor(Module.class)
@Named("application")
public class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(MessageSource.class)
.withClassifier(AnnotationUtils.named("foo"))
.toProvider(new MessageSourceProvider("foofile"))
.asSingleton();
bind(MessageSource.class)
.withClassifier(AnnotationUtils.named("bar"))
.toProvider(new MessageSourceProvider("barfile"))
.asSingleton();
}
}
Then make use of any of these bindings like so:
1
2
3
4
@griffon.transform.MessageSourceAware('foo')
class Bean {
String name
}
12. Resource Management
This chapter describes resource management and injection features available to all applications.
12.1. Locating Classpath Resources
Resources can be loaded form the classpath using the standard mechanism provided by the
Java runtime, that is, by asking a ClassLoader
instance to load a resource URL
or to 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 component:
scrollPane {
textArea(columns: 40, rows: 20,
text: this.class.classLoader.getResource('someTextFile.txt').text)
}
In order to reduce visual clutter, and also to provide an abstraction over resource location,
applications rely on ResourceHandler
, which exposes the following contract:
-
URL getResourceAsURL(String name)
-
InputStream getResourceAsStream(String name)
-
List<URL> getResources(String name)
-
ClassLoader classloader()
Thus, the previous example can be rewritten in this way:
scrollPane {
textArea(columns: 40, rows: 20,
text: application.resourceHandler.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, such as picking the correct
ClassLoader
.
12.2. The @ResourcesAware AST Transformation
Any component may gain the ability to locate resources through a ResourceHandler
instance. You need only annotate the class with @griffon.transform.ResourcesAware
and it will automatically gain all methods exposed by ResourceHandler
.
This feature is just a shortcut to avoid obtaining the application instance by objects which do not hold a reference to it.
Here’s an example of a custom bean which is able to locate resources:
1
2
3
4
@griffon.transform.ResourcesAware
class Bean {
String name
}
This class can be used in the following way:
1
2
3
4
5
6
7
8
class SampleService {
@Inject Bean bean
InputStream fetchResource(String arg) {
bean.name = arg
bean.getResourceAsStream(bean.name)
}
}
12.3. ResourceResolver
Applications have the ability to resolve internationalizable messages by leveraging
the behavior exposed by ResourceResolver
. This interface exposes the following methods:
-
Object resolveResource(String key)
-
Object resolveResource(String key, Locale locale)
-
Object resolveResource(String key, Object[] args)
-
Object resolveResource(String key, Object[] args, Locale locale)
-
Object resolveResource(String key, List args)
-
Object resolveResource(String key, List args, Locale locale)
-
Object resolveResource(String key, Map args)
-
Object resolveResource(String key, Map args, Locale locale)
-
Object resolveResorceValue(String key, Locale locale)
The first set throws NoSuchResourceException
if a message could not be resolved given
the key sent as argument. The following methods take an additional defaultValue
parameter which will be used if no configured resource is found. If this optional parameter
were to be null, then the key
is used as the literal value of the resource; in other words,
these methods never throw NoSuchResourceException
nor return null
unless the passed-in key
is null.
-
Object resolveResource(String key, Object defaultValue)
-
Object resolveResource(String key, Locale locale, Object defaultValue)
-
Object resolveResource(String key, Object[] args, Object defaultValue)
-
Object resolveResource(String key, Object[] args, Locale locale, Object defaultValue)
-
Object resolveResource(String key, List args, Object defaultValue)
-
Object resolveResource(String key, List args, Locale locale, Object defaultValue)
-
Object resolveResource(String key, Map args, Object defaultValue)
-
Object resolveResource(String key, Map args, Locale locale, Object defaultValue)
There is also another set of methods which convert the resource value using PropertyEditor
s:
-
<T> T resolveResourceConverted(String key, Class<T> type)
-
<T> T resolveResourceConverted(String key, Locale locale, Class<T> type)
-
<T> T resolveResourceConverted(String key, Object[] args, Class<T> type)
-
<T> T resolveResourceConverted(String key, Object[] args, Locale locale, Class<T> type)
-
<T> T resolveResourceConverted(String key, List args, Class<T> type)
-
<T> T resolveResourceConverted(String key, List args, Locale locale, Class<T> type)
-
<T> T resolveResourceConverted(String key, Map args, Class<T> type)
-
<T> T resolveResourceConverted(String key, Map args, Locale locale, Class<T> type)
-
<T> T resolveResorceValue(String key, Locale locale, Class<T> type)
with default value support too:
-
<T> T resolveResourceConverted(String key, Object defaultValue, Class<T> type)
-
<T> T resolveResourceConverted(String key, Locale locale, Object defaultValue, Class<T> type)
-
<T> T resolveResourceConverted(String key, Object[] args, Object defaultValue, Class<T> type)
-
<T> T resolveResourceConverted(String key, Object[] args, Locale locale, Object defaultValue, Class<T> type)
-
<T> T resolveResourceConverted(String key, List args, Object defaultValue, Class<T> type)
-
<T> T resolveResourceConverted(String key, List args, Locale locale, Object defaultValue, Class<T> type)
-
<T> T resolveResourceConverted(String key, Map args, Object defaultValue, Class<T> type)
-
<T> T resolveResourceConverted(String key, Map args, Locale locale, Object defaultValue, Class<T> type)
The simplest way to resolve a message is thus
getApplication().getResourceResolver().resolveResource('menu.icon')
The set of methods that take a List
as arguments are meant to be used from Groovy
code, whereas those that take an Object[]
are meant for Java code; this leads to
better idiomatic code, as the following examples show:
getApplication().getResourceResolver()
.resolveResource('groovy.icon.resource', ['small']))
getApplication().getResourceResolver()
.resolveResource("java.icon.resource", new Object[]{"large"});
Of course you may also use List
versions in Java, like this:
getApplication().getResourceResolver()
.resolveResource("hybrid.icon.resource", Arrays.asList("medium"));
12.3.1. Message Formats
There are three types of resource formats supported by default. Additional formats may be supported if the right plugins are installed. Resources may be configured using either properties files or Groovy scripts; please refer to the configuration section.
Standard Format
The first set of resource formats are those supported by the JDK’s
MessageFormat
facilities. These formats work with all versions of the resolveResource()
method that
take a List
or an Object[]
as arguments. Examples follow. First, the resource
definitions stored in a properties file:
1
menu.icon = /img/icons/menu-{0}.png
Assuming there are three icon files stored at griffon-app/resources/img/icons
whose
filenames are menu-small.png
, menu-medium.png
and menu-large.png
, a component may
resolve any of them with
Object icon = getApplication().getResourceResolver()
.resolveResource('menu.icon', ['large'])
Map Format
The following format is non-standard (i.e, not supported by MessageFormat
) and can
only be resolved by Griffon. This format uses symbols instead of numbers as placeholders
for arguments. Thus the previous messages can be rewritten as follows:
1
menu.icon = /img/icons/menu-{:size}.png
Which may be resolved in this manner:
Object icon = getApplication().getResourceResolver()
.resolveResource('menu.icon', [size: 'large'])
Groovy format
Groovy scripts have one advantage over properties files as you can embed custom logic that may conditionally resolve a resource based on environmental values or generate a message on the fly. In order to accomplish this, resources must be defined as closures. The following message uses the value of the current running environment to determine the text of a warning to be displayed on a label:
1
2
3
4
5
6
import java.awt.Rectangle
direct.instance = new Rectangle(10i, 20i, 30i, 40i)
computed.instance = { x, y, w, h ->
new Rectangle(x, y, w, h)
}
12.3.2. Type Conversion
Note that the return value of resolveResource
is marked as Object
, but you’ll get
a String
from the first two formats. You’ll have to rely on property editors
in order to transform the value into the correct type. Injected resources
are automatically transformed to the expected type.
Here’s how it can be done:
import javax.swing.Icon
import java.beans.PropertyEditor
import griffon.core.editors.PropertyEditorResolver
...
Object iconValue = getApplication().getResourceResolver()
.resolveResource('menu.icon', ['large'])
PropertyEditor propertyEditor = PropertyEditorResolver.findEditor(Icon)
propertyEditor.setAsText(String.valueOf(iconValue))
Icon icon = propertyEditor.getValue()
As an alternative you may call resolveResourceConverted
instead.
12.3.3. Reference Keys
There may be times where you would want to have 2 keys reference the same value,
as if one key were an alias for the other. ResourceResolver
supports the notion of
referenced keys for this matter. In order to achieve this, the value of the alias
key must define the aliased key with a special format, for example:
1
2
action.icon = /img/icons/action-{0}.png
hello.icon = @[action.icon]
Resolving those keys results in
assert getApplication()
.getResourceResolver()
.resolveResource('action.icon', ['copy']) == '/img/icons/action-copy.png'
assert getApplication()
.getResourceResolver()
.resolveResource('hello.icon', ['paste']) == '/img/icons/action-paste.png'
12.4. ResourceResolver Configuration
Resources may be configured in either properties files or Groovy scripts.
Groovy scripts have precedence over properties files, should there be two files that
match the same basename
. The default configured basename
is “resources”; thus
the application will search for the following resources in the classpath.
-
resources.properties
-
resources.groovy
Of course, Groovy scripts are only enabled if you add a dependency to the griffon-groovy
module to your project. The default basename
may be changed to some other value,
or additional basenames may be specified too; it’s just a matter of configuring a
Module override:
1
2
3
4
5
6
7
8
9
10
11
12
@ServiceProviderFor(Module.class)
@Named("application")
@DependsOn("core")
public class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(ResourceResolver.class)
.withClassifier(named("applicationResourceResolver"))
.toProvider(new ResourceResolverProvider("custom_basename"))
.asSingleton();
}
}
Both properties files and Groovy scripts are subject to a locale-aware loading mechanism
described below. For a Locale set to de_CH_Basel
, the following resources will be
searched for and loaded:
-
resources.properties
-
resources.groovy
-
resources_de.properties
-
resources_de.groovy
-
resources_de_CH.properties
-
resources_de_CH.groovy
-
resources_de_CH_Basel.properties
-
resources_de_CH_Basel.groovy
Properties files and Groovy scripts used for internationalization purposes are usually
placed under griffon-app/resources
. The default resources.properties
file is placed in
this directory upon creating an application using the standard project templates.
12.5. The @ResourceResolverAware AST Transformation
Any component may gain the ability to resolve resources through a ResourceResolver
instance. You need only annotate the class with @griffon.transform.ResourceResolverAware
and it will automatically gain all methods exposed by ResourceResolver
.
This feature is just a shortcut to retrieving the application instance by objects which do not hold a reference to it.
Here’s an example of a custom bean that’s able to resolve resources:
1
2
3
4
@griffon.transform.ResourceResolverAware
class Bean {
String name
}
This class can be used in the following way:
1
2
3
4
5
6
7
8
class SampleService {
@Inject Bean bean
String lookupValues(String arg) {
bean.name = arg
bean.resolveResource('some.resource.key', [bean.name])
}
}
The application’s ResourceResolver
will be injected to annotated beans if no name is
specified as an argument to ResourceResolverAware
. You may define multiple ResourceResolver
bindings as long as you qualify them with a distinct name, such as
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ServiceProviderFor(Module.class)
@Named("application")
public class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(ResourceResolver.class)
.withClassifier(AnnotationUtils.named("foo"))
.toProvider(new ResourceResolverProvider("foofile"))
.asSingleton();
bind(ResourceResolver.class)
.withClassifier(AnnotationUtils.named("bar"))
.toProvider(new ResourceResolverProvider("barfile"))
.asSingleton();
}
}
Then make use of any of these bindings, like so:
1
2
3
4
@griffon.transform.ResourceResolverAware('foo')
class Bean {
String name
}
12.6. Resource Injection
Resources may be automatically injected into any instance created via the application’s
Injector
. Injection points must be annotated with @griffon.core.resources.InjectedResource
which can be set on properties (Groovy), fields (Java and Groovy) and setter methods (Java and Groovy).
@InjectedResource is a perfect companion to models, as the following example shows:
1
2
sample.SampleModel.griffonLogo = /griffon-logo-48x48.png
logo = /griffon-logo-{0}x{0}.png
1
2
3
4
5
6
7
8
9
10
11
12
package sample
import griffon.core.resources.InjectedResource
import javax.swing.Icon
import griffon.core.artifact.GriffonModel
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@InjectedResource Icon griffonLogo
@InjectedResource(key='logo', args=['16']) Icon smallGriffonLogo
@InjectedResource(key='logo', args=['64']) Icon largeGriffonLogo
}
@InjectedResource
assumes a naming convention in order to determine the resource key
to use. These are the rules applied by the default by ResourcesInjector
:
-
If a value is specified for the
key
argument, then use it as is. -
otherwise, construct a key based in the field name prefixed with the full qualified class name of the field’s owner.
You may also specify a default value if the resource definition is not found; however,
be aware that this value must be set as a String, thus guaranteeing a type conversion.
An optional format
value may be specified as a hint to the PropertyEditor used during
value conversion, for example:
1
2
3
4
5
6
7
8
9
10
package sample
import griffon.core.resources.InjectedResource
import griffon.core.artifact.GriffonModel
@griffon.metadata.ArtifactProviderFor(GriffonModel)
class SampleModel {
@InjectedResource(defaultValue='10.04.2013 2:30 PM', format='dd.MM.yyyy h:mm a')
Date date
}
12.7. Property Editors
Resource injection makes use of the PropertyEditor
mechanism provided by the java.beans
package. The default ResourcesInjector
queries
PropertyEditorManager
whenever a resource value must be transformed to a target type.
PropertyEditorManager provides methods for registering custom PropertyEditors; it also
follows a class naming convention to load PropertyEditors should a custom one not be
programmatically registered. Griffon applications will automatically load and register
PropertyEditors from the following classpath resource: /META-INF/services/java.beans.PropertyEditor
.
Each line follows the format
target.type = full.qualified.classname
The following table enumerates the default PropertyEditors loaded by Griffon at startup.
Type | Editor Class | Format |
---|---|---|
java.lang.String |
griffon.core.editors.StringPropertyEditor |
|
java.io.File |
griffon.core.editors.FilePropertyEditor |
|
java.net.URL |
griffon.core.editors.URLPropertyEditor |
|
java.net.URI |
griffon.core.editors.URIPropertyEditor |
|
java.math.BigDecimal |
griffon.core.editors.BigDecimalPropertyEditor |
'currency', 'percent' |
java.math.BigInteger |
griffon.core.editors.BigIntegerPropertyEditor |
'currency', 'percent' |
java.lang.Boolean |
griffon.core.editors.BooleanPropertyEditor |
'boolean', 'query', 'switch' |
java.lang.Byte |
griffon.core.editors.BytePropertyEditor |
'currency', 'percent' |
java.lang.Short |
griffon.core.editors.ShortPropertyEditor |
'currency', 'percent' |
java.lang.Integer |
griffon.core.editors.IntegerPropertyEditor |
'currency', 'percent' |
java.lang.Long |
griffon.core.editors.LongPropertyEditor |
'currency', 'percent' |
java.lang.Float |
griffon.core.editors.FloatPropertyEditor |
'currency', 'percent' |
java.lang.Double |
griffon.core.editors.DoublePropertyEditor |
'currency', 'percent' |
java.util.Calendar |
griffon.core.editors.CalendarPropertyEditor |
|
java.util.Date |
griffon.core.editors.DatePropertyEditor |
|
java.util.Locale |
griffon.core.editors.LocalePropertyEditor |
<language>[_<country>[_<variant>]] |
Where the following apply:
-
'currency' and 'percent' are literal values.
-
'boolean' accepts
true
andfalse
as values. -
'query' accepts
yes
andno
as values. -
'switch' accepts
on
andoff
as values.
Core UI Toolkit dependencies, such as griffon-swing
, griffon-javafx
, and griffon-pivot
deliver
additional PropertyEditors. The following tables summarize these additions:
Type | Editor Class | Format |
---|---|---|
java.awt.Color |
griffon.swing.editors.ColorPropertyEditor |
#RGB ; #RGBA ; #RRGGBB; #RRGGBBAA ; Color constant |
java.awt.Dimension |
griffon.swing.editors.DimensionPropertyEditor |
width, height |
java.awt.Font |
griffon.swing.editors.FontPropertyEditor |
family-style-size |
java.awt.GradientPaint |
griffon.swing.editors.GradientPaintPropertyEditor |
x1, y1, #RGB, x2, y2, #RGB, CYCLIC |
java.awt.Image |
griffon.swing.editors.ImagePropertyEditor |
path/to/image_file |
java.awt.Insets |
griffon.swing.editors.InsetsPropertyEditor |
top, left, bottom, right |
java.awt.LinearGradientPaint |
griffon.swing.editors.LinearGradientPaintPropertyEditor |
xy, y1, x2, x2, [0.0, 1.0], [#RGB, #RGB], REPEAT |
java.awt.Point |
griffon.swing.editors.PointPropertyEditor |
x, y |
java.awt.Polygon |
griffon.swing.editors.PolygonPropertyEditor |
x1, y1, x2, y2, …, xn, yn |
java.awt.RadialGradientPaint |
griffon.swing.editors.RadialGradientPaintPropertyEditor |
xy, y1, r, fx, fy, [0.0, 1.0], [#RGB, #RGB], REPEAT |
java.awt.Rectangle |
griffon.swing.editors.RectanglePropertyEditor |
x, y, width, height |
java.awt.geom.Point2D |
griffon.swing.editors.Point2DPropertyEditor |
x, y |
java.awt.geom.Rectangle2D |
griffon.swing.editors.Rectangle2DPropertyEditor |
x, y, width, height |
java.awt.image.BufferedImage |
griffon.swing.editors.BufferedImagePropertyEditor |
path/to/image_file |
javax.swing.Icon |
griffon.swing.editors.IconPropertyEditor |
path/to/image_file |
Where the following apply:
-
R
,G
,B
andA
represent an hexadecimal number. -
CYCLIC may be
true
orfalse
. -
REPEAT must be one of
MultipleGradientPaint.CycleMethod
. -
GradientPaint supports another format: x1, y1 | x2, y2, | #RGB, #RGB | CYCLIC
-
Color supports all color constants defined by
griffon.swing.support.Colors
. -
All color formats are supported by gradient editors.
The following styles are supported by FontPropertyEditor
:
-
BOLD
-
ITALIC
-
BOLDITALIC
-
PLAIN
Type | Editor Class | Format |
---|---|---|
javafx.geometry.Dimension2D |
griffon.javafx.editors.Dimension2DPropertyEditor |
width, height |
javafx.geometry.Insets |
griffon.javafx.editors.InsetsPropertyEditor |
top, left, bottom, right |
javafx.geometry.Point2D |
griffon.javafx.editors.Point2DPropertyEditor |
x, y |
javafx.geometry.Rectangle2D |
griffon.javafx.editors.Rectangle2DPropertyEditor |
x, y , width, height |
javafx.scene.image.Image |
griffon.javafx.editors.ImagePropertyEditor |
path/to/image_file |
javafx.scene.paint.Color |
griffon.javafx.editors.ColorPropertyEditor |
#RGB ; #RGBA ; #RRGGBB; #RRGGBBAA ; Color constant |
javafx.scene.paint.LinearGradient |
griffon.javafx.editors.LinearGradientPropertyEditor |
LinearGradient.parse() |
javafx.scene.paint.RadialGradient |
griffon.javafx.editors.RadialGradientPropertyEditor |
RadialGradient.parse() |
javafx.scene.paint.Paint |
griffon.javafx.editors.PaintPropertyEditor |
Where the following applies:
-
R
,G
,B
andA
represent an hexadecimal number.
Type | Editor Class | Format |
---|---|---|
java.awt.Color |
griffon.pivot.editors.ColorPropertyEditor |
#RGB ; #RGBA ; #RRGGBB; #RRGGBBAA ; Color constant |
org.apache.pivot.wtk.Bounds |
griffon.pivot.editors.BoundsPropertyEditor |
x, y , width, height |
org.apache.pivot.wtk.Dimensions |
griffon.pivot.editors.DimensionsPropertyEditor |
width, height |
org.apache.pivot.wtk.Insets |
griffon.pivot.editors.InsetsPropertyEditor |
top, left, right, bottom |
org.apache.pivot.wtk.Point |
griffon.pivot.editors.PointPropertyEditor |
x, y |
Where the following apply:
-
R
,G
,B
andA
represent an hexadecimal number. -
Color supports all color constants defined by
griffon.pivot.support.Colors
.
Since Griffon 2.4.0, there’s a core-java8
package that delivers JDK8 specific property editors:
Type | Editor Class | Format |
---|---|---|
java.time.LocalDate |
griffon.core.editors.LocalDatePropertyEditor |
|
java.time.LocalDateTime |
griffon.core.editors.LocalDateTimePropertyEditor |
|
java.time.LocalTime |
griffon.core.editors.LocalTimePropertyEditor |
|
java.util.Calendar |
griffon.core.editors.ExtendedCalendarPropertyEditor |
|
java.util.Date |
griffon.core.editors.ExtendedDatePropertyEditor |
These versions of Calendar
and Date
property editors accept all formats as the previous core editors, while also
being able to transform values from the java.time
package.
13. Addons
Addons allow plugin authors to perform the following tasks:
-
Perform initialization code during the Initialize phase.
-
Supply additional configuration that can be used to define new
MVCGroupConfiguration
s.
Addons are automatically registered as ShutdownHandler
s with the application
instance. They are also registered as event handlers with the application’s
EventRouter
.
Addons must be registered with a Module
in order to be discovered by the runtime.
Here’s a simple example of a custom Addon that prints out the name of an MVCGroup
when said group is initialized:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.acme;
import griffon.core.mvc.MVCGroup;
import griffon.core.mvc.MVCGroupConfiguration;
import org.codehaus.griffon.runtime.core.addon.AbstractGriffonAddon;
import javax.annotation.Nonnull;
import javax.inject.Named;
@Named("inspector")
public class InspectorAddon extends AbstractGriffonAddon {
public void onInitializeMVCGroup(@Nonnull MVCGroupConfiguration configuration, @Nonnull MVCGroup group) {
System.out.println("MVC group " + group.getMvcType() + " initialized");
}
}
And here is how it can be registered with a Module
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.acme;
import griffon.core.addon.GriffonAddon;
import griffon.core.injection.Module;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import javax.inject.Named;
@Named("inspector")
@ServiceProviderFor(Module.class)
public class InspectorModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(GriffonAddon.class)
.to(InspectorAddon.class)
.asSingleton();
}
}
13.1. The AddonManager
The AddonManager
is responsible for keeping track of instantiated
GriffonAddon
s. You may use this manager to query which addons have been
registered with the application, then conditionally enabling further features
if a particular addon is instantiated or not.
The name of a GriffonAddon
is used as the key to register it with the AddonManager
,
in other words, the previous inspector addon can be queried in the following way:
GriffonAddon addon = application.getAddonManager().findAddon("inspector");
14. Testing
The following sections describe the testing support provided by Griffon.
14.1. Unit Testing
Classes under test that do not require dependency injection can be tested without any additional support from Griffon. For those classes that required dependency injection you have the following options:
GuiceBerry
If the project relies on griffon-guice-2.15.0
for injecting dependencies at runtime then you may use GuiceBerry
when writing tests. GuiceBerry can resolve Guice modules and perform injection on the test class itself.
testCompile 'com.google.guiceberry:guiceberry:3.3.1'
<dependency>
<groupId>com.google.guiceberry</groupId>
<artifactId>guiceberry</artifactId>
<artifactId>3.3.1</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import com.google.guiceberry.GuiceBerryModule;
import com.google.guiceberry.junit4.GuiceBerryRule;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import org.junit.Rule;
import org.junit.Test;
import javax.inject.Singleton;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
public class MyGuiceBerryTest {
@Rule
public final GuiceBerryRule guiceBerry = new GuiceBerryRule(TestModule.class);
@Inject
private FooBar classUnderTest;
@Test
public void testIt() {
assertThat(classUnderTest.foobar(), equalTo("foobar"));
}
public static final class TestModule extends AbstractModule {
@Override
protected void configure() {
install(new GuiceBerryModule());
bind(Foo.class).to(DefaultFoo.class).in(Singleton.class);
bind(Bar.class).to(DefaultBar.class).in(Singleton.class);
bind(FooBar.class).to(DefaultFooBar.class).in(Singleton.class);
}
}
public static interface FooBar {
String foobar();
}
public static interface Foo {
String foo();
}
public static interface Bar {
String bar();
}
public static class DefaultFooBar implements FooBar {
private final Foo foo;
private final Bar bar;
@Inject
public DefaultFooBar(Foo foo, Bar bar) {
this.foo = foo;
this.bar = bar;
}
public String foobar() { return foo.foo() + bar.bar(); }
}
public static class DefaultFoo implements Foo {
public String foo() { return "foo"; }
}
public static class DefaultBar implements Bar {
public String bar() { return "bar"; }
}
}
You may use GuiceBerry with both JUnit and Spock based tests.
Jukito
Jukito combines the power of JUnit, Google Guice, and Mockito in one single package. The advantage of
Jukito over plain GuiceBerry is that injection points that are not defined by a TestModule
will be automatically mocked with
Mockito.
testCompile 'org.jukito:jukito:1.4'
<dependency>
<groupId>org.jukito</groupId>
<artifactId>jukito</artifactId>
<artifactId>1.4</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import org.jukito.JukitoRunner;
import org.jukito.JukitoModule;
import com.google.inject.Inject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Singleton;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.when;
@RunWith(JukitoRunner.class)
public class MyJukitoTest {
public static class TestModule extends JukitoModule {
protected void configureTest() {
bind(FooBar.class).to(DefaultFooBar.class).in(Singleton.class);
}
}
@Inject
private FooBar classUnderTest;
@Before
public void setupMocks(Foo foo, Bar bar) {
when(foo.foo()).thenReturn("foo");
when(bar.bar()).thenReturn("bar");
}
@Test
public void testIt() {
assertThat(classUnderTest.foobar(), equalTo("foobar"));
}
public static interface FooBar {
String foobar();
}
public static interface Foo {
String foo();
}
public static interface Bar {
String bar();
}
public static class DefaultFooBar implements FooBar {
private final Foo foo;
private final Bar bar;
@Inject
public DefaultFooBar(Foo foo, Bar bar) {
this.foo = foo;
this.bar = bar;
}
public String foobar() { return foo.foo() + bar.bar(); }
}
}
Jukito should be used with JUnit alone.
14.1.1. The GriffonUnitRule
While the previous options work perfectly well with non-Griffon artifacts there will be times when you need to setup more
test bindings that provide fake or real collaborators. The GriffonUnitRule
rule class provides this
behavior and more. This class behaves in a similar way as GuiceBerry and Jukito in the sense that it can inject dependencies
in the current testcase, but it’ll also bootstrap a GriffonApplication
using real modules. You have the choice to
override any modules as needed.
GriffonUnitRule
must be applied using JUnit’s @Rule
annotation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package sample.javafx.java;
import griffon.core.artifact.ArtifactManager;
import griffon.core.test.GriffonUnitRule;
import griffon.core.test.TestFor;
import javafx.embed.swing.JFXPanel;
import org.junit.Rule;
import org.junit.Test;
import javax.inject.Inject;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
@TestFor(SampleController.class)
public class SampleControllerTest {
static {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
// force initialization JavaFX Toolkit
new JFXPanel();
}
@Inject
private ArtifactManager artifactManager;
private SampleController controller;
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule();
@Test
public void executeSayHelloActionWithNoInput() {
final SampleModel model = artifactManager.newInstance(SampleModel.class);
controller.setModel(model);
controller.invokeAction("sayHello");
await().atMost(2, SECONDS)
.until(model::getOutput, notNullValue());
assertEquals("Howdy stranger!", model.getOutput());
}
@Test
public void executeSayHelloActionWithInput() {
final SampleModel model = artifactManager.newInstance(SampleModel.class);
model.setInput("Griffon");
controller.setModel(model);
controller.invokeAction("sayHello");
await().atMost(2, SECONDS)
.until(model::getOutput, notNullValue());
assertEquals("Hello Griffon", model.getOutput());
}
}
You can define any of the following arguments to tweak it’s behavior and of the application under test:
startupArgs |
An array of literal arguments. Similar to the startup args sent to a real application during launch. |
applicationClass |
Defines the application class to use. Default value is set to |
applicationBootstrapper |
Defines the streategies used to locate modules and bootstrap the application. Default value is
set to |
This rule can be used with both JUnit and Spock based tests.
The following sections describe all available options for overriding modules in a testcase.
Overriding Module Bindings
The default TestApplicationBootstrapper
applies the following strategy to locate suitable modules that should be used
during the bootstrap sequence of the application under test:
-
If the testcase wants to override all modules. If so, consume those modules, no more checks are applied.
-
If the testcase wants to override some modules. If so, consume those modules and continue with the next check.
-
If the testcase defines bindings using inner classes. If so, create a
Module
on the fly with those bindings and continue with the next check. -
If the testcase defines individual bindings using annotated fields. If so, create a
Module
on the fly with those bindings.
In order to override an existing binding you must match the source type and the qualifier (if it exist). Please review the Binding Equivalencies section if you have any doubts regarding the rules.
Override All Modules
There are two ways to define if a testcase wants to override all available modules:
-
implement the
TestModuleAware
interface; providing a non-empty value for themodules()
method. -
annotate a method with
@TestModules
making sure it returns a non-emptyList<Module>
instance.
Overriding Some Modules
There are two ways to define if a testcase wants to override some available modules:
-
implement the
TestModuleAware
interface; providing a non-empty value for themoduleOverrides()
method. -
annotate a method with
@TestModuleOverrides
making sure it returns a non-emptyList<Module>
instance.
It’s recommended to implement TestingModule instead of Module as the
TestApplicationBootstrapper will make sure the former are placed after previous module definitions. This guarantees that
test bindings override previous bindings.
|
Defining Bindings on the TestCase
Bindings defined in this way must use the @BindTo
annotation to define the source type. If a qualifier is attached
to the type then it will be set on the binding too.
You may define a concrete type of a javax.inject.Provider
. The binding will use prototype scope unless the target inner
class is annotated with javax.inject.Singleton
. Some examples follow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import griffon.core.test.GriffonUnitRule;
import org.junit.Rule;
import griffon.inject.BindTo;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Named;
import javax.inject.Singleton;
import griffon.core.event.EventRouter;
import griffon.core.Context;
public class SomeTest {
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule();
@BindTo(EventRouter.class)
public static class CustomEventRouter implements EventRouter { ... } (1)
@BindTo(Context.class)
@Named("applicationContext")
@Singleton
public static class ContextProvider implements Provider<Context> { ... } (2)
// tests
}
1 | overrides EventRouter in prototype scope. |
2 | overrides an specific binding matching type and qualifier. |
Field bindings are more flexible that inner class bindings as you can define the instance to be associated with the source
type. This instance may be a concrete class that implements the source type, or a Provider
. For example, the bindings seen
in the previous example can be rewritten as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import griffon.core.test.GriffonUnitRule;
import org.junit.Rule;
import griffon.inject.BindTo;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Named;
import javax.inject.Singleton;
import griffon.core.event.EventRouter;
import griffon.core.Context;
public class SomeTest {
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule();
@BindTo(EventRouter.class)
private CustomEventRouter customEventRouter; (1)
@BindTo(Context.class)
@Named("applicationContext")
@Singleton
private Provider<Context> contextProvider = new Provider<Context> { ... } (2)
// tests
}
1 | overrides EventRouter in prototype scope. |
2 | overrides an specific binding matching type and qualifier. |
14.1.2. The @TestFor Annotation
The @TestFor
annotation comes in handy when testing Griffon artifacts, as it will automatically instantiate the
given type and set it on a field following a naming convention. The convention is to use the value for getArtifactType()
of the corresponding artifact GriffonClass
. The folowing table summarizes these values:
Type | Field Name |
---|---|
|
controller |
|
model |
|
service |
|
view |
It’s worth mentioning that this annotation will not instantiate additional MVC members that the current artifact under test may require. This design is on purpose in order to accommodate mocking of additional MVC members.
This annotation can be used with both JUnit and Spock based tests.
14.1.3. Mocking
There are several options for mocking types in test cases. Spock comes with its own solution. If you’re writing tests
with JUnit then we recommend you to have a look at Mockito. Both of these mocking options can be paired with
GriffonUnitRule
in order to supply bindings. You can use any of the techniques discussed in the previous sections to
define a binding, perhaps field bindings are the ones that are easier to grasp, as they can be used to define an
instance value, in this case, the mock object itself.
Take for example a CalculatorService
that requires an instance of a Calculator
. We want to test the service in
isolation which means we must mock the Calculator
. Given that the service is a Griffon artifact we can use both
GriffonUnitRule
and @TestFor
to reduce the amount of setup.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import griffon.core.test.GriffonUnitRule;
import griffon.core.test.TestFor;
import org.junit.Rule;
import org.junit.Test;
import griffon.inject.BindTo;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
@TestFor(SampleService.class)
public class CalculatorServiceTest {
private CalculatorServiceTest service; (1)
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule();
@Test
public void addTwoNumbers() {
// given:
when(calculator.add(anyInt(), anyInt())).thenReturn(3); (3)
// when:
int result = service.add(1, 2);
// then
assertThat(, equalTo(3));
}
@BindTo(Calculator.class)
private Calculator calculator = mock(Calculator.class); (2)
}
1 | matches the type and name of the artifact under test |
2 | instantiates the mock |
3 | prepares the mock for stubbing |
This technique can be applied for mocking any types except Griffon artifacts. Griffon artifacts must be mocked in a
slightly different way due to their relationship with Griffon internals. But don’t worry, is not that different, as a
matter of fact we’ve already covered how it can be done when explaining all possible ways to override a binding. In this
case we must use the verbose option, which is, defining an explicit Module
. Why do we need to do this? Because
Griffon artifacts must be initialized as lazily as possible. Using @BindTo
is too eager for them. Here’s a working
example of a controller
that mocks a service
while also creating a live instance for its model
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package org.example;
import griffon.core.artifact.ArtifactManager;
import griffon.core.injection.Module;
import griffon.core.test.GriffonUnitRule;
import griffon.core.test.TestFor;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.codehaus.griffon.runtime.core.injection.AbstractTestingModule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.util.List;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(JUnitParamsRunner.class) (1)
@TestFor(SampleController.class)
public class SampleControllerTest {
private SampleController controller;
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule();
@Inject private ArtifactManager artifactManager;
@Inject private SampleService sampleService; (2)
@Test
@Parameters({",Howdy stranger!",
"Test, Hello Test"})
public void sayHelloAction(String input, String output) { (3)
// given:
SampleModel model = artifactManager.newInstance(SampleModel.class); (4)
controller.setModel(model);
// expect:
assertThat(model.getOutput(), nullValue());
// expectations
when(sampleService.sayHello(input)).thenReturn(output);
// when:
model.setInput(input);
controller.sayHello();
// then:
assertThat(model.getOutput(), equalTo(output));
verify(sampleService, only()).sayHello(input);
}
@Nonnull
private List<Module> moduleOverrides() {
return asList(new AbstractTestingModule() { (5)
@Override
protected void doConfigure() {
bind(SampleService.class)
.toProvider(() -> mock(SampleService.class))
.asSingleton();
}
});
}
}
1 | parameterize this test using JUnitParams` |
2 | injected by GriffonUnitRule |
3 | parameterized test arguments |
4 | create a live model instance |
5 | configure the service mock |
14.2. Integration Testing
lorem ipsum
14.2.1. Swing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package sample.swing.java;
import griffon.core.test.GriffonFestRule;
import org.fest.swing.fixture.FrameFixture;
import org.junit.Rule;
import org.junit.Test;
public class SampleIntegrationTest {
static {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
System.setProperty("griffon.swing.edt.violations.check", "true");
System.setProperty("griffon.swing.edt.hang.monitor", "true");
}
@Rule
public final GriffonFestRule fest = new GriffonFestRule();
private FrameFixture window;
@Test
public void typeNameAndClickButton() {
// given:
window.textBox("inputField").enterText("Griffon");
// when:
window.button("sayHelloButton").click();
// then:
window.label("outputLabel").requireText("Hello Griffon");
}
@Test
public void doNotTypeNameAndClickButton() {
// given:
window.textBox("inputField").enterText("");
// when:
window.button("sayHelloButton").click();
// then:
window.label("outputLabel").requireText("Howdy stranger!");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package sample.swing.java
import griffon.core.test.GriffonFestRule
import org.fest.swing.fixture.FrameFixture
import org.junit.Rule
import spock.lang.Specification
class SampleIntegrationSpec extends Specification {
static {
System.setProperty('org.slf4j.simpleLogger.defaultLogLevel', 'trace')
System.setProperty('griffon.swing.edt.violations.check', 'true')
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
}
@Rule
public final GriffonFestRule fest = new GriffonFestRule()
private FrameFixture window
void 'Get default message if no input is given'() {
given:
window.textBox('inputField').enterText('Griffon')
when:
window.button('sayHelloButton').click()
then:
window.label('outputLabel').requireText('Hello Griffon')
}
void 'Get hello message if input is given'() {
given:
window.textBox('inputField').enterText('')
when:
window.button('sayHelloButton').click()
then:
window.label('outputLabel').requireText('Howdy stranger!')
}
}
14.2.2. JavaFX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package sample.javafx.java;
import griffon.javafx.test.GriffonTestFXRule;
import org.junit.Rule;
import org.junit.Test;
import static org.testfx.api.FxAssert.verifyThat;
import static org.testfx.matcher.control.LabeledMatchers.hasText;
public class SampleIntegrationTest {
static {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
}
@Rule
public GriffonTestFXRule testfx = new GriffonTestFXRule("mainWindow");
@Test
public void typeNameAndClickButton() {
// given:
testfx.clickOn("#input").write("Griffon");
// when:
testfx.clickOn("#sayHelloActionTarget");
// then:
verifyThat("#output", hasText("Hello Griffon"));
}
@Test
public void doNotTypeNameAndClickButton() {
// given:
testfx.clickOn("#input").write("");
// when:
testfx.clickOn("#sayHelloActionTarget");
// then:
verifyThat("#output", hasText("Howdy stranger!"));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package sample.javafx.java
import griffon.javafx.test.GriffonTestFXRule
import org.junit.Rule
import spock.lang.Specification
import static org.testfx.api.FxAssert.verifyThat
import static org.testfx.matcher.control.LabeledMatchers.hasText
class SampleIntegrationSpec extends Specification {
static {
System.setProperty('org.slf4j.simpleLogger.defaultLogLevel', 'trace')
}
@Rule
public GriffonTestFXRule testfx = new GriffonTestFXRule('mainWindow')
void 'Get default message if no input is given'() {
given:
testfx.clickOn('#input').write('')
when:
testfx.clickOn('#sayHelloActionTarget')
then:
verifyThat('#output', hasText('Howdy stranger!'))
}
void 'Get hello message if input is given'() {
given:
testfx.clickOn('#input').write('Griffon')
when:
testfx.clickOn('#sayHelloActionTarget')
then:
verifyThat('#output', hasText('Hello Griffon'))
}
}
14.2.3. Pivot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package sample.pivot.java;
import com.google.inject.Inject;
import griffon.core.mvc.MVCGroupManager;
import griffon.pivot.test.GriffonPivotFuncRule;
import org.apache.pivot.wtk.PushButton;
import org.apache.pivot.wtk.TextInput;
import org.junit.Rule;
import org.junit.Test;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.awaitility.Awaitility.fieldIn;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
public class SampleIntegrationTest {
static {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
System.setProperty("griffon.swing.edt.violations.check", "true");
System.setProperty("griffon.swing.edt.hang.monitor", "true");
}
@Rule
public final GriffonPivotFuncRule pivot = new GriffonPivotFuncRule();
@Inject
private MVCGroupManager mvcGroupManager;
@Test
public void typeNameAndClickButton() {
pivot.runInsideUISync(() -> {
// given:
pivot.find("inputField", TextInput.class).setText("Griffon");
// when:
pivot.find("sayHelloButton", PushButton.class).press();
});
SampleModel model = (SampleModel) mvcGroupManager.getModels().get("sample");
await().atMost(5, SECONDS)
.until(fieldIn(model)
.ofType(String.class)
.andWithName("output"),
notNullValue());
// then:
pivot.runInsideUISync(() -> assertEquals("Hello Griffon", pivot.find("outputField", TextInput.class).getText()));
}
@Test
public void doNotTypeNameAndClickButton() {
pivot.runInsideUISync(() -> {
// given:
pivot.find("inputField", TextInput.class).setText("");
// when:
pivot.find("sayHelloButton", PushButton.class).press();
});
SampleModel model = (SampleModel) mvcGroupManager.getModels().get("sample");
await().atMost(5, SECONDS)
.until(fieldIn(model)
.ofType(String.class)
.andWithName("output"),
notNullValue());
// then:
pivot.runInsideUISync(() -> assertEquals("Howdy stranger!", pivot.find("outputField", TextInput.class).getText()));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package sample.pivot.java
import griffon.pivot.test.GriffonPivotFuncRule
import org.apache.pivot.wtk.PushButton
import org.apache.pivot.wtk.TextInput
import org.junit.Rule
import spock.lang.Specification
import static java.util.concurrent.TimeUnit.SECONDS
import static org.awaitility.Awaitility.await
class SampleIntegrationSpec extends Specification {
static {
System.setProperty('org.slf4j.simpleLogger.defaultLogLevel', 'trace')
System.setProperty('griffon.swing.edt.violations.check', 'true')
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
}
@Rule
public final GriffonPivotFuncRule pivot = new GriffonPivotFuncRule()
void 'Get default message if no input is given'() {
pivot.runInsideUISync {
// given:
pivot.find('input', TextInput).text = 'Griffon'
// when:
pivot.find('sayHelloButton', PushButton).press()
}
await().atMost(5, SECONDS)
// then:
pivot.runInsideUISync {
assert 'Hello Griffon' == pivot.find('output', TextInput).text
}
}
void 'Get hello message if input is given'() {
pivot.runInsideUISync {
// given:
pivot.find('input', TextInput).text = ''
// when:
pivot.find('sayHelloButton', PushButton).press()
}
await().atMost(5, SECONDS)
// then:
pivot.runInsideUISync {
assert 'Howdy stranger!' == pivot.find('output', TextInput).text
}
}
}
14.3. Functional Testing
lorem ipsum
14.3.1. JavaFX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package sample.javafx.java;
import griffon.javafx.test.FunctionalJavaFXRunner;
import griffon.javafx.test.GriffonTestFXClassRule;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.testfx.api.FxAssert.verifyThat;
import static org.testfx.matcher.control.LabeledMatchers.hasText;
@RunWith(FunctionalJavaFXRunner.class)
public class SampleFunctionalTest {
static {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
}
@ClassRule
public static GriffonTestFXClassRule testfx = new GriffonTestFXClassRule("mainWindow");
@Test
public void _01_doNotTypeNameAndClickButton() {
// given:
testfx.clickOn("#input").write("");
// when:
testfx.clickOn("#sayHelloActionTarget");
// then:
verifyThat("#output", hasText("Howdy stranger!"));
}
@Test
public void _02_typeNameAndClickButton() {
// given:
testfx.clickOn("#input").write("Griffon");
// when:
testfx.clickOn("#sayHelloActionTarget");
// then:
verifyThat("#output", hasText("Hello Griffon"));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package sample.javafx.java
import griffon.javafx.test.GriffonTestFXClassRule
import spock.lang.Specification
import spock.lang.Stepwise
import static org.testfx.api.FxAssert.verifyThat
import static org.testfx.matcher.control.LabeledMatchers.hasText
@Stepwise
class SampleFunctionalSpec extends Specification {
static {
System.setProperty('org.slf4j.simpleLogger.defaultLogLevel', 'trace')
}
private static GriffonTestFXClassRule testfx = new GriffonTestFXClassRule('mainWindow')
void setupSpec() {
testfx.setup()
}
void cleanupSpec() {
testfx.cleanup()
}
void 'Get default message if no input is given'() {
given:
testfx.clickOn('#input').write('')
when:
testfx.clickOn('#sayHelloActionTarget')
then:
verifyThat('#output', hasText('Howdy stranger!'))
}
void 'Get hello message if input is given'() {
given:
testfx.clickOn('#input').write('Griffon')
when:
testfx.clickOn('#sayHelloActionTarget')
then:
verifyThat('#output', hasText('Hello Griffon'))
}
}
15. Build Tools
15.1. SDKMAN
From SDKMAN’s website
SDKMAN is a tool for managing parallel Versions of multiple Software Development Kits on most Unix-based systems. It provides a convenient command line interface for installing, switching, removing and listing Candidates.
SDKMAN can be used to install and keep up to date other build tools that make your life
easier when developing Griffon projects. These tools are lazybones
and gradle
.
Installing SDKMAN itself is as easy as typing the following on a command prompt:
$ curl -s http://get.sdkman.io | bash
Next, install the latest versions of lazybones
and gradle
by invoking
$ sdk install lazybones
$ sdk install gradle
SDKMAN works on POSIX compliant environments, even on Windows if Cygwin is installed. We recommend you to install Babun shell as it enables many more features than plain Cygwin.
15.2. Lazybones
Lazybones allows you to create a new project structure for any framework or library for which the tool has a template.
15.2.1. Configuration
All standard Griffon templates are published to https://bintray.com/griffon/griffon-lazybones-templates.
You must configure this repository with Lazybones settings file. Edit $USER_HOME/.lazybones/config.groovy
and paste the following content:
1
2
3
4
bintrayRepositories = [
"griffon/griffon-lazybones-templates",
"pledbrook/lazybones-templates"
]
Invoking the lazybones list
command should result in all currently available Griffon project
templates to be displayed in the output.
15.2.2. Templates
The following templates are available at the standard template repository:
griffon-swing-java |
A template that initializes an application with Swing and Java. |
griffon-swing-groovy |
A template that initializes an application with Swing and Groovy. |
griffon-javafx-java |
A template that initializes an application with JavaFX and Java. |
griffon-javafx-kotlin |
A template that initializes an application with JavaFX and Kotlin. |
griffon-javafx-groovy |
A template that initializes an application with JavaFX and Groovy. |
griffon-pivot-java |
A template that initializes an application with Pivot and Java. |
griffon-pivot-groovy |
A template that initializes an application with Pivot and Groovy. |
griffon-lanterna-java |
A template that initializes an application with Lanterna and Java. |
griffon-lanterna-groovy |
A template that initializes tan application with Lanterna and Groovy. |
griffon-plugin |
A template that initializes a Griffon plugin project. |
All application project templates include a subtemplate named artifact that provides the following artifact templates:
model |
A standard Model artifact. |
view |
A toolkit specific View artifact. |
controller |
A standard Controller artifact. |
service |
A standard Service artifact. |
test |
A standard unit Test artifact. |
mvcgroup |
Initializes Model, View, Controller, Test and IntegrationTest artifacts for a single MVC group. |
The swing
, javafx
and pivot
artifact templates provided additional artifact templates:
integrationTest |
A standard integration Test artifact. |
functionalTest |
A standard functional Test artifact. |
You can invoke any of these templates in the following ways:
$ lazybones generate artifact::controller
This command creates a new Controller artifact. You’ll be asked for a package and a class name.
$ lazybones generate artifact::mvcgroup::com.acme::group
This command creates an MVC Group whose package is com.acme and class name is group. There will be 5 new artifacts in total.
15.3. Gradle
Gradle is the preferred build tool for a Griffon project. The Lazybones
templates create a default build.gradle
file that contains the minimum configuration
to build, test and package a Griffon application or plugin.
15.3.1. The "griffon" Plugin
The Griffon plugin adds default dependencies and conventional configuration to a Griffon project. This configuration follows the standard Griffon project layout.
Usage
To use the Griffon plugin, include the following in your build script:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.codehaus.griffon:gradle-griffon-plugin:2.15.0'
}
}
apply plugin: 'org.codehaus.griffon.griffon'
This plugin performs the following configurations when applied to a project:
-
Registers jcenter() , http://dl.bintray.com/griffon/griffon-plugins and mavenLocal() as default repositories.
-
Applies the following plugins:
idea
,java
,application
. -
Creates additional compile-time only configurations: compileOnly, testCompileOnly.
-
Resolves plugin dependencies using the griffon configuration.
-
Adjusts javadoc/groovydoc/idea/eclipse classpaths given the new configurations.
-
Configures standard source directories with
main
andtest
source sets. -
Adjusts how
main
andtest
resource processing is performed.
The following dependencies are also added by default:
-
on
compile
-
org.codehaus.griffon:griffon-core:2.15.0
-
-
on
compileOnly
-
org.codehaus.griffon:griffon-core-compile:2.15.0
-
-
on
testCompile
-
org.codehaus.griffon:griffon-core-test:2.15.0
-
-
on
testCompileOnly
-
org.codehaus.griffon:griffon-core-compile:2.15.0
-
If the toolkit conventional property is defined (plugins may opt to skip it), then the following dependencies are added:
-
on
compile
-
org.codehaus.griffon:griffon-<toolkit>:2.15.0
-
If the groovy
plugin is applied, then the following dependencies are also added:
-
on
compile
-
org.codehaus.griffon:griffon-groovy:2.15.0
-
-
on
compile
-
org.codehaus.griffon:griffon-<toolkit>-groovy:2.15.0
-
-
on
compileOnly
-
org.codehaus.griffon:griffon-groovy-compile:2.15.0
-
-
on
testCompileOnly
-
org.codehaus.griffon:griffon-groovy-compile:2.15.0
-
The griffon
configuration can be used to resolve dependencies using BOM files.
The griffon-scaffolding-plugin
comprises the following modules:
-
griffon-scaffolding - the core of the plugin, UI toolkit agnostic.
-
griffon-scaffolding-swing - Swing specific additions.
-
griffon-scaffolding-javafx-groovy - Groovy enhancements via
BuilderCustomizer
. -
griffon-scaffolding-javafx - JavaFX specific additions.
-
griffon-scaffolding-javafx-groovy - Groovy enhancements via
BuilderCustomizer
. -
griffon-scaffolding-groovy-compile - AST transformations.
As you can see, this is quite a large set. You can manually define any of these dependencies in the build file, but given the many combinations it may be a bit hard to determine which dependencies should be added and which shouldn’t. The griffon configuration can make this decision for you; you just have to use it in the following way:
dependencies {
griffon 'org.codehaus.griffon.plugins:griffon-scaffolding-plugin:0.0.0-SNAPSHOT'
}
This will add all required dependencies to your build by taking into account the project’s
choice of UI toolkit and whether the groovy
plugin has been applied or not. This
behavior can be configured and/or disabled by using the conventional properties
described in the next section.
Convention properties
The Griffon plugin adds some properties to the project, which you can use to configure its behaviour.
Property name | Type | Default value | Description |
---|---|---|---|
disableDependencyResolution |
boolean |
false |
Disable automatic inclusion of dependencies defined with the griffon configuration. |
includeDefaultRepositories |
boolean |
true |
Force inclusion of default repositories ( |
includeGroovyDependencies |
boolean |
- |
Force inclusion of Groovy dependencies defined with the griffon configuration. |
toolkit |
String |
_ |
The UI toolkit to use. May be left unset. Valid values are |
version |
String |
2.15.0 |
The Griffon version to use for Griffon core dependencies. |
applicationIconName |
String |
griffon.icns |
The name of the application icon to display on OSX’s dock. Icon file must reside inside |
The includeGroovyDependencies property has 3 states: unset
, false
and true
.
Groovy dependencies will be added automatically to the project only if the value
of includeGroovyDependencies is unset
(default) and the groovy
plugin has been
applied to the project or if the value of includeGroovyDependencies is set to true
.
When the value of includeGroovyDependencies is set to false
then Groovy dependencies
will not be added, even if the groovy
plugin has been applied. This is useful for
Java projects that use Spock for testing, as you need the groovy
plugin in
order to compile Spock specifications but you wouldn’t want Groovy dependencies to
be pulled in for compilation.
15.3.2. The "griffon-build" Plugin
The Griffon Build plugin enables useful tasks required for plugin authors, such as generation of a plugin BOM file and more.
15.3.3. Dependencies
Whether you’re using the griffon
plugin or not, it’s very important that you take special
note of the dependencies ending with -compile
. As an application developer, these
dependencies belong to either compileOnly or testCompileOnly configurations, as these
dependencies contain functionality that should not be exposed at runtime, such as compile-time
metadata generation via Jipsy, Gipsy and other AST transformations.
The only reason for a -compile
dependency to appear on a compile or testCompile configuration
is for testing out new compile-time metadata generators. This task is usually performed in
plugin projects.
15.4. Maven
Application projects can also be built using Maven. The Lazybones
templates create a default pom.xml
file that contains the minimum configuration
to build, test and package a Griffon application. The bulk of the conventions are performed by
the application-master-pom.pom.
15.4.1. Plugins
The application-master-pom
configures the following plugins:
Group | ArtifactId | Version |
---|---|---|
org.codehaus.mojo |
appassembler-maven-plugin |
2.0.0 |
org.codehaus.mojo |
build-helper-maven-plugin |
3.0.0 |
org.eluder.coveralls |
coveralls-maven-plugin |
4.3.0 |
org.codehaus.mojo |
exec-maven-plugin |
1.6.0 |
org.codehaus.mojo |
findbugs-maven-plugin |
3.0.5 |
pl.project13.maven |
git-commit-id-plugin |
2.2.3 |
org.codehaus.gmavenplus |
gmavenplus-plugin |
1.6 |
org.jacoco |
jacoco-maven-plugin |
0.7.9 |
com.zenjava |
javafx-maven-plugin |
8.8.3 |
org.codehaus.mojo |
jdepend-maven-plugin |
2.0 |
org.apache.maven.plugins |
maven-antrun-plugin |
1.8 |
org.apache.maven.plugins |
maven-assembly-plugin |
3.1.0 |
org.apache.maven.plugins |
maven-changes-plugin |
2.12.1 |
org.apache.maven.plugins |
maven-checkstyle-plugin |
2.17 |
org.apache.maven.plugins |
maven-compiler-plugin |
3.7.0 |
org.apache.maven.plugins |
maven-dependency-plugin |
3.0.1 |
org.apache.maven.plugins |
maven-javadoc-plugin |
2.10.4 |
org.apache.maven.plugins |
maven-jxr-plugin |
2.5 |
org.bsc.maven |
maven-processor-plugin |
3.3.2 |
org.apache.maven.plugins |
maven-project-info-reports-plugin |
2.9 |
org.apache.maven.plugins |
maven-release-plugin |
2.5.3 |
org.apache.maven.plugins |
maven-shade-plugin |
3.1.0 |
org.apache.maven.plugins |
maven-site-plugin |
3.6 |
org.apache.maven.plugins |
maven-surefire-plugin |
2.20 |
org.apache.maven.plugins |
maven-surefire-report-plugin |
2.20 |
org.codehaus.mojo |
versions-maven-plugin |
2.4 |
Of which the following are applied by default:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.bsc.maven</groupId>
<artifactId>maven-processor-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
</plugins>
15.4.2. Dependencies
All Griffon core dependencies have been added using a <dependencyManagement>
block as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-core</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-core-compile</artifactId>
<version>${griffon.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-core-test</artifactId>
<version>${griffon.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-groovy</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-groovy-compile</artifactId>
<version>${griffon.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-swing</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-swing-groovy</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-fest-test</artifactId>
<version>${griffon.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-javafx</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-javafx-groovy</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-javafx-test</artifactId>
<version>${griffon.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-lanterna</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-lanterna-groovy</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-pivot</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-pivot-groovy</artifactId>
<version>${griffon.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-pivot-test</artifactId>
<version>${griffon.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.griffon</groupId>
<artifactId>griffon-guice</artifactId>
<version>${griffon.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>${spock.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
It’s very important that you take special note of the dependencies ending with -compile
. As an
application developer, these dependencies belong to the provided scope, since these
dependencies contain functionality that should not be exposed at runtime, such as
compile-time metadata generation via Jipsy, Gipsy and other AST transformations.
You must exclude these dependencies from the maven-surefire-plugin
. The following is the
default configuration provided by the master pom:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${plugin.surefire.version}</version>
<inherited>true</inherited>
<configuration>
<includes>
<include>**/*Test.*</include>
<include>**/*Spec.*</include>
</includes>
<classpathDependencyExcludes>
<classpathDependencyExclude>
org.codehaus.griffon:griffon-core-compile
</classpathDependencyExclude>
<classpathDependencyExclude>
org.codehaus.griffon:griffon-groovy-compile
</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration>
</plugin>
15.4.3. Profiles
The master pom enables a few profiles to take care of special tasks.
Run
This profile compiles and runs the application. Enable it with maven -Prun
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<profile>
<id>run</id>
<build>
<defaultGoal>process-classes</defaultGoal>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${plugin.exec.version}</version>
<inherited>true</inherited>
<configuration>
<mainClass>${application.main.class}</mainClass>
<systemProperties>
<systemProperty>
<key>griffon.env</key>
<value>dev</value>
</systemProperty>
</systemProperties>
</configuration>
<executions>
<execution>
<id>run-app</id>
<phase>process-classes</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
Binary
This profile packages the application using the maven-appassembler-plugin
.
Enable it with maven -Pbinary
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<profile>
<id>binary</id>
<build>
<defaultGoal>package</defaultGoal>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>appassembler-maven-plugin</artifactId>
<version>${plugin.appassembler.version}</version>
<configuration>
<assembleDirectory>${project.build.directory}/binary</assembleDirectory>
<repositoryLayout>flat</repositoryLayout>
<repositoryName>lib</repositoryName>
<extraJvmArguments>-Dgriffon.env=prod</extraJvmArguments>
<programs>
<program>
<mainClass>${application.main.class}</mainClass>
<id>${project.artifactId}</id>
</program>
</programs>
</configuration>
<executions>
<execution>
<id>make-distribution</id>
<phase>package</phase>
<goals>
<goal>assemble</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
Distribution
This profile packages the application using the maven-assembly-plugin
.
Enable it with maven -Pdistribution
. You must execute the binary
profile before running the distribution
profile. You CANNOT combine
both profiles.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<profile>
<id>distribution</id>
<build>
<defaultGoal>package</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${plugin.assembly.version}</version>
<configuration>
<descriptors>
<descriptor>maven/assembly-descriptor.xml</descriptor>
</descriptors>
<outputDirectory>${project.build.directory}/distributions</outputDirectory>
<workDirectory>${project.build.directory}/assembly/work</workDirectory>
</configuration>
<executions>
<execution>
<id>make-distribution</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
Izpack
This profile creates a universal installer using IzPack.
Enable it with maven -Pizpack
. You must execute the distribution
profile before running the izpack
profile. You CANNOT combine
both profiles.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<profile>
<id>izpack</id>
<dependencies>
<dependency>
<groupId>org.codehaus.izpack</groupId>
<artifactId>izpack-standalone-compiler</artifactId>
<version>${izpack-standalone.version}</version>
<optional>true</optional>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<defaultGoal>process-sources</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>${plugin.antrun.version}</version>
<executions>
<execution>
<id>process-sources</id>
<phase>process-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<ant antfile="maven/prepare-izpack.xml"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
Shade
This profile creates a single JAR by combining the application’s classes
and its dependencies. Enable it with maven -Pshade
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<profile>
<id>shade</id>
<build>
<defaultGoal>package</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${plugin.shade.version}</version>
<inherited>true</inherited>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>${application.main.class}</Main-Class>
</manifestEntries>
</transformer>
<transformer implementation="org.kordamp.shade.resources.ServicesResourceTransformer"/>
<transformer implementation="org.kordamp.shade.resources.ServicesResourceTransformer">
<path>META-INF/griffon</path>
</transformer>
<transformer implementation="org.kordamp.shade.resources.ServicesResourceTransformer">
<path>META-INF/types</path>
</transformer>
<transformer implementation="org.kordamp.shade.resources.PropertiesFileTransformer">
<paths>
<path>META-INF/editors/java.beans.PropertyEditor</path>
</paths>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.kordamp.shade</groupId>
<artifactId>maven-shade-ext-transformers</artifactId>
<version>${plugin.shade_ext_transformers.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</profile>
15.5. IntelliJ IDEA
There is no need to install a plugin in IntelliJ Idea in order to develop Griffon applications, as every Griffon project is a valid Gradle/Maven project.
You must also have Annotation Processing enabled in order for compile time annotations such
as @ArtifactProviderFor
to be picked up automatically. Open up Preferences and
navigate to Build, Execution Deployment > Compiler > Annotation Processors
There is special support for code suggestions when dealing with Groovy artifacts or Groovy classes annotated with a special set of annotations. This support is delivered using the GDSL feature found in IntelliJ. The following table summarizes the enhancements delivered by this feature:
Path | Type |
---|---|
griffon-app/controllers/**/*Controller.groovy |
griffon.core.artifact.GriffonController |
griffon-app/models/**/*Model.groovy |
griffon.core.artifact.GriffonModel |
griffon-app/services/**/*Service.groovy |
griffon.core.artifact.GriffonService |
griffon-app/views/**/*View.groovy |
griffon.core.artifact.GriffonView |
Annotation | Type |
---|---|
@griffon.transform.EventPublisher |
griffon.core.event.EventPublisher |
@griffon.transform.MVCAware |
griffon.core.mvc.MVCHandler |
@griffon.transform.ThreadingAware |
griffon.core.threading.ThreadingHandler |
@griffon.transform.ResourcesAware |
griffon.core.resources.ResourceHandler |
@griffon.transform.MessageSourceAware |
griffon.core.i18n.MessageSource |
@griffon.transform.ResourceResolverAware |
griffon.core.resources.ResourceResolver |
@griffon.transform.Observable |
griffon.core.Observable |
@griffon.transform.Vetoable |
griffon.core.Vetoable |
IntelliJ Community Edition has a plugin for developing Griffon 1.x applications. This plugin is not needed for Griffon 2.x. |
15.6. Eclipse
There is no need to install a plugin in Eclipse in order to develop Griffon applications, as every Griffon project is a valid Gradle/Maven project.
You must install the Gradle Buildship and Groovy plugins from the marketplace.
There is special support for code suggestions when dealing with Groovy artifacts or Groovy classes annotated with a special set of annotations. This support is delivered using the DSLD feature found in Eclipse if the Groovy Eclipse plugin is installed. The following table summarizes the enhancements delivered by this feature:
Path | Type |
---|---|
griffon-app/controllers/**/*Controller.groovy |
griffon.core.artifact.GriffonController |
griffon-app/models/**/*Model.groovy |
griffon.core.artifact.GriffonModel |
griffon-app/services/**/*Service.groovy |
griffon.core.artifact.GriffonService |
griffon-app/views/**/*View.groovy |
griffon.core.artifact.GriffonView |
Annotation | Type |
---|---|
@griffon.transform.EventPublisher |
griffon.core.event.EventPublisher |
@griffon.transform.MVCAware |
griffon.core.mvc.MVCHandler |
@griffon.transform.ThreadingAware |
griffon.core.threading.ThreadingHandler |
@griffon.transform.ResourcesAware |
griffon.core.resources.ResourceHandler |
@griffon.transform.MessageSourceAware |
griffon.core.i18n.MessageSource |
@griffon.transform.ResourceResolverAware |
griffon.core.resources.ResourceResolver |
@griffon.transform.Observable |
griffon.core.Observable |
@griffon.transform.Vetoable |
griffon.core.Vetoable |
Finally, Annotation Processing must be manually enabled. You must do this on a per project basis. Search for Annotation Processing in the project’s properties and tick the checkbox to activate this option.
You must also define every single JAR file that provides APT processors. The most basic
ones are jipsy
and griffon-core-compile
. These JARs are found in your build tools'
cache and/or local repository.
As a rule, all griffon-*-compile
JARs provide APT processors and AST transformations.
16. Contributing
Griffon is an open source project with an active community, and we rely heavily on that community to help make Griffon better. As such, there are various ways in which people can contribute to Griffon. One of these is by writing plugins and making them publicly available. In this chapter, we’ll look at some of the other options.
16.1. Issues
Griffon uses Github Issues to track issues in the core framework, its documentation and its website. If you’ve found a bug or wish to see a particular feature added, this is the place to start. You’ll need to create a (free) Github account in order to either submit an issue or comment on an existing one.
When submitting issues, please provide as much information as possible; and in the case of bugs, make sure you explain which versions of Griffon and various plugins you are using. Also, an issue is much more likely to be dealt with if you attach a reproducible sample application.
16.1.1. Reviewing issues
There may be a few old issues, some of which may no longer be valid. The core team can’t track down these alone, so a very simple contribution that you can make is to verify one or two issues occasionally.
Which issues need verification? Just pick an open issue and check whether it’s still relevant.
Once you’ve verified an issue, simply edit it by adding a "Last Reviewed" comment. If you think the issue can be closed, then also add a "Flagged" comment and a short explanation why.
16.2. Build
If you’re interested in contributing fixes and features to the core framework, you will have to learn how to get hold of the project’s source, build it, and test it with your own applications. Before you start, make sure you have:
-
A JDK (1.7 or above)
-
A git client
Once you have all the prerequisite packages installed, the next step is to download
the Griffon source code, which is hosted at GitHub in several
repositories owned by the griffon
GitHub user. This
is a simple case of cloning the repository you’re interested in. For example, to
get the core framework run:
$ git clone https://github.com/griffon/griffon.git
This will create a griffon
directory in your current working directory containing
all the project source files. The next step is to get a Griffon installation from the source.
16.2.1. Running the test suite
All you have to do to run the full suite of tests is:
$ ./gradlew test
These will take a while, so consider running individual tests using the command line. For example, to run the test case @MappingDslTests@ simply execute the following command:
$ ./gradlew -Dtest.single=EnvironmentTests :griffon-core:test
Note that you need to specify the sub-project that the test case resides in, because the top-level "test" target won’t work….
16.3. Patches
If you want to submit patches to the project, you simply need to fork the repository on GitHub rather than clone it directly. Then you will commit your changes to your fork and send a pull request for a core team member to review.
16.3.1. Forking and Pull Requests
One of the benefits of GitHub is the way that you can easily contribute to a project by forking the repository and sending pull requests with your changes.
What follows are some guidelines to help ensure that your pull requests are speedily dealt with and provide the information we need. They will also make your life easier!
Create a local branch for your changes
Your life will be greatly simplified if you create a local branch to make your changes on. For example, as soon as you fork a repository and clone the fork locally, execute
$ git checkout -b mine
This will create a new local branch called mine
based off the master
branch.
Of course, you can name the branch whatever you like - you don’t have to use mine
.
Create an Issue for non-trivial changes
For any non-trivial changes, raise an issue if one doesn’t already exist. That helps us keep track of what changes go into each new version of Griffon.
Include Issue ID in commit messages
This may not seem particularly important, but having an issue ID in a commit message means that we can find out at a later date why a change was made. Include the ID in any and all commits that relate to that issue. If a commit isn’t related to an issue, then there’s no need to include an issue ID.
Make sure your fork is up to date
Since the core developers must merge your commits into the main repository, it makes life much easier if your fork on GitHub is up to date before you send a pull request.
Let’s say you have the main repository set up as a remote called upstream
and you
want to submit a pull request. Also, all your changes are currently on the local mine
branch but not on master
. The first step involves pulling any changes from the main
repository that have been added since you last fetched and merged:
$ git checkout master
$ git pull upstream
This should complete without any problems or conflicts. Next, rebase your local branch against the now up-to-date master:
$ git checkout mine
$ git rebase master
What this does is rearrange the commits such that all of your changes come after the most recent one in master. Think adding some cards to the top of a deck rather than shuffling them into the pack.
You’ll now be able to do a clean merge from your local branch to master:
$ git checkout master
$ git merge mine
Finally, you must push your changes to your remote repository on GitHub, otherwise the core developers won’t be able to pick them up:
$ git push
You’re now ready to send the pull request from the GitHub user interface.
Say what your pull request is for
A pull request can contain any number of commits and it may be related to any number of issues. In the pull request message, please specify the IDs of all issues that the request relates to. Also give a brief description of the work you have done, such as: "I refactored the resources injector and added support for custom number editors (GRIFFON-xxxx)".
Appendix A: Migrating from Griffon 1.x
Griffon 2.x has tried to remain as similar as possible to Griffon 1.x in terms of concepts and APIs; however, some changes were introduced which require your attention when migrating an application from Griffon 1.x.
A.1. Build Configuration
There is no longer a specific Griffon buildtime tool nor configuration settings. You must pick a build tool (we recommend Gradle) and use that tool’s configuration to match your needs. The simplest way to get started is to select an appropriate Lazybones template to create an empty project, and then copy the files you need.
Build time plugins and scripts are now within the realm of a particular build tool.
A.1.1. Dependencies
Dependencies used to be configured inside griffon-app/conf/BuildConfig.groovy
. Now
that the file is gone, you must configure dependencies using the native support of
the build tool you choose. Of particular note is that all griffon dependencies are
now standard JAR archives available from Maven compatible repositories, so they should
work no matter which dependency resolution you pick.
A.2. Runtime Configuration
A.2.1. Application and Config Scripts
The files Application.groovy
and Config.groovy
have been merged into a single
file: Config.groovy
. The log4j
DSL is no longer used, so please move your
logging settings to src/resources/log4j.properties
; you can also use the XML variant
if you want. Griffon 2.x does not force Log4j on you either; you’re free to pick
a suitable Slf4j binding of your choice.
A.2.2. Builder Script
The Builder.groovy
is no longer required. Its functions are now handled by
BuilderCustomizer
classes bound in a Module
. You must add
griffon-groovy-2.15.0.jar
as a compile dependency in order to use
BuilderCustomizers
. Here’s an example for adding a Miglayout
customization:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package griffon.builder.swing;
import griffon.inject.DependsOn;
import groovy.swing.factory.LayoutFactory;
import net.miginfocom.swing.MigLayout;
import groovy.util.Factory;
import org.codehaus.griffon.runtime.groovy.view.AbstractBuilderCustomizer;
import javax.inject.Named;
import java.util.LinkedHashMap;
import java.util.Map;
@DependsOn({"swing"})
@Named("miglayout-swing")
public class MiglayoutSwingBuilderCustomizer extends AbstractBuilderCustomizer {
public MiglayoutSwingBuilderCustomizer() {
Map<String, Factory> factories = new LinkedHashMap<>();
factories.put("migLayout", new LayoutFactory(MigLayout.class));
setFactories(factories);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.codehaus.griffon.runtime.miglayout;
import griffon.builder.swing.MiglayoutSwingBuilderCustomizer;
import griffon.core.injection.Module;
import griffon.inject.DependsOn;
import griffon.util.BuilderCustomizer;
import org.codehaus.griffon.runtime.core.injection.AbstractModule;
import org.kordamp.jipsy.ServiceProviderFor;
import javax.inject.Named;
@DependsOn("swing-groovy")
@Named("miglayout-swing-groovy")
@ServiceProviderFor(Module.class)
public class MiglayoutSwingGroovyModule extends AbstractModule {
@Override
protected void doConfigure() {
// tag::bindings[]
bind(BuilderCustomizer.class)
.to(MiglayoutSwingBuilderCustomizer.class)
.asSingleton();
// end::bindings[]
}
}
A.2.3. Events Script
The Events
script usually placed in griffon-app/conf
should be moved to the main
source directory (src/main/java
or src/main/groovy
depending on your preferences).
The following snippet shows a skeleton implementation:
1
2
3
4
5
6
7
package com.acme
import griffon.core.event.EventHandler
class ApplicationEventHandler implements EventHandler {
// event handlers as public methods
}
You must add event handlers as public methods following the conventions explained
in the consuming events section. Don’t forget to register this
class using a module. The default ApplicationModule
class provided by all basic
project templates is a good start.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.acme
import griffon.core.event.EventHandler
import griffon.core.injection.Module
import org.codehaus.griffon.runtime.core.injection.AbstractModule
import org.kordamp.jipsy.ServiceProviderFor
@ServiceProviderFor(Module)
class ApplicationModule extends AbstractModule {
@Override
protected void doConfigure() {
bind(EventHandler)
.to(ApplicationEventHandler)
.asSingleton()
}
}
A.3. Artifacts
All artifacts must be annotated with @ArtifactProviderFor
without exception.
Failure to follow this rule will make Griffon miss the artifact during bootstrap.
The value for this annotation must be the basic interface that defines the artifact’s
type, for example:
1
2
3
4
5
6
7
8
9
package sample
import griffon.core.artifact.GriffonModel
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class SampleModel {
...
}
Additionally, the app
property has been renamed to application
.
A.3.1. Controllers
Closure properties as actions are no longer supported. All actions must be defined as public methods.
A.3.2. Views
View scripts have been upgraded to classes. You can use the following skeleton View class as an starting point:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package sample
import griffon.core.artifact.GriffonView
import griffon.metadata.ArtifactProviderFor
import griffon.inject.MVCMember
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull def builder
@MVCMember @Nonnull def model
void initUI() {
builder.with {
(1)
}
}
}
1 | UI components |
Next, place the contents of your old View script inside 1.
A.4. Lifecycle Scripts
These scripts must also be migrated to full classes. Here’s the basic skeleton code for any lifecycle handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import griffon.core.GriffonApplication
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import javax.annotation.Nonnull
import javax.inject.Inject
class Initialize extends AbstractLifecycleHandler {
@Inject
Initialize(@Nonnull GriffonApplication application) {
super(application)
}
@Override
void execute() {
// do the work
}
}
A.5. Renamed Methods
The following tables describe methods that have been renamed in Griffon 2.x:
Griffon 1.x | Griffon 2.x |
---|---|
edt |
runInsideUISync |
doLater |
runInsideUIAsync |
doOutside |
runOutsideUI |
execInsideUISync |
runInsideUISync |
execInsideUIAsync |
runInsideUIAsync |
execOutsideUI |
runOutsideUI |
Griffon 1.x | Griffon 2.x |
---|---|
publish |
publishEvent |
publishAsync |
publishEventAsync |
publishOutsideUI |
publishEventOutsideUI |
event |
publishEvent |
eventAsync |
publishEventAsync |
eventOutsideUI |
publishEventOutsideUI |
A.6. Tests
Griffon 2.x no longer segregates tests between unit
and functional
. You must use
your build tool’s native support for both types (this is quite simple with Gradle).
Move all unit tests under src/test/java
or src/test/groovy
depending on your
choice of main language. The base GriffonUnitTestCase
class no longer exists. Use
any testing framework you’re comfortable with to write unit tests (Junit4, Spock, etc).
Use the following template if you need to write artifact tests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package sample
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.awaitility.Awaitility.await
import static java.util.concurrent.TimeUnit.SECONDS
@TestFor(SampleController)
class SampleControllerTest {
private SampleController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void testControllerAction() {
// given:
// setup collaborators
// when:
// stimulus
controller.invokeAction('nameOfTheAction')
// then:
// use Awaitility to successfully wait for async processing to finish
await().atMost(2, SECONDS)
assert someCondition
}
}
Appendix B: Module Bindings
The following sections display all bindings per module. Use this information to successfully override a binding on your own modules or to troubleshoot a module binding if the wrong type has been applied by the Griffon runtime.
B.1. Core
Module name: core
bind(ApplicationClassLoader.class)
.to(DefaultApplicationClassLoader.class)
.asSingleton();
bind(Metadata.class)
.toProvider(MetadataProvider.class)
.asSingleton();
bind(RunMode.class)
.toProvider(RunModeProvider.class)
.asSingleton();
bind(Environment.class)
.toProvider(EnvironmentProvider.class)
.asSingleton();
bind(ContextFactory.class)
.to(DefaultContextFactory.class)
.asSingleton();
bind(Instantiator.class)
.to(DefaultInstantiator.class)
.asSingleton();
bind(PropertiesReader.class)
.toProvider(PropertiesReader.Provider.class)
.asSingleton();
bind(ResourceBundleReader.class)
.toProvider(ResourceBundleReader.Provider.class)
.asSingleton();
bind(Context.class)
.withClassifier(named("applicationContext"))
.toProvider(DefaultContextProvider.class)
.asSingleton();
bind(ApplicationConfigurer.class)
.to(DefaultApplicationConfigurer.class)
.asSingleton();
bind(ResourceHandler.class)
.to(DefaultResourceHandler.class)
.asSingleton();
bind(ResourceBundleLoader.class)
.to(ClassResourceBundleLoader.class)
.asSingleton();
bind(ResourceBundleLoader.class)
.to(PropertiesResourceBundleLoader.class)
.asSingleton();
bind(ResourceBundleLoader.class)
.to(XmlResourceBundleLoader.class)
.asSingleton();
bind(CompositeResourceBundleBuilder.class)
.to(DefaultCompositeResourceBundleBuilder.class)
.asSingleton();
bind(ResourceBundle.class)
.withClassifier(named("applicationResourceBundle"))
.toProvider(new ResourceBundleProvider("Config"))
.asSingleton();
bind(ConfigurationManager.class)
.to(DefaultConfigurationManager.class)
.asSingleton();
bind(ConfigurationDecoratorFactory.class)
.to(DefaultConfigurationDecoratorFactory.class);
bind(Configuration.class)
.toProvider(ResourceBundleConfigurationProvider.class)
.asSingleton();
bind(ExecutorServiceManager.class)
.to(DefaultExecutorServiceManager.class)
.asSingleton();
bind(EventRouter.class)
.withClassifier(named("applicationEventRouter"))
.to(DefaultEventRouter.class)
.asSingleton();
bind(EventRouter.class)
.to(DefaultEventRouter.class);
bind(ResourceResolverDecoratorFactory.class)
.to(DefaultResourceResolverDecoratorFactory.class);
bind(MessageSourceDecoratorFactory.class)
.to(DefaultMessageSourceDecoratorFactory.class);
bind(ResourceResolver.class)
.withClassifier(named("applicationResourceResolver"))
.toProvider(new ResourceResolverProvider("resources"))
.asSingleton();
bind(MessageSource.class)
.withClassifier(named("applicationMessageSource"))
.toProvider(new MessageSourceProvider("messages"))
.asSingleton();
bind(ResourceInjector.class)
.withClassifier(named("applicationResourceInjector"))
.to(DefaultApplicationResourceInjector.class)
.asSingleton();
bind(ExecutorService.class)
.withClassifier(named("defaultExecutorService"))
.toProvider(DefaultExecutorServiceProvider.class)
.asSingleton();
bind(UIThreadManager.class)
.to(DefaultUIThreadManager.class)
.asSingleton();
bind(MVCGroupConfigurationFactory.class)
.to(DefaultMVCGroupConfigurationFactory.class)
.asSingleton();
bind(MVCGroupFactory.class)
.to(DefaultMVCGroupFactory.class)
.asSingleton();
bind(MVCGroupManager.class)
.to(DefaultMVCGroupManager.class)
.asSingleton();
for (Lifecycle lifecycle : Lifecycle.values()) {
bind(LifecycleHandler.class)
.withClassifier(named(lifecycle.getName()))
.toProvider(new LifecycleHandlerProvider(lifecycle.getName()))
.asSingleton();
}
bind(WindowManager.class)
.to(NoopWindowManager.class)
.asSingleton();
bind(ActionManager.class)
.to(DefaultActionManager.class)
.asSingleton();
bind(ActionFactory.class)
.to(DefaultActionFactory.class)
.asSingleton();
bind(ActionMetadataFactory.class)
.to(DefaultActionMetadataFactory.class)
.asSingleton();
bind(ArtifactManager.class)
.to(DefaultArtifactManager.class)
.asSingleton();
bind(ArtifactHandler.class)
.to(ModelArtifactHandler.class)
.asSingleton();
bind(ArtifactHandler.class)
.to(ViewArtifactHandler.class)
.asSingleton();
bind(ArtifactHandler.class)
.to(ControllerArtifactHandler.class)
.asSingleton();
bind(ArtifactHandler.class)
.to(ServiceArtifactHandler.class)
.asSingleton();
bind(PlatformHandler.class)
.toProvider(PlatformHandlerProvider.class)
.asSingleton();
bind(AddonManager.class)
.to(DefaultAddonManager.class)
.asSingleton();
bind(EventHandler.class)
.to(DefaultEventHandler.class)
.asSingleton();
bind(ExceptionHandler.class)
.toProvider(GriffonExceptionHandlerProvider.class)
.asSingleton();
B.2. Groovy
Module name: groovy
bind(ConfigReader.class)
.toProvider(ConfigReader.Provider.class)
.asSingleton();
bind(ResourceBundleLoader.class)
.to(GroovyScriptResourceBundleLoader.class)
.asSingleton();
bind(GriffonAddon.class)
.to(GroovyAddon.class)
.asSingleton();
bind(EventRouter.class)
.withClassifier(named("applicationEventRouter"))
.to(GroovyAwareDefaultEventRouter.class)
.asSingleton();
bind(EventRouter.class)
.to(GroovyAwareDefaultEventRouter.class);
bind(MVCGroupFactory.class)
.to(GroovyAwareMVCGroupFactory.class)
.asSingleton();
bind(MVCGroupManager.class)
.to(GroovyAwareMVCGroupManager.class)
.asSingleton();
bind(BuilderCustomizer.class)
.to(CoreBuilderCustomizer.class)
.asSingleton();
bind(ResourceResolverDecoratorFactory.class)
.to(GroovyAwareResourceResolverDecoratorFactory.class);
bind(MessageSourceDecoratorFactory.class)
.to(GroovyAwareMessageSourceDecoratorFactory.class);
B.3. Swing
Module name: swing
bind(SwingWindowDisplayHandler.class)
.withClassifier(named("defaultWindowDisplayHandler"))
.to(DefaultSwingWindowDisplayHandler.class)
.asSingleton();
bind(SwingWindowDisplayHandler.class)
.withClassifier(named("windowDisplayHandler"))
.to(ConfigurableSwingWindowDisplayHandler.class)
.asSingleton();
bind(WindowManager.class)
.to(DefaultSwingWindowManager.class)
.asSingleton();
bind(UIThreadManager.class)
.to(SwingUIThreadManager.class)
.asSingleton();
bind(ActionManager.class)
.to(SwingActionManager.class)
.asSingleton();
bind(ActionFactory.class)
.to(SwingActionFactory.class)
.asSingleton();
bind(GriffonAddon.class)
.to(SwingAddon.class)
.asSingleton();
B.4. Swing Builder
Module name: swing-groovy
Depends on: swing
bind(BuilderCustomizer.class)
.to(SwingBuilderCustomizer.class)
.asSingleton();
bind(SwingWindowDisplayHandler.class)
.withClassifier(named("windowDisplayHandler"))
.to(GroovyAwareConfigurableSwingWindowDisplayHandler.class)
.asSingleton();
B.5. JavaFX
Module name: javafx
bind(JavaFXWindowDisplayHandler.class)
.withClassifier(named("defaultWindowDisplayHandler"))
.to(DefaultJavaFXWindowDisplayHandler.class)
.asSingleton();
bind(JavaFXWindowDisplayHandler.class)
.withClassifier(named("windowDisplayHandler"))
.to(ConfigurableJavaFXWindowDisplayHandler.class)
.asSingleton();
bind(WindowManager.class)
.to(DefaultJavaFXWindowManager.class)
.asSingleton();
bind(UIThreadManager.class)
.to(JavaFXUIThreadManager.class)
.asSingleton();
bind(ActionManager.class)
.to(JavaFXActionManager.class)
.asSingleton();
bind(ActionFactory.class)
.to(JavaFXActionFactory.class)
.asSingleton();
bind(ActionMatcher.class)
.toInstance(ActionMatcher.DEFAULT);
B.6. JavaFX Builder
Module name: javafx-groovy
Depends on: javafx
bind(BuilderCustomizer.class)
.to(JavafxBuilderCustomizer.class)
.asSingleton();
bind(JavaFXWindowDisplayHandler.class)
.withClassifier(named("windowDisplayHandler"))
.to(GroovyAwareConfigurableJavaFXWindowDisplayHandler.class)
.asSingleton();
B.7. Lanterna
Module name: lanterna
bind(GUIScreen.class)
.toProvider(GUIScreenProvider.class)
.asSingleton();
bind(LanternaWindowDisplayHandler.class)
.withClassifier(named("defaultWindowDisplayHandler"))
.to(DefaultLanternaWindowDisplayHandler.class)
.asSingleton();
bind(LanternaWindowDisplayHandler.class)
.withClassifier(named("windowDisplayHandler"))
.to(ConfigurableLanternaWindowDisplayHandler.class)
.asSingleton();
bind(WindowManager.class)
.to(DefaultLanternaWindowManager.class)
.asSingleton();
bind(UIThreadManager.class)
.to(LanternaUIThreadManager.class)
.asSingleton();
bind(ActionManager.class)
.to(LanternaActionManager.class)
.asSingleton();
bind(ActionFactory.class)
.to(LanternaActionFactory.class)
.asSingleton();
B.8. Lanterna Builder
Module name: lanterna-groovy
Depends on: lanterna
bind(BuilderCustomizer.class)
.to(LanternaBuilderCustomizer.class)
.asSingleton();
bind(LanternaWindowDisplayHandler.class)
.withClassifier(named("windowDisplayHandler"))
.to(GroovyAwareConfigurableLanternaWindowDisplayHandler.class)
.asSingleton();
B.9. Pivot
Module name: pivot
bind(PivotWindowDisplayHandler.class)
.withClassifier(named("defaultWindowDisplayHandler"))
.to(DefaultPivotWindowDisplayHandler.class)
.asSingleton();
bind(PivotWindowDisplayHandler.class)
.withClassifier(named("windowDisplayHandler"))
.to(ConfigurablePivotWindowDisplayHandler.class)
.asSingleton();
bind(WindowManager.class)
.to(DefaultPivotWindowManager.class)
.asSingleton();
bind(UIThreadManager.class)
.to(PivotUIThreadManager.class)
.asSingleton();
bind(ActionManager.class)
.to(PivotActionManager.class)
.asSingleton();
bind(ActionFactory.class)
.to(PivotActionFactory.class)
.asSingleton();
B.10. Pivot Builder
Module name: pivot-groovy
Depends on: pivot
bind(BuilderCustomizer.class)
.to(PivotBuilderCustomizer.class)
.asSingleton();
bind(PivotWindowDisplayHandler.class)
.withClassifier(named("windowDisplayHandler"))
.to(GroovyAwareConfigurablePivotWindowDisplayHandler.class)
.asSingleton();
Appendix C: AST Transformations
The following list summarizes all AST transformations available to Groovy-based
projects when the griffon-groovy-compile-2.15.0.jar
dependency is
added to a project:
Appendix D: System Properties
The following list summarizes all System properties that the Griffon runtime may use
Property | Chapter |
---|---|
|
|
|
|
|
|
|
Appendix E: Sample Applications
This appendix showcases the same application implemented with different languages
and different UI toolkits. The application presents a very simple form where a user
is asked for his or her name. Once a button is clicked a reply will appear within
the same window. In order to achieve this, Models hold 2 observable properties: the
first to keep track of the input
, the second to do the same for the output
.
Views are only concerned with values coming from the model and as such never
interact directly with Controllers. Controllers in turn only interact with Models
and a Service used to transform the input value into the output value. The single
controller action observes the rules for invoking computations outside of the UI
thread and updating UI components inside the UI thread.
These are some screenshots of each of the applications we’ll cover next.
The goal of these applications is to showcase the similarities and differences of each of them given their implementation language and UI toolkit.
E.1. Swing
Let’s begin with Swing, as it’s probably the most well known Java UI toolkit. First we’ll show the Java version of an artifact, then we’ll show its Groovy counterpart.
E.1.1. Model
Instances of GriffonModel
implement the Observable
interface which
means they know how to handle observable properties out of the box. We only need to be
concerned about triggering a java.beans.PropertyChangeEvent
when a property changes value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package sample.swing.java;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;
@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
private String input; (1)
private String output; (1)
public String getInput() { (2)
return input;
}
public void setInput(String input) {
firePropertyChange("input", this.input, this.input = input); (3)
}
public String getOutput() { (2)
return output;
}
public void setOutput(String output) {
firePropertyChange("output", this.output, this.output = output); (3)
}
}
1 | Define a private field for the property |
2 | Property accessor |
3 | Property mutator must fire a PropertyChangeEvent |
The code is quite straightforward; there’s nothing much to see here other than making sure to follow the rules for creating observable properties. The Groovy version sports a short hand thanks to the usage of the @Observable AST transformation.
One key difference between the Java and the Groovy version is that the Groovy Model
does not extend a particular class. This is due to Griffon being aware of its own
conventions and applying the appropriate byte code manipulation (via AST transformations).
The compiled Model class does implement the GriffonModel
interface as required by
the framework. This type of byte code manipulation is expected to work for every Groovy
based artifact.
1
2
3
4
5
6
7
8
9
10
11
package sample.swing.groovy
import griffon.core.artifact.GriffonModel
import griffon.metadata.ArtifactProviderFor
import griffon.transform.Observable
@ArtifactProviderFor(GriffonModel)
class SampleModel {
@Observable String input (1)
@Observable String output (1)
}
1 | Observable property |
Properties become observable by simply annotating them with @Observable. The Groovy compiler will generate the required boilerplate code, which just so happens to be functionally equivalent to what we showed in the Java version.
E.1.2. Controller
Controllers provide actions that are used to fill up the application’s interaction.
They usually manipulate values coming from Views via Model properties. Controllers may
rely on additional components, such as Services, to do they work. This is exactly our
case, as there’s a SampleService
instance injected into our controllers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package sample.swing.java;
import griffon.core.artifact.GriffonController;
import griffon.core.controller.ControllerAction;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javax.annotation.Nonnull;
import javax.inject.Inject;
@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
private SampleModel model; (1)
@Inject
private SampleService sampleService; (2)
@MVCMember
public void setModel(@Nonnull SampleModel model) {
this.model = model;
}
@ControllerAction
public void sayHello() { (3)
final String result = sampleService.sayHello(model.getInput());
runInsideUIAsync(new Runnable() { (4)
@Override
public void run() {
model.setOutput(result);
}
});
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Automatically run off the UI thread |
4 | Get back inside the UI thread |
Of particular note is the fact that actions are always executed outside of the UI thread
unless otherwise configured with an @Threading
annotation. Once we have computed the right
output, we must inform the View of the new value. This is done by updating the model
inside the UI thread 4.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package sample.swing.groovy
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull
SampleModel model (1)
@Inject
private SampleService sampleService (2)
@ControllerAction
void sayHello() { (3)
String result = sampleService.sayHello(model.input)
model.output = result (4)
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Automatically run off the UI thread |
4 | Get back inside the UI thread |
The Groovy version of the Controller is much terser, of course. However, there’s a nice feature available to Groovy Swing: Model properties bound to UI components are always updated inside the UI thread.
E.1.3. Service
Services are tasked to work with raw data and I/O; they should never interact with
Views and Models directly, though you may have additional components injected to them.
The following service shows another facility provide by the
GriffonApplication
interface:
MessageSource
, capable of resolving i18n-able
resources.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package sample.swing.java;
import griffon.core.artifact.GriffonService;
import griffon.core.i18n.MessageSource;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonService;
import static griffon.util.GriffonNameUtils.isBlank;
import static java.util.Collections.singletonList;
@javax.inject.Singleton
@ArtifactProviderFor(GriffonService.class)
public class SampleService extends AbstractGriffonService {
public String sayHello(String input) {
MessageSource messageSource = getApplication().getMessageSource();
if (isBlank(input)) {
return messageSource.getMessage("greeting.default");
} else {
return messageSource.getMessage("greeting.parameterized", singletonList(input));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package sample.swing.groovy
import griffon.core.artifact.GriffonService
import griffon.core.i18n.MessageSource
import griffon.metadata.ArtifactProviderFor
import static griffon.util.GriffonNameUtils.isBlank
@javax.inject.Singleton
@ArtifactProviderFor(GriffonService)
class SampleService {
String sayHello(String input) {
MessageSource ms = application.messageSource
isBlank(input) ? ms.getMessage('greeting.default') : ms.getMessage('greeting.parameterized', [input])
}
}
E.1.4. View
We come to the final piece of the puzzle: the View. Components are arranged in a one column vertical grid:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package sample.swing.java;
import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.swing.artifact.AbstractSwingGriffonView;
import javax.annotation.Nonnull;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Toolkit;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;
import static java.util.Arrays.asList;
import static javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractSwingGriffonView {
private SampleController controller; (1)
private SampleModel model; (1)
@MVCMember
public void setController(@Nonnull SampleController controller) {
this.controller = controller;
}
@MVCMember
public void setModel(@Nonnull SampleModel model) {
this.model = model;
}
@Override
public void initUI() {
JFrame window = (JFrame) getApplication()
.createApplicationContainer(Collections.<String, Object>emptyMap());
window.setName("mainWindow");
window.setTitle(getApplication().getConfiguration().getAsString("application.title"));
window.setSize(320, 120);
window.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
window.setIconImage(getImage("/griffon-icon-48x48.png"));
window.setIconImages(asList(
getImage("/griffon-icon-48x48.png"),
getImage("/griffon-icon-32x32.png"),
getImage("/griffon-icon-16x16.png")
));
getApplication().getWindowManager().attach("mainWindow", window); (2)
window.getContentPane().setLayout(new GridLayout(4, 1));
window.getContentPane().add(
new JLabel(getApplication().getMessageSource().getMessage("name.label"))
);
final JTextField nameField = new JTextField();
nameField.setName("inputField");
nameField.getDocument().addDocumentListener(new DocumentListener() { (3)
@Override
public void insertUpdate(DocumentEvent e) {
model.setInput(nameField.getText());
}
@Override
public void removeUpdate(DocumentEvent e) {
model.setInput(nameField.getText());
}
@Override
public void changedUpdate(DocumentEvent e) {
model.setInput(nameField.getText());
}
});
window.getContentPane().add(nameField);
Action action = toolkitActionFor(controller, "sayHello"); (4)
final JButton button = new JButton(action);
button.setName("sayHelloButton");
window.getContentPane().add(button);
final JLabel outputLabel = new JLabel();
outputLabel.setName("outputLabel");
model.addPropertyChangeListener("output", new PropertyChangeListener() { (3)
@Override
public void propertyChange(PropertyChangeEvent evt) {
outputLabel.setText(String.valueOf(evt.getNewValue()));
}
});
window.getContentPane().add(outputLabel);
}
private Image getImage(String path) {
return Toolkit.getDefaultToolkit().getImage(SampleView.class.getResource(path));
}
}
1 | MVC member injected by MVCGroupManager |
2 | Attach window to WindowManager |
3 | Apply component-to-model binding |
4 | Hook in controller action by name |
Here we can appreciate at 3 how Model properties are bound to View components, and also how controller actions can be transformed into toolkit actions that may be applied to buttons 4 for example.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package sample.swing.groovy
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull
FactoryBuilderSupport builder (1)
@MVCMember @Nonnull
SampleModel model (1)
void initUI() {
builder.with {
application(title: application.configuration['application.title'], (2)
id: 'mainWindow', size: [320, 160],
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]) {
gridLayout(rows: 4, cols: 1)
label(application.messageSource.getMessage('name.label'))
textField(id: 'inputField', text: bind(target: model, 'input')) (3)
button(sayHelloAction, id: 'sayHelloButton') (4)
label(id: 'outputLabel', text: bind { model.output }) (3)
}
}
}
}
1 | MVC member injected by MVCGroupManager |
2 | Create window and attach it to WindowManager |
3 | Apply component-to-model binding |
4 | Hook in controller action by name |
The Groovy version is again much terser thanks to the SwingBuilder DSL.
Notice how easy it is to bind 3 model properties using the bind
node.
The controller action is also transformed into a UI toolkit specific action; however,
this time it’s easier to grab: by convention all controller actions are exposed as variables
to the corresponding builder
.
E.1.5. Resources
The last file we’ll touch is the one that holds the i18n-able content. Griffon supports several formats. Here we’re showing the standard one as found in many Java projects.
1
2
3
name.label = Please enter your name
greeting.default = Howdy stranger!
greeting.parameterized = Hello {0}
E.1.6. Statistics
The following statistics show the number of lines per artifact required by both applications:
+---------------------------------+--------+--------+
| Name | Files | LOC |
+---------------------------------+--------+--------+
| Configuration | 1 | 24 |
| Controllers | 1 | 28 |
| Groovy Integration Test Sources | 1 | 31 |
| Java Integration Test Sources | 1 | 27 |
| Java Sources | 3 | 52 |
| Java Test Sources | 1 | 48 |
| Lifecycle | 1 | 30 |
| Models | 1 | 21 |
| Properties | 3 | 5 |
| Services | 1 | 19 |
| Views | 1 | 87 |
+---------------------------------+--------+--------+
| Totals | 15 | 372 |
+---------------------------------+--------+--------+
+---------------------------------+--------+--------+
| Name | Files | LOC |
+---------------------------------+--------+--------+
| Configuration | 1 | 13 |
| Controllers | 1 | 19 |
| Groovy Integration Test Sources | 2 | 58 |
| Groovy Sources | 3 | 51 |
| Groovy Test Sources | 1 | 47 |
| Lifecycle | 1 | 17 |
| Models | 1 | 9 |
| Properties | 3 | 5 |
| Services | 1 | 13 |
| Views | 1 | 28 |
+---------------------------------+--------+--------+
| Totals | 15 | 260 |
+---------------------------------+--------+--------+
E.2. JavaFX
JavaFX is a next generation UI toolkit and will eventually replace Swing, so it’s a good idea to get started with it now. Among the several features found in JavaFX you’ll find
-
observable properties
-
component styling with CSS
-
FXML: a declarative format for defining UIs
-
and more!
E.2.1. Model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package sample.javafx.java;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;
import javax.annotation.Nonnull;
@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
private StringProperty input; (1)
private StringProperty output; (1)
@Nonnull
public final StringProperty inputProperty() { (2)
if (input == null) {
input = new SimpleStringProperty(this, "input");
}
return input;
}
public void setInput(String input) { (3)
inputProperty().set(input);
}
public String getInput() { (3)
return input == null ? null : inputProperty().get();
}
@Nonnull
public final StringProperty outputProperty() { (2)
if (output == null) {
output = new SimpleStringProperty(this, "output");
}
return output;
}
public void setOutput(String output) { (3)
outputProperty().set(output);
}
public String getOutput() { (3)
return output == null ? null : outputProperty().get();
}
}
1 | Define a private field for the property |
2 | Property accessor |
3 | Pass-thru values to Property |
The Model makes use of JavaFX’s observable properties. These properties are roughly equivalent in behavior to the ones we saw back in the Swing example; however, they provide much more behavior than that; values may be cached automatically, for example, avoiding needless updating of bindings.
1
2
3
4
5
6
7
8
9
10
11
package sample.javafx.groovy
import griffon.core.artifact.GriffonModel
import griffon.metadata.ArtifactProviderFor
import griffon.transform.FXObservable
@ArtifactProviderFor(GriffonModel)
class SampleModel {
@FXObservable String input (1)
@FXObservable String output (1)
}
1 | Observable property |
As with @Observable
we find that Groovy-based JavaFX models can use another AST
transformation named @FXObservable
. This transformation generates equivalent byte code
to the Java based Model we saw earlier.
E.2.2. Controller
Have a look at the controller for this application. See if you can spot the differences from its Swing counterpart.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package sample.javafx.java;
import griffon.core.artifact.GriffonController;
import griffon.core.controller.ControllerAction;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javax.annotation.Nonnull;
import javax.inject.Inject;
@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
private SampleModel model; (1)
@Inject
private SampleService sampleService; (2)
@MVCMember
public void setModel(@Nonnull SampleModel model) {
this.model = model;
}
@ControllerAction
public void sayHello() { (3)
final String result = sampleService.sayHello(model.getInput());
runInsideUIAsync(() -> model.setOutput(result)); (4)
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Automatically run off the UI thread |
4 | Get back inside the UI thread |
Did you spot any differences? No? That’s because the two controllers are 100% identical. How can this be? This is the power of separating concerns between MVC members. Because the Controller only talks to the View via the Model, and the Model exposes an identical contract for its properties; we didn’t have to change the Controller at all.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package sample.javafx.groovy
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull
SampleModel model (1)
@Inject
private SampleService sampleService (2)
@ControllerAction
void sayHello() { (3)
String result = sampleService.sayHello(model.input)
runInsideUIAsync { model.output = result } (4)
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Automatically run off the UI thread |
4 | Get back inside the UI thread |
As opposed to its Swing counterpart, here we have to add an explicit threading block when
updating model properties. This is because the bind
node for JavaFX components is not
aware of the same rules as the bind
node for Swing components. Nevertheless, the
code remains short and to the point.
E.2.3. Service
Given that the service operates with raw data and has no ties to the toolkit in use, we’d expect no changes from the Swing example.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package sample.javafx.java;
import griffon.core.artifact.GriffonService;
import griffon.core.i18n.MessageSource;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonService;
import static griffon.util.GriffonNameUtils.isBlank;
import static java.util.Collections.singletonList;
@javax.inject.Singleton
@ArtifactProviderFor(GriffonService.class)
public class SampleService extends AbstractGriffonService {
public String sayHello(String input) {
MessageSource messageSource = getApplication().getMessageSource();
if (isBlank(input)) {
return messageSource.getMessage("greeting.default");
} else {
return messageSource.getMessage("greeting.parameterized", singletonList(input));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package sample.javafx.groovy
import griffon.core.artifact.GriffonService
import griffon.core.i18n.MessageSource
import griffon.metadata.ArtifactProviderFor
import static griffon.util.GriffonNameUtils.isBlank
@javax.inject.Singleton
@ArtifactProviderFor(GriffonService)
class SampleService {
String sayHello(String input) {
MessageSource ms = application.messageSource
isBlank(input) ? ms.getMessage('greeting.default') : ms.getMessage('greeting.parameterized', [input])
}
}
E.2.4. View
Views are the artifacts that are most impacted by the choice of UI toolkit. You may remember we mentioned FXML as one of the strong features delivered by JavaFX, so we chose to implement the Java-based View by reading an fxml file by convention.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package sample.javafx.java;
import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import javax.annotation.Nonnull;
import java.util.Collections;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
@MVCMember @Nonnull
private SampleController controller; (1)
@MVCMember @Nonnull
private SampleModel model; (1)
@FXML
private TextField input; (2)
@FXML
private Label output; (2)
@Override
public void initUI() {
Stage stage = (Stage) getApplication()
.createApplicationContainer(Collections.emptyMap());
stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
stage.setWidth(400);
stage.setHeight(120);
stage.setScene(init());
getApplication().getWindowManager().attach("mainWindow", stage); (3)
}
// build the UI
private Scene init() {
Scene scene = new Scene(new Group());
scene.setFill(Color.WHITE);
scene.getStylesheets().add("bootstrapfx.css");
Node node = loadFromFXML();
model.inputProperty().bindBidirectional(input.textProperty());
model.outputProperty().bindBidirectional(output.textProperty());
((Group) scene.getRoot()).getChildren().addAll(node);
connectActions(node, controller); (4)
return scene;
}
}
1 | MVC member injected by MVCGroupManager |
2 | Create window and attach it to WindowManager |
3 | Injected by FXMLLoader |
4 | Hook actions by convention |
FXMLLoader
can inject components to an instance as long as that instance exposes
fields annotated with @FXML
; fields names must match component ids 2
as defined in the fxml file, which is shown next:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-License-Identifier: Apache-2.0
Copyright 2008-2018 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity"
prefHeight="80.0" prefWidth="384.0"
xmlns:fx="http://javafx.com/fxml"
fx:controller="sample.javafx.java.SampleView">
<Label layoutX="14.0" layoutY="14.0" text="Please enter your name:"/>
<TextField fx:id="input" layoutX="172.0" layoutY="11.0"
prefWidth="200.0"/>
<Button layoutX="172.0" layoutY="45.0"
mnemonicParsing="false"
prefWidth="200.0"
text="Say hello!"
styleClass="btn, btn-primary"
fx:id="sayHelloActionTarget"/> (1)
<Label layoutX="14.0" layoutY="80.0" prefWidth="360.0" fx:id="output"/>
</AnchorPane>
1 | Naming convention for automatic action binding |
Please pay special attention to the fx:id
given to the button. Griffon applies a naming
convention to match controller actions to JavaFX components that can handle said actions.
Let’s review what we have here:
-
SampleController
exposes an action namedsayHello
-
the button has an
fx:id
value ofsayHelloActionTarget
Given this we infer that the fx:id
value must be of the form <actionName>ActionTarget
.
The naming convention is one of two steps; you must also connect the controller using
a helper method 4 as shown in the View.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package sample.javafx.groovy
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull
FactoryBuilderSupport builder (1)
@MVCMember @Nonnull
SampleModel model (1)
void initUI() {
builder.application(title: application.configuration['application.title'],
name: 'mainWindow', sizeToScene: true, centerOnScreen: true) { (2)
scene(fill: WHITE, width: 400, height: 120,
stylesheets: ['/bootstrapfx.css', '/sample/javafx/groovy/sample.css']) {
anchorPane {
label(leftAnchor: 14, topAnchor: 11,
text: application.messageSource.getMessage('name.label'))
textField(leftAnchor: 172, topAnchor: 11, prefWidth: 200, id: 'input',
text: bind(model.inputProperty())) (3)
button(leftAnchor: 172, topAnchor: 45, prefWidth: 200,
styleClass: ['btn', 'btn-primary'],
id: 'sayHelloActionTarget', sayHelloAction) (4)
label(leftAnchor: 14, topAnchor: 80, prefWidth: 200, id: 'output',
text: bind(model.outputProperty())) (3)
}
}
}
}
}
1 | MVC member injected by MVCGroupManager |
2 | Create window and attach it to WindowManager |
3 | Apply component-to-model binding |
4 | Hook actions by convention |
The Groovy version of the View uses the GroovyFX DSL instead of FXML. You’ll find that this DSL is very similar to SwingBuilder.
E.2.5. Resources
Finally, the resources for this application are identical to the Swing version.
1
2
3
name.label = Please enter your name
greeting.default = Howdy stranger!
greeting.parameterized = Hello {0}
E.2.6. Statistics
The following statistics show the number of lines per artifact required by both applications:
+---------------------------------+--------+--------+
| Name | Files | LOC |
+---------------------------------+--------+--------+
| CSS Sources | 1 | 3 |
| Configuration | 1 | 24 |
| Controllers | 1 | 23 |
| FXML Sources | 1 | 21 |
| Groovy Functional Test Sources | 1 | 35 |
| Groovy Integration Test Sources | 1 | 29 |
| Groovy Test Sources | 1 | 46 |
| Java Functional Test Sources | 1 | 28 |
| Java Integration Test Sources | 1 | 25 |
| Java Sources | 2 | 24 |
| Java Test Sources | 1 | 43 |
| Models | 1 | 38 |
| Properties | 3 | 5 |
| Services | 1 | 19 |
| Views | 1 | 47 |
+---------------------------------+--------+--------+
| Totals | 18 | 410 |
+---------------------------------+--------+--------+
+---------------------------------+--------+--------+
| Name | Files | LOC |
+---------------------------------+--------+--------+
| CSS Sources | 1 | 3 |
| Configuration | 1 | 13 |
| Controllers | 1 | 19 |
| Groovy Functional Test Sources | 2 | 63 |
| Groovy Integration Test Sources | 2 | 54 |
| Groovy Sources | 2 | 23 |
| Groovy Test Sources | 2 | 88 |
| Models | 1 | 9 |
| Properties | 3 | 5 |
| Services | 1 | 13 |
| Views | 1 | 31 |
+---------------------------------+--------+--------+
| Totals | 17 | 321 |
+---------------------------------+--------+--------+
E.3. Lanterna
Lanterna is a Java library allowing you to write easy semi-graphical user interfaces in a text-only environment, very much like the C library curses, but with more functionality. Lanterna supports xterm-compatible terminals and terminal emulators such as konsole, gnome-terminal, putty, xterm and many more. One of the main benefits of lanterna is that it’s not dependent on any native library, but runs 100% in pure Java.
E.3.1. Model
Even though Lanterna UI components do not expose observable properties in any way, it’s a good thing to use observable properties in the Model, and so the following Model is identical to the Swing version.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package sample.lanterna.java;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;
@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
private String input; (1)
private String output; (1)
public String getInput() { (2)
return input;
}
public void setInput(String input) {
firePropertyChange("input", this.input, this.input = input); (3)
}
public String getOutput() { (2)
return output;
}
public void setOutput(String output) {
firePropertyChange("output", this.output, this.output = output); (3)
}
}
1 | Define a private field for the property |
2 | Property accessor |
3 | Property mutator must fire a PropertyChangeEvent |
For reasons we’ll see in the Groovy View and Controller, we decided to skip a Model for the Groovy version. This also demonstrates that even though an MVC group is the smallest building block, you can still configure how it’s assembled. Have a look at the application’s configuration to find out how:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package sample.lanterna.groovy
application {
title = 'Lanterna + Groovy'
startupGroups = ['sample']
autoShutdown = true
}
mvcGroups {
// MVC Group for "sample"
'sample' {
view = 'sample.lanterna.groovy.SampleView'
controller = 'sample.lanterna.groovy.SampleController'
}
}
E.3.2. Controller
We find that for the Java version, the Controller is identical to the Swing
and JavaFX versions. For the Groovy one we notice that both input
and output
view components are accessed directly. We know we’ve said in the past
that a Controller should never do this, but because Lanterna exposes no bind mechanism,
the Groovy binding implementation would look as verbose as the Java version; we
decided to take a shortcut for demonstration purposes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package sample.lanterna.java;
import griffon.core.artifact.GriffonController;
import griffon.core.controller.ControllerAction;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javax.annotation.Nonnull;
import javax.inject.Inject;
@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
private SampleModel model; (1)
@Inject
private SampleService sampleService; (2)
@MVCMember
public void setModel(@Nonnull SampleModel model) {
this.model = model;
}
@ControllerAction
public void sayHello() { (3)
final String result = sampleService.sayHello(model.getInput());
runInsideUIAsync(new Runnable() { (4)
@Override
public void run() {
model.setOutput(result);
}
});
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Automatically run off the UI thread |
4 | Get back inside the UI thread |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package sample.lanterna.groovy
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull
FactoryBuilderSupport builder (1)
@Inject
private SampleService sampleService (2)
@ControllerAction
void sayHello() { (3)
String result = sampleService.sayHello(builder.input.text)
runInsideUIAsync { (4)
builder.output.text = result
}
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Automatically run off the UI thread |
4 | Get back inside the UI thread |
E.3.3. Service
Again, services should not be affected by the choice of UI tookit, so the following Service definitions are identical to the previous ones we saw earlier.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package sample.lanterna.java;
import griffon.core.artifact.GriffonService;
import griffon.core.i18n.MessageSource;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonService;
import static griffon.util.GriffonNameUtils.isBlank;
import static java.util.Collections.singletonList;
@javax.inject.Singleton
@ArtifactProviderFor(GriffonService.class)
public class SampleService extends AbstractGriffonService {
public String sayHello(String input) {
MessageSource messageSource = getApplication().getMessageSource();
if (isBlank(input)) {
return messageSource.getMessage("greeting.default");
} else {
return messageSource.getMessage("greeting.parameterized", singletonList(input));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package sample.lanterna.groovy
import griffon.core.artifact.GriffonService
import griffon.core.i18n.MessageSource
import griffon.metadata.ArtifactProviderFor
import static griffon.util.GriffonNameUtils.isBlank
@javax.inject.Singleton
@ArtifactProviderFor(GriffonService)
class SampleService {
String sayHello(String input) {
MessageSource ms = application.messageSource
isBlank(input) ? ms.getMessage('greeting.default') : ms.getMessage('greeting.parameterized', [input])
}
}
E.3.4. View
We’d expect the View to follow the same pattern we’ve seen in the previous examples;
that is, create two components that will be bound to model properties and a button
that’s connected to a Controller action. Here we see that the Java version is rather
verbose due to the fact that Lanterna has no observable UI components. This is the
reason we must explicitly set the value of the input
component in
the input
Model property as soon as the button is clicked.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package sample.lanterna.java;
import com.googlecode.lanterna.gui.Window;
import com.googlecode.lanterna.gui.component.Label;
import com.googlecode.lanterna.gui.component.Panel;
import com.googlecode.lanterna.gui.component.TextBox;
import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.lanterna.support.LanternaAction;
import griffon.lanterna.widgets.MutableButton;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.lanterna.artifact.AbstractLanternaGriffonView;
import javax.annotation.Nonnull;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractLanternaGriffonView {
private SampleController controller; (1)
private SampleModel model; (1)
@MVCMember
public void setController(@Nonnull SampleController controller) {
this.controller = controller;
}
@MVCMember
public void setModel(@Nonnull SampleModel model) {
this.model = model;
}
@Override
public void initUI() {
Window window = (Window) getApplication()
.createApplicationContainer(Collections.<String, Object>emptyMap());
getApplication().getWindowManager().attach("mainWindow", window); (2)
Panel panel = new Panel(Panel.Orientation.VERTICAL);
panel.addComponent(new Label(getApplication().getMessageSource().getMessage("name.label")));
final TextBox input = new TextBox();
panel.addComponent(input);
LanternaAction sayHelloAction = toolkitActionFor(controller, "sayHello");
final Runnable runnable = sayHelloAction.getRunnable();
sayHelloAction.setRunnable(new Runnable() { (3)
@Override
public void run() {
model.setInput(input.getText());
runnable.run();
}
});
panel.addComponent(new MutableButton(sayHelloAction)); (4)
final Label output = new Label();
panel.addComponent(output);
model.addPropertyChangeListener("output", new PropertyChangeListener() { (3)
@Override
public void propertyChange(PropertyChangeEvent evt) {
output.setText(String.valueOf(evt.getNewValue()));
}
});
window.addComponent(panel);
}
}
1 | MVC member injected by MVCGroupManager |
2 | Create window and attach it to WindowManager |
3 | Apply component-to-model binding |
4 | Hook actions by convention |
Fortunately, we can rely on the observable output
Model property to write back the
value to the output
component as soon as said property is updated.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package sample.lanterna.groovy
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull
FactoryBuilderSupport builder (1)
void initUI() {
builder.with {
application(id: 'mainWindow') { (2)
verticalLayout()
label(application.messageSource.getMessage('name.label'))
textBox(id: 'input')
button(sayHelloAction) (3)
label(id: 'output')
}
}
}
}
1 | MVC member injected by MVCGroupManager |
2 | Create window and attach it to WindowManager |
3 | Hook actions by convention |
Here we can see there’s no binding code in the View; for this reason, the Controller has to access UI components directly.
E.3.5. Resources
The resources file is identical to the ones found in the other applications. There are in fact no changes brought about by the choice of UI toolkit.
1
2
3
name.label = Please enter your name
greeting.default = Howdy stranger!
greeting.parameterized = Hello {0}
E.3.6. Statistics
The following statistics show the number of lines per artifact required by both applications:
+------------------------+--------+--------+
| Name | Files | LOC |
+------------------------+--------+--------+
| Configuration | 1 | 24 |
| Controllers | 1 | 28 |
| Java Sources | 2 | 24 |
| Java Test Sources | 1 | 55 |
| Models | 1 | 21 |
| Properties | 3 | 5 |
| Services | 1 | 19 |
| Views | 1 | 57 |
+------------------------+--------+--------+
| Totals | 11 | 233 |
+------------------------+--------+--------+
+------------------------+--------+--------+
| Name | Files | LOC |
+------------------------+--------+--------+
| Configuration | 1 | 12 |
| Controllers | 1 | 21 |
| Groovy Sources | 2 | 23 |
| Groovy Test Sources | 1 | 54 |
| Properties | 3 | 5 |
| Services | 1 | 13 |
| Views | 1 | 21 |
+------------------------+--------+--------+
| Totals | 10 | 149 |
+------------------------+--------+--------+
E.4. Pivot
Apache Pivot is an open-source platform for building installable Internet applications (IIAs). It combines the enhanced productivity and usability features of a modern user interface toolkit with the robustness of the Java platform.
We decided to implement this application in the same fashion as the Lanterna application for the same reason: Pivot UI components are not observable. You’ll notice that the Java Model is identical, and there’s no Groovy Model.
E.4.1. Model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package sample.pivot.java;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;
@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
private String input; (1)
private String output; (1)
public String getInput() { (2)
return input;
}
public void setInput(String input) {
firePropertyChange("input", this.input, this.input = input); (3)
}
public String getOutput() { (2)
return output;
}
public void setOutput(String output) {
firePropertyChange("output", this.output, this.output = output); (3)
}
}
1 | Define a private field for the property |
2 | Property accessor |
3 | Property mutator must fire a PropertyChangeEvent |
E.4.2. Controller
The Pivot Controllers follow the same rules as the Lanterna ones, and as such you’ll see there are no differences between one another.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package sample.pivot.java;
import griffon.core.artifact.GriffonController;
import griffon.core.controller.ControllerAction;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javax.annotation.Nonnull;
import javax.inject.Inject;
@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
private SampleModel model; (1)
@Inject
private SampleService sampleService; (2)
@MVCMember
public void setModel(@Nonnull SampleModel model) {
this.model = model;
}
@ControllerAction
public void sayHello() { (3)
final String result = sampleService.sayHello(model.getInput());
runInsideUIAsync(new Runnable() { (4)
@Override
public void run() {
model.setOutput(result);
}
});
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Automatically run off the UI thread |
4 | Get back inside the UI thread |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package sample.pivot.groovy
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonController)
class SampleController {
@MVCMember @Nonnull
FactoryBuilderSupport builder (1)
@Inject
private SampleService sampleService (2)
@ControllerAction
void sayHello() { (3)
String result = sampleService.sayHello(builder.input.text)
runInsideUIAsync { (4)
builder.output.text = result
}
}
}
1 | MVC member injected by MVCGroupManager |
2 | Injected by JSR 330 |
3 | Automatically run off the UI thread |
4 | Get back inside the UI thread |
E.4.3. Service
Services remain constant again; no surprise, right?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package sample.pivot.java;
import griffon.core.artifact.GriffonService;
import griffon.core.i18n.MessageSource;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonService;
import static griffon.util.GriffonNameUtils.isBlank;
import static java.util.Collections.singletonList;
@javax.inject.Singleton
@ArtifactProviderFor(GriffonService.class)
public class SampleService extends AbstractGriffonService {
public String sayHello(String input) {
MessageSource messageSource = getApplication().getMessageSource();
if (isBlank(input)) {
return messageSource.getMessage("greeting.default");
} else {
return messageSource.getMessage("greeting.parameterized", singletonList(input));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package sample.pivot.groovy
import griffon.core.artifact.GriffonService
import griffon.core.i18n.MessageSource
import griffon.metadata.ArtifactProviderFor
import static griffon.util.GriffonNameUtils.isBlank
@javax.inject.Singleton
@ArtifactProviderFor(GriffonService)
class SampleService {
String sayHello(String input) {
MessageSource ms = application.messageSource
isBlank(input) ? ms.getMessage('greeting.default') : ms.getMessage('greeting.parameterized', [input])
}
}
E.4.4. View
If you squint, you’ll see that the View is almost identical to the Swing
and Lanterna Views. Besides using toolkit specific components, we notice
that both input
and output
Model properties have to be explicitly bound using
the native support exposed by the toolkit.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package sample.pivot.java;
import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import griffon.pivot.support.PivotAction;
import griffon.pivot.support.adapters.TextInputContentAdapter;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.wtk.BoxPane;
import org.apache.pivot.wtk.Button;
import org.apache.pivot.wtk.Label;
import org.apache.pivot.wtk.Orientation;
import org.apache.pivot.wtk.PushButton;
import org.apache.pivot.wtk.TextInput;
import org.apache.pivot.wtk.Window;
import org.codehaus.griffon.runtime.pivot.artifact.AbstractPivotGriffonView;
import javax.annotation.Nonnull;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractPivotGriffonView {
private SampleController controller; (1)
private SampleModel model; (1)
@MVCMember
public void setController(@Nonnull SampleController controller) {
this.controller = controller;
}
@MVCMember
public void setModel(@Nonnull SampleModel model) {
this.model = model;
}
@Override
public void initUI() {
Window window = (Window) getApplication()
.createApplicationContainer(Collections.<String, Object>emptyMap());
window.setTitle(getApplication().getConfiguration().getAsString("application.title"));
window.setMaximized(true);
getApplication().getWindowManager().attach("mainWindow", window); (2)
BoxPane vbox = new BoxPane(Orientation.VERTICAL);
try {
vbox.setStyles("{horizontalAlignment:'center', verticalAlignment:'center'}");
} catch (SerializationException e) {
// ignore
}
vbox.add(new Label(getApplication().getMessageSource().getMessage("name.label")));
TextInput input = new TextInput();
input.setName("inputField");
input.getTextInputContentListeners().add(new TextInputContentAdapter() { (3)
@Override
public void textChanged(TextInput arg0) {
model.setInput(arg0.getText());
}
});
vbox.add(input);
PivotAction sayHelloAction = toolkitActionFor(controller, "sayHello");
final Button button = new PushButton(sayHelloAction.getName());
button.setName("sayHelloButton");
button.setAction(sayHelloAction); (4)
vbox.add(button);
final TextInput output = new TextInput();
output.setName("outputField");
output.setEditable(false);
model.addPropertyChangeListener("output", new PropertyChangeListener() { (3)
@Override
public void propertyChange(PropertyChangeEvent evt) {
output.setText(String.valueOf(evt.getNewValue()));
}
});
vbox.add(output);
window.setContent(vbox);
}
}
1 | MVC member injected by MVCGroupManager |
2 | Create window and attach it to WindowManager |
3 | Apply component-to-model binding |
4 | Hook actions by convention |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package sample.pivot.groovy
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SampleView {
@MVCMember @Nonnull
FactoryBuilderSupport builder (1)
void initUI() {
builder.with {
application(title: application.configuration['application.title'],
id: 'mainWindow', maximized: true) { (2)
vbox(styles: "{horizontalAlignment:'center', verticalAlignment:'center'}") {
label(application.messageSource.getMessage('name.label'))
textInput(id: 'input')
button(id: 'sayHelloButton', sayHelloAction) (3)
textInput(id: 'output', editable: false)
}
}
}
}
}
1 | MVC member injected by MVCGroupManager |
2 | Create window and attach it to WindowManager |
3 | Hook actions by convention |
E.4.5. Resources
Here is the resource file, again unchanged:
1
2
3
name.label = Please enter your name
greeting.default = Howdy stranger!
greeting.parameterized = Hello {0}
E.4.6. Statistics
The following statistics show the number of lines per artifact required by both applications:
+---------------------------------+--------+--------+
| Name | Files | LOC |
+---------------------------------+--------+--------+
| Configuration | 1 | 24 |
| Controllers | 1 | 28 |
| Groovy Integration Test Sources | 1 | 37 |
| Java Integration Test Sources | 1 | 52 |
| Java Sources | 2 | 24 |
| Java Test Sources | 1 | 48 |
| Models | 1 | 21 |
| Properties | 3 | 5 |
| Services | 1 | 19 |
| Views | 1 | 71 |
+---------------------------------+--------+--------+
| Totals | 13 | 329 |
+---------------------------------+--------+--------+
+---------------------------------+--------+--------+
| Name | Files | LOC |
+---------------------------------+--------+--------+
| Configuration | 1 | 12 |
| Controllers | 1 | 21 |
| Groovy Integration Test Sources | 2 | 76 |
| Groovy Sources | 2 | 23 |
| Groovy Test Sources | 1 | 48 |
| Properties | 3 | 5 |
| Services | 1 | 13 |
| Views | 1 | 23 |
+---------------------------------+--------+--------+
| Totals | 12 | 221 |
+---------------------------------+--------+--------+
Appendix F: Builder Nodes
The following tables summarizes all builder nodes supplied by the default UI Toolkit dependencies:
F.2. JavaFX
Node |
Type |
accordion |
javafx.scene.control.Accordion |
action |
griffon.javafx.support.JavaFXAction |
affine |
|
anchorPane |
javafx.scene.layout.AnchorPane |
application |
javafx.scene.Stage |
arc |
javafx.scene.shape.Arc |
arcTo |
javafx.scene.shape.ArcTo |
areaChart |
javafx.builders.AreaChartBuilder |
barChart |
javafx.builders.BarChartBuilder |
blend |
javafx.scene.effect.Blend |
bloom |
javafx.scene.effect.Bloom |
borderPane |
javafx.scene.layout.BorderPane |
bottom |
groovyx.javafx.factory.BorderPanePosition |
bottomInput |
|
boxBlur |
javafx.scene.effect.BoxBlur |
bubbleChart |
javafx.builders.BubbleChartBuilder |
bumpInput |
|
button |
javafx.scene.control.Button |
categoryAxis |
javafx.scene.chart.CategoryAxis |
center |
groovyx.javafx.factory.BorderPanePosition |
checkBox |
javafx.scene.control.CheckBox |
checkMenuItem |
javafx.scene.control.MenuBar |
choiceBox |
javafx.scene.control.ChoiceBox |
circle |
javafx.scene.shape.Circle |
clip |
|
closePath |
javafx.scene.shape.ClosePath |
colorAdjust |
javafx.scene.effect.ColorAdjust |
colorInput |
javafx.scene.effect.ColorInput |
column |
groovyx.javafx.factory.GridRowColumn |
constraint |
groovyx.javafx.factory.GridConstraint |
container |
javafx.scene.Parent |
content |
groovyx.javafx.factory.Titled |
contentInput |
|
contextMenu |
javafx.scene.control.ContextMenu |
cubicCurve |
javafx.scene.shape.CubicCurve |
cubicCurveTo |
javafx.scene.shape.CubicCurveTo |
customMenuItem |
javafx.scene.control.MenuBar |
displacementMap |
javafx.scene.effect.DisplacementMap |
distant |
javafx.scene.effect.Light.Distant |
dividerPosition |
javafx.scene.control.DividerPosition |
dropShadow |
javafx.scene.effect.DropShadow |
effect |
javafx.scene.effect.Effect |
ellipse |
javafx.scene.shape.Ellipse |
fadeTransition |
javafx.animation.FadeTransition |
fileChooser |
javafx.stage.FileChooser |
fill |
javafx.scene.paint.Paint |
fillTransition |
javafx.animation.FadeTransition |
filter |
javafx.stage.FilterChooser.ExtensionFilter |
flowPane |
javafx.scene.layout.FlowPane |
fxml |
javafx.scene.Node |
gaussianBlur |
javafx.scene.effect.GaussianBlur |
glow |
javafx.scene.effect.Glow |
graphic |
groovyx.javafx.factory.Graphic |
gridPane |
javafx.scene.layout.GridPane |
group |
javafx.scene.Group |
hLineTo |
javafx.scene.shape.HLineTo |
hbox |
javafx.scene.layout.HBox |
htmlEditor |
javafx.scene.web.HTMLEditor |
hyperlink |
javafx.scene.control.Hyperlink |
image |
javafx.scene.image.Image |
imageInput |
javafx.scene.effect.ImageInput |
imageView |
javafx.scene.image.ImageView |
innerShadow |
javafx.scene.effect.InnerShadow |
label |
javafx.scene.control.Label |
left |
groovyx.javafx.factory.BorderPanePosition |
lighting |
javafx.scene.effect.Lighting |
line |
javafx.scene.shape.Line |
lineChart |
javafx.builders.LineChartBuilder |
lineTo |
javafx.scene.shape.LineTo |
linearGradient |
javafx.builders.LinearGradientBuilder |
listView |
javafx.scene.control.ListView |
mediaPlayer |
javafx.scene.media.MediaPlayer |
mediaView |
javafx.scene.media.MediaView |
menu |
javafx.scene.control.MenuBar |
menuBar |
javafx.scene.control.MenuBar |
menuButton |
javafx.scene.control.MenuBar |
menuItem |
javafx.scene.control.MenuBar |
metaComponent |
<any GroovyFX builder node> |
motionBlur |
javafx.scene.effect.MotionBlur |
moveTo |
javafx.scene.shape.MoveTo |
node |
javafx.scene.Node |
nodes |
java.util.List |
noparent |
java.util.List |
numberAxis |
javafx.scene.chart.NumberAxis |
onAction |
javafx.event.EventHandler |
onBranchCollapse |
groovyx.javafx.ClosureEventHandler |
onBranchExpand |
groovyx.javafx.ClosureEventHandler |
onChildrenModification |
groovyx.javafx.ClosureEventHandler |
onDragDetected |
javafx.event.EventHandler |
onDragDone |
javafx.event.EventHandler |
onDragDropped |
javafx.event.EventHandler |
onDragEntered |
javafx.event.EventHandler |
onDragExited |
javafx.event.EventHandler |
onDragOver |
javafx.event.EventHandler |
onEditCancel |
groovyx.javafx.ClosureEventHandler |
onEditCommit |
groovyx.javafx.ClosureEventHandler |
onEditStart |
groovyx.javafx.ClosureEventHandler |
onGraphicChanged |
groovyx.javafx.ClosureEventHandler |
onMouseClicked |
javafx.event.EventHandler |
onMouseDragged |
javafx.event.EventHandler |
onMouseEntered |
javafx.event.EventHandler |
onMouseExited |
javafx.event.EventHandler |
onMousePressed |
javafx.event.EventHandler |
onMouseReleased |
javafx.event.EventHandler |
onMouseWheelMoved |
javafx.event.EventHandler |
onTreeItemCountChange |
groovyx.javafx.ClosureEventHandler |
onTreeNotification |
groovyx.javafx.ClosureEventHandler |
onValueChanged |
groovyx.javafx.ClosureEventHandler |
pane |
javafx.scene.layout.Pane |
parallelTransition |
javafx.animation.ParallelTransition |
path |
javafx.scene.shape.Path |
pathTransition |
javafx.animation.PathTransition |
pauseTransition |
javafx.animation.PauseTransition |
perspectiveTransform |
javafx.scene.effect.PerspectiveTransform |
pieChart |
javafx.scene.chart.PieChart |
point |
javafx.scene.effect.Light.Point |
polygon |
javafx.scene.shape.Polygon |
polyline |
javafx.scene.shape.Polyline |
popup |
javafx.stage.Popup |
progressBar |
javafx.scene.control.ProgressBar |
progressIndicator |
javafx.scene.control.ProgressIndicator |
quadCurve |
javafx.scene.shape.QuadCurve |
quadCurveTo |
javafx.scene.shape.QuadCurveTo |
radialGradient |
javafx.builders.RadialGradientBuilder |
radioButton |
javafx.scene.control.RadioButton |
radioMenuItem |
javafx.scene.control.MenuBar |
rectangle |
javafx.scene.shape.Rectangle |
reflection |
javafx.scene.effect.Reflection |
right |
groovyx.javafx.factory.BorderPanePosition |
rotate |
|
rotateTransition |
javafx.animation.RotateTransition |
row |
groovyx.javafx.factory.GridRowColumn |
scale |
|
scaleTransition |
javafx.animation.ScaleTransition |
scatterChart |
javafx.builders.ScatterChartBuilder |
scene |
javafx.scene.Scene |
scrollBar |
javafx.scene.control.ScrollBar |
scrollPane |
javafx.scene.control.ScrollPane |
separator |
javafx.scene.control.Separator |
separatorMenuItem |
javafx.scene.control.MenuBar |
sepiaTone |
javafx.scene.effect.SepiaTone |
sequentialTransition |
javafx.animation.SequentialTransition |
series |
javafx.scene.chart.XYChart.Series |
shadow |
javafx.scene.effect.Shadow |
shear |
|
slider |
javafx.scene.control.Slider |
splitMenuButton |
javafx.scene.control.MenuBar |
splitPane |
javafx.scene.control.SplitPane |
spot |
javafx.scene.effect.Light.Spot |
stackPane |
javafx.scene.layout.StackPane |
stage |
javafx.scene.Stage |
stop |
javafx.scene.paint.Stop |
stroke |
javafx.scene.paint.Paint |
strokeTransition |
javafx.animation.StrokeTransition |
stylesheets |
java.util.List |
svgPath |
javafx.scene.shape.SVGPath |
tab |
javafx.scene.control.Tab |
tabPane |
javafx.scene.control.TabPane |
tableColumn |
javafx.scene.control.TableColumn |
tableRow |
javafx.scene.control.TableRow |
tableView |
javafx.scene.control.TableView |
text |
javafx.scene.text.Text |
textArea |
javafx.scene.control.TextArea |
textField |
javafx.scene.control.TextField |
tilePane |
javafx.scene.layout.TilePane |
title |
groovyx.javafx.factory.Titled |
titledPane |
javafx.scene.control.TitledPane |
toggleButton |
javafx.scene.control.ToggleButton |
toolBar |
javafx.scene.control.ToolBar |
tooltip |
javafx.scene.control.Tooltip |
top |
groovyx.javafx.factory.BorderPanePosition |
topInput |
|
transition |
javafx.animation.Transition |
translate |
|
translateTransition |
javafx.animation.TranslateTransition |
treeItem |
javafx.scene.control.TreeItem |
treeView |
javafx.scene.control.TreeView |
vLineTo |
javafx.scene.shape.VLineTo |
vbox |
javafx.scene.layout.VBox |
webEngine |
javafx.scene.web.WebEngine |
webView |
javafx.scene.web.WebView |
F.3. Lanterna
Node | Type |
---|---|
action |
griffon.lanterna.support.LanternaAction |
actionListBox |
com.googlecode.lanterna.gui.component.ActionListBox |
actions |
java.util.ArrayList |
application |
com.googlecode.lanterna.gui.Window |
bean |
java.lang.Object |
borderLayout |
com.googlecode.lanterna.gui.layout.BorderLayout |
button |
griffon.lanterna.widgets.MutableButton |
checkBox |
com.googlecode.lanterna.gui.component.CheckBox |
container |
com.googlecode.lanterna.gui.Component |
emptySpace |
com.googlecode.lanterna.gui.component.EmptySpace |
hbox |
com.googlecode.lanterna.gui.component.Panel |
horisontalLayout |
com.googlecode.lanterna.gui.layout.HorisontalLayout |
horizontalLayout |
com.googlecode.lanterna.gui.layout.HorisontalLayout |
label |
com.googlecode.lanterna.gui.component.Label |
list |
java.util.ArrayList |
panel |
com.googlecode.lanterna.gui.component.Panel |
passwordBox |
com.googlecode.lanterna.gui.component.PasswordBox |
progressBar |
com.googlecode.lanterna.gui.component.ProgressBar |
table |
com.googlecode.lanterna.gui.component.Table |
textArea |
com.googlecode.lanterna.gui.component.TextArea |
textBox |
com.googlecode.lanterna.gui.component.TextBox |
vbox |
com.googlecode.lanterna.gui.component.Panel |
verticalLayout |
com.googlecode.lanterna.gui.layout.VerticalLayout |
widget |
com.googlecode.lanterna.gui.Component |
F.4. Pivot
Node | Type |
---|---|
accordion |
org.apache.pivot.wtk.Accordion |
action |
griffon.pivot.imlp.DefaultAction |
actions |
java.util.ArrayList |
activityIndicator |
org.apache.pivot.wtk.ActivityIndicator |
application |
org.apache.pivot.wtk.Window |
baselineDecorator |
org.apache.pivot.wtk.effects.BaselineDecorator |
bean |
java.lang.Object |
blurDecorator |
org.apache.pivot.wtk.effects.BlurDecorator |
border |
org.apache.pivot.wtk.Border |
bounds |
org.apache.pivot.wtk.Bounds |
box |
org.apache.pivot.wtk.BoxPane |
boxPane |
org.apache.pivot.wtk.BoxPane |
button |
org.apache.pivot.wtk.PushButton |
buttonData |
org.apache.pivot.wtk.content.ButtonData |
buttonDataRenderer |
org.apache.pivot.wtk.content.ButtonDataRenderer |
buttonGroup |
org.apache.pivot.wtk.ButtonGroup |
bxml |
org.apache.pivot.wtk.Component |
calendar |
org.apache.pivot.wtk.Calendar |
calendarButton |
org.apache.pivot.wtk.CalendarButton |
calendarButtonDataRenderer |
org.apache.pivot.wtk.content.CalendarButtonDataRenderer |
calendarDateSpinnerData |
org.apache.pivot.wtk.content.CalendarDateSpinnerData |
cardPane |
org.apache.pivot.wtk.CardPane |
checkbox |
org.apache.pivot.wtk.Checkbox |
clipDecorator |
org.apache.pivot.wtk.effects.ClipDecorator |
colorChooser |
org.apache.pivot.wtk.ColorChooser |
colorChooserButton |
org.apache.pivot.wtk.ColorChooserButton |
container |
org.apache.pivot.wtk.Container |
dialog |
org.apache.pivot.wtk.Dialog |
dimensions |
org.apache.pivot.wtk.Dimensions |
dropShadowDecorator |
org.apache.pivot.wtk.effects.DropShadowDecorator |
easingCircular |
org.apache.pivot.wtk.effects.Circular |
easingCubic |
org.apache.pivot.wtk.effects.Cubic |
easingExponential |
org.apache.pivot.wtk.effects.Exponential |
easingLinear |
org.apache.pivot.wtk.effects.Linear |
easingQuadratic |
org.apache.pivot.wtk.effects.Quadratic |
easingQuartic |
org.apache.pivot.wtk.effects.Quartic |
easingQuintic |
org.apache.pivot.wtk.effects.Quintic |
easingSine |
org.apache.pivot.wtk.effects.Sine |
expander |
org.apache.pivot.wtk.Expander |
fadeDecorator |
org.apache.pivot.wtk.effects.FadeDecorator |
fileBrowser |
org.apache.pivot.wtk.FileBrowser |
fileBrowserSheet |
org.apache.pivot.wtk.FileBrowserSheet |
flowPane |
org.apache.pivot.wtk.FlowPane |
form |
org.apache.pivot.wtk.Form |
formFlag |
org.apache.pivot.wtk.From.Flag |
formSection |
org.apache.pivot.wtk.Form.Section |
frame |
org.apache.pivot.wtk.Frame |
grayscaleDecorator |
org.apache.pivot.wtk.effects.GrayscaleDecorator |
gridFiller |
org.apache.pivot.wtk.GridPane.Filler |
gridPane |
org.apache.pivot.wtk.GridPane |
gridRow |
org.apache.pivot.wtk.GridPane.Row |
hbox |
org.apache.pivot.wtk.BoxPane |
imageView |
org.apache.pivot.wtk.ImageView |
insets |
org.apache.pivot.wtk.Insets |
label |
org.apache.pivot.wtk.Label |
linkButton |
org.apache.pivot.wtk.LinkButton |
linkButtonDataRenderer |
org.apache.pivot.wtk.content.LinkButtonDataRenderer |
listButton |
org.apache.pivot.wtk.ListButton |
listButtonColorItemRenderer |
org.apache.pivot.wtk.content.ListButtonColorItemRenderer |
listButtonDataRenderer |
org.apache.pivot.wtk.content.ListButtonDataRenderer |
listView |
org.apache.pivot.wtk.ListView |
menu |
org.apache.pivot.wtk.Menu |
menuBar |
org.apache.pivot.wtk.MenuBar |
menuBarItem |
org.apache.pivot.wtk.MenuBar.Item |
menuBarItemDataRenderer |
org.apache.pivot.wtk.content.MenuBarItemDataRenderer |
menuButton |
org.apache.pivot.wtk.MenuButton |
menuButtonDataRenderer |
org.apache.pivot.wtk.content.MenuButtonDataRenderer |
menuItem |
org.apache.pivot.wtk.Menu.Item |
menuItemDataRenderer |
org.apache.pivot.wtk.content.MenuItemDataRenderer |
menuPopup |
org.apache.pivot.wtk.MenuPopup |
meter |
org.apache.pivot.wtk.Meter |
noparent |
java.util.ArrayList |
numericSpinnerData |
org.apache.pivot.wtk.content.NumericSpinnerData |
overlayDecorator |
org.apache.pivot.wtk.effects.OverlayDecorator |
palette |
org.apache.pivot.wtk.Palette |
panel |
org.apache.pivot.wtk.Panel |
panorama |
org.apache.pivot.wtk.Panorama |
picture |
org.apache.pivot.wtk.media.Picture |
point |
org.apache.pivot.wtk.Point |
pushButton |
org.apache.pivot.wtk.PushButton |
radioButton |
org.apache.pivot.wtk.RadioButton |
reflectionDecorator |
org.apache.pivot.wtk.effects.ReflectionDecorator |
rollup |
org.apache.pivot.wtk.Rollup |
rotationDecorator |
org.apache.pivot.wtk.effects.RotationDecorator |
saturationDecorator |
org.apache.pivot.wtk.effects.SaturationDecorator |
scaleDecorator |
org.apache.pivot.wtk.effects.ScaleDecorator |
scrollBar |
org.apache.pivot.wtk.ScrollBar |
scrollBarScope |
org.apache.pivot.wtk.ScrollBar.Scope |
scrollPane |
org.apache.pivot.wtk.ScrollPane |
separator |
org.apache.pivot.wtk.Separator |
shadeDecorator |
org.apache.pivot.wtk.effects.ShadeDecorator |
sheet |
org.apache.pivot.wtk.Sheet |
slider |
org.apache.pivot.wtk.Slider |
span |
org.apache.pivot.wtk.Span |
spinner |
org.apache.pivot.wtk.Spiner |
splitPane |
org.apache.pivot.wtk.SplitPane |
stackPane |
org.apache.pivot.wtk.StackPane |
tabPane |
org.apache.pivot.wtk.TabPane |
tablePane |
org.apache.pivot.wtk.TablePane |
tablePaneColumn |
org.apache.pivot.wtk.TablePane.Column |
tablePaneFiller |
org.apache.pivot.wtk.TablePane.Filler |
tablePaneRow |
org.apache.pivot.wtk.TablePane.Row |
tagDecorator |
org.apache.pivot.wtk.effects.TagDecorator |
textArea |
org.apache.pivot.wtk.TextArea |
textInput |
org.apache.pivot.wtk.TextInput |
tooltip |
org.apache.pivot.wtk.Tooltip |
translationDecorator |
org.apache.pivot.wtk.effects.TranslationDecorator |
vbox |
org.apache.pivot.wtk.BoxPane |
watermarkDecorator |
org.apache.pivot.wtk.effects.WatermarkDecorator |
widget |
org.apache.pivot.wtk.Component |
window |
org.apache.pivot.wtk.Window |