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:
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:
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 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() {
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");
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.
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>
<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.
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>
<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:
javaType.PositiveInteger=Integer
db.mysql.type.PositiveInteger=INTEGER
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:
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