Introduction
Goals
With Dueuno Elements you can develop backoffice web applications writing code in a single programming language: Apache Groovy. No need to know HTML, CSS o Javascript.
With Dueuno Elements you can develop and maintain web applications with requirements such as:
-
Authenticate & authorize users (who can access and what they can do)
-
Implement several business features with a coherent user interface
-
Display and edit data in table format (CRUD)
-
Display charts, dashboards, etc. to build Business Intelligence applications
-
Let users customize their localization preferences with country specific formats (languages, dates, numbers, currencies, quantities, etc.)
-
Develop multi-device applications. Each application will automatically work on a desktop pc with a mouse as well as on a mobile device with a finger.
-
Develop multi-tenant applications. The same application can serve different clients with separate databases.
The main purpose of Dueuno Elements is to decouple the business logic from the User Interface (GUI). This lowers the costs of maintaining the application: just one language instead of 5 (HTML, CSS, JavaScript, Java, SQL), less skilled people can join the team and no need to upgrade the GUI when new web technologies/standards gets available. |
Non-Goals
Dueuno Elements is NOT a solution for:
-
Creating classical websites (text + images + basic user interaction)
-
Creating graphical/animated web applications
-
Developing applications where the client retains strict control on how the user interface will look like
For such cases you can just use the beautiful Grails.
Quick Start
Basics
To develop Dueuno Elements applications you need to learn the basics of the Apache Groovy programming language and the basics of the Grails framework.
Learning Groovy
The Groovy programming language has been around for more than 20 years now, it’s been the second language to be developed for the JVM. You can read the documentation and access the Groovy learning resources
Learning Grails
Grails has been helping web developers for 20 years now, it still is a growing and evolving technology so what you want to do is reading the latest documentation on the Grails website
Run
We are going to create our first Dueuno Elements application, ready?
-
Download and install IntelliJ IDEA Community Edition.
-
Download the Application Template
-
Unzip it into your home directory
-
Open it with IntelliJ IDEA
-
Run the application from the Gradle sidebar clicking on
project-name → Tasks → application → bootRun
-
| Running application…
Configuring Spring Security Core …
… finished configuring Spring Security Core
Grails application running at https://localhost:8080 in environment: development
The first run will take some time since it has to download all the needed dependencies. |
We can now navigate to https://localhost:8080 to see the login screen. Login with the credentials super/super
to play around with the basic features of a plain _Dueuno Elements application.
Basics
In Dueuno Elements everything is a component. All visual objects of the application are derived from the same base class Component and they can be assembled together like we do with a LEGO set.
Some of these components are automatically created and managed by Dueuno Elements for each application instance. Let’s give them a quick look.
The Shell
We call Shell the Dueuno Elements GUI. Each Dueuno Elements application share a common user experience and content structure.
Login
Where you can log in.
Home
Where you can find your favourite Features
Application Menu
Where you can find the complete Features list.
User Menu
Where you can find the user options.
Navigation Bar
The user can access (from left to right) (1) the Main Menu, (2) the Home - where users can find their favourite features - and (3) the User Menu.
Content
Each Feature will display as an interactive screen we call Content that will occupy the main area surrounded by the Shell.
Contents can be displayed as modals. A modal content is rendered inside a dialog window. This lets the user focus on a specific part of the Feature to accomplish subtasks like editing a specific object.
Modals can be displayed in three sizes: normal
(default), wide
and fullscreen
.
User Messages
The application can display messages to the user to send alerts or confirm actions.
Responsiveness
All Dueuno Elements applications work both on desktop computers and on mobile devices by design and without the developer having to cope with it. Here is how an application looks like on a Desktop, on a Tablet and on a Mobile Phone.
Project Structure
Dueuno Elements applications are Grails applications. The project structure, follows the conventions over configuration design paradigm so each folder contains specific source file types.
Filesystem
/myapp (1) /grails-app (2) /controllers (3) /services (4) /domain (5) /i18n (6) /init (7) /conf (8) /src /main /groovy (9)
1 | Project root |
2 | Web Application root |
3 | User Interface. Each class name under this directory must end with Controller (Eg. PersonController ) |
4 | Business Logic. Each class name under this directory must end with Service (Eg. PersonService ) |
5 | Database. Each class name under this directory must begin with T (Eg. TPerson ) |
6 | Translations |
7 | Initialization |
8 | Configuration files |
9 | Other application source files |
Each folder contains the package structure, so for example if your application main package is myapp
the source file structure will look like this:
/myapp /grails-app /controllers /myapp MyController.groovy /services /myapp MyService.groovy /domain /myapp MyDomainClass.groovy /init /myapp BootStrap.groovy /src /main /groovy /myapp MyClass.groovy
Features
A Dueuno Elements application is a set of Features.
Each Feature consists of a set of visual objects the user can interact with to accomplish specific tasks. You can identify each Feature as an item in the application menu on the left. Clicking a menu item will display the content of the selected Feature.
To configure the application Features we register them in the BootStrap.groovy
file (See [registerFeature]):
class BootStrap {
ApplicationService applicationService (1)
def init = { servletContext ->
applicationService.init { (2)
registerFeature( (3)
controller: 'person',
icon: 'fa-user',
)
}
}
}
1 | ApplicationService is the object in charge of the application setup |
2 | The init = { servletContext → … } Grails closure is executed each time the application starts up |
3 | Within the applicationService.init { … } closure you can call any of the applicationService methods. In this case the method registerFeature() |
Controllers
A Feature links to a controller.
A controller is a set of actions.
All actions that a user can take on the application (eg. a click on a button) are coded as methods of a controller. Each action corresponds to a URL that will be submitted from the browser to the server. The URL follows this structure:
http://my.company.com/${controllerName}/${actionName}
For example the following Controller contains two actions that can be called like this:
http://my.company.com/person/index (1) http://my.company.com/person/edit/1 (2)
class PersonController implements ElementsController {
def index() { (1)
dispaly ...
}
def edit() { (2)
display ... (3)
}
}
1 | The index action. It’s the default one, it can also be called omitting the action name, eg. http://my.company.com/person |
2 | The edit action |
3 | The display method ends each action and tells the browser what component to display |
Services
We don’t implement business logic in Controllers. We do it in Services. Each Service is a class implementing several methods we can call from a Controller.
For example the following Service implements the method sayHello()
.
class PersonService {
String sayHello() {
return "Hi folks!"
}
}
We can call it from a Controller like this:
class PersonController implements ElementsController {
PersonService personService (1)
def index() {
def hello = personService.sayHello()
display message: hello (2)
}
}
1 | Service injection, the variable name must be the camelCase version of the PascalCase class name |
2 | the display method renders objects on the browser, in this case a message |
Database
Each application has a DEFAULT
database connection defined in the grails-app/conf/application.yml
file. This DEFAULT
connection cannot be changed at runtime and it is used by Dueuno Elements to store its own database.
-
You can configure multiple databases per environment (DEV, TEST, PRODUCTION, ect) in the
application.yml
, see: https://docs.grails.org/latest/guide/single.html#environments -
You can edit/create database connections at runtime from the Dueuno Elements GUI accessing with the
super
user from the menuSystem Configuration → Connection Sources
-
You can programmatically create database connections at runtime with the
ConnectionSourceService
as follows:
class BootStrap {
ApplicationService applicationService (1)
ConnectionSourceService connectionSourceService (2)
def init = { servletContext ->
applicationService.onInstall { String tenantId -> (3)
connectionSourceService.create( (4)
name: 'runtimeDatasource',
driverClassName: 'org.h2.Driver',
dbCreate: 'update',
username: 'sa',
password: '',
url: 'jdbc:h2:mem:DYNAMIC_CONNECTION;LOCK_TIMEOUT=10000 DB_CLOSE_ON_EXIT=TRUE',
)
}
}
}
1 | ApplicationService is the object in charge of the application setup |
2 | ConnectionSourceService service injection |
3 | The onInstall { … } closure is called only the first time the application runs for the DEFAULT Tenant and each time a new Tenant is created |
4 | The create() method creates a new connection and connects to it. Once created the application will automatically connect to it each time it boots up. Connection details can be changed via GUI accessing as super from the menu System Configuration → Connection Sources |
Tenants
Multi-Tenants applications share the code while connecting to different databases, usually one for each different company. This way data is kept separated with no risk of disclosing data from one company to the other.
Each application user can only belong to one Tenant. If a person needs to access different Tenants two different accounts must be created. To configure and manage users for a Tenant you have to access the application as the admin user. For each Tenant a default admin user is created with the same name as the Tenant (E.g. the Tenant called 'TEST' is going to have a 'test' user which is the Tenant administrator.
The default password for such users corresponds to their names. To change the password you need to log in with the admin user and change it from the User Profile . Go to User Menu (top right) → Profile .
|
New Tenants can be created from the Dueuno Elements GUI accessing as super
from the menu System Configuration → Tenants
. If multi-tenancy is not a requirement to your application you will be using the DEFAULT
Tenant which is automatically created.
User Management
Users can access a Dueuno Elements application with credentials made of a username and a secret password. Each user must be configured by the Tenant’s admin
user from the menu System Administration → Users
and System Administration → Groups
.
Building Applications
In this chapter we are going through the building of a Dueuno Elements application.
CRUD
One of the most useful GUI pattern is the CRUD (Create, Read, Update, and Delete). It is based on the four basic operations available to work with persistent data and databases.
Applications are made of features, we register one to work with movies (See [features]).
class BootStrap {
ApplicationService applicationService (1)
def init = { servletContext ->
applicationService.init {
registerFeature( (2)
controller: 'movie',
icon: 'fa-film',
favourite: true,
)
}
}
}
1 | See [application] |
2 | See [registerFeature] |
We are going to implement a simple database with GORM for Hibernate on top of which we can build our GUI.
class TMovie implements MultiTenant<TMovie> {
LocalDateTime dateCreated
String title
Integer released
static hasMany = [actors: TActor]
static constraints = {
}
}
class TActor implements MultiTenant<TActor> {
LocalDateTime dateCreated
String firstname
String lastname
static constraints = {
}
}
To create a CRUD user interface we are going to implement a controller with the following actions. The business logic will be implemented into a service to keep it decoupled from the GUI.
@Secured(['ROLE_CAN_EDIT_MOVIES']) (1)
class MovieController implements ElementsController { (2)
def index() {
// will display a list of movies
}
def create() { (3)
// will display a form with the movie title
}
def onCreate() { (3)
// will create the movie record on the database
}
def edit() {
// will display the details of a movie
}
def onEdit() {
// will update the movie record on the database
}
def onDelete() {
// will delete a movie record from the database
}
}
1 | Only users with the ROLE_CAN_EDIT_MOVIES authority can access the actions in this controller. |
2 | Implementing ElementsController the Dueuno Elements API will become available |
3 | As a convention, all actions building and displaying a GUI are named after a verb or a name while all actions that execute a business logic are identified by a name starting with on . |
We are going to use the ContentList
content to list the records, the ContentCreate
and ContentEdit
contents to create a new record and edit an existing one (See [contents]).
@Secured(['ROLE_CAN_EDIT_MOVIES'])
class MovieController implements ElementsController {
MovieService movieService (1)
def index() {
def c = createContent(ContentList)
c.table.with {
filters.with {
addField(
class: TextField,
id: 'find',
label: 'default.filters.text',
cols: 12,
)
}
sortable = [
title: 'asc',
]
columns = [
'title',
'released',
]
body.eachRow { TableRow row, Map values ->
// Do not execute slow operations here to avoid slowing down the table rendering
}
}
c.table.body = movieService.list(c.table.filtersParams, c.table.fetchParams)
c.table.paginate = movieService.count(c.table.filtersParams)
display content: c
}
private buildForm(TMovie obj = null) {
def c = obj
? createContent(ContentEdit)
: createContent(ContentCreate)
c.form.with {
validate = TMovie
addField(
class: TextField,
id: 'title',
)
addField(
class: NumberField,
id: 'released',
)
}
if (obj) {
c.form.values = obj
}
return c
}
def create() {
def c = buildForm()
display content: c, modal: true
}
def onCreate() {
def obj = movieService.create(params)
if (obj.hasErrors()) {
display errors: obj
return
}
display action: 'index'
}
def edit() {
def obj = movieService.get(params.id)
def c = buildForm(obj)
display content: c, modal: true
}
def onEdit() {
def obj = movieService.update(params)
if (obj.hasErrors()) {
display errors: obj
return
}
display action: 'index'
}
def onDelete() {
try {
movieService.delete(params.id)
display action: 'index'
} catch (e) {
display exception: e
}
}
}
1 | Service injection, see the implementation below |
We will implement the database operations using GORM for Hibernate, the default Object Relational Mapper used by Grails.
@CurrentTenant
class MovieService {
private DetachedCriteria<TMovie> buildQuery(Map filters) {
def query = TMovie.where {}
if (filters.containsKey('id')) query = query.where { id == filters.id }
if (filters.find) {
String search = filters.find.replaceAll('\\*', '%')
query = query.where {
title =~ "%${search}%"
}
}
// Add additional filters here
return query
}
TMovie get(Serializable id) {
// Add any relationships here (Eg. references to other DomainObjects or hasMany)
Map fetch = [
actors: 'join',
]
return buildQuery(id: id).get(fetch: fetch)
}
List<TMovie> list(Map filterParams = [:], Map fetchParams = [:]) {
if (!params.sort) params.sort = [dateCreated: 'asc']
// Add single-sided relationships here (Eg. references to other Domain Objects)
// DO NOT add hasMany relationships, you are going to have troubles with pagination
// params.fetch = [
// actors: 'join',
// ]
def query = buildQuery(filterParams)
return query.list(fetchParams)
}
Integer count(Map filterParams = [:]) {
def query = buildQuery(filterParams)
return query.count()
}
TMovie create(Map args = [:]) {
if (args.failOnError == null) args.failOnError = false
TMovie obj = new TMovie(args)
obj.save(flush: true, failOnError: args.failOnError)
return obj
}
TMovie update(Map args = [:]) {
Serializable id = ArgsException.requireArgument(args, 'id')
if (args.failOnError == null) args.failOnError = false
TMovie obj = get(id)
obj.properties = args
obj.save(flush: true, failOnError: args.failOnError)
return obj
}
void delete(Serializable id) {
TMovie obj = get(id)
obj.delete(flush: true, failOnError: true)
}
}
Run the application with gradle bootRun
, you should be able to create, list, edit and delete movies.
Whats next?
Read the Dueuno Elements Book