jZeno

pure java web development

  • Increase font size
  • Default font size
  • Decrease font size

Introduction

In the following tutorial we will examine a small jZeno demo application. It is a basic example but it will show a lot of aspects of a jZeno application. The application that we are going to use is a simple order system. Customers can order products and these products are managed by an administrator. All source code is included in the jzeno download.

The tutorial below is as of version 1.2.0 of jZeno. The previous tutorial is located here .

Credit

We would like to thank Glen Vermeylen and Joeri Van Geystelen for creating this jZeno tutorial. This online version of the tutorial is an adaptation of their original PDF version. Thanks guys !

jZeno

We will now give a quick overview of the jZeno platform. The architecture of jZeno is based on the classic 3-tier architecture. The application is divided in 3 different tiers where each tier has his specific responsibility :

  • Presentation Layer : This layer has the responsibility to provide the view of the application and provide the user interface towards the user.
  • Business Layer : This layer contains the business logic of the application.
  • Domain Layer : This layer has the responsibility to persist the business data in a database.

These layers clearly separate different concerns of the application. A graphical representation of the implementation of those different technical layers in jZeno  :

jzeno-layers.jpg

Parts that are implemented as part of the framework are shown in light grey. Parts that are implemented by the application developers is shown in dark grey. A short description of functionality in the different layers is described below.

Presentation Layer

This is where the developer implements the user interface in pure Java code, using a rich set of components as described below, and interacting with domain level concepts that are managed by the Business Layer.

  • Rendering Engine. jZeno comes with a highly optimized rendering engine. This takes care of constructing AJAX based web-pages based on your presentation code. This layer takes care of dealing with technical details such as HTTP, HTML, CSS, JavaScript, DOM, Networking, Multi-Threading, etc...
  • Dynamic Components. jZeno comes bundled with a number of components to create a rich user experience, to ease development. These components take care of synchronizing state between the browser and your server, validating user input, dispatching events to event handlers, updating domain model objects, etc...

Business Layer

This is where the developer implements the specific business rules for the application and ultimately manipulates the Domain Layer. Queries to the database are written in a database independent way using the open source hibernate persistence framework. In essence developers are working with objects of the domain layer, using and manipulating them, and never need to worry about synchronizing these objects to the database. jZeno transparently manages your Hibernate session and will automatically synchronize the domain model objects with the persistent storage.

  • Business Services. jZeno comes with a stack of services that facilitate things like security checking to authorize access to the Business Layer, dealing with dead-lock situations, setting up database transactions, managing ORM sessions, performance measuring, etc...

Domain Layer

Domain model objects represent cocepts in your problem domain, and are automatically synchronized to the database.

  • ORM Layer. Hibernate is used to persist and query the domain layer. jZeno eases the use of hibernate by managing hibernate sessions transparently, as well as automatically mapping java classes to database tables (as well as dynamically creating the database schema as you write your software). jZeno assumes that you databases are 'application databases' and not 'integration databases'. This is to say that we assume your application to encapsulate it's database. Integration should be done using web services, messaging, etc..
  • Relational Database. jZeno applications are independent of which specific database you are using for storage, easing migration in the future, and allowing you to choose the optimal database without having to worry about vendor lock-in.

jZeno advantages

  • It's cross-browser display technology is AJAX based, and uses very compact interactions with the server. Web requests are compressed dramatically, greatly reducing the network bandwidth usage. jZeno has been succesfully tested on IE, Firefox,Opera,Konqueror and Safari.
  • The client for the application is a “thin” client with a rich user interface. Most of the processing occurs on the server and the client only has the responsibility to display the application. No extra software needs to be installed, only an AJAX – compliant web browser is needed.
  • Is 100% written in Java and therefore it runs on any J2EE compliant (servlet) container like Tomcat, Weblogic, Websphere, etc..
  • Comes with built-in LIVE performance measuring and heap inspection.
  • It has a short learning period, because of the reduced set of API's developers need to learn.
  • Increases productivity. Among other things it eliminates most server restarts during development through dynamic class reloading.
  • Supports reporting in PDF, Excel, RTF, HTML, ... through Jasper Reports.
  • ...

The Tutorial Appliciation

This tutorial will be creating a simple application that integrates many different parts of jZeno. To give you an idea of what the application will look like, we'll give you a short tour :

First of all users need to log on to the system. After all not all users are allowed to view all data and/or perform all actions.

tutorial-logon.jpg

 

After this users can order items :

tutorial-order.jpg

And they can view their orders. (Administrators can view all orders)

tutorial-display.jpg

And finally only administrators can configure new or existing products :

tutorial-admin2.jpg

An interactive version of this tutorial is also available online here .

 

Domain Layer - Data Model

In the model we will map our data towards the database. Our domain model looks like this :

tutorial-uml.jpg

 

Hibernate configuration

