1. Introduction

simple-jpa is a Griffon plugin for developing JPA and Swing based desktop application. The main goal of simple-jpa is to allow developer to concentrate on business logic. simple-jpa provides much functionality that is needed when working with JPA, therefore, frees developer from writing high-ceremony code.

simple-jpa is very useful for rapidly developing Swing-based database oriented desktop application. It can also be used for prototyping.

The following is a list of some of simple-jpa’s features:

Scaffolding

simple-jpa can generate an MVCGroup based on a domain class. This will speed up development.

Dynamic finders

simple-jpa injects dynamic finders to controllers (or services). With dynamic finders, developer can perform a query on JPA entities (or domain objects) quickly and easily. simple-jpa also supports the execution of JPA named query, JPQL and native SQL.

Transaction management

Unlike web-based applications, desktop applications do not require Java Transaction API (JTA). simple-jpa automatically provides and manages transaction for each method in controllers (can be configured by using annotation). By default, simple-jpa will share EntityManager across transaction in a way that is suitable for desktop application.

Bean Validation API (JSR-303) support

In the case of failed validation, simple-jpa will automatically present error messages in Swing-based view. Developer can also configure error notification and its behavior.

Common database application features

simple-jpa adds the following to all domain classes: an id (auto generated primary key), fields that store created time and last modified time (will be filled automatically), and a soft delete flag (soft delete is marking the object as inactive without deleting it from database).

Swing nodes for database application

simple-jpa provides template renderer for effortlessly represent domain object in JTable, JList or JComboBox. It also provides new nodes that can be used in Griffon’s view such as tagChooser, numberTextField, maskTextField, and dateTimePicker.

Integration testing

simple-jpa is using dbUnit in integration testing to fill database with predefined data from a Microsoft Excel file (or csv file). This way, every test cases will be executed with the same table data.

This version is not compatible with Griffon 2.0. You will need Griffon 1.5 and upgrade its Groovy version to at least Groovy 2.3.

2. Getting Started

This section will help you write your first simple-jpa application. You should have installed JDK 7 and Griffon 1.5 if you haven’t done so. You can find more information about Griffon’s installation in http://griffon.codehaus.org/guide/latest/guide/gettingStarted.html.

simple-jpa binary is compiled using Groovy 2.3 while the latest Griffon shipped with Groovy 2.2. Unfortunately, Groovy 2.3 binary is not backward compatible with previous version. If you run simple-jpa in Griffon 1.5, you will encouter strange Exception such as java.lang.NoClassDefFoundError: org/codehaus/groovy/runtime/typehandling/ShortTypeHandling. To fix this, you can either upgrade Griffon to use Groovy 2.3 or rebuild simple-jpa from source. See http://www.jroller.com/aalmiray/entry/running_griffon_with_an_alternate for more information on upgrading Groovy used by Griffon.

The article explains how to edit griffon-cli-1.5.0.jar by using CLI. As an alternative, you can also open griffon-cli-1.5.0.jar in 7zip, right click build.properties and select Edit.

You can execute the following command to determine Groovy version used by Griffon:

C:\> griffon -v

If Griffon has been setup properly, you are ready to create your first Griffon application:

C:\> griffon create-app myapp

Griffon creates and stores your new project in a folder named myapp. You should move to this folder:

C:\> cd myapp

Install simple-jpa plugin by executing the following script:

C:\myapp> griffon install-plugin simple-jpa

The first thing to do when using simple-jpa is calling create-simple-jpa to setup persistence layer in your application. Don’t forget to make sure your database server is ready. If you don’t have a database server installed in your computer, you can use Apache Derby as embedded database by executing the following script:

C:\myapp> griffon create-simple-jpa -user=steven -password=12345 -database=C:/mydb -jdbc=derby-embedded

You don’t need to install anything to use Apache Derby as embedded database because it is embedded in your application. Your data will be stored in C:\mydb. If you accidentally delete this folder, you need to recreate this folder by appending create=true to JDBC URL in griffon-app\conf\metainf\persistence.xml:

<property name="javax.persistence.jdbc.url" value="jdbc:derby:C:/mydb;create=true" />

Lets create your first domain classes:

C:\myapp> griffon create-domain-class Invoice LineItem

You can their properties and methods by changing the content of Invoice.groovy and LineItem.groovy in C:\myapp\src\main\domain:

Invoice.groovy
package domain

import groovy.transform.*
import simplejpa.DomainClass
import javax.persistence.*
import org.hibernate.annotations.Type
import javax.validation.constraints.*
import org.hibernate.validator.constraints.*
import org.joda.time.*

@DomainClass @Entity @Canonical
class Invoice {

  @NotBlank
  String number

  @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
  LocalDate date

  @ElementCollection(fetch=FetchType.EAGER) @NotEmpty
  List<LineItem> items = []

  void add(LineItem item) {
    items << item
  }

  BigDecimal total() {
    items.sum { it.total() }
  }

}
LineItem.groovy
package domain

import groovy.transform.*
import simplejpa.DomainClass
import javax.persistence.*
import org.hibernate.annotations.Type
import javax.validation.constraints.*
import org.hibernate.validator.constraints.*
import org.joda.time.*

@Embeddable @Canonical
class LineItem {

  @NotBlank @Size(min=2, max=100)
  String productName

  @NotNull @Min(value=1l)
  Integer qty

  @NotNull @Min(value=0l)
  BigDecimal price

  BigDecimal total() {
    qty * price
  }

}

Now, you can instruct simple-jpa to generate basic presentation layer (scaffolding):

C:\myapp> griffon generate-all * -startupGroup=MainGroup

If you change your domain classes later, you can overwrite the generated code by calling:

C:\myapp> griffon generate-all * -startupGroup=MainGroup -forceOverwrite

Try to run your application:

C:\myapp> griffon run-app

You’ve just created Swing-based Java application that uses JPA. It is a working application! You can add new record, edit or delete existing record. You can search for existing records. You can also double-click on table row to modify line items.

getting started

If you close and re-run your application, tables will be recreated to reflect the latest change in domain classes. You can disable this by remove the following line from griffon-app/conf/metainf/persistence.xml:

<property name="javax.persistence.schema-generation.database.action" value="drop-and-create" />

If you think logger is too noisy, you change log4j configuration in griffon-app/conf/Config.groovy into:

log4j = {
   appenders {
      console name: 'stdout', layout: pattern(conversionPattern: '%d [%t] %-5p %c - %m%n')
   }

   root {
      error 'stdout'
   }
}

Now if you close and re-run your application, existing records are still there because they’re persisted in C:\mydb by Apache Derby.

Want to learn more about programming with simple-jpa? Close your application and execute the following script:

C:\myapp> griffon simple-jpa-console

This will launch Groovy Console where you can input Groovy code and execute them on the fly. Click on simple-jpa, MVC Groups in the menu bar and place a checkmark in invoice MVC group. Type the following code and press Ctrl + R to run it:

getting started simple jpa console

Try to delete existing code in Groovy console (without closing or relaunching a new console) and type a different one, such as:

invoiceController.findAllInvoice()

Press Ctrl + R to execute this script and the result will be displayed right away. By using simple-jpa-console, you easily test the result of finders or experiment on the right JP QL without relaunching your application.

3. Persistence

By default, simple-jpa uses Hibernate JPA as persistence layer for your application. You have to execute create-simple-jpa to setup persistence layer in your application. This is the first script you will need to execute when you start a new project.

3.1. Setup

In order to setup your persistence layer, you need to call create-simple-jpa script. This is usually performed once for every new project. create-simple-jpa will create database user and schema for you if they don’t exists. Current version of simple-jpa only supports MySQL Server and Apache Derby Embedded setup. If your database is not supported, you should configure your database manually.

If you use MySQL Server, you can execute create-simple-jpa like:

griffon create-simple-jpa -user=steven -password=12345 -database=sample
   -provider=hibernate -jdbc=mysql -rootPassword=secret

Because hibernate is default value for -provider and mysql is default value for -jdbc, you can ommit them:

griffon create-simple-jpa -user=steven -password=12345 -database=sample
   -rootPassword=secret

create-simple-jpa will check if sample database is exists or not. If sample database doesn’t exists, it will be created. Because this operation requires root user and its password, you need to provide a value in -rootPassword. This password won’t be saved in your application. create-simple-jpa also creates new database user called steven with default password 12345 if it doesn’t exists yet. This user will have full privilleges on sample database.

If you want to use Derby Embedded, you can execute create-simple-jpa like:

griffon create-simple-jpa -user=steven -password=12345 -database=C:/Users/steven/mydb
  -jdbc=derby-embedded -rootPassword=secret

When using -jdbc=derby-embedded, it is better to use absolute path for -database value. Derby embedded database will be created at the specified location. The draw back of using absolute path is you must make sure database is copied to the proper location when you distribute your application.

Root password is not required when using Derby embedded. If you didn’t specify root password, create-simple-jpa will create root user with password equals to user password (in this sample, it is 12345).

In addition to setup your database, create-simple-jpa also performs the following steps:

  • Create griffon-app/conf/metainf/persistence.xml that contains information required to connect to database, such as JDBC URL, database username and password.

  • Create griffon-app/conf/metainf/orm.xml that register AuditingEntityListener. This is required if you want to use Auditing feature.

  • Create griffon-app/i18n/ValidationMessages.properties. You can edit this file if you want to change Validation Message.

  • Add dependencies to JPA provider and JDBC driver in griffon-app/conf/BuildConfig.groovy.

If you want create-simple-jpa to perform the operations above without touching your database, use -skipDatabase argument:

griffon create-simple-jpa -user=steven -password=12345 -database=sample -skipDatabase

You can instruct your JPA provider to recreate database objects (tables) based on current domain classes by executing generate-schema script:

griffon generate-schema -target=database -action=drop-and-create

Rather than directly executing in target database, generate-schema can also store the generated SQL statements in a file:

griffon generate-schema -target=script -action=create -createTarget=mydatabase.sql

The command above will create mydatabase.sql in current directory. This file contains SQL statements to create tables required by your application.

3.2. JPA Configurations

By default, JPA provider will read persistence layer configurations stored in persistence.xml. The following is a sample persistence.xml created by create-simple-jpa script:

persistence.xml
<?xml version="1.0" encoding="UTF-8"?><persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/persistence" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
  <persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
    <class>domain.Product</class>
    <class>domain.LineItem</class>
    <class>domain.Invoice</class>
    <properties>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/exercises"/>
      <property name="javax.persistence.jdbc.user" value="steven"/>
      <property name="javax.persistence.jdbc.password" value="12345"/>
      <property name="hibernate.connection.autocommit" value="false"/>
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
      <property name="hibernate.connection.provider_class" value="org.hibernate.c3p0.internal.C3P0ConnectionProvider"/>
      <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
      <property name="jadira.usertype.autoRegisterUserTypes" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

You can add new or edit existing JPA configurations inside <properties/>. For example, setting javax.persistence.schema-generation.database.action to drop-and-create makes your JPA provider to recreate database tables everytime application is launched. You may want to disable this to make application startup faster. If you disable auto schema generation, you can still use generate-schema to create database tables manually.

In addition to persistence.xml, simple-jpa also reads JPA configurations stored in Config.groovy and external properties file. If more than one property are found, simple-jpa uses the value based on the following order:

  1. Configuration passed as system properties (must start with javax.persistence) have the first priority.

  2. Configuration stored in properties file have the first priority.

  3. Configuration stored in Config.groovy.

  4. Configuration stored in persistence.xml.

The following lines show a sample configurations added to Config.groovy:

Config.groovy
griffon {
    simplejpa {
        entityManager {
            properties  {
                javax.persistence.jdbc.driver = 'com.mysql.jdbc.Driver'
                javax.persistence.jdbc.url = 'jdbc:mysql://localhost/mydatabase'
                javax.persistence.jdbc.user = 'scott'
                javax.persistence.jdbc.password = 'tiger'
                hibernate.connection.autocommit = 'false'
                hibernate.dialect = 'org.hibernate.dialect.MySQL5Dialect'
            }
        }
    }
}

The advantage of storing configurations in Config.groovy is you can have different JPA configurations per Griffon environments. For example, the following configurations use different database for different environments:

Config.groovy
griffon {
  simplejpa {
    entityManager {
      properties {
        environments {
          development {
            javax.persistence.jdbc.url = 'jdbc:mysql://localhost/exercises'
            javax.persistence.jdbc.user = 'steven'
            javax.persistence.jdbc.password = '12345'
          }
          test {
            javax.persistence.jdbc.url = 'jdbc:mysql://localhost/test'
            javax.persistence.jdbc.user = 'test'
            javax.persistence.jdbc.password = 'secret'
            javax.persistence.'schema-generation'.database.action = 'drop-and-create'
          }
        }
      }
    }
  }
}

If you run the application by using command like run-app, it will activate development environment. In this case, JPA uses database jdbc:mysql://localhost/exercises. If you activate test environment (for example by running test-app), JPA uses database jdbc:mysql://localhost/test. It also recreate tables in that database.

To avoid storing sensitive information such as JDBC URL, username dan password in your source code, you can take advantage of Griffon feature to include properties file from Config.groovy. For example, if you store JPA configuration to hibernate.properties, you can add the following line to Config.groovy to include your properties file:

Config.groovy
griffon.config.locations = ['classpath:hibernate.properties']

You may want to configure your source code repository to make sure your properties file will never committed to public server.

simple-jpa can also be configured to read JPA properties from external properties file. You can define the location of this properties file by adding the following line to Config.groovy:

griffon.simplejpa.entityManager.propertiesFile = 'C:/example/db.properties'

A sample db.properties will look like:

C:/example/db.properties
javax.persistence.jdbc.url = 'jdbc:mysql://localhost/test'
javax.persistence.jdbc.user = 'test'
javax.persistence.jdbc.password = 'secret'

Configurations stored in this properties file will override existing configurations in Config.groovy and persistence.xml, but can be overriden by JVM system properties. If you didn’t change griffon.simplejpa.entityManager.propertiesFile, it defaults to simplejpa.properties. This means you can always override JPA configuration for existing distribution by adding a new file called simplejpa.properties in the same location when you launched the application.

simple-jpa supports obfuscating configuration value. For example, if you want to store obfuscated version of 12345, you need to execute the following command to retrieve its obsfuscated version:

griffon obfuscate -generate='12345'

Then, you can use the obfuscated version for any value in any location (such as Config.groovy, properties file or system propeties). For example, you can add the following line to Config.groovy:

griffon {
  simplejpa {
    entityManager {
      properties {
        javax.persistence.jdbc.password = 'obfuscated:YUqF9w6l5lpvNyH+1tnJBg=='
      }
    }
  }
}
While obfuscation makes it harder for lay people to read your configuration file, it doesn’t actually increase your security. Anyone can easily display the original string by executing griffon obfuscate -reverse. You should never publish obfuscated value if you don’t want people to know the original value.

3.3. Domain Class

simple-jpa provides persistence methods to deal with persistent domain class (marked by @Entity). To create such entity, you can use create-domain-class script, for example:

griffon create-domain-class Invoice LineItem

The command creates two new classes: domain.Invoice and domain.LineItem. It also add these classes to persistence.xml. You can change the default base packages for domain classes by setting griffon.simplejpa.domain.package in Config.groovy.

You can also specify subpackage when executing create-domain-class, for example:

griffon create-domain-class sales.Invoice sales.LineItem inventory.Product

The command creates three new classes: domain.sales.Invoice, domain.sales.LineItem and domain.inventory.Product.

Persistent domain classes in simple-jpa is a normal JPA entities. Just like when using JPA in Java, you can decorate JPA entity with JPA annotations such as @Entity, @OneToMany, @ManyToMany, @ManyToOne and others. See JPA documentation for more information about JPA annotations. The following show examples of persistent domain classes in simple-jpa:

Invoice.groovy
package domain

// import statements are not shown.

@DomainClass @Entity @Canonical
class Invoice {

  @NotEmpty @Size(min=5, max=5)
  String number


  @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
  LocalDate date

  @ElementCollection @OrderColumn @NotEmpty
  List<LineItem> items = []

  void add(LineItem item) {
    items << item
  }

  BigDecimal total() {
    items.sum { it.total() }
  }

}
LineItem.groovy
package domain

// import statements are not shown.

@Embeddable @Canonical
class LineItem {

  @NotNull @ManyToOne
  Product product

  @NotNull @Min(0l)
  BigDecimal price

  @NotNull @Min(1l)
  BigDecimal qty

  BigDecimal total() {
    price * qty
  }

}
Product.groovy
package domain

// import statements are not shown.

@DomainClass @Entity @Canonical
class Product {

  @NotEmpty @Size(min=2, max=50)
  String name

  @NotNull @Min(value=1l)
  BigDecimal retailPrice

}

@DomainClass is special annotation provided by simple-jpa. This annotation automatically adds the following property to the annotated class:

@Id @GeneratedValue(strategy=GenerationType.TABLE)   (1)
Long id  (2)

String deleted = 'N'  (3)

Date createdDate   (4)

String createdBy   (4)

Date modifiedDate  (4)

String modifiedBy  (4)
1 To change generation strategy, add desired strategy to idGenerationStrategy attribute.
2 @DomainClass(excludeId=true) will not generate this property.
3 @DomainClass(excludeDeletedFlag=true) will not generate this property.
4 @DomainClass(excludeAuditing=true) will not generate these properties.

You aren’t required to add @DomainClass to every persistent domain classes, but some features such as Auditing will not work without the properties generated by @DomainClass. Of course, you can still code by hand the required properties in every entities.

3.4. Persistence Methods

To make it possible for Griffon’s artifacts to manage domain classes, simple-jpa injects persistence methods to them. By default, persistence methods are injected into controller, but you can change it by adding the following line to Config.groovy:

Config.groovy
griffon.simplejpa.finders.injectInto = [ 'service', 'repository' ]

The configuration above will inject persistence methods to services and repositories. Repository is a custom artifact type provided by simple-jpa. You can create a new repository by using griffon create-repository, for example:

griffon create-repository MyRepository

The script creates MyRepository.groovy in griffon-app/repositories.

To retrieve instance of repository, you can use code like:

def myRepository = SimpleJpaUtil.instance.repositoryManager.findRepository('MyRepository')

For a simple application, it is usually acceptable to inject persistence methods to controllers. If you want a clear separation, you should inject persistence methods only to repositories. You must add @Transaction annotation to the injected artifacts to enable Transaction. Class generated by create-repository already has @Transaction, but Griffon’s controllers do not have @Transaction by default.

The following is list of persistence methods injected by simple-jpa:

  • persist(entity)

    Use this method to save new entity. It is a shortcut for entityManager.persist().

  • merge(entity)

    Use this method to add detached entity to current EntityManager. It is a shortcut for entityManager.merge().

  • remove(entity)

    Delete an entity. It is a shortcut for entityManager.remove().

  • softDelete(entity)

    Set deleted property of an entity into 'Y'.

  • validate(object)

    See Validation for more information.

  • Finders

    See Finders for more information.

  • getEntityManager()

    Returns an EntityManager for current session. This method can’t be renamed.

  • Transaction methods

    See Transaction Methods for more information.

  • All public methods of EntityManager.

    simple-jpa exposes all public methods of EntityManager to injected class. This means you can directly call methods such as lock(), refresh(), or detach() in injected class. For more information about EntityManager, see JPA documentation. These methods can’t be renamed.

All persistence methods are public so you can still call them from different class.

To avoid conflict with existing methods in injected class, simple-jpa can add prefix to persistence methods. For example, you can add the following line to Config.groovy:

griffon.simplejpa.finders.prefix = 'jpa'

Now, every persistence methods that can be renamed will have jpa prefix. For example, you have to execute jpaPersist() rather than persist().

The easiest way to learn simple-jpa persistence methods is by using simple-jpa-console script:

griffon simple-jpa-console

It will launch a Groovy console where you can write code snippet and see the result.

simple jpa console

The main advantage of Groovy console is you can edit existing code and execute it directly by selecting Script, Run (Ctrl+R). This is many times faster than relaunching application by using griffon run-app. You will find Groovy console very useful in testing your code snippet or understanding the result of persistence methods.

4. Scaffolding

Scaffolding feature from simple-jpa can be used to generate MVC groups based on existing domain classes. This is very useful in the early phase of application development as it provides guide for further development. Scaffolding can be invoked by using generate-all command or by enabling it in Config.groovy.

For example, MVC groups for domain class Student can be generated by using the following command:

griffon generate-all Student

simple-jpa will search for definition of Student class based on information from griffon-app/conf/metainf/persistence.xml. If it is found, simple-jpa will perform the following actions:

  • Create a new griffon-app/models/project/StudentModel.groovy if it doesn’t exists.

  • Create a new griffon-app/views/project/StudentView.groovy if it doesn’t exists.

  • Create a new griffon-app/controllers/project/StudentController.groovy if it doesn’t exists.

  • Search for student MVC group. If it doesn’t exists, create a new MVC group with the following content:

    Application.groovy
    // MVC Group for "student"
    'student' {
        model      = 'project.StudentModel'
        view       = 'project.StudentView'
        controller = 'project.StudentController'
    }
  • Create a new test/integration/project/StudentTest.groovy if it doesn’t exists.

  • Create a new data.xls if it doesn’t exists. Add a single empty sheet named as student to the Microsoft Excel file. This Excel file will act as data source when invoking StudentTest.groovy. See Testing for more information.

  • Add messages to griffon-app/i18n/messages.properties if they don’t exists.

  • Add a generic exception handler in griffon-app/conf/Events.groovy if it doesn’t exists.

By default, simple-jpa will not modify the content of existing files. To force simple-jpa to overwrite existing files, add -forceOverwrite parameter when invoking generate-all. For example:

griffon generate-all -forceOverwrite Student

The default target package for generated MVC artifacts is project. To use a different target package, -generatedPackage should be passed when invoking generate-all. For example:

griffon generate-all -generatedPackage=com.jocki.exercises Student

The command will place generated MVC artifacts in com.jocki.exercises package and register the following MVC group:

Application.groovy
// MVC Group for "student"
'student' {
    model      = 'com.jocki.exercises.StudentModel'
    view       = 'com.jocki.exercises.StudentView'
    controller = 'com.jocki.exercises.StudentController'
}

simple-jpa is smart enough to recognize package hierarchy in domain classes. It will try to preserve the source package hierarchy in the resulting MVC artifacts. For example, if Teacher is located in domain.staff, then the following command:

griffon generate-all -generatedPackage=com.jocki.exercises Teacher

will place the generated MVC artifacts in com.jocki.exercises.staff and register the following MVC group:

// MVC Group for "teacher"
'teacher' {
    model      = 'com.jocki.exercises.staff.TeacherModel'
    view       = 'com.jocki.exercises.staff.TeacherView'
    controller = 'com.jocki.exercises.staff.TeacherController'
}

In Griffon, startup group is the first MVC group to be instantiated and displayed. By default, generate-all will not modify the value of current startup group. This behaviour can be changed by using -setStartup parameter. If -setStartup is passed to generate-all, the command will set startup group to the generated MVC group. For example, the following command will generate teacher MVC group and set it as startup group:

griffon generate-all -setStartup Teacher

Since 0.8, simple-jpa generates view as a panel. Because the generated view doesn’t have top level container such as JFrame, it can’t be displayed by itself. The recommended way to present the view is by creating a startup group that will act as top level container.

simple-jpa can also create a dedicated startup group that serves as container for existing domain class based MVC groups. To generate a dedicated startup group, -startupGroup parameter should be used with the startup group’s name as its value. For example, the following command will generate mainGroup MVC group and set it as startup group:

griffon generate-all -startupGroup=MainGroup

The generated startup group’s view has a toolbar to launch domain class based MVC groups. It uses MainTabbedPane to display selected MVC group.

default startup group

You can add a new tab to main group from any MVC group by using code like:

MainGroupView mainView = app.getMvcGroupManager()['mainGroup'].view
mainView.mainTab.addMVCTab('mvcGroupName', [arg1: param1], "Tab Caption")

addMVCTab() always create new MVC group and display it in a new tab. This means you can have multiple unrelated instances of the same MVC group.

Domain class based MVC group generation and startup group generation can also be combined in a single command execution. For example:

griffon generate-all Teacher -startupGroup=MainGroup

New MVC groups may be generated in the future after startup group has been created. These new MVC groups will not appear in current startup group’s toolbar. One of possible solutions is recreate the startup group by including -forceOverwrite to overwrite existing startup group:

griffon generate-all -startupGroup=MainGroup -forceOverwrite

generate-all can accept multiple domain classes separated by space. For example:

griffon generate-all Student Teacher Classroom

