Fornax-Platform Log In   View a printable version of the current page.  
  5. CRUD GUI Tutorial (CSC)
  Dashboard > Fornax-Platform > ... > Sculptor (CSC) > 5. CRUD GUI Tutorial (CSC)
General


Projects


Latest News



Global Reports
Find all pages that aren't linked from anywhere
Find all undefined pages
Feed for new pages
Added by Andreas Källberg, last edited by Patrik Nordwall on Jul 04, 2008  (view change) show comment
Labels: 
(None)

Sculptor CRUD Gui Tutorial

This tutorial describes how to generate, customize and use the CRUD gui, and it will use the Helloworld example application as base.

I.e. we start out with the Hello World model:

model.design
Application Universe {
    basePackage=org.helloworld

    Module milkyway {
        Service PlanetService {
            String sayHello(String planetName);
            protected findByExample => PlanetRepository.findByExample;
        }

        Entity Planet {
            String name key;
            String message;

            Repository PlanetRepository {
                findByExample;

            }
        }

    }
}

For guidance on environment setup, see the Archetype tutorial.  

There are two different implementations for the web project:

  • Spring webflow with pure JSP
  • Spring webflow with JSF and Facelets

Note that you use different archetype for creating the chosen variant. Explained in Archetype tutorial.
Some of the descriptions in this tutorial only apply to one of the variants, when that is the case it is marked with JSP only and JSF only.

Table of content:

So, before you start, set up the project structure, import to eclipse, apply an initial model, generate code, etc, all according to Archetype tutorial.

Tutorial

Complete JUnit tests

For the business tier project see part 3 of the Hello World Tutorial.

To complete the web projects JUnit tests:

1. Run CreatePlanetTest as JUnit Test. Red bar.
Adjust the method populateFormSuccess to make it populate required fields:

@Override
    protected void populateFormSuccess(MockParameterMap parameters) {
        parameters.put("name", "Earth");
        parameters.put("message", "Hello from Earth");
    }

Adjust the method populateFormError to do like this:

@Override
    protected void populateFormError(MockParameterMap parameters) {
        parameters.put("name", "");
        parameters.put("message", "");
    }

2. Run. Green bar!

3. Do the same for the rest of the failing tests.

Redesign of model 

Ok, so, according to the Hello World Tutorial, the model looks like above.

The problem with the Hello world model (see above) is that the model isn't going to be very interesting to run the CRUD GUI with. We need at least one more entity and we need some services to do operations on them (for more info about what's generated, see the Explanations part).

 1. So, in step one we do a redesign of our model to look something like this:

model.design
Application Universe {
    basePackage=org.helloworld

    Module milkyway {
        Service PlanetService {
            String sayHello(String planetName);
            protected findByExample => PlanetRepository.findByExample;
        }
        Entity Planet {
            scaffold
            String name key;
            String message;
            Integer diameter nullable;
            Integer population nullable;
            - Set<@Moon> moons opposite planet;

            Repository PlanetRepository {
                findByExample;
                findByKeys;
                save;
            }
        }
        Entity Moon {
            not aggregateRoot // belongs to Planet Aggregate
            String name key;
            Integer diameter nullable;
            - @Planet planet opposite moons;
        }
    }
}
If you use the scaffold key word on an entity, you automatically get CRUD services for that entity

2. Re-generate the code in both the business tier project and the web project

You can save a lot of time by just generate source instead of doing a full build, i.e. maven install, run the external maven task mvn-generate-sources

3. Fix failing test once more

4. Recreate the database (as described here)

5. Deploy the application to the server (as described here)

6. Try and run the CRUD GUI on http://localhost:8080/helloworld-web. Click on the 'Create planet' menu choice and create a planet, and possible, create an optional Moon in conjunction to the Planet. When the planet is created, you can browse the complete list of Planets by clicking on the 'List all planets' menu choice.

It isn't harder  

Custom logic

Ok, so, now we have a running application, but, it is a very general one. It doesn't contain any specific logic.

Let's assume that we have a use case stating that:

"When a Planet is created, none of its Moons can have a diameter bigger than the planets"

