Desktop Development - The Fun Way

Tutorial - Griffon: Building Desktop Applications with Groovy - Part 4

  

Making Contacts Persistent

Time to finish up the application. We have a few tasks ahead of us:

  • Fill the code required by each controller action

  • Save the contact list to a database

  • Make sure to load the contacts from the database when the application starts up

 Filling up the actions is a straightforward operation, as the bulk of the data manipulation is already taken care of by the bindings we have put in place. Listing 7 shows the final code for AddressbookController.

Listing 7 - AddressbookController

package addressbook
class AddressbookController {
    def model
    def storageService

    void newAction(evt) {
        model.selectedIndex = -1
        model.currentContact.contact = new Contact()
    }
    
    void saveAction(evt) {
        // push changes to domain object
        model.currentContact.updateContact()
        boolean isNew = model.currentContact.contact.id < 1
        // save to db
        storageService.store(model.currentContact.contact)
        // if is a new contact, add it to the list
        if(isNew) {
            def cpm = new ContactPresentationModel()
            cpm.contact = model.currentContact.contact
            model.contacts << cpm
        }
    }
    
    void deleteAction(evt) {
        if(model.currentContact.contact && model.currentContact.contact.id) {
            // delete from db
            storageService.remove(model.currentContact.contact)
            // remove from contact list
            execInsideUIAsync {
                model.removeContact(model.currentContact.contact)
                model.selectedIndex = -1
            }
        }
    }
    
    void dumpAction(evt) {
        storageService.dump()
    }

    void mvcGroupInit(Map args) {
        execFuture {
            List<ContactPresentationModel> list = storageService.load().collect([]) {
                new ContactPresentationModel(contact: it)
            }
            execInsideUIAsync {
                model.contacts.addAll(list)
            }
        }
    }
}

 

The first action, newAction, is concerned by resetting the current selection (if any) and creating an empty Contact. The saveAction() should push the changes from the presentation model back to the domain object, store the data in the database and in the case that this is a new contact, add it to the list of contacts. Notice that when dealing with database concerns we’ll delegate to another component called storageService which we’ll see right after we finish describing the Controller. The third action deletes a contact first by removing it from the database, then removing it from the contacts list. We added a fourth action that will be used to dump the database contents into the console. The last piece of information found in the code relates to loading the data from the database and filling up the contacts list. The method name is special as it’s a hook into the MVC lifecycle. This particular method will be called after all MVC members have been instantiated, think of it as a member initializer. We’re ready to have a look at the storageService component.

Services in Griffon are nothing more than regular classes but they receive special treatment from the framework. For example, they are treated as singletons and will be automatically injected into MVC members as long as the member defines a property whose name matches the service name. Armed with this knowledge we’ll create a StorageService class, like this:

$ griffon create-service storage

This will create a file in griffon-app/services/addressbook/StorageService.groovy with default content. Listing 8 shows the code that must be put inside that file for the application to work.

Listing 8

package addressbook
class StorageService {
    List<Contact> load() {
        withSql { dsName, sql ->
            List tmpList = []
            sql.eachRow('SELECT * FROM contacts') { rs ->
                tmpList << new Contact(
                    id:       rs.id,
                    name:     rs.name,
                    lastname: rs.lastname,
                    address:  rs.address,
                    company:  rs.company,
                    email:    rs.email
                )
            }
            tmpList
        }
    }

    void store(Contact contact) {
        if(contact.id < 1) {
            // save
            withSql { dsName, sql ->
                String query = 'select max(id) max from contacts'
                contact.id = (sql.firstRow(query).max as long) + 1
                List params = [contact.id]
                for(property in Contact.PROPERTIES) {
                    params << contact[property]
                }
                String size = Contact.PROPERTIES.size()
                String columnNames = 'id, ' + Contact.PROPERTIES.join(', ')
                String placeHolders = (['?'] * size + 1)).join(',')
                sql.execute("""insert into contacts ($columnNames)
                   values ($placeHolders""", params)
            }
        } else {
            // update
            withSql { dsName, sql ->
                List params = []
                for(property in Contact.PROPERTIES) {
                    params << contact[property]
                }
                params << contact.id
                String clauses = Contact.PROPERTIES.collect([]) { prop ->
                    "$prop = ?"
                }.join(', ')
                sql.execute("""update contacts
                    set $clauses where id = ?""", params)
            }
        }
    }
    
    void remove(Contact contact) {
        withSql { dsName, sql ->
            sql.execute('delete from contacts where id = ?', [contact.id])
        }
    }
    
    void dump() {
        withSql { dsName, sql ->
            sql.eachRow('SELECT * FROM contacts') { rs ->
                println rs
            } 
        }
    }
}

 