As a shortcut, * can be used as domain class name to generate all registered domain classes. For example:

griffon generate-all *

Since version 0.8, scaffolding in simple-jpa is performed by an instance of Generator. The default generator that shipped with simple-jpa is simplejpa.scaffolding.generator.basic.BasicGenerator. To select another generator, add -generator parameter when invoking generate-all. For example:

griffon generate-all -generator=my.custom.Generator *

Parameters that have been discussed so far can also be stored in Config.groovy. The following table lists all possible values that can be added to Config.groovy:

Table 1. Scaffolding Configuration
Key Expected Value Description

griffon.simplejpa.scaffolding.auto

true or false.

If true, scaffolding is always performed everytime application is launched.

griffon.simplejpa.scaffolding.generator

A string.

The full class name that is instance of Generator. Default value is simplejpa.scaffolding.generator.basic .BasicGenerator.

griffon.simplejpa.scaffolding.generatedPackage

A string.

The location of target package for MVC artifacts. Default value is project.

griffon.simplejpa.scaffolding.startupGroup

A string.

The name of startup group. If it is not defined, no startup group will be generated.

griffon.simplejpa.scaffolding.ignoreLazy

true or false.

If true, lazy attributes will be included in generated code. Default value is true.

griffon.simplejpa.scaffolding.forceOverwrite