jZeno uses hibernate to perform object-relational mapping. Hibernate only needs minimal configuration :

  • hibernate.properties

A quick overview of the needed configuration will be given, but for more detail, cfr. the hibernate documentation at www.hibernate.org

In hibernate.properties, we configure how hibernate connects to it's database. This example configures hibernate to work with a local H2 database. jZeno comes with H2 built-in to allow you to experiment with it without the need to install a database.

 

#
#Mon May 15 15:30:04 CEST 2006
hibernate.c3p0.timeout=60
hibernate.connection.driver_class=org.h2.Driver
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.connection.url=jdbc:h2:~/jZenoTutorialDatabase/database
hibernate.connection.username=sa
hibernate.connection.password=
hibernate.show_sql=true

hibernate.cache.provider_class=org.hibernate.cache.EhCacheProvider

hibernate.c3p0.max_statements=0
hibernate.c3p0.max_size=100
hibernate.c3p0.idle_test_period=20
hibernate.c3p0.min_size=10

 

In case you are wondering where our test data is coming from, it is inserted by the application before the login page is shown (Take a look at TestDbSetupFacade for details).

Modelbeans

These are classic hibernate POJO’s. We only need to supply a minimum of extra meta-information with annotations. The data from the database will be represented by instances of these classes. For more information about how jZeno can map your domain model automatically to the database click here .

 

package net.sf.jzeno.tutorial.model;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import net.sf.jzeno.model.automap.PersistentEntity;

public class Order extends PersistentEntity {
	private static final long serialVersionUID = 1L;

	private Set orderItems = new HashSet();

	private User user;

	private Date orderDate;

	public User getUser() {
		return user;
	}

	public void setUser(User user) {
		this.user = user;
	}

	public Set getOrderItems() {
		return orderItems;
	}

	public void setOrderItems(Set orderItems) {
		this.orderItems = orderItems;
	}

	public Date getOrderDate() {
		return orderDate;
	}

	public void setOrderDate(Date date) {
		this.orderDate = date;
	}

	public BigDecimal getPrice() {
		BigDecimal ret = new BigDecimal(0).setScale(2,
				BigDecimal.ROUND_HALF_EVEN);
		for (OrderItem orderItem : orderItems) {
			ret = ret.add(orderItem.getProduct().getPrice().multiply(
					new BigDecimal(orderItem.getAmount())));
		}
		return ret;
	}
}

 

Our persistent classes need to derive from a common baseclass net.sf.jzeno.model.automap.PersistentEntity.

The User class needs to implement jZeno's User interface so it can take advantage of jZeno's built-in security support.

 

package net.sf.jzeno.tutorial.model;

import java.util.HashSet;
import java.util.Set;

import net.sf.jzeno.model.automap.PersistentEntity;
import net.sf.jzeno.model.automap.annotation.AutoMapString;

public class User extends PersistentEntity implements net.sf.jzeno.model.User {
	private static final long serialVersionUID = 1L;

	@AutoMapString(maxLength = 64, required = true)
	private String userName;

	private Set roles = new HashSet();

	@AutoMapString(maxLength = 64, required = true)
	private String password;

	public String getUserName() {
		return userName;
	}

	public boolean hasPermission(String permissionName) {
		for (Role role : roles) {
			if (role.hasPermission(permissionName)) {
				return true;
			}
		}
		return false;
	}

	public boolean hasRole(String roleName) {
		for (Role role : roles) {
			if (role.getName().equals(roleName)) {
				return true;
			}
		}
		return false;
	}

	public Set getRoles() {
		return roles;
	}

	public void setRoles(Set roles) {
		this.roles = roles;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getPassword() {
		return password;
	}
}

 

Note the methods hasRole(String role) and hasPermission(String permission). These methods are required by the interface net.sf.jzeno.model.User. It is up to your application to implement a suitable strategy for checking permission on a user object... In the example a straight-forward implementation is used. The model objects Role and Permission are iterated to determine the security context of the current user.

 

Business Layer - Business Facades

This layer contains the business logic to get and store data in the data layer. Business facades and GUI components can be reloaded by jZeno if you put them in the src/rjava folder, meaning you don't need to restart your server when code has changed but you can just click the "reload" button (that's the reload button in jZeno - not the browser's reload button). Now for the business facades. The following business facades are needed for the tutorial to be able to work :