Each one of the service methods makes use of a method named withSql. This method becomes available if we install another plug-in. Let’s do that now:

$ griffon install-plugin gsql

Perfect. We now have enabled Groovy SQL support in our little application. Groovy SQL is another DSL on top of regular SQL. With it, you can make SQL calls using a programmatic API that closely resembles working with objects and object graphs. As a matter of fact, you can even apply Groovy closures, Groovy strings, and other Groovy tricks, like those shown in the implementation of the StorageService class. There are two more items we must take care of before launching the application again. We must tell the GSQL plug-in that the withSql method must be applied to services; secondly, we must define the database schema. As we said earlier there’s no GORM API yet, database schemas must be defined by hand.

The first task is accomplished by editing the file griffon-app/conf/Config.groovy and appending the following line

griffon.datasource.injectInto = ['service']

The second task gets done by creating a file in griffon-app/resources/schema.ddl with the following content:

 

DROP TABLE IF EXISTS contacts;

CREATE TABLE contacts(

id INTEGER NOT NULL PRIMARY KEY,

name VARCHAR(30) NOT NULL,

lastname VARCHAR(30) NOT NULL,

address VARCHAR(100) NOT NULL,

company VARCHAR(50) NOT NULL,

email VARCHAR(100) NOT NULL

);

Good, we’re done. What about seeding the database with some initial data? In Grails this is done by editing a file named BootStrap.groovy, in Griffon it’s done by editing griffon-app/conf/BootstrapGsql.groovy. Let’s add an entry to the contacts table, as Listing 9 shows.


import groovy.sql.Sql
class BootstrapGsql {
    def init = { String dataSourceName = 'default', Sql sql ->
        def contacts = sql.dataSet('contacts')
        contacts.add(
            id: 1,
            name: 'Andres',
            lastname: 'Almiray',
            address: 'Kirschgartenstrasse 5 CH-4051 Switzerland',
            company: 'Canno Engineering AG',
            email: 'andres.almiray@canoo.com'
        )
    }

    def destroy = { String dataSourceName = ‘default’, Sql sql ->
    }
} 

Phew. Now we’re really done. Launch the application once more. You should see one entry in the contacts list as Figure 4 shows. Select it with the mouse and either press enter or double-click on it. This will make the contact the active one and place all of its values in the middle form. Edit some of its properties, then click on the save button. Create a new contact and save it too. Now click on the dump button. You should see as many rows in the output as entries can be found in the contacts list.

Figure 4: Address book application with one entry selected

The Aftermath

We have had quite the whirlwind tour on the Griffon basics. Granted, there are more things that meet the eye in just a few pages and code listings. I hope this has been enough to wet your appetite for more, or at least consider giving Griffon a try. All of the behaviour we wrote so far fits in a mere 291 lines of code. Don’t believe me? Run the following command:

 

$ griffon stats

 

A table listing all sources by type will appear, similar to this one:

 

+----------------------+-------+-------+

| Name | Files | LOC |

+----------------------+-------+-------+

| Models | 1 | 32 |

| Views | 1 | 45 |

| Controllers | 1 | 39 |

| Services | 1 | 57 |

| Lifecycle | 5 | 3 |

| Groovy/Java Sources | 2 | 40 |

| Unit Tests | 1 | 13 |

| Integration Tests | 1 | 15 |

| Configuration | 2 | 47 |

+----------------------+-------+-------+

| Totals | 15 | 291 |

+----------------------+-------+-------+

Nice. The full source code for this application can be found at GitHub. There are more things that we could add to this application. For example, there’s no error handling whatsoever. What if SQL is not your cup of tea? No problem, you can pick any of the supported NoSQL options. Or maybe Swing is not for you. No problem either, Griffon supports SWT and JavaFX as well. Changing UI toolkits will mean changing the View mostly while keeping the other components almost intact. There are definitely many choices ahead of you.

Author Bio: Andres Almiray

Andres Almiray is a Java/Groovy developer with more than 12 years of experience in software design and development. He has been involved in web and desktop application development since the early days of Java. His current interests include Groovy and desktop apps. He is a true believer of open source and has participated in popular projects like Groovy, Griffon, JMatter and DbUnit, as well as starting his own projects (Json-lib, EZMorph, GraphicsBuilder, JideBuilder). Andres is a founding member and current project lead of the Griffon framework. He likes to spend time with his beloved wife, Ixchel, when not hacking around. 

Additional Resources

http://www.glazedlists.com

http://www.miglayout.com

 

This article originally appeared in Java Tech Journal - Groovy. Read more of that issue here.

Andres Almiray
Andres Almiray

What do you think?

Comments

Latest opinions