true or `false.

If true, existing files will be overwritten. Default value is false.

griffon.simplejpa.scaffolding.skipExcel

true or false

If true, not Excel file (for integration testing) will be created. Default value is false.

griffon.simplejpa.scaffolding.dateTimeStyle

'DEFAULT', 'SHORT', 'MEDIUM', 'LONG', or 'FULL'.

Represent formatting styles for auditing properties that are instances of java.util.Date.

griffon.simplejpa.scaffolding.target

A string that consists of domain class names or '*'.

List of domain classes to generate. If '*', all domain classes will be generated. Default value is '*'.

For example, the configurations below:

Config.groovy
griffon {
    simplejpa {
        scaffolding {
            generatedPackage = 'com.jocki.exercises'
            startupGroup = 'MainGroup'
            forceOverwrite = true
        }
    }
}

is identical with executing the following command:

griffon generate-all -generatedPackage=com.jocki.exercises -startupGroup=MainGroup -forceOverwrite *

Another benefit of storing generate-all parameters as configuration keys is automatic scaffolding. The scaffolding process can be automated if griffon.simplejpa.scaffolding.auto is set to true. For example, this configuration will automatically run scaffolding process:

griffon {
    simplejpa {
        scaffolding {
            auto = true
            startupGroup = 'MainGroup'
        }
    }
}

Automatic scaffolding will be performed whenever Griffon is compiling classes, such as when project is launched from griffon run-app command. Automatic scaffolding will not run in production when no class compilations being carried out.

4.1. Basic Generator

Basic generator is the default generator used by simple-jpa. It supports the following attribute types in domain class:

Table 2. Supported Attribute Types
Attribute Type SwingBuilder node Class

String, Character

textField()

JTextField

Boolean

checkBox()

JCheckbox

Byte, Short, Integer, Long, Float, Double, BigInteger

numberTextField()

JFormattedTextField

BigDecimal

decimalTextField()

JFormattedTextField

DateTime, LocalDateTime, LocalDate, LocalTime

dateTimePicker()

DateTimePicker

Enum

comboBox()

JComboBox with EnumComboBoxModel

List, Set

button() if relation is one-to-many or tagChooser() if relation is many-to-many

JButton or TagChooser

Any Entity Object

button() if relation is one-to-one or comboBox() if relation is one-to-many

JButton or JComboBox

Basic generator doesn’t support native data types such as int, float, or double because they are not nullable.
Basic generator will generate TODO comments in the generated code to provide information or warning to user. It is safe to delete these TODO comments.

For example, the following domain class:

@DomainClass @Entity @Canonical
class Student {

    String name

    Integer age

    @Type(type = "org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    LocalDate birthDate

    Boolean registered

    @Enumerated
    GRADE grade

    @ManyToOne
    Teacher teacher

    @ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
    List<Classroom> classrooms = []

}

enum GRADE {
    GRADE_1, GRADE_2, GRADE_3
}

will be generated as:

generated view basic attributes

To create a new record, user should enter required values in the editing area and click the 'Save’ button.

To update existing record, user must first select a row in table, enter the updated values in the editing area, and click the 'Save’ button.

To remove record from database, user must click the 'Delete’ button that will appear if table’s row is selected.

For domain class that have one-to-one association, basic generator generates a dialog to create, edit or remove the related entity. This feature requires cascading to be activated for the attribute.

For example, the following attribute declaration:

@OneToOne(cascade=CascadeType.ALL, orphanRemoval=true)
Teacher teacher

is represented by JButton that if clicked will open a dialog that allows user to modify the related Teacher. If this button is clicked in create operation, the dialog can be used to create a new instance of Teacher entity:

generated view one to one

If this button is clicked in update operation, the dialog can be used to update or delete existing Teacher entity:

generated view one to one update

The naming convention for one-to-one MVC group and its artifact’s name is the target entity name with 'AsPair’ as suffix. For example, if target entity is Teacher, basic generator generates TeacherAsPairModel, TeacherAsPairView, and TeacherAsPairController. They are not to be confused with the standalone MVC group for Teacher such as TeacherModel, TeacherView, and TeacherController that may also exists in the project (for example they are used for displaying list of Teacher in their own screen and not as popup).

If the annotated attribute in the generated code doesn’t have cascade set to CascadeType.ALL, org.hibernate.TransientPropertyValueException will be raised when saving the entity. To fix this, either add a proper cascading to object mapping or change the generated code to make it work without cascading.

Basic generator also treats @Embedded attributes as equivalent of one-to-one attributes. For example, the following mapping generates the same view as the previous one:

@Embedded
Teacher teacher
User can press ESC button as a shortcut to close popup dialog.
User can press Enter or double click a selected row in table to display the first one-to-many or one-to-one popup dialog in the view.

For one-to-many associations, basic generator also generates a dialog. For example, the following attribute declaration:

@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
Set<Classroom> classrooms = new HashSet<>()

is represented by JButton that if clicked will open a dialog that allows user to add or remove the list of Classroom entities that are associated with current entity. If this button is clicked in create operation, the dialog can be used to populate the collection with one or more Classroom:

generated view one to many

If this button is clicked in update operation, the dialog can be used to add new entity to the collection, edit the value of entity in the collection, or remove an entity from the collection:

generated view one to many update

The naming convention for one-to-many attribute is the target entity name with 'AsChild’ as suffix. For example, if target entity is Classroom, basic generator generates ClassroomAsChildModel, ClassroomAsChildView, and ClassroomAsChildController. This is not to be confused with ClassroomModel, ClassroomView or ClassroomController that may also exists in the project.

Basic generator will also treat @ElementCollection attributes as equivalent of one-to-many attributes. For example, the following mapping creates the same view as the previous one:

@ElementCollection(fetch=FetchType.EAGER)
Set<Classroom> classrooms = new HashSet<>()

For bidirectional associations, basic generator generates inverse attributes as labels because they are not editable. For example, the following domain classes:

@DomainClass @Entity @Canonical
class Invoice {

    String number

    @OneToOne(cascade=CascadeType.ALL, orphanRemoval=true)
    Delivery delivery

}

@DomainClass @Entity @Canonical(excludes='invoice')
class Delivery {

    String ticketNumber

    @OneToOne(mappedBy='delivery')
    Invoice invoice

}

will be generated as:

generated view one to one bidirectional
Don’t forget to add excludes to @Canonical in bidirectional association to avoid infinitive recursion!

4.2. DDD Generator

simple-jpa also shipped with a DDD generator that can selected by using -generator parameter such as in the following execution:

griffon generate-all -generator=simplejpa.scaffolding.generator.ddd.DDDGenerator *

DDD generator rely on basic generator to perform most of its works. It generates views that are identical to those generated by basic generator. The only distinction is this generator will move JPA related methods and database transactions from controller into a separate repository.

For every @Entity annotated domain classes, DDD generator will create their corresponding repository classes. simple-jpa supports a custom Griffon’s artifact called repository. For example, if there is an entity called Student, DDD generator will create a new repository called StudentRepository in griffon-app/repositories. This artifact is a singleton. It is automatically injected into controller (and other Griffon’s artifacts) by defining a variable such as studentRepository.

To retrieve all repositories, use RepositoryManager.getRepositories() such as:

def repositories = SimpleJpaUtil.instance.repositoryManager.repositories
println "All repositories: $repositories"

Repository artifact is lazy initialized. This means it won’t be created if it is not used. RepositoryManager.findRepository() can be used to retrieve an instance of repository. This method will create a new instance that will be reused later (singleton) when it is called for the first time.

InvoiceRepository repository = SimpleJpaUtil.instance.repositoryManager.findRepository('Invoice')

DDD generator disables dynamic methods in controller and adds dynamic methods to repository by adding the following line to Config.groovy:

griffon.simplejpa.finders.injectInto = ['repository']

4.3. Customization

A generator usually has classes and multiple templates. Reusable logic for view generation is stored in classes. They will be invoked by templates. For example, the generator class is always available to template as g variable.

The easiest way to customize a generator is to modify its template. install-templates command can be used to install templates for simple-jpa built-in generator to current project:

griffon install-templates

The command will copy template files to src/templates/artifacts. The following is list of all template files used by simple-jpa built-in generators:

Table 3. Template Files
Name Generator Purpose

SimpleJpaDomainClass.groovy

create-domain-class

Domain class generation

SimpleJpaRepository.groovy

DDD

Repository for entity

StartupModel.groovy

Basic, DDD

Startup group’s model

StartupController.groovy

Basic, DDD

Startup group’s controller

StartupView.groovy

Basic, DDD

Startup group’s view

SimpleJpaModel.groovy

Basic, DDD

Domain-class based model

SimpleJpaView.groovy

Basic, DDD

Domain-class based view

SimpleJpaController.groovy

Basic

Domain-class based controller

SimpleJpaDDDController.groovy

DDD

Domain-class based controller

SimpleJpaIntegrationTest.groovy

Basic, DDD

Integration test case

SimpleJpaPairModel.groovy

Basic, DDD

one-to-one popup model

SimpleJpaPairView.groovy

Basic, DDD

one-to-one popup view

SimpleJpaPairController.groovy

Basic

one-to-one popup controller

SimpleJpaDDDPairController.groovy

DDD

one-to-one popup controller

SimpleJpaChildModel.groovy

Basic, DDD

one-to-many popup model

SimpleJpaChildView.groovy

Basic, DDD

one-to-many popup view

SimpleJpaChildController.groovy

Basic

one-to-many popup controller

SimpleJpaDDDChildController.groovy

DDD

one-to-many popup controller

After these templates have been installed into current project, the next invocation of generate-all will be based on them.

For a more complex customization, a new generator may be created. The new generator can be extended from existing generator or simplejpa.scaffolding.generator.Generator. All generators have the following important methods:

  • generate(DomainClass domainClass) will be invoked when generating files for individual domain class.

  • generateStartupGroup(Map<String,DomainClass> domainClasses) will be invoked when generating startup group.

  • generateExtra(Map<String,DomainClass> domainClasses) is an optional method that will be invoked after generating files for individual domain class.

For example, the following is a sample declaration of custom generator that does nothing:

package generator

import simplejpa.scaffolding.DomainClass
import simplejpa.scaffolding.Scaffolding
import simplejpa.scaffolding.generator.basic.BasicGenerator

class MyGenerator extends BasicGenerator {

    MyGenerator(Scaffolding scaffolding) {
        super(scaffolding)
    }

    @Override
    void generate(DomainClass domainClass) {
        println "Generating ${domainClass.name}"
        // call super.generate(domainClass) for default operation (generating MVC artifacts)
    }

    @Override
    void generateStartupGroup(Map<String, DomainClass> domainClasses) {
        println "Generating startup group..."
    }

    @Override
    void generateExtra(Map<String, DomainClass> domainClasses) {
        println "Generating extra..."
    }

}

The following command will use the custom generator:

griffon -generator=generator.MyGenerator *

5. Validation

simple-jpa uses Bean Validation API (JSR 303) for validation. By default, it uses Hibernate Validator as Bean Validation API implementation. The following are some of common annotations supported by Hibernate Validator:

Table 4. Bean Validation Constraints
Annotation Description

@NotNull

Checks that the annotated value is not null

@NotBlank

Checks that the annotated character sequence is not null and the trimmed length is greater than 0.

@NotEmpty

Can be used for Collection to check whether the annotated element is not null or empty.

@Size(min,max)

Checks if the annotated element’s size is between min and max (inclusive).

@Max(value)

Checks whether the annotated value is less than or equal to the specified maximum.

@Min(value)

Checks whether the annotated value is higher than or equal to the specified minimum.

These annotations can be added to properties of domain class, for example:

Invoice.groovy
@Canonical(excludes='lineItems')
class Invoice {

    @NotBlank @Size(min=10, max=10)
    String number

    @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    LocalDate date

    @ElementCollection @OrderColumn @NotEmpty
    List<ItemFaktur> lineItems = []

}

5.1. Validation Method

You can use validate() method from injection enhanced artifact to validate an object. The first argument for this method is an object that will be validated. It is the only required argument. validate() returns true if requested object is successfully validated or false if it failed.

For example, the following code will print error message to console if invoice object is not valid:

// The following code assumes that controller is injection enhanced.
// You can also choose to enabled injection to other artifact such as repository.
if (controller.validate(invoice)) {
   println "invoice is valid."
} else {
   println "invoice is not valid."
}

The second parameter for validate() determine what validation group is in effect. The default value is Default group. To use a different group, you can pass the group class to validate(), for example:

Invoice.groovy
@Canonical(excludes='lineItems')
class Invoice {

    @NotBlank(groups=[FirstStep,SecondStep])
    @Size(min=10, max=10, groups=[FirstStep,SecondStep])
    String number

    @NotNull(groups=[FirstStep,SecondStep])
    @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    LocalDate date

    @ElementCollection @OrderColumn @NotEmpty(groups=[SecondStep])
    List<ItemFaktur> lineItems = []

}
// empty line items is acceptable and treated as valid
if (!controller.validate(invoice, FirstStep)) {
    println "first step validation for invoice is not valid!"
}

// now, empty line items is not valid
if (!controller.validate(invoice, SecondStep)) {
   println "second step validation for invoice is not valid!"
}

The third parameter accepts an instance of view model (Griffon’s model). Use this to present error in presentation layer by highlighting related view’s component. See the next section for more information about error notification.

5.2. Notification In View

To help you in displaying information about failed validation, simple-jpa injects the following members to all view models (model in MVC group):

Table 5. Validation Members Available in View Model
Members Declaration Description

def errors = new ConcurrentHashMap()

This is a bindable map that stores validation messages.

boolean hasError()

Return true if errors is not empty.

All builder’s nodes also has a special errorPath attribute that can be used to represent associated validation path (like property name of validated objects). If errors contains key equals to errorPath, the respective component will be highlighted.

For example, the following code will highlight the first JTextField:

MyView.groovy
application(pack: true) {
    flowLayout()
    textField(columns: 20, errorPath: 'firstName')
    textField(columns: 20, errorPath: 'lastName')
}
MyController.groovy
class MyController {

    def model
    def view

    void mvcGroupInit(Map args) {
       model.errors['firstName'] = 'First name is not valid!'
    }

}
highlight component on error

When you start typing on the highlighted JTextField, its red background will disappear. This also remove existing entry in model.errors.

By default, simple-jpa use red background to notify failed validation. You can also create your own notification method by extending ErrorNotification. For example, the following implementation of ErrorNotification will highlight border on error:

MyErrorNotification.groovy
package main

import java.awt.Color
import javax.swing.*
import javax.swing.border.*
import simplejpa.validation.*

class MyErrorNotification extends ErrorNotification {

  Border highlightBorder = BorderFactory.createLineBorder(Color.RED)
  Border normalBorder

  public MyErrorNotification(JComponent node, ObservableMap errors, String errorPath) {
    super(node, errors, errorPath)
    normalBorder = node.getBorder()
  }

  void performNotification() {
    if (errors[errorPath]) {
      node.setBorder(highlightBorder)
    } else {
      node.setBorder(normalBorder)
    }
  }

}

To assign MyErrorNotification to a single component, you can use errorNotification attribute:

textField(columns: 20, errorPath: 'firstName', errorNotification: main.MyErrorNotification)
border component on error

To use MyErrorNotification on all components, add the following line to Config.groovy:

griffon.simplejpa.validation.defaultErrorNotificationClass = 'main.MyErrorNotification'

The component responsible for removing error message is called ErrorCleaner. The following table lists all implementations of ErrorCleaner by default:

Table 6. Default Implementation of ErrorCleaner
Component Implementation Activation Condition

JTextField

JTextFieldErrorCleaner

When user typed in text box.

JComboBox, JCheckBox

JComboBoxErrorCleaner

When user changed selection.

JButton, JRadioButton

JRadioButton

When user clicked the button.

JXDatePicker

JXDatePickerErrorCleaner

When user changes date.

simplejpa.swing.DateTimePicker

DateTimePickerErrorCleaner

When user changes selection.

simplejpa.swing.TagChooser

TagChooserErrorCleaner

When user changes selection.

You can create your own implementation by creating a new class that implements ErrorCleaner. For example, the following implementation removes error notification after one second:

MyErrorCleaner.groovy
package main

import simplejpa.validation.*
import javax.swing.*
import java.awt.event.ActionListener

class MyErrorCleaner implements ErrorCleaner {

    void addErrorCleaning(JComponent component, ObservableMap errors, String errorPath) {
        new Timer(1000, {
            errors.remove(errorPath)
        } as ActionListener).start()
    }

}

To register this ErrorCleaner for all JTextField, you can add the following line to Config.groovy:

griffon.simplejpa.validation.errorCleaners = [
  'javax.swing.JTextField': 'main.MyErrorCleaner'
]

If you want to apply the new ErrorCleaner to all components, use '*' instead of 'javax.swing.JTextField as key:

griffon.simplejpa.validation.errorCleaners = [
  '*': 'main.MyErrorCleaner'
]

5.3. Validation Message

If validation is failed, simple-jpa stores validation message for the failed paths in model.errors. You can use values stored in model.errors to determine which paths have been failed and what are their corresponding messages. You can also change the default validation message by editing griffon-app\i18n\ValidationMessage.properties.

The following code shows how to retrieve validation messages:

MyModel.groovy
import groovy.beans.Bindable
import org.hibernate.validator.constraints.NotBlank

class MyModel {

   @NotBlank String firstName

   String lastName

}
MyView.groovy
application(pack: true) {
    flowLayout()
    textField(columns: 20, text: bind('firstName', target: model), errorPath: 'firstName')
    textField(columns: 20, text: bind('lastName', target: model), errorPath: 'lastName')
    button('Save', actionPerformed: controller.save)
}
MyController.groovy
class MyController {

    def model
    def view

    def save = {
        if (validate(model)) {
            println 'Your input have been validated and passed.'
        } else {
            println 'Validation failed!'
            model.errors.each { k, v ->
                println "$k $v"
            }
            // This will display the following error message
            // if first name is blank:
            //
            // Validation failed!
            // firstName may not be empty
            //
        }
    }

}

If you want to display validation mesage as JLabel in view, you can use errorLabel() node. For example, you can change the previous view into:

MyView.groovy
application(pack: true) {
    flowLayout()
    textField(columns: 20, text: bind('firstName', target: model), errorPath: 'firstName')
    errorLabel(path: 'firstName')
    textField(columns: 20, text: bind('lastName', target: model), errorPath: 'lastName')
    errorLabel(path: 'lastName')
    button('Save', actionPerformed: controller.save)
}
error label

errorLabel() is only visible if model.errors contains a key equals to its errorPath.

5.4. Miscellaneous

In some cases, validation may be failed because JTextField value in model is an empty String rather than null value. To ensure consistent behaviour, simple-jpa can be configured to translate all empty String into null value before performing validation. This feature is disabled by default. To enable it, add the following line to Config.groovy:

griffon.simplejpa.validation.convertEmptyStringToNull = true

6. View

To assist you in building presentation layer, simple-jpa provides several custom nodes that are typically used in business application. They can only be used if your presentation layer is Swing based and you are using Groovy’s SwingBuilder (default in Griffon).

6.1. Custom Text Fields

Use numberTextField() to bind text component with numeric property in view model. For example:

MyModel.groovy
class MyModel {

   @Bindable Long amount

}
MyView.groovy
application(pack: true) {
    flowLayout()
    numberTextField(columns: 20, bindTo: 'amount')
    button('Process', actionPerformed: { println model.amount })
}

If you want to bind to a BigDecimal property in view model, you need to use decimalTextField(). For example:

MyModel.groovy
class MyModel {

   @Bindable Long amount

}
MyView.groovy
application(pack: true) {
    flowLayout()
    decimalTextField(columns: 20, bindTo: 'amount')
    button('Process', actionPerformed: { println model.amount })
}

Both numberTextField() and decimalTextField() generates a JFormattedTextField. The default formatter used by them is DecimalFormat.getNumberInstance(). You can use different formatter by using one of 'currency', 'percent', or 'integer' as value for type attribute. For example:

MyView.groovy
application(pack: true) {
    flowLayout()
    label('Default')
    decimalTextField(columns: 20, bindTo: 'amount')
    label('Currency')
    decimalTextField(columns: 20, bindTo: 'amount', type: 'currency')
    label('Percent')
    decimalTextField(columns: 20, bindTo: 'amount', type: 'percent')
    label('Integer')
    decimalTextField(columns: 20, bindTo: 'amount', type: 'integer')
    button('Process', actionPerformed: { println model.amount })
}
number format

You can also change the property of current formatter by using attribute name that starts with nf and followed by formatter’s property name, for example:

MyView.groovy
application(pack: true) {
    flowLayout()
    label('Default')
    decimalTextField(columns: 20, bindTo: 'amount')
    label('Custom Formatter')
    decimalTextField(columns: 20, bindTo: 'amount',
      nfMinimumFractionDigits: 3, nfMinimumIntegerDigits: 5)
    button('Process', actionPerformed: { println model.amount })
}
custom formatter

If you want a MaskFormatter, you can use maskTextField():

MyView.groovy
application(pack: true) {
    flowLayout()
    label('Default')
    maskTextField(columns: 20, mask: 'AAA-#####-##', bindTo: 'identifier')
    button('Process', actionPerformed: { println model.identifier })
}
mask text field

In the sample above, mask 'AAA-####-##' allows user to input number of letter for the first three characters. The rests should be number.

6.2. Date Time Picker

You can use dateTimePicker() to allow user to input date or time (or both). This component should be binded to date data type from Joda Time library. If you don’t need time input or binding to property that uses Joda Time type, you can just use JXDatePicker from SwingX instead of this component.

dateVisible or timeVisible of this component determine which input is visible. By default, both values are true.

MyView.groovy
application(pack: true) {
    flowLayout()
    label('Date Only: ')
    dateTimePicker(dateVisible: true, timeVisible: false)
    label('Time Only:')
    dateTimePicker(dateVisible: false, timeVisible: true)
    label('Both:')
    dateTimePicker()
}
date time picker

dateTimePicker() offers several bindable properties:

Table 7. Bindable Properties In dateTimePicker()
Property Name Description

localDate

Current value as LocalDate.

localDateTime

Current value as LocalDateTime.

dateTime

Current value as DateTime.

dateMidnight

Current value as DateMidnight.

localTime

Current value as LocalTime.

In most cases, you will need only one of those properties, but you can also bind to multiple properties to get the same value in different types:

MyModel.groovy
import org.joda.time.*

class MyModel {

   @Bindable LocalDate myLocalDate
   @Bindable LocalDateTime myLocalDateTime
   @Bindable DateMidnight myDateMidnight

}
MyView.groovy
application(pack: true) {
    flowLayout()
    dateTimePicker(
      localDate: bind('myLocalDate', target: model),
      localDateTime: bind('myLocalDateTime', target: model),
      dateMidnight: bind('myDateMidnight', target: model)
    )
    label('myLocalDate is ')
    label(text: bind {model.myLocalDate})
    label('myLocalDateTime is ')
    label(text: bind {model.myLocalDateTime})
    label('myDateMidnight is ')
    label(text: bind {model.myDateMidnight})
}
date time picker binding

dateTimePicker() internally uses JXDatePicker for date entry and JSpinner time entry. They are exposed as properties called datePicker and timeSpinner.

If you want to perform a code everytime date or time is changed, you can add a closure to selectedValueChanged property:

MyView.groovy
application(pack: true) {
    flowLayout()
    dateTimePicker(
      localDateTime: bind('myLocalDateTime', target: model),
      selectedValueChanged: {
          println "Current value is ${model.myLocalDateTime.toString('dd-MM-YYYY hh:mm')}"
      }
    )
}

6.3. Table

Table is one of the most important components in business application. That is why simple-jpa provides its own custom JTable called glazedTable(). It is designed to be binded to EventList from Glazed Lists library. If you don’t use Glazed Lists, you can just use the default table() instead of glazedTable().

glazedTable() should contains one or more glazedColumn() to represent every columns in table. Every glazedColumn() will display property or execute method of the object contained in the binded EventList.

For example, lets assume you have created the following domain classes:

Invoice.groovy
package domain

// import statements...

@DomainClass @Entity @Canonical
class Invoice {

        @NotEmpty @Size(min=5, max=5)
        String number


        @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
        LocalDate date

        @ElementCollection @OrderColumn @NotEmpty
        List<LineItem> items = []

        void add(LineItem item) {
                items << item
        }

        BigDecimal total() {
                items.sum { it.total() }
        }

}
LineItem.groovy
package domain

// import statements...

@Embeddable @Canonical
class LineItem {

        @NotEmpty @Size(min=2, max=50)
        String name

        @NotNull @Min(0l)
        BigDecimal price

        @NotNull @Min(1l)
        BigDecimal qty

        BigDecimal total() {
                price * qty
        }

}

A very simple use case of glazedTable() will be:

MyModel.groovy
import org.joda.time.*
import ca.odell.glazedlists.BasicEventList

class MyModel {

   BasicEventList myTable = new BasicEventList()

}
MyController.groovy
import domain.*
import org.joda.time.*

class MyController {

    def model
    def view

    void mvcGroupInit(Map args) {
        Invoice invoice1 = new Invoice('INV01', LocalDate.parse('2015-01-01'))
        invoice1.add(new LineItem('Product1', 100, 1))
        invoice1.add(new LineItem('Product2', 200, 2))

        Invoice invoice2 = new Invoice('INV02', LocalDate.parse('2015-01-02'))
        invoice2.add(new LineItem('Product3', 300, 3))
        invoice2.add(new LineItem('Product4', 400, 4))

        execInsideUISync {
            model.myTable << invoice1
            model.myTable << invoice2
        }
    }

}
MyView.groovy
application(pack: true) {
    borderLayout()
    scrollPane(constraints: CENTER) {
      glazedTable(list: model.myTable) {
        glazedColumn(name: 'Invoice Number', property: 'number')
        glazedColumn(name: 'Date', property: 'date')
      }
    }
}
simple glazed table

As you can see, name attribute of glazedColumn() set column’s caption and property attribute determine the column’s value. The following table lists all attributes for glazedColumn():

Table 8. Attributes for glazedColumn()
Name Description

name

Set column caption.

expression or exp

A closure that will be executed to evaluate every row in this column.

property

A string to represent property that will be displayed for every row in this column.

columnClass

A Class to represent the columns’ class.

comparator

A Comparator to determine how sorting was done for this column.

visible

A boolean value to determine if this column should be displayed or not. This attribute is bindable.

glazedColumn() is an instance of TableColumn. This means you can also set all of TableColumn public properties such as modelIndex, width, cellEditor, and cellRenderer in glazedColumn().

To retrieve column’s value based on custom calculation rather than property name, you can set a closure for expression attribute. You can also use short name exp instead of expression. Inside this closure, it refer to the object for current row.

For example, you can add third column which values are retrieved from Invoice.total() method:

MyView.groovy
application(pack: true) {
    borderLayout()
    scrollPane(constraints: CENTER) {
      glazedTable(list: model.myTable) {
        glazedColumn(name: 'Invoice Number', property: 'number')
        glazedColumn(name: 'Date', property: 'date')
        glazedColumn(name: 'Total', exp: { it.total() })
      }
    }
}
glazed column expression

You can change the size of a column by using its width property. If you pass a number to width, it will set minWidth, preferredWidth and maxWidth of current TableColumn to the same value. This means user won’t be able to resize the column. You can set the value for each of minWidth, preferredWidth and maxWidth by passing a List to width.

MyView.groovy
application(pack: true) {
    borderLayout()
    scrollPane(constraints: CENTER) {
      glazedTable(list: model.myTable) {
        glazedColumn(name: 'Invoice Number', property: 'number', width: 50)
        glazedColumn(name: 'Date', property: 'date', width: [50, 100])
        glazedColumn(name: 'Total', exp: { it.total() }, width: [50, 100, 200])
      }
    }
}
glazed column width

In the table above, first column is fixed and can’t be resized. The second column can be resized to a minimum 50 pixel and there is no limit for its maximum size. The third column can’t be resized to a size more than 200 pixels.

glazedColumn() can accept instance of DefaultTableHeaderRenderer, TableCellRenderer or TableCellEditor as child nodes. The common use case is to pass a TableCellRenderer to glazedColumn() in order to format the presentation of this column. You can use templateRenderer() to create an instance of TableCellRenderer that supports simple-jpa template renderer expression. See Template Renderer for more information about template renderer expression.

For example, you can add uppercase format to the first column, date format to the second column and currency format to the third column as in the following view:

MyView.groovy
application(pack: true) {
    borderLayout()
    scrollPane(constraints: CENTER) {
      glazedTable(list: model.myTable) {
        glazedColumn(name: 'Invoice Number', property: 'number') {
          templateRenderer('this:upperCase')
        }
        glazedColumn(name: 'Date', property: 'date') {
          templateRenderer(exp: {it?.toString('dd-MM')?:'-'})
        }
        glazedColumn(name: 'Total', exp: { it.total() }) {
          templateRenderer('this:currencyFormat')
        }
      }
    }
}
glazed column renderer
Why add templateRenderer while you can convert all of your column to string by using exp in glazedColumn? Converting all values to string is simpler, but you will loose natural ordering when sorting a column. If you use templateRenderer, sorting a column is not performed by comparing formatted value but instead it is based on the original (unformatted) value.

Because templateRenderer() generates an instance of JLabel, you can set public properties of resulting JLabel by using their name as attribute in templateRenderer(). For example, to right align the third column, you can use the following code:

MyView.groovy
import static javax.swing.SwingConstants.*

application(pack: true) {
    borderLayout()
    scrollPane(constraints: CENTER) {
      glazedTable(list: model.myTable) {
        glazedColumn(name: 'Invoice Number', property: 'number')
        glazedColumn(name: 'Date', property: 'date') {
          templateRenderer(exp: {it?.toString('dd-MM-YYYY')?:'-'})
        }
        glazedColumn(name: 'Total', exp: { it.total() }, columnClass: Integer) { (1)
          templateRenderer('this:currencyFormat', horizontalAlignment: RIGHT)    (2)
        }
      }
    }
}
1 This causes column header to be right aligned.
2 Right align every cell for this column.
glazed column alignment

templateRenderer also supports custom condition that set its property based on some conditions. For example, the following view will set font color for third column to red if its value is less than $1000:

MyView.groovy
import static javax.swing.SwingConstants.*
import java.awt.Color

application(pack: true) {
    borderLayout()
    scrollPane(constraints: CENTER) {
      glazedTable(list: model.myTable) {
        glazedColumn(name: 'Invoice Number', property: 'number')
        glazedColumn(name: 'Date', property: 'date') {
          templateRenderer(exp: {it?.toString('dd-MM-YYYY')?:'-'})
        }
        glazedColumn(name: 'Total', exp: { it.total() }, columnClass: Integer) {
          templateRenderer('this:currencyFormat', horizontalAlignment: RIGHT) {
            condition(if_: {it < 1000}, then_property_: 'foreground', is_: Color.RED, else_is_: Color.BLACK)
            condition(if_: {isSelected}, then_property_: 'foreground', is_: Color.WHITE)
          }
        }
      }
    }
}
glazed column conditional
Using condition() inside templateRenderer() is useful for simple condition. If you have a complex rendering calculation, consider creating your own implementation of TableCellRenderer.

In addition to glazedColumn(), glazedTable() also accepts menuItem() as child node. This will add new menu when user right click on the table. By default, popup menu for glazedTable() consists of only two menu items: copy cell and print. The following is a sample view that add new popup menus:

MyView.groovy
import static javax.swing.SwingConstants.*

actions {
  action(id: 'menu1', name: 'Menu 1', closure: { println 'Menu1 is selected' })
  action(id: 'menu2', name: 'Menu 2', closure: { println 'Menu2 is selected' })
}

application(pack: true) {
    borderLayout()
    scrollPane(constraints: CENTER) {
      glazedTable(list: model.myTable) {
        glazedColumn(name: 'Invoice Number', property: 'number')
        glazedColumn(name: 'Date', property: 'date') {
          templateRenderer(exp: {it?.toString('dd-MM-YYYY')?:'-'})
        }
        glazedColumn(name: 'Total', exp: { it.total() }, columnClass: Integer) {
          templateRenderer('this:currencyFormat', horizontalAlignment: RIGHT)
        }
        menuItem(action: menu1)
        menuItem(action: menu2)
      }
    }
}
glazed table popupmenu

The following table lists attributes that can be used in glazedTable():

Table 9. Attributes for glazedTable()
Name Type Description

list

EventList

Source value for this table.

sortingStrategy

ca.odell.glazedlists.impl.gui.SortingStrategy

Sorting strategy for this table. If list is not a SortedList and sortingStrategy is not defined, list will be converted into a SortedList.

onValueChanged

Closure

This closure will be executed if table selection is changed.

isRowSelected

Boolean

true is row in table is selected. This property is bindable.

isNotRowSelected

Boolean

true is nothing is selected in table. This property is bindable.

tableFormat

GlazedTableFormat

GlazedTableFormat used by TableModel in this table.

doubleClickAction

Action

Action that will be triggered when user double click a row in this table.

enterKeyAction

Action

Action that will be triggered when user press Enter key while row is selected in this table.

glazedTable() creates an instance of JTable so you can also set any public properties of JTable as attribute in glazedTable().

6.4. Template Renderer

simple-jpa a new templateRenderer attribute to comboBox() and list() node to allow you customize renderer without creating a new renderer class for every components. templateRenderer() in glazedColumn() also uses the same thing.

templateRenderer attribute accepts a closure or string. If templateRenderer is a closure, it will be executed and the result is displayed. Inside the closure, it refers to original value. You can also call the following built-in functions from inside this closure:

Table 10. Built-in functions in templateRenderer
Function Description

floatFormat(v, d)

Format a number using number style and limit fraction digits to d digits.

numberFormat(v)

Format a number using number style.

percentFormat(v)

Format a number using percent style.

currencyFormat(v)

Format a number using currency style.

lowerCase(v)

Change string to lower case.

upperCase(v)

Change string to upper case.

titleCase(v)

Change string to title case.

For example, the following code show how objects are displayed in JComboBox and JList without using templateRenderer:

MyModel.groovy
import ca.odell.glazedlists.*
import ca.odell.glazedlists.swing.*

class LatihanModel {

   BasicEventList myList = new BasicEventList()
   DefaultEventComboBoxModel listModel = new DefaultEventComboBoxModel(myList)

}
MyController.groovy
import domain.*
import org.joda.time.*

class LatihanController {

    def model
    def view

    void mvcGroupInit(Map args) {
        Invoice invoice1 = new Invoice('inv01', LocalDate.parse('2015-01-01'))
        invoice1.add(new LineItem('Product1', 100, 1))
        invoice1.add(new LineItem('Product2', 200, 2))

        Invoice invoice2 = new Invoice('inv02', LocalDate.parse('2015-01-02'))
        invoice2.add(new LineItem('Product3', 300, 3))
        invoice2.add(new LineItem('Product4', 400, 4))

        execInsideUISync {
            model.myList << invoice1
            model.myList << invoice2
        }
    }

}
MyView.groovy
application(pack: true) {
    flowLayout()
    comboBox(model: model.listModel)
    list(model: model.listModel)
}
normal combobox and list

Here is what it looks like after adding templateRenderer in view:

MyView.groovy
application(pack: true) {
    flowLayout()
    comboBox(model: model.listModel, templateRenderer: { it.number })
    list(model: model.listModel, templateRenderer: { "${it.number} Qty: ${it.items.size()}" })
}
combobox and list with template renderer

You can also pass a string to templateRenderer. The string can be a property name or method call. If it is a method call, it should be starts with '#' character. You can also call built-in functions by adding ':' followed by a function name.

MyView.groovy
import static javax.swing.SwingConstants.*

application(pack: true) {
    borderLayout()
    scrollPane(constraints: CENTER) {
      list(model: model.listModel, templateRenderer: "#total:currencyFormat")
    }
}
combobox and list with string template renderer

6.5. Dialog Utils

simple-jpa provides mvcPopupButton() as a helper node to create a JButton that will display a modal dialog if it is clicked. The following steps will be performed if mvcPopupButton() is clicked:

  • Create a new temporary MVC group specified by its mvcGroup attribute. If args is closure, execute args and pass it as arguments for the new MVC group.

  • If onBeforeDisplay is specified, call it. The generated button and args will be passed as arguments for onBeforeDisplay.

  • Find a JPanel called mainPanel in view and display it in a modal dialog. It is a convension that all main panel should be named mainPanel.

  • If onFinish is specified, when user closed the modal dialog, onFinish will be executed. Temporary model, view, and controller is passed as arguments for onFinish.

The following code show how to use mvcPopupButton():

mvcPopupButton(text: 'Click Me!', mvcGroup: 'anotherMVCGroup', dialogProperties:
  [title: 'New Dialog', size: new Dimension(900,420)], onFinish: { m, v, c ->
     println m.result
  }
)

mvcPopupButton() relies to DialogUtils to create modal dialog. The following is lists of all available methods in DialogUtils:

  • showMVCGroup(MVCGroup mvcGroup, GriffonView view, Map dialogProperties = [:], LayerUI layerUI = null, Closure onFinish = null)

    Use this method to display view from existing MVC group instance in modal dialog.

  • showMVCGroup(String mvcGroupName, Map args = [:], GriffonView view, Map dialogProperties = [:], LayerUI layerUI, Closure onFinish = null)

    This method creates a temporary MVC group instance and display its view in modal dialog. Use this method if you need to pass an LayerUI.

  • showMVCGroup(String mvcGroupName, Map args = [:], GriffonView view, Map dialogProperties = [:], Closure onFinish = null)

    This method creates a temporary MVC group instance and display its view in modal dialog.

    Example:

    def args = [parentList: model.items]
    def props = [title: 'Items']
    DialogUtils.showMVCGroup('lineItemAsChild', args, view, props) { m, v, c ->
      model.items.clear()
      model.items.addAll(m.lineItemList)
    }
  • showAndReuseMVCGroup(String mvcGroupName, Map args = [:], GriffonView view, Map dialogProperties = [:], LayerUI layerUI = null, Closure onFinish = null)

    This method creates a new MVC group instance and display its view in modal dialog. The created MVC group instance won’t be destroyed.

  • confirm(Component parent, String message, String title, int messageType = JOptionPane.QUESTION_MESSAGE)

    Display a confirmation message and returns true if user confirms the dialog. This method always run in EDT.

    Example:

    if (!DialogUtils.confirm(view.mainPanel, 'Do you want to continue?', 'Delete Confirmation')) {
        return
    }
  • message(Component parent, String message, String title, int messageType = JOptionPane.INFORMATION_MESSAGE)

    Display message dialog. This method always run in EDT.

    Example:

    DialogUtils.message(null, errorMessage, 'Error', JOptionPane.ERROR_MESSAGE)
  • input(Component parent, String message, String title, int messageType = JOptionPane.QUESTION_MESSAGE)

    Display input dialog and return user input as string. This method always run in EDT.

    Example:

    String updatedScore = DialogUtils.input(view.mainPanel, 'Please Enter your new score:', 'New Score')

7. Transaction

simple-jpa uses EntityManager per transaction strategy. A new EntityManager is created when transaction begins and it is destroyed when transaction is finished. Using EntityManager in Griffon is a bit tricky since EntityManager is not thread-safe while Griffon may execute code in different thread. To solve this problem, simple-jpa creates a new EntityManager for each different threads.

7.1. @Transaction Annotation

You can use @Transaction annotation to wrap a method in transaction. This annotation should only be used on artifacts that have been injected by Persistence Methods. Using @Transaction on artifacts that don’t have persistence methods will raise errors.

The following code show the basic usage of @Transaction:

class MyRepository {

  @Transaction
  void register() {
   ...
  }

}

You can also apply @Transaction to all methods in the class by simply add the annotation at class level.

@Transaction
class MyRepository {

  void register() {
     ...
  }

  void unregister() {
     ...
  }

}

@Transaction accepts a value that can be Policy.NORMAL or Policy.SKIP.

Policy.NORMAL (default value) supports nested transaction. If methodA() calls methodB() and both methods are annotated by @Transaction, they are executed in the same transaction. If one of methodA() and methodB() raises an Exception, none of their queries will be committed.

MyRepository.groovy
@Transaction
class MyRepository {

    void methodA() {
        MyEntity entityA = new MyEntity('Entity A')
        persist(entityA)
        methodB()
    }

    void methodB() {
        MyEntity entityB = new MyEntity('Entity B')
        persist(entityB)
        throw new RuntimeException('Something fails here!')
    }

}
MyController.groovy
class MyController {

    MyRepository myRepository

    def test = {
        // Both entityA and entityB are not persisted to database
        // because methodB() fails with Exception.
        myRepository.methodA()
    }

}

Nested transaction only work if methods are called from within the same thread. If methodB() is executed by using method such as app.execFuture() or app.eventAsync(), simple-jpa can’t guarantee it will be part of caller transaction. This is especially true if thread pool is involved.

MyRepository.groovy
@Transaction
class MyRepository {

    def app

    void methodA() {
        MyEntity entityA = new MyEntity('Entity A')
        persist(entityA)
        app.execFuture { methodB() }
    }

    void methodB() {
        MyEntity entityB = new MyEntity('Entity B')
        persist(entityB)
        throw new RuntimeException('Something fails here!')
    }

}
MyController.groovy
class MyController {

    MyRepository myRepository

    def test = {
        // entityA is persisted to database but
        // entityB is *NOT* persisted to database!
        myRepository.methodA()
    }

}
To avoid unexpected random errors such as duplicate primary key or missing entity, don’t use multithreading in persistence layer!

Policy.SKIP is used to tell simple-jpa to not apply transaction AST transformation to the annotated method. This can be useful if you use @Transaction at class level and want to avoid wrapping few methods in transaction.

MyController.groovy
@Transaction
class MyController {

    MyRepository myRepository

    def create = {
        // Calling this closure will start a new transaction
        // or continue existing transaction if caller is in transaction.
    }

    def remove = {
        // Calling this closure will start a new transaction
        // or continue existing transaction if caller is in transaction.
    }

    @Transaction(Transaction.Policy.SKIP)
    def calculate = {
        // Calling this closure will *NOT* start a new transaction!
        // If caller is in transaction, it will be part of that transaction.
    }

}

7.2. Transaction Methods

You can also create transaction without using @Transaction annotation. This can be done by calling transaction methods manually. Because transaction methods is part of persistence methods, they are only available in injected artifacts. The following is list of transaction methods:

  • beginTransaction()

    Use this method to mark the beginning of new transaction.

  • commitTransaction()

    Use this method to commit current transaction. This method will destroys EntityManager if current transaction is not nested.

  • rollbackTransaction()

    Use this method to rollback current transaction. This method will destroys EntityManager.

  • withTransaction(closure)

    Execute closure inside transaction. Inside this closure, you can directly call persistence methods.

For example, if controllers are injected with persistence methods, you can use the following code to create new transaction:

MyController.groovy
class MyController {

  def save = {
    beginTransaction()
      try {
        ... // perform works here
        commitTransaction()
      } catch (Exception ex) {
        rollbackTransaction()
      }
  }

}

withTransaction() allows you to wrap a closure inside transaction. For example, you can replace the previous sample code into:

MyController.groovy
class MyController {

  def save = {
    withTransaction {
      ... // perform works here
    }
  }

}

You can always call persistence methods directly inside the closure. This is very useful if you call withTransaction from another class, for example:

class MyDomainClass {

   MyRepository myRepository

   public BigDecimal calculate() {
      ...
      myRepository.withTransaction {
         ...
         executeQuery("FROM Invoice i WHERE i.dueDate > :date", aDate)
         ...
      }
      ...
   }

}

8. Auditing

8.1. Auditable Domain Class

Classes annotated by @DomainClass will automatically have the following auditing properties:

Table 11. Auditing Properties In Domain Class
Name Type

createdDate

java.util.Date

createdBy

String

modifiedDate

java.util.Date

modifiedBy

String

To disable automatic creation of auditing properties for a domain class, use excludeAuditing property. For example:

@Entity @DomainClass(excludeAuditing=true) @Canonical
class Customer {

}

The value for auditing properties are set by JPA entity listener simplejpa.AuditingEntityListener. create-simple-jpa command by default will create griffon-app/conf/metainf/orm.xml to register simplejpa.AuditingEntityListener as default entity listener. The default content of generated orm.xml is:

griffon-app/conf/metainf/orm.xml
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings version="2.0" xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd">

<persistence-unit-metadata>
    <persistence-unit-defaults>
        <entity-listeners>
            <entity-listener class="simplejpa.AuditingEntityListener" />
        </entity-listeners>
    </persistence-unit-defaults>
</persistence-unit-metadata>

</entity-mappings>
You can completely disable auditing by removing the line that register simplejpa.AuditingEntityListener in orm.xml.
You can also register your own custom JPA Entity Listener in orm.xml.

Built-in scaffolding’s generator will generate views that has auditing properties in them. By default, generated views display java.util.Date by its string representation such as Wed Aug 20 13:09:11 ICT 2014. To display java.util.Date by using a custom formatter, style for both date and time can be specified in Config.groovy:

Config.groovy
griffon.simplejpa.scaffolding.dateTimeStyle = 'MEDIUM'

The configuration above will generate the following code in controller:

def selectionChanged = {
   ...
   model.created = selected.createdDate?DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(selected.createdDate):null
   model.modified = selected.modifiedDate?DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(selected.modifiedDate):null
   ...
}

The following is list of available styles (see http://docs.oracle.com/javase/tutorial/i18n/format/dateFormat.html for more information):

Table 12. Possible Values for Date Time Style Configuration
Style Sample Value in U.S. Locale Sample Value in French Locale

DEFAULT

Jun 30, 2009 7:03:47 AM

30 juin 2009 07:03:47

SHORT

6/30/09 7:03 AM

30/06/09 07:03

MEDIUM

Jun 30, 2009 7:03:47 AM

30 juin 2009 07:03:47

LONG

June 30, 2009 7:03:47 AM PDT

30 juin 2009 07:03:47 PDT

FULL

Tuesday, June 30, 2009 7:03:47 AM PDT

mardi 30 juin 2009 07 h 03 PDT

You can always change the generated code to use any custom formatter.

8.2. Logged User

simple-jpa expects the value of current user is stored in SimpleJpaUtil.user. Value for this property must be an an implementation of AuditableUser. For example, developer can create a domain class that implement AuditableUser such as:

User.groovy
package domain

import ...

@DomainClass @Entity @Canonical
class User implements AuditableUser {

    @NotEmpty @Size(min=2, max=50)
    String name

    @Override
    String getUserName() {
        return name
    }

}

The following code can be used to set current (logged) user:

User user = new User('guest')
SimpleJpaUtil.instance.user = user

The code above will set createdBy and modifiedBy of new entity or updated entity to 'guest'.

You’re not required to set current user. If SimpleJpaUtil.user is null, value for createdBy and modifiedBy will be ignored. Regardless whether SimpleJpaUtil.user is available or not, createdTime and modifiedTime are always updated.

8.3. Login Dialog

simple-jpa can display a login dialog by using SwingX JXLoginPane at startup time. To enable this feature, a service that is derived from org.jdesktop.swingx.auth.LoginService is required. The following is typical steps to create such service:

  1. Create a new service by using Griffon’s create-service command. For example:

    griffon create-service DatabaseLoginService
  2. Change the generated service (in griffon-app/services folder) and extends it from org.jdesktop.swingx.auth.LoginService. For example:

    DatabaseLoginService.groovy
    package sample
    
    import domain.User
    import org.jdesktop.swingx.auth.LoginService
    import simplejpa.SimpleJpaUtil
    
    class DatabaseLoginService extends LoginService {
    
        @Override
        boolean authenticate(String name, char[] password, String server) throws Exception {
            if (name == 'jocki' && new String(password) == 'toor') {
                SimpleJpaUtil.instance.user = new User('jocki')
                return true
            }
            false
        }
    
    }

    If you add @Transaction to DatabaseLoginService and add the following line to Config.groovy (See [finders-2] for more information):

    Config.groovy
    griffon.simplejpa.finders.injectInto = ['controller', 'service']

    You will have access to simple-jpa dynamic finders in authenticate() method. Typically, you will query the requested user from database and check if user’s password is correct.

  3. Add the service name as value for griffon.simplejpa.auditing.loginService in Config.groovy. For example:

    Config.groovy
    griffon.simplejpa.auditing.loginService = 'DatabaseLoginService'

When application is started, the following login dialog will be displayed:

login dialog

If user cancels the dialog, simple-jpa will terminate the application. If user enters the correct credentials (which means LoginService.authenticate() returns true), simple-jpa will display startup group as usual.

9. Finders

Finders are part of persistence methods that are used to retrieve JPA entities from database. In addition to static methods, simple-jpa also supports dynamic finder. With dynamic finder, you can find entity based on certain pattern in method name.

9.1. Query Methods

simple-jpa injects the following query methods:

  • executeNamedQuery(namedQuery, map, config)

    Execute JPA named query.

  • executeQuery(jpql, config)

    Execute JP QL.

  • executeNativeQuery(sql)

    Execute SQL.

To use named queries, you must defined them first, for example:

Invoice.groovy
@NamedQuery(name='Invoice.LargeQty', query='''
        FROM Invoice inv LEFT JOIN inv.items i WHERE i.qty > :limit
''')
@DomainClass @Entity @Canonical
class Invoice {

        ...

}

Now you can call Invoice.LargeAmount by using code like:

def result = myRepository.executeNamedQuery('Invoice.LargeQty', [limit: 500])
println "Invoices with large qty in one of their items:"
println result.collect { it.number }.join(',')

Named queries are parsed when application is launched. Although they make application startup slower, they are quick to serve you when you need them because they’re prepared.

As an alternative to named query, you can always directly call JP QL by using executeQuery(), for example:

def result = myRepository.executeQuery(
   'FROM Invoice inv LEFT JOIN inv.items i  WHERE i.qty > :limit',
   [:], [limit: 500])
Don’t forget that you can use simple-jpa-console script to launch Groovy console to try query methods described in this chapter.

JPA can hurt performance if your query return a very large result. This is because JPA provider needs to translate every rows from native SQL query into object (entities). Creating a lot of objects in a short time may leads to memory shortage in heap. To avoid these problems, you can use executeNativeQuery() to execute SQL:

def result = myRepository.executeNativeQuery('''
   SELECT i.number, SUM(li.qty * li.price) AS total
   FROM Invoice i
   LEFT JOIN Invoice_Items li ON li.invoice_id = i.id
   GROUP BY i.number
''')
// result is an array
println result[0][0]  // print first column of first record
println result[0][1]  // print second column of first record
println result[1][0]  // print first column of second record
println result[1][1]  // print second column of second record
Don’t use executeNativeQuery() if not necessary. Your code become prone to mismatch between query and domain classes. If you find yourself create a lot of SQL queries, you need to consider to use native SQL approach instead of JPA.

9.2. Dynamic Finder

Dynamic finder works by following certain pattern in method name. For example, if you want to select all Invoice, you can call the following method:

List result = myRepository.findAllInvoice()
println result.collect { it.number }.join(',')

Finder starts with findAll always return a List. If nothing is found, it returns an empty List.

You can add a criteria by using adding by to the finder followed by property name, for example:

List result = myRepository.findAllInvoiceByNumber('INV02')
println result.collect { it.number }.join(',')

If you want to return only a single instance, drop all from the finder name, for example:

Invoice result = myRepository.findInvoiceByNumber('INV02')
println result?.number

The finder returns an instance of Invoice or null if nothing is found.

You can combine multiple criteria by using logical operator and and or, for example:

myRepository.findAllInvoiceByNumberAndDate('INV02', LocalDate.parse('2015-02-01'))

In the previous examples, you’ve searched based on equality (eq). simple-jpa dynamic finders also supports different operators such as:

  • greaterThanEqualTo or ge

  • lessThanEqualTo or le

  • greaterThan or gt

  • lessThan or lt

  • isNotMember

  • isNotEmpty

  • isNotNull

  • notEqual or ne

  • isMember

  • isEmpty

  • isNull

  • like

  • notLike

  • between

For example, to search for all invoices created in this month, you can use the following code:

def dateBegin = LocalDate.now().dayOfMonth().withMinimumValue()
def dateEnd = LocalDate.now().dayOfMonth().withMaximumValue()
def result = myRepository.findAllInvoiceByDateBetween(dateBegin, dateEnd)

In this another example, you find products by name using like operator:

myRepository.findAllProductByNameLike('A%')

You can search for nested properties by using double underscore (__) as separator. For example, the following query finds all products based on supplier’s location:

myRepository.findAllProductBySupplier__City__NameLike('a city%')

9.3. Dsl Finder

In addition to using dynamic finder, you can also perform query by using Dsl finder. Such finder requires a closure as argument. The following code show how to use Dsl Finder:

def dateBegin = LocalDate.now().dayOfMonth().withMinimumValue()
def dateEnd = LocalDate.now().dayOfMonth().withMaximumValue()
myRepository.findAllInvoiceByDsl {
    number eq('INV02')
    and()
    date between(dateBegin, dateEnd)
}

The advantage of using Dsl closure is you can build the query conditions based on certain condition. You can also add any code inside the closure. For example, the following code add query condition only if certain variables are not null:

def dateBeginSearch = LocalDate.now().dayOfMonth().withMinimumValue()
def dateEndSearch = LocalDate.now().dayOfMonth().withMaximumValue()
def numberSearch = null
myRepository.findAllInvoiceByDsl {
  if (numberSearch) {
    number eq(numberSearch)
  }
  if (dateBeginSearch && dateEndSearch) {
    date between(dateBeginSearch, dateEndSearch)
  }
}

You can also use nested properties by using double underscores (__) as separator, for example:

myRepository.findAllProductByDsl {
  supplier__name isIn(['supplier a', 'supplier b'])
  and()
  name eq('A product')
}

9.4. Named Entity Graph

Named entity graph is a new feature in JPA 2.1. It allows you to define a flexible fetch graph without modifying your current query. For example, you can define named entity graph such as:

Invoice.groovy
package domain

// import statements are not shown

@NamedEntityGraph(name='Invoice.Items', attributeNodes=[
  @NamedAttributeNode('items')
])
@DomainClass @Entity @Canonical(excludes='items')
class Invoice {

  @NotEmpty @Size(min=5, max=5)
  String number

  @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
  LocalDate date

  @ElementCollection @OrderColumn @NotEmpty
  List<LineItem> items = []

  void add(LineItem item) {
    items << item
  }

  BigDecimal total() {
    items.sum { it.total() }
  }

}

If you want to use named entity graph in simple-jpa finders, you should always start the name with domain class name followed by a period (.) before the actual name. To use named entity graph, add fetch or load to dynamic finder, for example:

// items is lazy loaded.  It will trigger SQL query when code refer to it.
// result is not safe to be passed to outside transaction because
// SQL query is required to retrieve items.
def result = myRepository.findAllInvoice()

// items has been loaded.
// anotherResult is safe to be passed to outside transaction because
// items is safe to read from anywhere.
def anotherResult = myRepository.findAllInvoiceFetchItems()

In addition to using fetch in dynamic finder, you can also use named entity graph by passing them query configuration using fetchGraph or loadGraph as key. See Query Configuration for more information.

9.5. Query Configuration

Most of finder methods described so far can receive a configuration parameter in form of Map. You can use the following values as its keys:

  • excludeSubclass

    The value must be a string. Normal query returns all instances of current class and its subclasses. For example, if Employee has three subclasses Teacher, Researcher, and Staff, then findAllEmploye() returns instances of Employee, Teacher, Researcher and Staff. If you want to return only Teacher and Staff, you can use the following code:

    myRepository.findAllEmployee([excludeSubclass: 'Researcher'])

    Assuming Employee is not an abstract class and it have instances, then you can use following code to only return instances of Employee:

    myRepository.findAllEmployee([excludeSubclass: '*'])
  • flushMode

    The value must be one of FlushModeType.COMMIT or FlushModeType.AUTO. Use this key to override flush mode for current query.

  • excludeDeleted

    The value is a boolean (true or false). If it is true, query will not return soft deleted entities. An entity is considered as soft deleted if its deleted property is equals to 'Y'.

  • orderBy

    The value must be a string. Use this key to sort current query. If you have multiple attributes to sort, separate them by comma, for example:

    myRepository.findAllInvoice([orderBy: 'number,date'])

    If you want to sort by nested property, use double underscore (__) as separator.

  • orderDirection

    The value must be one of the following string: asc or desc. This key is used to specify the direction of order in orderBy. For example, the following query will find all invoices sorted by number in ascending direction and date in descending direction:

    myRepository.findAllInvoice([orderBy: 'number,date', orderDirection: 'asc,desc'])
  • page

    The value must be a number starting from 1. Use this key together with pageSize to limit query result. Default value for page is 1.

  • pageSize

    The value must be a positive number. Use this key to set the size for current page. If page is specified but pageSize is not set, then it is assumed to be 10. If both page and pageSize is not specified, there will be no limit for this query.

    For example, to retrieve only 3 first invoices, you can use the following configuration:

    myRepository.findAllInvoice([pageSize:3])
  • allowDuplicate

    The value is a boolean (true or false). Set it to true to never returns duplicate entities. This configuration adds SELECT DISTINCT to current query.

  • fetchGraph

    This key accepts a String or an instance of EntityGraph created by using entityManager.createEntityGraph(). If you have defined named entity graph called Invoice.Items, you can use it in finders like:

    myRepository.findAllInvoice([fetchGraph: 'Invoice.Items'])

    Or, you can also build the named entity graph in runtime such as in this sample:

    myRepository.withTransaction {
        def em = getEntityManager()
        def g = em.createEntityGraph(Invoice)
        g.addAttributeNodes('items')
        findAllInvoice([fetchGraph: g])
    }
  • loadGraph

    This key accepts a String or an instance of EntityGraph created by using entityManager.createEntityGraph(). See the description of fetchGraph for more information.

You can also add some of the query configurations described above in Config.groovy. This way the configuration is globally applied to all finders that can accepts it. The following table lists all query configurations that can be added in Config.groovy:

Table 13. Query Configuration In Config.groovy
Name Default Value Description

griffon.simplejpa.entityManager.defaultFlushMode

FlushModeType.AUTO

You can also change the flush mode to FlushModeType.COMMIT (or just 'COMMIT').

griffon.simplejpa.finders.alwaysExcludeSoftDeleted

false

If this value is true, finders will not return soft deleted entities.

griffon.simplejpa.finders.alwaysAllowDuplicate

true

The default value always add SELECT DISTINCT to the generated query. If you don’t need this feature, you can increase performance by setting this value to false.

10. Testing

10.1. Unit Test

In unit test, you’re usually testing domain classes. This test should never requires database connection. Unfortunately, sometimes domain class may depends on persistence methods in repository, for example:

InvoiceRepository.groovy
class RegistrationRepository {

  // This methods hits database to retrieve last number and increase it by one.
  Integer nextNumber() {
    ...
  }

}
Registration.groovy
class Registration {

  String reserveNumber(Integer input) {
    ...
    def repo = SimpleJpaUtil.instance.repositoryManager.getRepository('RegistrationRepository')
    def number = repo.nextNumber()
    ...
  }

}

You can’t test Registration.reserveNumber() directly because it calls RegistrationRepository.nextNumber() that requires access to database. For example, the following unit test will fail:

RegistrationTests.groovy
class RegistrationTests extends GriffonUnitTestCase {

  void testReserveNumber() {
      Registration r = new Registration()
      assertEquals('REG-001', r.reserveNumber(1))
  }

}

To solve this problem, you need to create a stub repository, for example:

RegistrationTests.groovy
class RegistrationTests extends GriffonUnitTestCase {

  void setUp() {
    super.setUp()
    super.registerMetaClass(NumberingRepository)
    NumberingRepository.metaClass.reserveNumber = {
        // stub for reserveNumber() that always return 0.
        return 0
    }
    StubRepositoryManager stubRepositories = new StubRepositoryManager()
    stubRepositories.instances['NumberingRepository'] = new NumberingRepository()
    SimpleJpaUtil.instance.repositoryManager = stubRepositories
  }

  void testReserveNumber() {
    Registration r = new Registration()
    assertEquals('REG-001', r.reserveNumber(1))
  }

}

10.2. Integration Test

simple-jpa provides DbUnitTestCase to help you in performing integration testing that actually hits database. DbUnitTestCase uses DbUnit to populate databases with certain records for every test case. The following sample show a basic usage of DbUnitTestCase:

InvoiceTest.groovy
class InvoiceTest extends DbUnitTestCase {

  InvoiceRepository invoiceRepository

  protected void setUp() {
     super.setUp()
     setUpDatabase('/project/data.xlsx')
     invoiceRepository = SimpleJpaUtil.instance.repositoryManager.findRepository('Invoice')
  }

  void testDelete() {
     Invoice newInvoice = invoiceRepository.findInvoiceById(-1l)
     invoiceRepository.remove(newInvoice)
     newInvoice = invoiceRepository.findInvoiceById(-1l)
     assertNull(newInvoice)
  }

}

When you execute the test by using griffon test-app, setUpDatabase() is called before executing test methods. By default, setUpDatabase() performs a clean insert (DatabaseOperation.CLEAN_INSERT) on database. Tables contents are deleted first then new records are inserted based on the value in /project/data.xlsx. Because this process is repeated for every test methods, you can assume that database is in the expected state when test methods begin.

DbUnitTestCase supports the following format as data source:

  • Microsoft Excel binary format if filename ends with .xls. Every sheets represents a table. Rows are table records and columns are table columns.

  • Microsoft Excel XML format (the newer version) if filename ends with .xlsx.

  • XML file if filename ends with .xml.

  • If DbUnitTestCase can’t decide the type of data source, it assumes the data source is in CSV format.

DbUnitTestCase also executes the following SQL files if it founds them in root package:

  • before.sql is executed before cleaning and executing test method.

  • clean.sql is executed before inserting data to database.

  • after.sql is executed after database has been populated. Test method begins after this script is executed.

To avoid problem related to foreign key constraints, you can drop your table manually in before.sql or clean.sql in the correct order. If you use MySQL Server, you can use SET FOREIGN_KEY_CHECKS=0 to disable foreign key constraints and SET FOREIGN_KEY_CHECKS=1 to enable foreign key constraints.

By default, DbUnitTestCase performs DatabaseOperation.CLEAN_INSERT for every methods. You can change this by setting insertOperation with a different value, for example:

class InvoiceTest extends DbUnitTestCase {

  protected void setUp() {
     super.setUp()
     setUpDatabase('/project/data.xlsx', null, false, DatabaseOperation.UPDATE)
  }

}

For advanced use case, you can always override or execute public methods provided by DbUnitTestCase:

  • loadMVC(mvcGroup)

    Create MVCGroup instance specified by the name. You can then use model, view and controller property to access this MVC instance members.

  • beforeSetupDatabase()

    Use this method to execute before.sql if it is found in root package. This method is called at the beginning of test method.

  • cleanDataset()

    Use this method to execute clean.sql if it is found in root package. This method is called before insertOperation is executed.

  • afterSetupDatabase()

    Use this method to execute after.sql if it is found in root package. This method is called after insertOperation is executed and before test method begins.

  • execute(list)

    Execute a list of SQL statements.

  • cleanInsert()

    Perform a DatabaseOperation.CLEAN_INSERT based on current data source.

  • truncateTable()

    Perform a DatabaseOperation.TRUNCATE_TABLE based on current data source.

  • deleteAll()

    Perform a DatabaseOperation.DELETE_ALL based on current data source.

  • refresh()

    Perform a DatabaseOperation.REFRESH based on current data source.

Appendix A: Script

simple-jpa scripts can be called just like any Griffon’s commands:

griffon [command-name] [argument1] [argument2] ...

To display more information for a command, call it with -info argument:

griffon [command-name] -info

A.1. create-simple-jpa

This command is usually the first command that will be invoked before working with Java Persistence API (JPA). It will create persistence.xml and orm.xml in current project. It will also create some resource files that are commonly required when working with JPA.

The syntax for this command is:

create-simple-jpa -user=[databaseUser] -password=[databasePassword]
    -database=[databaseName] -rootPassword=[databaseRootPassword]
    -provider=[JPAProvider] -jdbc=[databaseType]

or

create-simple-jpa -user=[databaseUser] -password=[databasePassword]
    -database=[databaseName] -provider=[JPAProvider]
    -jdbc=[databaseType] -skipDatabase
user

The name of database user. JPA will establish connection to database by using the specified user name. If user name doesn’t exists, it will be created automatically.

password

The password used when establishing connection to the database.

database

The database name or schema name. If this database doesn’t exists, it will be created automatically. The specified user will also be granted privilleges to use this database.

rootPassword

The password for database root/administrator. To create user and database and grants privilleges, this command requires password for root/administrator user. This value will never be stored in project files.

provider

The name of JPA provider that will be used. The default value for this parameter is hibernate.

jdbc

The name of JDBC driver that will be used. The default value for this parameter is mysql. The following available values are mysql for using MySQL JDBC or derby-embedded for using Apache Derby embedded database JDBC.

skipDatabase

If this argument exists, create-simple-jpa will not create user and database automatically. It will only write to persistence.xml and assume required database schema and user is available.

For example, the following command will generate persistence.xml with a connection to MySQL database (user: steven, password: 12345, database schema: sample), uses Hibernate JPA, and creates user steven and sample schema if they are not exists:

griffon create-simple-jpa -user=steven -password=12345 -database=sample
    -rootPassword=secret

The following command will do the same as the previous one:

griffon create-simple-jpa -user=steven -password=12345 -database=sample
    -provider=hibernate -jdbc=mysql -rootPassword=secret

The following command will generate persistence.xml with a connection to MySQL database (user: scott, password: tiger, database schema: ha), uses Hibernate JPA, and will not check if required user and schema are available:

griffon create-simple-jpa -user=scott -password=tiger -database=ha
    -skip-database

If you use -jdbc=derby-embedded, -database should be a full path to directory such as C:/Users/me/mydb.

A.2. create-domain-class

This command will create a new empty domain class and register it in persistence context file. Before creating domain class, the project must has persistence.xml file in metainf directory. To generate required files for working with JPA, use create-simple-jpa command.

Domain class will be generated in the package specified by griffon.simplejpa.model.package value. The default package is domain.

To change the default template used for generating domain clasess, execute install-templates command and edit SimplaJpaDomainClass.groovy.

The syntax for this command is:

create-domain-class [domainClassName]

or

create-domain-class [domainClassName] [domainClassName] ...

or

create-domain-class [domainClassName],[domainClassName], ...
domainClassName

The name of domain class that will be generated.

Examples:

griffon create-domain-class Student
griffon create-domain-class Teacher Student
griffon create-domain-class Teacher,Student

A.3. generate-all

This command will create a new MVCGroup based on a domain class. The generated MVCGroup (consists of a view, a model and a controller) has the ability to perform CRUD operations on a domain class. This command can also generate a startup MVCGroup that act as container for the others.

Domain classes should be located in the package specified by griffon.simplejpa.model.package in Config.groovy. The default value for package is domain.

When the value of griffon.simplejpa.finders.alwaysExcludeSoftDeleted is true, the generated controller will call softDelete() instead of remove().

To change the default template used by this command, execute install-templates command and edit the generated template files.

The syntax for this command is:

generate-all * [-generatedPackage] [-forceOverwrite] [-setStartup]
    [-skipExcel] [-startupGroup=value]

or

griffon generate-all [domainClassName] [-generatedPackage]
    [-forceOverwrite] [-setStartup] [-skipExcel]
    [-startupGroup=value]

or

generate-all [domainClassName] [domainClassName] ...
    [-generatedPackage] [-forceOverwrite] [-setStartup] [-skipExcel]
    [-startupGroup=value]
domainClassName

The name of domain class the will be manipulated by the generated MVCGroup. Each domain class will have their own MVCGroup generated. If this value is *, then all domain classes will be processed.

generatedPackage (optional)

The target package. By default, the value for this parameter is project.

forceOverwrite (optional)

If exists, this script will replace existing files without any notifications.

setStartup (optional)

If exists, this script will set the generated MVCGroup as startup (the MVCGroup will be launched when program starts). If this argument is present when generating more than one MVCGroup, then only the last MVCGroup will be set as startup group.

skipExcel (optional)

If exists, this script will not create Microsoft Excel file for integration testing (DbUnit).

startupGroup (optional)

The name for MVCGroup that serves as startup group. The generated MVCGroup will not based on any domain class, instead it will act as a container for the other domain classes’ based MVCGroups.

For example, the following command will generate MVCGroup for all domain classes:

griffon generate-all *

The following command will generate MVCGroup for all domain classes, overwriting existing files, and set the last MVCGroup as startup:

griffon generate-all * -forceOverwrite -setStartup

The following command will generate MVCGroup for domain class Student, Teacher, and Classroom:

griffon generate-all Student Teacher Classroom

The following command will generate MVCGroup for domain class Student and generate a container MVCGroup which name is MainGroup:

griffon generate-all Student -startupGroup=MainGroup

The following command will generate a container MVCGroup which name is MainGroup:

griffon generate-all -startupGroup=MainGroup

A.4. install-templates

This command will add templates used by simple-jpa to current project in /src/templates/artifacts. This command is useful for changing templates that is used by simple-jpa generator. Developer can edit the templates and the next invocation of simple-jpa generator will based on them.

The syntax for this command is:

install-templates

Example:

griffon install-templates

A.5. simple-jpa-console

This command will launch Groovy Console loaded with Griffon and simple-jpa. Developer can use this command to test or execute code interactively.

For each loaded MVCGroup, there are three variables to refer to its model, view, and controller. For example, if MVCGroup name is student, developer can refer to its model, view, or controller by using the following variables: studentModel, studentController and studentView. Developer can also use app to refer to GriffonApplication. To display list of available variables, select Script, Inspect Variables.

When console is started, it only loads startup MVCGroup. To load the another MVCGroup, select simple-jpa, MVCGroups.

The syntax for this command is:

simple-jpa-console

Examples:

griffon simple-jpa-console

A.6. generate-schema

This command will generate database schema based on current domain models mapping to database or scripts. Developer can use this command to retrieve SQL scripts that can be used to populate new database schema for current application.

The syntax for this command is:

generate-schema -target=database -action=[action]

or

generate-schema -target=database -action=[action] -data=[script.sql]

or

generate-schema -target=script -action=[action]
                -dropTarget=[script.sql]
                -createTarget=[script.sql]
target

One of database or script. If target is database, this script will create database objects in the database configured in persistence.xml. You shouldn’t need this target because by default, database objects will be dropped and generated when application is launched. If target is script, this command will generate SQL scripts that can be executed later (perhaps in a new database schema).

action

Valid values are none, create, drop-and-create, and drop.

data (optional)

Contains SQL script location that will be executed after database objects are created. The purpose of this script is to initialize database (for example, populating tables with initial data).

dropTarget

Available if target is script. This is the value of file that will be generated and contains DDL DROP scripts.

createTarget

Available if target is script. This is the value of file that will be generated and contains DDL CREATE scripts.

Examples:

griffon generate-schema -target=database -action=drop-and-create
griffon generate-schema -target=script -action=drop-and-create
                        -dropTarget=drop.sql -createTarget=target.sql

A.7. obfuscate

Use this command to generate obfuscated value that can be added to configuration file or simplejpa.properties. This is useful to hide sensitive information such as database password from novice users.

The syntax for this command is:

obfuscate -generate=[value]

or

obfuscate -reverse=[value]

Examples:

griffon obfuscate -generate=mypassword

The command above will generate obfuscated:AGHJLPazOUvt5ZjzRNnKaA==. This can be used as a substitution for configurations that accepts string value. For example, it can be used in Config.groovy:

griffon {
   simplejpa {
      entityManager {
         javax.persistence.jdbc.password = "obfuscated:AGHJLPazOUvt5ZjzRNnKaA=="
      }
   }
}

A.8. create-repository

Use this command to create a new repository. It is recommended to generate repository automatically by using DDD generator rather than create them manually using this command.

The syntax for this command is:

create-repository [name]

This command will create new repository class in griffon-app/repositories.

Examples:

create-repository Invoice