  • net.sf.jzeno.tutorial.business.BusinessFactory.java
  • net.sf.jzeno.tutorial.business.OrderCriteria.java
  • net.sf.jzeno.tutorial.business.OrderFacade.java
  • net.sf.jzeno.tutorial.business.OrderFacadeImpl.java
  • net.sf.jzeno.tutorial.business.ProductCriteria.java
  • net.sf.jzeno.tutorial.business.ProductFacade.java
  • net.sf.jzeno.tutorial.business.ProductFacadeImpl.java
  • net.sf.jzeno.tutorial.business.TestDbSetupFacade.java (This facade fills the database with a customer and admin user)
  • net.sf.jzeno.tutorial.business.TestDbSetupFacadeImpl.java
  • net.sf.jzeno.tutorial.business.UserManagerFacade.java
  • net.sf.jzeno.tutorial.business.UserManagerFacadeImpl.java

The contract for writing business facades under jzeno is that you create an interface with the facade name you want to use and add the suffix “Impl” for the implementation.

configuration.properties 

This file specifies jZeno specific configuration properties. 

#
#Fri Jul 27 15:56:25 CEST 2007
use.precreation=true
source.folder=/home/codehawk/jZeno/template/src/java
suppress.security.aspect=false
global.exception.handler.class.name=net.sf.jzeno.DefaultGlobalExceptionHandler
default.layout.classname=net.sf.jzeno.echo.ZenoLayout
enable.class.reloading=true
business.package=net.sf.jzeno.tutorial.business
style.class.name=net.sf.jzeno.echo.GenericStyle
mail.sendmail=false
invalid.marker.class.name=net.sf.jzeno.echo.SpecificInvalidMarker
override.readonly.attributes=false
application.title=jZeno Tutorial
initial.screen.classname=net.sf.jzeno.tutorial.screen.LoginScreen
optimistic.lock.exception.handler.class.name=net.sf.jzeno.DefaultOptimisticLockExceptionHandler
timeformat.class.name=net.sf.jzeno.util.ZenoTimeFormat
dateformat.class.name=net.sf.jzeno.util.ZenoDateFormat

automap=true
automap.packages=net.sf.jzeno.tutorial.model
automap.schema=jZenoTutorial
automap.catalog=
automap.log=false

Note the entries related to automapping. All of the entries in this file can also be viewed/changed through jZeno's general settings screen. Take a look at this screen, and don't forget to read the tooltips that provide info on the meaning of every setting.

UserManagerFacade

Our UserManagerFacade implements jZeno's UserManagerFacade because jZeno needs to now how to create an anonymous user. Such an anonymous user is used to establish a security context for users that have not logged on to the system. Typically this would be an implementation of net.sf.jzeno.model.User with very little permissions in the system. The class implementing this createAnonymousUser must be called UserManagerFacade (for the interface and UserMangerFacadeImpl for the implementation). 

The authenticate() method will be called by our LoginScreen.

PersistencySupport.swizzle(user, "roles.permissions"); tells jZeno to also load the attached roles and their permissions. You need to do this because once you leave the business method your hibernate session will be closed and you won't be ably to lazy load the user's relates model objects. In this case we know that we will also need the user's roles and their permissions. As long as you are writing code inside you business facade there's no need for swizzling. In the presentation layer however all model objects are disconnected from their ORM session.

Interface 

package net.sf.jzeno.tutorial.business;

import net.sf.jzeno.tutorial.model.User;

public interface UserManagerFacade extends
		net.sf.jzeno.business.UserManagerFacade {
	User authenticate(String username, String password);
}

Implementation 

package net.sf.jzeno.tutorial.business;

import net.sf.jzeno.aop.Criteria;
import net.sf.jzeno.aop.PersistencySupport;
import net.sf.jzeno.tutorial.model.User;

import org.hibernate.criterion.Restrictions;

public class UserManagerFacadeImpl implements UserManagerFacade {
	public User authenticate(String userName, String password) {
		Criteria crit = PersistencySupport.createCriteria(User.class);
		crit.add(Restrictions.eq("userName", userName));
		crit.add(Restrictions.eq("password", password));
		User user = (User) crit.uniqueResult();

		PersistencySupport.swizzle(user, "roles,roles.permissions");
		return user;
	}

	public User createAnonymousUser() {
		return new User();
	}
}

ProductFacade

The find() method illustrates a way to retrieve objects based upon variable parameters. Create a "Criteria" object which contains a nullable property for each field you want to filter at the moment we only need to find a product by name so there's no need in adding additional criteria, but if the need should arise to find products by description or price, you can add them).

ProductCriteria

package net.sf.jzeno.tutorial.business;

public class ProductCriteria {
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

GenericFacade

In net.sf.jzeno.business.GenericFacade(Impl) a generic business method saveOrUpdate() is used by the tutorial to update information in the database. Notice that this method returns the object we save. This is important because when a client passes an object to a facade, jZeno perfomrs calls to the business layers with call-by-value semantics. So the object in the business method is different instance from the one on the client, any change made to it will not be visible to the client. Most of the time you won't even notice there is a problem since it is likely you want to update the database to values entered on the screen so you can't really see the object you are using actually represents old data. However, jZeno uses an optimistic locking strategy, so every model object has a version field which gets incremented on each update, and this field also needs to be kept in sync on the presentation layer. If this layer doesn't refresh the object after a database change, the version field in the database would become greater than on the presentation layer. If you then want to update this object a second time, hibernate would see that the version of the object you want to update is smaller than the one in the database, and a OptimisticLockException will be thrown. Therefor the presentation layer allways uses the return value to replace it's model object.

ProductFacadeImpl

 

package net.sf.jzeno.tutorial.business;

import java.util.List;

import net.sf.jzeno.aop.Criteria;
import net.sf.jzeno.aop.PersistencySupport;
import net.sf.jzeno.tutorial.model.Product;

import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;

public class ProductFacadeImpl implements ProductFacade {
  public Product save(Product p) {
    return p;
  }