Ok, what to do about that one?
Well, we decide that we will implement this as a validation in the client (perhaps not the best place, but for this example it will do).

1.  Start by creating the CreatePlanetValidatorTest JUnit test case, and put it in the src/test/java source directory

2. We implement the test as if we expect the validation to produce an error:

public void testValidatePlanet() {
        // the form bean that is up for validation
        CreatePlanetForm form = new CreatePlanetForm();
        form.setName("FooPlanet");
        form.setDiameter(1000);
        Moon moon = new Moon("BarMoon");
        moon.setDiameter(2000);
        form.addMoon(moon);

        BindException errors = new BindException(form, "createPlanetForm");
        // the test target
        CreatePlanetValidator validator = new CreatePlanetValidator();
        validator.validatePlanet(form, errors);
        assertTrue(errors.hasErrors());
    }

3. Run the test. Red bar.

4. Implement the logic in CreatePlanetValidator as:

@Override
    public void validatePlanet(CreatePlanetForm form, Errors errors) {
        Planet planet = form.toModel();
        int planetDiameter = planet.getDiameter();
        Collection<Moon> moons = planet.getMoons();
        for (Moon moon : moons) {
            if (planetDiameter <= moon.getDiameter()) {
                errors.reject("createPlanet.faultyDiameter");
            }
        }
    }

5. Run the test again. Green bar!

6. Deploy and integration test it in the application.

Since you haven't change the structure of the CreatePlanetValidator you don't need to restart the server and you don't need to copy the class, the debugger will do that for you

Custom generation

Alright, we have the application up and running. We have added some logic so it performs as we want. But, are we satisfied with how it behave...? Doesn't we think that those  pesky 'confirm'-steps are really annoying?
Lets do something about it.
And lets do it in a way that will stay, and will apply on future changes to the model.
We change the generation so that we just makes a browser round trip and immediate redirects to the next step.

So, whats the easiest way of manipulate the generation without checking out the source?
In src/main/resources/templates there are a file called WebSpecialCases.xpt. It is a hook into the generation and here we can add generation logic. And in our case we will add an advice that will short circuit the generation of the confirmation page and instead have our own version of it (JSP only):

«IMPORT sculptormetamodel»
«IMPORT sculptorguimetamodel»
«EXTENSION extensions::helper»
«EXTENSION extensions::properties»
«EXTENSION extensions::guihelper»

«AROUND templates::SpringWebflowCrudGuiFlowJsp::createUpdateFlowJspConfirmInc FOR UserTask»
    «FILE "WEB-INF/jsp/generated/" + module.name + "/" + name + "Confirm.inc" TO_GEN_WEBROOT»
	<script type="text/javascript">
		document.location=this.module.application.name».htm?_flowExecutionKey=${flowExecutionKey}&_eventId=submit";
	</script>
    «ENDFILE»
«ENDAROUND»
 

Thats it!

The above is an example of custom generation through an advice. Of course are there better ways of accomplish the above task.

Explanations

What's generated?

In short, a fully functional CRUD web application, based on spring webflow.

In this version of sculptor there aren't any gui specific parts in the DSL (this will probably change in the future), so the whole application is generated based on the same model as for the business tier application (you know, the one with the services etc). Besides that there are generation rules that looks at the service api and decides what to generate based on how they are formed. For instance, the menu is completely driven from what services an entity have available. If an entity have a 'save' service, the 'Create' menu option for that entity will be generated.

JSP only

Look inside the web project and drill down to src/main/webapp/WEB-INF/jsp to see all jsp's. 

There are some general pages like index.jsp, error.jsp etc, and then there are all domain object specific ones. Each module has a separate subfolder with pages, and each page has two parts. One that is always regenerated, and one that is generated once. The ones that are always generated are located under the generated sub folder.

So, for example, the create planet input page is a composite of these parts:

  • jsp/genereated/header.inc
  • jsp/milkyway/createPlanetForm.jsp
  • jsp/generated/milkyway/createPlanetForm.inc
  • jsp/generated/footer.inc

And here, the changeable part is the createPlanetForm.jsp page.

JSF only

