Skip to content

Latest commit

 

History

History
387 lines (266 loc) · 15.9 KB

01.md

File metadata and controls

387 lines (266 loc) · 15.9 KB
title image summary
Building Prototypes
../images/prototype.jpg
It's well known Java it not for creating prototypes and there are plenty of blogs on the Internet to prove it.

Quick prototypes are useful for fleshing out ideas and moving the project forward, but can give a false sense that the project is complete and ready for production. What if the prototype could be created quickly in Java, then enhanced and refactored all the way to production? What if the end result could be refactored into a library so the next project is even faster?

What makes prototyping in Java slow? While Java has a jar for almost anything, the language tends toward verbose. Everything needs a type, Java Beans need lots of boilerplate code, and so do classes. Every change means a compile and a server restart. Top that off with imports and library management, and it's a wonder any Java project ever gets off the ground. What's needed is a way to create prototypes that leverage Java's strengths without writing any code.

First, a few arbitrary rules:

  1. Declarations are OK
  2. Code generated by the IDE is OK
  3. It's OK to break some rules.

With those first two rules, here is the now (in)famous Hotel booking application using Tapestry 5.4. First step is creating the project structure. Maven will handle dependency management and builds, so the project structure is predetermined.

  1. Project folder with src/main/(java,resources,webapp)
  2. Package directories in the source folders com.trsvax.(components,entites,pages,services)
  3. A web.xml file
  4. A pom.xml file

Needing two xml files may seem an intimidating start. The good news: they are mostly boilerplate and xml is declarative. Next is the data model. The typical example has booking, hotel and user, but these include address and credit card which should be their own types. This project will use Hibernate/Derby for persistence and entity objects that have only fields and annotations. While the IDE could provide getters/setters, there is no real need. These objects have only one purpose -- to map data to and from the database. Eliminating methods makes them cleaner and prevents business logic from creeping in.

The Hotel entity looks like this.