  @SuppressWarnings("unchecked")
  public List find(ProductCriteria productCriteria) {
    Criteria crit = PersistencySupport.createCriteria(Product.class);

    if (productCriteria != null) {
      if (productCriteria.getName() != null) {
        crit.add(Restrictions.eq("name", productCriteria.getName()));
      }
    }

    crit.addOrder(Order.asc("name"));
    return crit.list();
  }
}

 

Presentation Layer - GUI

Now we will develop the user interface of our application. The file menu.properties configures the application menu (placed in src/java):

  • menu.properties

We have 2 custom components (src/rjava):

  • net.sf.jzeno.tutorial.components.ProductNameEditor.java
  • net.sf.jzeno.tutorial.components.SaveCancelProductToolbar.java

... and 4 screens (src/rjava):

net.sf.jzeno.tutorial.screen.LoginScreen.java
net.sf.jzeno.tutorial.screen.OrderListScreen.java
net.sf.jzeno.tutorial.screen.OrderProductScreen.java
net.sf.jzeno.tutorial.screen.ProductAdminScreen.java

The details of the menu.properties file can also be edited using the Menu Editor which is available if you log in as admin.

Screen and Layout

A screen in jZeno is the specific content you wish to display. In most applications however, you have besides the variable screen content also a part you always want to display, such as a header, menu, footer, .... . This is implemented in jZeno through the concept of a Layout. Think of a layout as a screen which contains different screens, but also a title, menu, footer,... or anything else that always needs to stay visible). You can create a layout by extending the AbstractLayout. jZeno calls the method setContent(Component component) when it wants to place a new screen inside the layout. In this tutorial we will use the built-in default layout net.sf.jzeno.echo.ZenoLayout.We encourage you to take a look at the implementation of this screen. Remember that jZeno's .jar file comes with both bytecode as well as source code, so you can at any time drill into the sources of any jZeno class (use CTRL+left click in eclipse, or use CTRL-T to call up the source of any class)

The menu is specified in the file menu.properties, where menu entries are entered in the form:

 

menu.menuItem=Menu Title,fully.qualified.screenName,requiredPermissionToViewMenuItem

 

The default layout and screen are specified in configuration.properties

 

initial.screen.classname=net.sf.jzeno.tutorial.screen.LoginScreen
default.layout.classname=net.sf.jzeno.echo.ZenoLayout

 

LoginScreen

Let's look at our login screen. How the layout is composed is pretty self-explanatory: It basically boils down to creating hierarchies of grids for positioning. Look at how there are 2 equivalent ways to initialize components.

 

DynaButton loginButton = new DynaButton(null, "", "");
loginButton.setText("login");
loginButton.setActionCommand("login");

 

and

 

mainGrid.add(new DynaButton(null, "", "text=InitializeB,actionCommand=initDb"));

 

The shorthand method will use reflection to set the properties on the component. The actionCommand is a property on our loginScreen which is called once the button is pressed (in this case we have specified a method public void login(ActionEvent event) and public void initDb() . You can specify either zero parameters, an ActionEvent or a PropertyComponent as a parameter in your event handler and jZeno will find it.

Look at how we receive input from the user.

 

usernameField = new DynaTextField(getClass(), "username", "");

 

This process is known as binding. With the above statement we tell jZeno to make the property "username" of the LoginScreen accessible to the textfield. From now on the visual text field will allways show and modify the string that was exposed as property username.

 

package net.sf.jzeno.tutorial.screen;

import net.sf.jzeno.aop.SecuritySupport;
import net.sf.jzeno.business.BusinessFactory;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.Precreation;
import net.sf.jzeno.echo.components.DynaGrid;
import net.sf.jzeno.echo.components.KeyActionCommand;
import net.sf.jzeno.echo.components.NavigationHistory;
import net.sf.jzeno.echo.components.Screen;
import net.sf.jzeno.echo.components.TaskBar;
import net.sf.jzeno.echo.components.Title;
import net.sf.jzeno.echo.databinding.DynaButton;
import net.sf.jzeno.echo.databinding.DynaLabel;
import net.sf.jzeno.echo.databinding.DynaPasswordField;
import net.sf.jzeno.echo.databinding.DynaTextField;
import net.sf.jzeno.tutorial.business.TestDbSetupFacade;
import net.sf.jzeno.tutorial.business.UserManagerFacade;
import net.sf.jzeno.tutorial.model.User;
import net.sf.jzeno.util.FastFactory;
import nextapp.echo.Component;
import nextapp.echo.EchoConstants;
import nextapp.echo.event.ActionEvent;
import echopoint.Label;

public class LoginScreen extends Screen implements Precreation {
	private static final long serialVersionUID = 1L;

	private static final String TITLE = "Welcome to the jZeno tutorial application.";

	private String username;

	private String password;

	private DynaPasswordField passwordField;

	private DynaTextField usernameField;

	public LoginScreen() {
		DynaGrid mainGrid = new DynaGrid();
		addCentered(mainGrid);
		mainGrid.setColumns(1);
		mainGrid.setCellMargin(2);
		mainGrid.setWidth(100);
		mainGrid.setWidthUnits(DynaGrid.PERCENT_UNITS);
		mainGrid.add(new Title(TITLE));
		mainGrid.add(new DynaLabel(
				"Login as administrator with username/password = 'admin'"));
		mainGrid.add(new DynaLabel(
				"Login as customer with username/password = 'customer'"));

		DynaGrid loginGrid = new DynaGrid(getClass(), "",
				"columns=2,width=0,cellMargin=2");
		mainGrid.addCentered(loginGrid);
		loginGrid.add(new Label("username:"));
		usernameField = new DynaTextField(getClass(), "username", "");
		loginGrid.add(usernameField);
		loginGrid.add(new Label("password:"));
		passwordField = new DynaPasswordField(getClass(), "password", "");
		loginGrid.add(passwordField);
		DynaGrid.Cell cell = new DynaGrid.Cell();
		cell.setRowSpan(2);
		DynaButton loginButton = new DynaButton(null, "", "");
		loginButton.setText("login");
		loginButton.setActionCommand("login");
		cell.add(loginButton);
		cell.setColumnSpan(2);
		cell.setHorizontalAlignment(EchoConstants.CENTER);
		loginGrid.add(cell);

		KeyActionCommand kac = new KeyActionCommand();
		kac.setActionCommand("login");
		add(kac);

		applyContext();
	}

	public String getHistoryTitle() {
		return null;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public void login(ActionEvent event) {
		String username = usernameField.getText();
		String password = passwordField.getText();
		User user = (User) BusinessFactory.getFacade(UserManagerFacade.class)
				.authenticate(username, password);
		if (user != null) {
			SecuritySupport.login(user);
			if (SecuritySupport.currentUserHasPermission("productAdminScreen")) {
				EchoSupport.setScreen(new ProductAdminScreen());
			} else {
				EchoSupport.setScreen(new OrderProductScreen());
			}
		} else {
			EchoSupport.addError("loginscreen.error.invalid.logon");
		}

		setTaskbarVisible(true);
	}

	public void initDb() {
		BusinessFactory.getFacade(TestDbSetupFacade.class).setupDb();
	}

	private boolean init = false;

	public void preRender() {
		setTaskbarVisible(false);
		if (!init) {
			initDb();
			init = true;
		}
	}

	private void setTaskbarVisible(boolean b) {
		TaskBar menuBar = (TaskBar) EchoSupport.findComponentByType(
				(Component) EchoSupport.getLayout(), TaskBar.class);
		menuBar.setVisible(b);
		menuBar.rebuild();
		NavigationHistory navigationHistory = (NavigationHistory) EchoSupport
				.findComponentByType((Component) EchoSupport.getLayout(),
						NavigationHistory.class);
		navigationHistory.setVisible(false);
	}

	public void applyContext() {
		if (FastFactory.isInsideContext()) {
			usernameField.setFocused(true);
		}
	}
}

 

ProductAdministrationScreen

This screen will be responsible for creating and updating our products. This is a good opportunity to demonstrate the DynaTable functionality. Look at the code used to display our product table :

ConstructionList cl = new ConstructionList();
cl.add("", "Name", "", StringViewer.class,"","required=true",ProductNameEditor.class);
cl.add("description","Description","",StringViewer.class,"description","",StringEditor.class);
cl.add("price","Price","",DoubleViewer.class,"price","",BigDecimalEditor.class);
cl.add("","","text=Edit,actionCommand=editProduct",DynaButton.class,"",
"saveCommand=saveProduct,cancelCommand=cancelEditProduct",
SaveCancelProductToolbar.class);
allProductsTable = new DynaTable(getClass(), "allProducts", "", cl);
add(allProductsTable);

The products table is bound to the list "allProducts" of our ProductAdminScreen and is created based upon a ConstructionList, that basically contains specifications for the columns of the table we are creating. When using a ConstructionList every add statement corresponds to adding a column in the table. For such a column you can specify a component and the property of the corresponding product to which this component is bound (by default it is bound to the entire Product). But notice how we seem to declare our binding twice for each column, this is because a DynaTable can have 2 modes for rendering a row: display and edit. For the "Name" column we use a StringViewer to display the name of a product, and we use our own component ProductNameEditor to edit the name. I will explain the code of the ProductNameEditor below, but know for now it verifies no duplicate product name is used. Also note that even though we only specify the class name of the ProductNameEditor, we can still alter the properties in the construction-hints specified ("required=true").
For the last column an edit-button will be rendered in display-mode with an event handler public void editProduct(). This function will get the product of the row where the button was clicked and will set that row to edit-mode:

 


public void editProduct(ActionEvent event) {
  Product p = (Product) ((CustomComponent) event.getSource()).getValue();
  allProductsTable.edit(p);
}

 

For edit-mode another custom component is used which renders a save and cancel button and will fire callback events as specified in the constructionhints field.

@SuppressWarnings("unchecked")
public void saveProduct(ActionEvent event) {
    EchoSupport.doValidationRecursively(this);
    if (EchoSupport.isValidRecursively(this)) {
        Product p = (Product) ((PropertyComponent) event.getSource()).getValue();
        p = BusinessFactory.getProductFacade().save(p);
        allProductsTable.apply();
        refreshAllProducts();
        allProductsTable.update();
    }
}
public void cancelEditProduct(ActionEvent event) {
    allProductsTable.cancel();
}

 

Notice the use of ((PropertyComponent)event.getSource()).getValue() what this does is use the action event's source, which is a PropertyComponent to retrieve the value that component is bound to. All jZeno dynamic components implement this interface and can be used in this way.

Finally, the process of adding a new product is also pretty simple:

 

@SuppressWarnings("unchecked")
    public void createNewProduct() {
        Product product = new Product();
        allProductsTable.addNew(product);
    }
}

 

ProductAdminScreen

 

package net.sf.jzeno.tutorial.screen;

import java.util.List;

import net.sf.jzeno.business.BusinessFactory;
import net.sf.jzeno.business.GenericFacade;
import net.sf.jzeno.echo.ConstructionList;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.Precreation;
import net.sf.jzeno.echo.components.DynaGrid;
import net.sf.jzeno.echo.components.Screen;
import net.sf.jzeno.echo.components.Title;
import net.sf.jzeno.echo.databinding.DynaButton;
import net.sf.jzeno.echo.databinding.DynaLink;
import net.sf.jzeno.echo.databinding.DynaTable;
import net.sf.jzeno.echo.databinding.PropertyComponent;
import net.sf.jzeno.echo.editor.BigDecimalEditor;
import net.sf.jzeno.echo.editor.StringEditor;
import net.sf.jzeno.echo.viewer.BigDecimalViewer;
import net.sf.jzeno.echo.viewer.StringViewer;
import net.sf.jzeno.tutorial.business.ProductFacade;
import net.sf.jzeno.tutorial.components.ProductNameEditor;
import net.sf.jzeno.tutorial.components.SaveCancelProductToolbar;
import net.sf.jzeno.tutorial.model.Product;
import net.sf.jzeno.util.FastFactory;
import nextapp.echo.event.ActionEvent;

public class ProductAdminScreen extends Screen implements Precreation {
	private static final long serialVersionUID = 1L;

	private static final String TITLE = "Product Administration";

	private DynaGrid mainGrid;

	private String name;

	private List allProducts;

	private DynaTable allProductsTable;

	public ProductAdminScreen() {
		mainGrid = new DynaGrid(getClass(), "", "columns=1,cellMargin=2");
		add(mainGrid);

		mainGrid.add(new Title(TITLE));

		ConstructionList cl = new ConstructionList();
		cl.add("name", "Name", "", StringViewer.class, "", "required=true",
				ProductNameEditor.class);
		cl.add("description", "Description", "", StringViewer.class,
				"description", "", StringEditor.class);
		cl.add("price", "Price", "", BigDecimalViewer.class, "price",
				"width=70", BigDecimalEditor.class);
		cl
				.add(
						"",
						"",
						"toolTipText=Edit Product,actionCommand=editProduct,iconName=edit.png",
						DynaLink.class,
						"",
						"saveCommand=saveProduct,cancelCommand=cancelEditProduct",
						SaveCancelProductToolbar.class);
		allProductsTable = new DynaTable(getClass(), "allProducts", "", cl);
		allProductsTable.setColumnWidthUnits(DynaTable.PIXEL_UNITS);
		allProductsTable.setColumnWidth(0, 200);
		allProductsTable.setColumnWidth(1, 200);
		allProductsTable.setColumnWidth(2, 80);
		allProductsTable.setColumnWidth(3, 50);
		allProductsTable.setWidth(-1);
		mainGrid.add(allProductsTable);
		mainGrid.add(new DynaButton(null, null,
				"actionCommand=createNewProduct,text=Create new Product"));

		applyContext();
	}

	private void refreshAllProducts() {
		allProducts = BusinessFactory.getFacade(ProductFacade.class).find(null);
	}

	public void editProduct(ActionEvent event) {
		Product p = (Product) ((PropertyComponent) event.getSource())
				.getValue();
		allProductsTable.edit(p);
	}

	public void saveProduct(ActionEvent event) {
		EchoSupport.doValidationRecursively(this);
		if (EchoSupport.isValidRecursively(this)) {
			Product p = (Product) ((PropertyComponent) event.getSource())
					.getValue();
			p = (Product) BusinessFactory.getFacade(GenericFacade.class)
					.saveOrUpdate(p);
			allProductsTable.apply();
			refreshAllProducts();
		}
	}

	public void cancelEditProduct(ActionEvent event) {
		allProductsTable.cancel();
	}

	public void createNewProduct() {
		Product product = new Product();
		allProductsTable.addNew(product);
	}

	public String getHistoryTitle() {
		return TITLE;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public List getAllProducts() {
		return allProducts;
	}

	public void setAllProducts(List added) {
		this.allProducts = added;
	}

	public void applyContext() {
		if (FastFactory.isInsideContext()) {
			refreshAllProducts();
		}
	}
}

 

SaveCancelProductToolbar

This is a very basic custom component. It provides 2 buttons (save and cancel) and allows the user of the component to specify the actionCommand when one of the buttons is pressed.

 

package net.sf.jzeno.tutorial.components;

import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.EventPropagator;
import net.sf.jzeno.echo.components.CustomComponent;
import net.sf.jzeno.echo.databinding.DynaLink;
import nextapp.echo.event.ActionEvent;

public class SaveCancelProductToolbar extends CustomComponent
    implements EventPropagator {
  private static final long serialVersionUID = 1L;

  private DynaLink saveButton;

  private DynaLink cancelButton;

  private String saveCommand = "save";

  private String cancelCommand = "cancel";

  public SaveCancelProductToolbar() {
    this(null, null, null);
  }

  public SaveCancelProductToolbar(Class beanClass, String property,
      String constructionHints) {
    super(beanClass, property, "");
    initGui();
    EchoSupport.executeHints(this, constructionHints);
  }

  public void initGui() {
    saveButton = new DynaLink(getClass(), "",
        "toolTipText=Save Product,iconName=save.gif,actionCommand=dummy");
    cancelButton = new DynaLink(getClass(), "",
        "toolTipText=Cancel,iconName=cancel.gif,actionCommand=dummy");
    add(saveButton);
    add(cancelButton);
  }

  public String getSaveCommand() {
    return saveCommand;
  }

  public void setSaveCommand(String saveCommand) {
    this.saveCommand = saveCommand;
  }

  public String getCancelCommand() {
    return cancelCommand;
  }

  public void setCancelCommand(String cancelCommand) {
    this.cancelCommand = cancelCommand;
  }

  public void propagateEvent(ActionEvent event) {
    Object source = event.getSource();
    if (source == saveButton) {
      fireActionEvent(new ActionEvent(this, getSaveCommand()));
    } else if (source == cancelButton) {
      fireActionEvent(new ActionEvent(this, getCancelCommand()));
    } else {
      fireActionEvent(event);
    }
  }
}

 

ProductNameEditor

Our ProductNameEditor show how easy it is to add some basic validation. The ProductNameEditor was bound to the root object (a product) of the table in the ProductAdminScreen and binds a StringEditor to the "name" property of this product. You see that the bound object is always accessible through the inherited property "value" (accessed by get/setValue()). We also want to add validation to the ProductNameEditor so it checks the name for uniqueness, this is done by adding a Validator to the object. When validation is called (see ProductAdministrationScreen.saveProduct()), this Validator will be asked for a list of ValidationErrors. These errors contain a component which will be visually marked and an error message which will be shown to the user.

 

package net.sf.jzeno.tutorial.components;

import java.util.ArrayList;
import java.util.List;

import net.sf.jzeno.business.BusinessFactory;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.ValidationError;
import net.sf.jzeno.echo.Validator;
import net.sf.jzeno.echo.components.CustomComponent;
import net.sf.jzeno.echo.databinding.PropertyComponent;
import net.sf.jzeno.echo.editor.StringEditor;
import net.sf.jzeno.tutorial.business.ProductCriteria;
import net.sf.jzeno.tutorial.business.ProductFacade;
import net.sf.jzeno.tutorial.model.Product;

public class ProductNameEditor extends CustomComponent {
	private static final long serialVersionUID = 1L;

	private StringEditor stringEditor;

	public ProductNameEditor() {
		this(null, null, null);
	}

	public ProductNameEditor(Class beanClass, String property,
			String constructionHints) {
		super(beanClass, property, null);

		stringEditor = new StringEditor(getClass(), "product.name", "");
		stringEditor.addValidator(new ProductNameValidator());
		add(stringEditor);
		EchoSupport.executeHints(this, constructionHints);
	}

	public Product getProduct() {
		return (Product) getValue();
	}

	public void setProduct(Product product) {
		setValue(product);
	}

	private class ProductNameValidator implements Validator {
		private static final long serialVersionUID = 1L;

		public List getValidationErrors(PropertyComponent pc) {
			List ret = new ArrayList();

			if (pc instanceof StringEditor) {
				StringEditor stringEditor = (StringEditor) pc;
				String name = stringEditor.getText();
				Product p = ProductNameEditor.this.getProduct();
				ProductCriteria productCriteria = new ProductCriteria();
				productCriteria.setName(name);
				List products = BusinessFactory.getFacade(
						ProductFacade.class).find(productCriteria);
				if (products.size() > 0) {
					for (Product product : products) {
						if (!p.equals(product)) {
							System.out.println(p.getId() + " - "
									+ product.getId());
							ret.add(new ValidationError(pc,
									"Product name already taken."));
						}
					}
				}
			}

			return ret;
		}
	}

	public boolean isRequired() {
		return stringEditor.isRequired();
	}

	public void setRequired(boolean required) {
		stringEditor.setRequired(required);
	}
}

 

OrderProductScreen

This screen is used to -you guessed it- place orders. The only interesting thing in this screen is how (error) messages can be added manually.

 

if(order.getOrderItems().size() > 0){
  BusinessFactory.getOrderFacade().saveOrder(order);
  EchoSupport.addMessage("productorderscreen.messages.order.placed");
  EchoSupport.setScreen(new OrderProductScreen());
}else{
  EchoSupport.addError("productorderscreen.errors.no.items.ordered");
}

 

OrderListScreen

Also, a very simple screen. SecuritySupport is used to see if the user has the permission to view other user's orders. If not, he can only see his own. Add current user to orderCriteria if he can only view his own orders:

 

OrderCriteria crit = new OrderCriteria();
if(SecuritySupport.currentUserHasPermission("orderlistscreen.edit")) {
  crit.setUser((User)
  SecuritySupport.getCurrentUser());
}
orders = BusinessFactory.getOrderFacade().find(crit);

 

 

package net.sf.jzeno.tutorial.screen;

import java.util.List;

import net.sf.jzeno.aop.SecuritySupport;
import net.sf.jzeno.echo.ConstructionList;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.Precreation;
import net.sf.jzeno.echo.components.DynaGrid;
import net.sf.jzeno.echo.components.Screen;
import net.sf.jzeno.echo.components.Title;
import net.sf.jzeno.echo.databinding.DynaTable;
import net.sf.jzeno.echo.viewer.DateViewer;
import net.sf.jzeno.echo.viewer.DoubleViewer;
import net.sf.jzeno.echo.viewer.StringViewer;
import net.sf.jzeno.tutorial.business.BusinessFactory;
import net.sf.jzeno.tutorial.business.OrderCriteria;
import net.sf.jzeno.tutorial.model.Order;
import net.sf.jzeno.tutorial.model.User;
import net.sf.jzeno.util.FastFactory;

public class OrderListScreen extends Screen implements Precreation {
  private static final long serialVersionUID = 1L;

  private static final String TITLE = "Orders";

  private DynaTable ordersTable;

  private List orders;

  @Override
  public String getHistoryTitle() {
    return TITLE;
  }

  public OrderListScreen() {
    this(null, null, null);
  }

  public OrderListScreen(Class beanClass, String property,
      String constructionHints) {
    DynaGrid mainGrid = new DynaGrid(getClass(), "", "columns=1");
    add(mainGrid);

    mainGrid.add(new Title(TITLE));

    ConstructionList cl = new ConstructionList();
    cl.add("id", "OrderNr", "", StringViewer.class);
    cl.add("orderDate", "Date Ordered", "", DateViewer.class);
    cl.add("user.userName", "Placed By", "", StringViewer.class);
    cl.add("price", "Total Price", "", DoubleViewer.class);
    ordersTable = new DynaTable(getClass(), "orders", "", cl);
    ordersTable.setWidth(-1);
    mainGrid.add(ordersTable);

    EchoSupport.executeHints(this, constructionHints);
    applyContext();
  }

  public void applyContext() {
    if (FastFactory.isInsideContext()) {
      OrderCriteria crit = new OrderCriteria();
      if (!SecuritySupport
          .currentUserHasPermission("orderlistscreen.edit")) {
        crit.setUser((User) SecuritySupport.getCurrentUser());
      }
      orders = BusinessFactory.getOrderFacade().find(crit);
    }
  }

  public List getOrders() {
    return orders;
  }

  public void setOrders(List orders) {
    this.orders = orders;
  }
}

 

Final note

We hope this tutorial has been helpful. More information can be found in the Docs section of this site. The how-to's cover many of the aspects in this tutorial in more detail.