Look inside the web project and drill down to src/main/webapp/WEB-INF/jsf to see all xhtml's. 

There are some general pages like index.xhtml, error.html etc, and then there are all domain object specific ones. Each module has a separate subfolder with pages, and each page has two parts. One that is always regenerated, and one that is generated once. The ones that are always generated are located under the generated sub folder.

So, for example, the create planet input page is a composite of these parts:

  • jsf/genereated/header.inc
  • jsf/milkyway/createPlanet.xhtml
  • jsf/generated/milkyway/createPlanet.inc
  • jsf/generated/footer.inc

And here, the changeable part is the createPlanet.xhtml page.

Java 

As sculptor always does, classes are generated with a base class (always regenerated), and a concrete class (generated once) where you can add your own logic.

For each domain object there are webflow implementation classes generated:

  • Action - each CRUD method gets its own action where the logic resides
  • Form - each CRUD method gets its own data carrier
  • Validator - each CRUD method gets its own validator
  • Property editor registrar -  each CRUD method gets its own property editor registrar (JSP only)

 Configuration

As always with spring there is a lot of configuration. The positive side here is that sculptor generates it for you. 

There are some application specific configuration files:

  • web.xml - the traditional servlet container configuration, here with a predefined webflow front controller
  • applicationContext.xml - the traditional spring configuration file
  • [application-name]-servlet-config.xml - the spring front controller configuration file
  • [application-name]-webflow-config.xml - the webflow framework configuration file

Beside these application wide configuration files, each domain object CRUD method has it own flow configuration file and own bean definition file. These reside in WEB-INF/flows. For instance, the planet domain object has:

  • Create
    • create-Planet-flow.xml
    • create-Planet-beans.xml
  • Read
    • view-Planet-flow.xml
    • view-Planet-beans.xml
  • Update
    • update-Planet-flow.xml
    • update-Planet-beans.xml
  • Delete
    • delete-Planet-flow.xml
    • delete-Planet-beans.xml

And besides the CRUD operations there is an additional flow:

  • List
    • list-Planet-flow.xml
    • list-Planet-beans.xml

 CSS and themes

 Sculptor will generate a default style and a default spring theme.

The style (CSS) will apply some color and position to the application. The positioning is best illustrated with a picture:

The generated style sheet is located in src/main/webapp/themes/basic/style.css and there is a generated property file for the default theme src/generated/resources/theme.properties. To add your own theme you add a corresponding properties file in src/main/resources/ and define your own style sheet.

E.g. src/main/resources/blue.properties
css=/themes/basic/blue.css

The theme of the user is changed when the request contains a theme parameter. E.g. of links to switch theme:

<a href="index.htm?theme=theme"><spring:message code="theme.default" text="Olive theme" /></a>
&nbsp;&nbsp;&nbsp;
<a href="index.htm?theme=blue"><spring:message code="theme.blue" text="Blue theme" /></a>

Internationalization

All texts can be defined in resource bundles. Sculptor generates resource bundles with suggestions of English texts based on the names in the DSL model. You can copy these to src/main/resources/i18n when you localize to a specific language. The generated files are located in src/generated/resources/i18n. The texts are separated in 2 files plus 1 for each module:

  • defaultMessages_en.properties
  • messages_en.properties
  • milkywayMessages_en.properties

Date format is defined in the messages resource bundle.

format.YearMonthDayPattern=yyyy-MM-dd
format.DateTimePattern=yyyy-MM-dd HH:mm

A special feature that is useful when the domain model evolves. Sculptor will display ??? at the texts in the gui that are not defined in a resource bundle, if you add the following to src/main/resources/sculptor-gui-generator.properties and regenerate.

sculptor-gui-generator.properties
gui.highlightMissingMessageResources=true

The locale of the user is changed when the request contains a locale parameter. E.g. of links to switch language:

<a href="index.htm?locale=en"><spring:message code="language.en" text="English" /></a>
&nbsp;&nbsp;&nbsp;
<a href="index.htm?locale=sv_SE"><spring:message code="language.sv" text="Svenska" /></a>

Required fields