[Hotel.java] (https://github.com/trsvax/HotelBooking/blob/master/src/main/java/com/trsvax/hotelbooking/entities/Hotel.java#L12)

package com.trsvax.hotelbooking.entities;

@Entity
public class Hotel {

	@Id
	@GeneratedValue
	public Long id;

	public String name;

	@ManyToOne(fetch=FetchType.LAZY)
	public Address address;

	public Integer stars;

	public BigDecimal price;

}

One more XML document for the Hibernate configuration, and the data model is complete.

Next, create a way to access the data. Now it's time to break some rules. The best (only) way to have type safe DOAs is with generics. Hibernate 5 supports this but 4 does not. It's easy to create a wrapper that works with either. In typical Java fashion it's going to take some boilerplate to accomplish this.

First comes the interface with some typical ways to access the data.

[DAO.java] (https://github.com/trsvax/HotelBooking/blob/master/src/main/java/com/trsvax/hotelbooking/services/DAO.java#L13)

package com.trsvax.hotelbooking.service

public interface DAO {

	public void save(Object entity);

	public <E> E findById(Class<E> clazz, Serializable id, boolean lock);

	public <E> List<E> findAll(Class<E> clazz);

	public <E> E findByQuery(Class<E> clazz, String queryString, Object ... objects );

	public <E> List<E> query(Class<E> clazz, String queryString, Object ... objects );

	public <E> List<E> namedQuery(Class<E> clazz, String queryName, Object ... objects );

	public <E> E findByNamedQuery(Class<E> clazz, String queryName, Object ... objects );

}

For the implementation and some rule breaking, it's going to take some code to implement a DAO. Againm one common DAO keeps business logic out of the DAO.

[HibernateDAO] (https://github.com/trsvax/HotelBooking/blob/master/src/main/java/com/trsvax/hotelbooking/services/hibernate/HibernateDAO.java#L21)

package com.trsvax.hotelbooking.services.hibernate;

      public class HibernateDAO implements DAO {

      	private final Session session;

      	public HibernateDAO (Session session) {
      		this.session = session;
      	}

      	public void save(Object entity) {
      		session.persist(entity);
      	}

      	@SuppressWarnings("unchecked")
      	@Override
      	public <E> E findById(Class<E> entityClass, Serializable id, boolean lock) {
      		return (E) session.load(entityClass, id);
      	}

      	@SuppressWarnings("unchecked")
      	@Override
      	public <E> List<E> findAll(Class<E> entityClass) {
      		return session.createCriteria(entityClass).list();
      	}

      	@SuppressWarnings("unchecked")
      	@Override
      	public <E> List<E> query(Class<E> clazz, String queryString, Object ... parameters )  {
      		return addParameters(session.createQuery(queryString), parameters).list();
      	}

      	@SuppressWarnings("unchecked")
      	@Override
      	public <E> E findByQuery(Class<E> clazz, String queryString, Object ... parameters )  {
      		return (E) addParameters(session.createQuery(queryString), parameters).uniqueResult();
      	}

      	@SuppressWarnings("unchecked")
      	@Override
      	public <E> List<E> namedQuery(Class<E> clazz, String queryName, Object ... parameters )  {
      		return addParameters(session.getNamedQuery(queryName), parameters).list();
      	}

      	@SuppressWarnings("unchecked")
      	public <E> E findByNamedQuery(Class<E> clazz, String queryName, Object ... parameters )  {
      		return (E) addParameters(session.getNamedQuery(queryName),parameters).uniqueResult();
      	}

      	Query addParameters(Query query, Object[] parameters) {		
      		map(parameters).entrySet().stream().forEach(e->query.setParameter(e.getKey(),e.getValue()));
      		return query;
      	}

      	Map<String,Object> map(Object[] parameters) {
      		return Stream.iterate(Arrays.asList(parameters), l -> l.subList(2, l.size()))
      		            .limit(parameters.length / 2)
      		            .collect(Collectors.toMap(l -> (String) l.get(0), l -> l.get(1)));				
      	}

      }

Every Tapestry application will have an AppModule that contains the declarations for setting up services, configuration etc. To turn the DAO into a service, it needs to bind to the interface in the AppModule.

AppModule.java

package com.trsvax.hotelbooking.services;

public static void bind(ServiceBinder binder) {
  binder.bind(DAO.class,HibernateDAO.class);
}

Finally, it's time to create the application. Many prototypes would start with this step, but the data model really drives the whole application, allowing Hibernate to build the database from the Java model.

The booking process starts by showing a list of hotels. Add a bit of SQL to put some in the database, and then it's time to create the first webpage. Tapestry provides a straightforward way to map classes to URLs, but there are a few tricks. If the class name contains Index it's not needed in the URL. If the classname contains the package name, it's also not needed, so the URL for hotel.HotelIndex will be just /hotel.

A class of IndexHotel would do the same thing, but makes searching with an IDE harder. This way, everything concerning a hotel starts with hotel. We just need a few declarations to get the DAO, export some properties to the template, and a single line of code to get the hotels.

HotelIndex.java

package com.trsvax.hotelbooking.pages.hotel;

public class HotelIndex {
	@Inject
	DAO dao;

	@Property
	Hotel hotel;

	public List<Hotel> getHotels() {
		return dao.query(Hotel.class," from Hotel ");
	}

}

The HotelIndex.tml file and a quick intro to Tapestry templates. Templates are XML files that look like HTML files with some extra tags. In this case the page needs to display a list hotels and the easiest way is with the grid component. The grid component iterates over the list of hotels showing the fields as column headings.

HotelIndex.tml

<!DOCTYPE html>
<html lang="en" t:type="layout"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
      xmlns:p="tapestry:parameter">
<body>

<h1>Hotels</h1>

<t:grid source="hotels" row="hotel"/>

</body>

</html>

Next, create a launch configuration for the application.

Tapestry includes a main method for running Jetty as a plain Java application. Set up the IDE to run org.apache.tapestry5.test.Jetty7Runner with the arguments "-context /hotel". Start it up and go to http://localhost:8080/hotel -- hopefully, there is a list of hotels.

It's tempting to plow ahead and finish the application, but first -- a quick lesson on Tapestry components.

Components are much like pages, but are in the components package, have no URLs, and can be embedded in other components/pages. Almost every project will need some kind of template to give the pages a similar look and feel. In Tapestry projects, this is typically called the Layout component. After creating Layout, add it into the HotelIndex page, restart the server and: Voila! All pages now have a common template.

Layout.java

package com.trsvax.hotelbooking.components;

public class Layout {

}

Layout.tml

<!DOCTYPE html>
<html lang="en"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
      xmlns:p="tapestry:parameter">
	<body >

		<div class="container" id="layoutContainer">       
			<t:body/>
		</div>

	</body>
</html>

This server restart happens pretty fast but after every change will get old quickly. Tapestry is able to handle many types of changes but only in development mode because no one would ever make changes to a running production system. Tapestry supports different run modes in the IOC so create a development module and set production mode to false.

DevelopmentModule.java

public class DevelopmentModule {

    public static void contributeApplicationDefaults(MappedConfiguration<String, Object> configuration) {
        configuration.add(SymbolConstants.PRODUCTION_MODE, false);
    }
}

Add a bit to the web.xml file

web.xml

<context-param>
    <param-name>tapestry.DevelopmentMode-modules</param-name>
    <param-value>
        com.trsvax.hotelbooking.services.modules.DevelopmentModule
    </param-value>
</context-param>

Then one more argument to the launch configuration -Dtapestry.execution-mode=DevelopmentMode which will cause Tapestry to also load the DevelopmentModule.

Next the booking page. Again a few declarations to get the DAO and expose some properties. The hotel property contains an additional @PageActivationContext declaration. This allows passing parameters to the page via the URL. Since Hotel is a Hibernate entity the URL contains the identity value and Tapestry will fetch the object automatically on page load.

BookingNew.java

package com.trsvax.hotelbooking.pages.hotel.booking;

public class BookingNew {

	@Property
	@PageActivationContext
	Hotel hotel;

	@Property
	Booking booking;

	@Property
	Object row;

	@Inject
	HotelService hotelService;

	@InjectPage
	BookingView bookingView;

	@CommitAfter
	Object onSuccess()  {
		booking.hotel = hotel;
		bookingView.status = hotelService.newBooking(booking);
		bookingView.booking = booking;
		return bookingView;
	}

}

BookingNew.tml

<!DOCTYPE html>
<html lang="en" t:type="layout"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
      xmlns:p="tapestry:parameter">
<body>

<h1>Book</h1>
<t:beaneditform t:id="form" object="booking"/>

</body>

</html>

Plus add a link to the new page in

HotelIndex.tml

<t:grid source="hotels" add="book" row="hotel">
<p:bookCell>
  <t:pagelink page="hotel/booking/new" context="hotel" >Book</t:pagelink>
</p:bookCell>
</t:grid>

The BookingNew.tml file contains a BeanEditForm component. This component will auto generate a form based on the Bean and it's properties. Viewing the page in the browser reveals a few things. First the date fields have a date picker because they are declared as Date. Perhaps the static type system is useful after all. Next the credit card fields are missing because Tapestry has no built in component to display a credit card object. No matter this is just a prototype.

Try it out by selecting a hotel on the HotelIndex page type some data into the BookingNew page and press submit. Voila a working application. Time to fire up the bug tracking process and request some enhancements. The current application needs at least two. First it would be nice to know who booked the room and secondly since this is no longer the 90s the site needs revenue.

For the first ticket it's clear the model supports credit card fields in the booking entity but they are not displayed. Tapestry supports editors for object types it just does not have one for a credit card. In order to edit an object Tapestry needs a Block capable of editing that kind of object. If only there was a component capable of editing any kind a object. BeanEditForm is built from among other things a BeanEditor. While all this seems complicated and recursive it only requires one line of code and a couple more additions in the AppModule to create an object editor. While there also configure Address.class to use the object editor.

ObjectEdit.java

public class ObjectEdit {
	@Property
	@Environmental
	private PropertyEditContext context;

	@Inject
	private BeanModelSource beanModelSource;

	@SuppressWarnings("unchecked")
	public BeanModel<?> getModel() {
		return beanModelSource.createEditModel(context.getPropertyType(), context.getContainerMessages());
	}
}

ObjectEdit.tml

<t:block id="Object">
 <t:beanEditor object="context.propertyValue" model="model"/>
</t:block>

AppModule.java

public static void contributeDefaultDataTypeAnalyzer(@SuppressWarnings("rawtypes") MappedConfiguration<Class, String> configuration) {
	 configuration.add(CreditCard.class, "Object");
	 configuration.add(Address.class, "Object");
}

  @Contribute(BeanBlockSource.class)
public static void provideDefaultBeanBlocks(Configuration<BeanBlockContribution> configuration) {
  	configuration.add( new EditBlockContribution("Object", "jq/blocks/ObjectEdit", "Object"));
  }

Restart the server for the 3rd time and now the booking form supports credit cards.

The second feature requires authentication and security. There is no reason to create a prototype for this it's time to just import a library.