Required fields are derived from the model. For attributes the rule are that if the attribute are 'nullable', the form field isn't required. For references we have two cases, 'one' and 'many' references.

For 'one' references the same rule as for attributes apply, i.e. if the reference is 'nullable' it isn't required in the gui. 

For 'many' references the 'nullable' keyword isn't available in the model. Here we have decided to use the 'required' model keyword instead. It isn't its original purpose but it solves the problem until we have the means for modelling whats required in the gui. So, for 'many' references, if the reference has the keyword 'required' in the model, the gui validates the reference that the Collection holding the instances has at least one element.   

Property Editors (JSP only)

If you try to enter a non digit in the diameter field of a Planet you will see an error message. PropertyEditors are used for conversion of textual input to object values and conversion of object values to display format. Default PropertyEditors are used for common types such as dates and numbers.
You can easily define your own property editor for a specific field by overriding registerCustomEditors in the registrar class. For example in CreatePlanetPropertyEditorRegistrar:

@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
    super.registerCustomEditors(registry);
    registry.registerCustomEditor(Integer.class,
            "diameter", new PositiveNumberEditor());
}

In the registrar class the message resources are available with the getMessagesAccessor method, which can be useful for defining locale aware settings, e.g. date pattern.

Another approach is to define the PropertyEditor in the code generator properties. This is the way Sculptor defines the editors for date and time types. You can define your own DSL type like this:

sculptor-generator.properties
javaType.PositiveInteger=Integer
db.mysql.type.PositiveInteger=INTEGER
sculptor-gui-generator.properties
propertyEditor.PositiveInteger=org.myframework.propertyeditor.PositiveIntegerEditor

This means that you can define Planet with PositiveInteger types and don't have to add any code in registrar classes:

model.design
Entity Planet {
        scaffold
        String name key;
        String message;
        PositiveInteger diameter nullable;
        PositiveInteger population nullable;
        - Set<@Moon> moons opposite planet;

Error handling

Two types of exceptions, ApplicationException and SystemException, are used as described in Advanced Tutorial. Error messages for these two types of error situations are displayed in different ways.

ApplicationException
When an ApplicationException occurs a message is displayed in the application pages. It is displayed in the same place as validation error messages.

Let us simulate an ApplicationException. The findById method throws PlanetNotFoundException, which is an ApplicationException, when a requested Planet doesn't exist. Click on List all Planets, right click on the id link of one Planet, and select Copy Link Location. Past into the browser URL field, but modify the last id request parameter, e.g. &id=999. When you try to load that page an error is displayed.

The actual error message can be defined in messages resource bundle. The errorCode of the ApplicationException is used as key to the resource bundle. Parameters of the exception can be used as message resource parameters.

org.helloworld.milkyway.exception.PlanetNotFoundException=Planet with id {0} doesn't exist.

The error handling for ApplicationException is done by an advice that intercepts all methods in the actions. It catch exceptions and bind the error message to the form errors object.

OptimisticLocking
When two users try to update the same entity simultaneously an OptimisticLockingException is thrown. This exception is treated in the same way as ApplicationException, i.e. error message on the same page.

SystemException
A separate error page is displayed when a SystemException or any other unexpected RuntimeException occurs. Normally it is only necessary to display a general error message, but it is possible to define individual error messages for specific exceptions. The errorCode in the SystemException or the name of the RuntimeException is used as key in the resource bundle.

org.fornax.cartridges.sculptor.framework.errorhandling.SystemException=System error ({0}), <br/>caused by: {1}
org.fornax.cartridges.sculptor.framework.errorhandling.DatabaseAccessException=System error. Database problem.

The fault barrier is implemented with a Spring exception resolver that directs to error.jsp, which resolves and displays the error message.

Hibernate Session

The GRUD Gui application uses a kind of Extended Session pattern for long Conversations. The same Hibernate session is used during a conversation, i.e. from a top flow and throughout its subflows, but it is disconnected in between each request. In practice this means that lazy loading of collections is possible when traversing associations.

However, all root objects are retrieved with service methods and transactional services perform all updating operations. The transaction boundary is still at the service layer, but the DomainObjects are not detached when used in the presentation tier.

Have a look at the JavaDoc and source code of OpenHibernateSessionInConversationListener
to learn more about the implementation details.

Resources 

 Source

The complete source code for this tutorial is available in Subversion.
Web Access (read only):

Anonymous Access (read only):

More Advanced Example

You can try the more comprehensive Library example, which also has a generated GRUD Gui. This application implements both the JSP and JSF variants, and both can be deployed and used in parallel.
Checkout these four projects from Subversion.

 Links 

Hi,
to test successfully the web layer using JUnit, I had to add into the following declaration into the file create-Planet-beans.xml

<bean class="org.helloworld.milkyway.serviceproxy.PlanetServiceProxy" id="planetServiceProxy"/>

It seems to not read the file applicationContext.xml, is it normal?
Oliv

Posted by Anonymous at Feb 14, 2008 11:48 | Permalink | Reply To This

Thanks, it must be a bug. We will take a look at it. /Patrik

Posted by Patrik Nordwall at Feb 14, 2008 12:52 | Permalink | Reply To This

Hi,

I generated the project structures and maven build files as shown in the Archetype Tutorial. After importing the projects to Eclipse and changing the model.design to the one shown at the begin of this tutorial, "mvn clean install" on the parent project fails with this error:

 [INFO] Compiling 15 source files to C:\Sculptor\helloworld-web\target\classes
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Compilation failure

C:\Sculptor\helloworld-web\src\main\java\org\helloworld\milkyway\web\UpdatePlanetPropertyEditorRegistrar.java:[9,8] cannot find symbol
symbol  : method registerCustomEditorsForForm(org.springframework.beans.PropertyEditorRegistry)
location: class org.helloworld.milkyway.web.UpdatePlanetPropertyEditorRegistrar

C:\Sculptor\helloworld-web\src\main\java\org\helloworld\milkyway\web\UpdatePlanetPropertyEditorRegistrar.java:[10,8] cannot find symbol
symbol  : method registerCustomEditorsForConfirm(org.springframework.beans.PropertyEditorRegistry)
location: class org.helloworld.milkyway.web.UpdatePlanetPropertyEditorRegistrar

[INFO] ------------------------------------------------------------------------
[INFO] Trace
org.apache.maven.BuildFailureException: Compilation failure
    at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoals(DefaultLifecycleExecutor.java:579)
    at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoalWithLifecycle(DefaultLifecycleExecutor.java:499)
    at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoal(DefaultLifecycleExecutor.java:478)
    at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoalAndHandleFailures(DefaultLifecycleExecutor.java:330)
    at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeTaskSegments(DefaultLifecycleExecutor.java:291)
    at org.apache.maven.lifecycle.DefaultLifecycleExecutor.execute(DefaultLifecycleExecutor.java:142)
    at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:336)
    at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:129)
    at org.apache.maven.cli.MavenCli.main(MavenCli.java:287)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.codehaus.classworlds.Launcher.launchEnhanced(Launcher.java:315)
    at org.codehaus.classworlds.Launcher.launch(Launcher.java:255)
    at org.codehaus.classworlds.Launcher.mainWithExitCode(Launcher.java:430)
    at org.codehaus.classworlds.Launcher.main(Launcher.java:375)
Caused by: org.apache.maven.plugin.CompilationFailureException: Compilation failure
    at org.apache.maven.plugin.AbstractCompilerMojo.execute(AbstractCompilerMojo.java:516)
    at org.apache.maven.plugin.CompilerMojo.execute(CompilerMojo.java:114)
    at org.apache.maven.plugin.DefaultPluginManager.executeMojo(DefaultPluginManager.java:451)
    at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoals(DefaultLifecycleExecutor.java:558)
    ... 16 more

Posted by Anonymous at Nov 01, 2008 21:51 | Permalink | Reply To This
Site powered by a free Open Source Project / Non-profit License (more) of Confluence - the Enterprise wiki.
Learn more or evaluate Confluence for your organisation.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.2.9 Build:#527 Sep 07, 2006) - Bug/feature request - Contact Administrators