From the Browser to the Database – Using multiple frameworks to get the job done

We are going to take a look at the technologies and code used to go from a form in a web browser to saving that data in our database.

The technology stack we are using for this overview includes

  • HTML5
  • Bootstrap 3
  • Sitemesh 2.4
  • Servlet 3
  • Spring 3.2
  • Spring MVC 3.2
  • Spring Data JPA 1
  • Bean Validation 1
  • JPA2/Hibernate4
  • Relational Database/Oracle 11g

We are going to review some code that is used to mock out the Customer Registration API for external postal applications.

First lets design our data model and create a script to create some database objects.


DECLARE
	v_sql LONG;
BEGIN

	begin
		v_sql := 'create table dev_users (
		id number primary key,
		username varchar2(255) not null,
		password varchar2(255) not null,
		description varchar2(255) not null,
		enabled integer not null,
		first_name varchar2(255) not null,
		last_name varchar2(255) not null,
		email varchar2(255) not null,
		phone_number varchar2(10) not null,
		phone_number_ext varchar2(4),
		address_line1 varchar2(255) not null,
		address_line2 varchar2(255),
		address_line3 varchar2(255),
		city varchar2(50) not null,
		state varchar2(2) not null,
		postal_code varchar2(5) not null,
		created_time timestamp not null
	)';
	execute immediate v_sql;
	exception when others  then
		IF SQLCODE = -955 THEN
	        NULL; -- suppresses ORA-00955 exception
	      ELSE
	         RAISE;
	      END IF;
	 end;
	 begin
		v_sql := 'create unique index dev_users_username_idx on dev_users(username)';
		execute immediate v_sql;
	exception when others  then
		IF SQLCODE = -955 THEN
	        NULL; -- suppresses ORA-00955 exception
	      ELSE
	         RAISE;
	      END IF;
	 end;
	 begin
		v_sql := 'create sequence dev_users_seq CACHE 50';
		execute immediate v_sql;
	exception when others  then
		IF SQLCODE = -955 THEN
	        NULL; -- suppresses ORA-00955 exception
	      ELSE
	         RAISE;
	      END IF;
	 end;
END;
/

You should notice right away that we are wrapping our create statements in an oracle procedure. This allows us to catch object already exists exceptions and allow our script to be idempotent, which just means it can be rerun over and over without any errors or side affects.

So we create a table that holds some basic user information, it has a single integer id based off a sequence which is a best practice to use a non business related key on our data and reduces how many columns we need to join across. We then create a unique index on our natural key the username which we will generally use to query specific user’s data. Also we remember to add our not null and type size constraints in the table definition.

After you’ve migrated the database (I’ve been using flyway for database management) with the new table then we can move onto creating our JPA object to load the data. I’ve added some additional comments explaining what some of the annotations do for us. Here we’ll also see our first look at the bean validation constraints.


package com.usps.ach.dev.user;
//imports

/**
 * This is a database representation of the custreg account api.
 *
 * @author stephen.garlick
 * @deprecated for use only in the dev profile
 */
@Entity //marks this class a jpa entity
@Immutable //marks it so an entity cannot be updated
@Cacheable //enables entity caching against the @Id field
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY, region = "devUser") //tells jpa to use a readonly cache and to use the devUser  cache defined in our ehcache.xml file
@NaturalIdCache //setups a cache using the fields marked with @NaturalId as the key
@Table(name = "dev_users") //override the base table name, the configured hibernate will use dev_user as default, but we like to pluralize our table names
@SequenceGenerator(sequenceName = "dev_users_seq", name = "dev_users_seq", allocationSize = 1) //sets up id generation based on the sequence we created. make sure allocationSize = 1 unless you plan on making your sequences increment more than 1 at a time.  this also makes sure hibernate doesnt use its HiLo strategy
@Deprecated //we just mark thsi deprecated so that we get a strong warning if someone tries to use this code in a non dev env.
public final class DevUser {
	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "dev_users_seq") //let hibernate generate our ids
	private Long id;

	@NaturalId
	@NotBlank(message = "{please.enter}") //bean validation constraint that checks if the field is null or empty string.  the message is a reference to a messagesource key used for i18n
	@Length(max = 255, message = "{length.exceeded}") //constraint that checks the fields length
	@Pattern(regexp = "[a-zA-Z0-9]*", message = "{alphanum.only}") //pattern to make sure the field only has alphanumeric characters
	@Column(unique = true)
	private String username;

	@NotBlank(message = "{please.enter}")
	@Length(max = 255, message = "{length.exceeded}")
	private String description;

	@NotBlank(message = "{please.enter}")
	@Length(max = 255, message = "{length.exceeded}")
	private String firstName;

	@NotBlank(message = "{please.enter}")
	@Length(max = 255, message = "{length.exceeded}")
	private String lastName;

	@NotBlank(message = "{please.enter}")
	@Length(max = 255, message = "{length.exceeded}")
	@Email(message = "{email.invalid}")
	private String email;

	@NotNull(message = "{please.enter}")
	@Length(min = 10, max = 10, message = "{please.enter.digit.length}")
	@Pattern(regexp = "[0-9]*", message = "{numeric.only}")
	private String phoneNumber;

	@Length(max = 4, message = "{length.exceeded}")
	@Pattern(regexp = "[0-9]*", message = "{numeric.only}")
	private String phoneNumberExt;

	@NotBlank(message = "{please.enter}")
	@Length(max = 255, message = "{length.exceeded}")
	private String addressLine1;

	@Length(max = 255, message = "{length.exceeded}")
	private String addressLine2;

	@Length(max = 255, message = "{length.exceeded}")
	private String addressLine3;

	@NotBlank(message = "{please.enter}")
	@Length(max = 50, message = "{length.exceeded}")
	@Pattern(regexp = "[a-zA-Z]*", message = "{alpha.only}")
	private String city;

	@NotNull(message = "{please.enter}")
	@Length(min = 2, max = 2, message = "{please.enter.alpha.length}")
	@Pattern(regexp = "[a-zA-Z]*", message = "{alpha.only}")
	private String state; //TODO: pull this out into an ENUM instead of a 2 char string

	@NotNull(message = "{please.enter}")
	@Length(min = 5, max = 5, message = "{please.enter.digit.length}")
	@Pattern(regexp = "[0-9]*", message = "{numeric.only}")
	private String postalCode;

	private Date createdTime;
	private String password;
	private Integer enabled;

	@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) //we create a mapping to our real user table so we can create a user object when a dev user is created
	@PrimaryKeyJoinColumn(referencedColumnName = "id") //one to one mapping using both tables primary key column (they will have same primary key in both tables)
	private User user;

	@PrePersist //jpa callback to add the time as our createdTime field
	public void createTime() {
		this.createdTime = new Date();
	}

	//make sure to override hash and equals to be against our natural key instead of using instance based.  also don't use the id field since it may not be set
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((username == null) ? 0 : username.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		DevUser other = (DevUser) obj;
		if (username == null) {
			if (other.username != null)
				return false;
		} else if (!username.equals(other.username))
			return false;
		return true;
	}
//getters and setters
}

Hibernate’s Improved Naming Strategy allows us to convert fields that are camel cased to database tables by converting everything to lowercase and inserting underscores before uppercase letters. Example firstName -> first_name, lastName->last_name, addressLine1 -> address_line1.

Next we use spring data to create a dynamic DAO for us, effectively eliminating the need for us to create our own dao layer


package com.usps.ach.dev.user;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * Spring data for manipulating our {@link DevUser}
 *
 * @author stephen.garlick
 * @deprecated only for use in the dev profile
 */
@Deprecated
@Repository
public interface DevUserRepository extends JpaRepository<DevUser, String> {
	public DevUser findByUsername(String username);

}

If you look at the JpaRepository you can see that it has a standard set of methods like findAll() find(ID) save(Entity) and other CRUD methods. We can create our own set of queries by simply adding a new method to our interface and spring data will generate the necessary JPA code for us. Here since we generally will be looking up users based on their username instead of their id we add a findByUsername method. You can do this with anyfield, ex. findByField1AndField2OrField3. If you have the spring data plugin for eclipse it will let you know if you’re methods are valid. There are a lot more advance techniques you can use here such as named queries and overriding the base implementation, but for basic crud operations this is the simplest form. See the spring-data-jpa documentation if you’d like to know more.

Next we’ll take a look at our service layer. The service layer’s job is to handle our transactions, possibly caching and to handle any extra logic necessary. I’ve excluded the interface and a few methods for brevity.


package com.usps.ach.dev.user;

//imports

/**
 * See {@link DevUserService}
 *
 * @author stephen.garlick
 * @deprecated this class is only for use in the dev profile
 */
@Profile(Profiles.DEV_SERVICES) //make sure this bean is only loaded when spring.profiles.active=DEV_SERVICES
@Service //marks this class for spring to create and inject
@Transactional //tells spring all methods in this class are to be run in transactions
@Deprecated //again deprecate so we can give a strong warning if anyone uses this outside the dev profiles
public final class DevUserServiceImpl implements DevUserService {

	@Autowired //wire in our repo by type
	private DevUserRepository devUserRepository;

	@Override
	public DevUser create(final DevUser devUser) {
		devUser.setPassword(devUser.getUsername());
		devUser.setEnabled(1);
		User user = new User();
		user.setUsername(devUser.getUsername());

		devUser.setUser(user); //since we have cascading set to ALL on our DevUser object the User object will automatically be persisted on save.  if you dont have cascade of ALL/Persist //then we'll get an exception here saying User is a transient object.
		return save(devUser);
	}

	private DevUser save(final DevUser devUser) {
		return devUserRepository.save(devUser);
	}

	@Override
	public boolean exists(final DevUser devUser) {
		return null != find(devUser.getUsername()); //return a boolean if a given entity is already created
	}

	@Override
	public DevUser find(final String username) {
		return devUserRepository.findByUsername(username); //user our defined method to query for our DevUser
	}

	@Override
	public Collection<DevUser> findAll() {
		return devUserRepository.findAll(); //delegate straight to our repo
	}
}

Finally we can look at our Spring MVC controller which will handle the incoming requests from the servlet container. We’ll want to follow the basic principals of REST in that we properly use the verbs GET to retrieve, POST to create, PUT to update, and DELETE to destroy. We’ll also use the proper HTTPStatus codes where applicable (ie user not found returns 404 page).

URLs will be designed as access to our entity resources. Since this a dev form we’ll be under the /dev/ namespace. As you’ll see below we use GET /dev/users/ to show all users, GET /dev/users/{username} to access a specific user and GET /dev/users/new to a form where new users are created and a POST /dev/users with the form data will create a new user. We won’t allow editing or deleting but if we did we would map /dev/users/{username}/edit which would have a form to do a PUT on /dev/users/{username} and a DELETE on /dev/users/{username} to destroy a resource.

Also we’ll be using our DevUser entity for form binding and validation in this tier.


package com.usps.ach.dev.user;
//imports

/**
 * This class handles creation of new {@link DevUser} to be used for logging in
 * under our dev profile. it is also intended to mock out the custreg account
 * api in our dev profile
 *
 * @author stephen.garlick
 * @deprecated only for use in the dev profile
 */
@Controller //marks this to be registered by spring
@Profile(Profiles.DEV_SERVICES) //makes it only picked up in the dev profile
@Deprecated //marked deprecated to hide deprecated warnings from our dependencies
public final class DevUserController {

	@Autowired
	private DevUserService devUserService;

	@Autowired
	private DevCridService devCridService;

	@RequestMapping(value = "dev/users/new", method = RequestMethod.GET)
	public String getNew(@ModelAttribute final DevUser devUser,
			final Model model) {
		return "dev/user/new";  //return the view for this new form
	}

	@RequestMapping(value = "dev/users", method = RequestMethod.GET)
	public String getIndex(final Model model) {
		setup(model);
		return "dev/user/index"; //return the index page for our users
	}

	@RequestMapping(value = "dev/users/{username}", method = RequestMethod.GET)
	public String viewUser(@PathVariable final String username,
			@ModelAttribute DevCrid devCrid, final Model model) {
		final DevUser devUser = devUserService.find(username);
		if (null == devUser)
			throw new NotFoundException(); //if null is returned for the user then it doesnt exists and we shoudl return 404
		final Collection devCrids = devCridService.findAll();
		model.addAttribute("devUser", devUser);
		model.addAttribute("devCrids", devCrids);
		return "dev/user/view";
	}

	private void setup(Model model) {
		model.addAttribute("users", devUserService.findAll());
	}

	/**
		Here we are creating a new user.  The user data will be passed in this paramter @Valid @ModelAttribute final DevUser devUser and the @Valid annotation will trigger all the bean valdiaton on our entity object.  If errors are found then spring will automatically populate them in the final BindingResult errors for us.
	**/
	@RequestMapping(value = "dev/users", method = RequestMethod.POST)
	public String create(@Valid @ModelAttribute final DevUser devUser,
			final BindingResult errors, final Model model,
			final RedirectAttributes redirectAttributes) {
		final boolean userExists = devUserService.exists(devUser);//do a manual valdation to make sure the user does not already exists.  this is not part of bean validation spec yet
		if (userExists)
			errors.rejectValue("username", "",
					"A user already exists with this username.");
		if (errors.hasErrors()) {
			return "dev/user/new"; //return the new form and display any errors
		}
		//otherwise create the user and redirect to its newly created view page.  Generally you want to redirect after a non GET operation.
		DevUser newUser = devUserService.create(devUser);
		redirectAttributes.addAttribute("username", newUser.getUsername());
		return "redirect:/dev/users/{username}";
	}
}

You may have noticed that all the classes we’ve covered are in the same package. This benefits us by allowing all related classes with a piece of functionality to be logically organized and makes it easier to find classes that are related to each other. This package naming scheme is preferable over the style of using packages to group components together (ie xx.xx.xx.controller xx.xx.xx.model etc).

Finally we can take a look at the views that we are returning from our spring controller which will allow the user to actually enter the information in the browser and submit it to our server.

users index /dev/users

<%@include file="/WEB-INF/jsp/shared/common-taglibs.jsp"%>
<!DOCTYPE html>
<html>
<head>
<title>Development Users</title>
</head>
<body id="users">
<div>
		<a href="<c:url value="/dev/users/new"/>"
			class="btn btn-primary btn-sm">Create New User</a></div>
<div>
<table class="table">
<thead>
<tr>
<td>Username</td>
<td>Description</td>
<td>Login</td>
</tr>
</thead>
<tbody>
				<c:forEach items="${users}" var="devUser">
<tr>
<td><a class="btn btn-primary btn-sm"
							href="<c:url value="/dev/users/${devUser.username}"/>">${devUser.username}</a></td>
<td>${devUser.description}</td>
<td><form action="login/process" method="POST"><input name="j_username" value="${devUser.username}" type="hidden"/><input name="j_password" value="${devUser.username}" type="hidden"/><button class="btn btn-primary btn-sm">Login</button></form></td>
</tr>
</c:forEach></tbody>
</table>
</div>
</body>
</html>

users new /dev/users/new

<%@include file="/WEB-INF/jsp/shared/common-taglibs.jsp"%>

<!DOCTYPE html>
<html>
<head>
<title>Development Create New User</title>
</head>
<body id="users">
<h3>Create a New User</h3>
<h5>The password will be the same as the username</h5>
<form:form commandName="devUser" method="POST" servletRelativeAction="/dev/users/"
			cssClass="form-horizontal">
			<t:bootstrap-text-input path="username" maxLength="255" label="Username" required="true"/>
			<t:bootstrap-text-input path="description" maxLength="255" label="Description" required="true"/>
			<t:bootstrap-text-input path="firstName" maxLength="255" label="First Name" required="true"/>
			<t:bootstrap-text-input path="lastName" maxLength="255" label="Last Name" required="true"/>
			<t:bootstrap-text-input path="email" maxLength="255" label="Email Address" required="true"/>
			<t:bootstrap-text-input path="phoneNumber" maxLength="10" label="Phone Number" required="true"/>
			<t:bootstrap-text-input path="phoneNumberExt" maxLength="4" label="Phone Number Extension"/>
			<t:bootstrap-text-input path="addressLine1" maxLength="255" label="Address Line 1" required="true"/>
			<t:bootstrap-text-input path="addressLine2" maxLength="255" label="Address Line 2"/>
			<t:bootstrap-text-input path="addressLine3" maxLength="255" label="Address Line 3"/>
			<t:bootstrap-text-input path="city" maxLength="50" label="City" required="true"/>
			<t:bootstrap-text-input path="state" maxLength="2" label="State" required="true"/>
			<t:bootstrap-text-input path="postalCode" maxLength="5" label="Postal Code" required="true"/>
<div class="col-lg-8">
				<form:button class="btn btn-primary btn-sm pull-right">Create</form:button></div>
</form:form>
</body>
</html>

user view /dev/users/{username}


<%@include file="/WEB-INF/jsp/shared/common-taglibs.jsp"%>
<!DOCTYPE html>
<html>
<head>
<title>Development User View</title>
</head>
<body id="users">
<div class="form-horizontal">
<h3>Development User</h3>
<t:bootstrap-text-input disabled="true" path="devUser.username"
			maxLength="255" label="Username" required="true" />
		<t:bootstrap-text-input disabled="true" path="devUser.description"
			maxLength="255" label="Description" required="true" />
		<t:bootstrap-text-input disabled="true" path="devUser.firstName"
			maxLength="255" label="First Name" required="true" />
		<t:bootstrap-text-input disabled="true" path="devUser.lastName"
			maxLength="255" label="Last Name" required="true" />
		<t:bootstrap-text-input disabled="true" path="devUser.email"
			maxLength="255" label="Email Address" required="true" />
		<t:bootstrap-text-input disabled="true" path="devUser.phoneNumber"
			maxLength="10" label="Phone Number" required="true" />
		<t:bootstrap-text-input disabled="true" path="devUser.phoneNumberExt"
			maxLength="4" label="Phone Number Extension" /></div>
<div>
<table class="table" id="user-crids">
<thead>
<tr>
<td>CRID</td>
<td>Company Name</td>
<td>Link Toggle</td>
</tr>
</thead>
<tbody>
				<c:forEach items="${devCrids}" var="crid">
<tr>
<td><a class="btn btn-primary btn-sm"
							href="<c:url value="/dev/crids/${crid.crid}"/>">${crid.crid}</a></td>
<td>${crid.companyName}</td>
<td><c:choose>
								<c:when test="${crid.achCrid.users.contains(devUser.user)}">
									<form:form servletRelativeAction="/dev/users/${devUser.username}/crids/${crid.crid}" method="DELETE"
										commandName="devCrid">
										<form:button class="btn btn-primary btn-sm">Unlink</form:button>
									</form:form>
								</c:when>
								<c:otherwise>
									<form:form servletRelativeAction="/dev/users/${devUser.username}/crids" method="POST"
										commandName="devCrid">
										<form:button class="btn btn-primary btn-sm">Link</form:button>
										<form:hidden path="crid" value="${crid.crid}" />
									</form:form>
								</c:otherwise>
							</c:choose></td>
</tr>
</c:forEach></tbody>
</table>
</div>
</body>
</html>

and all these views are wrapped in a sitemesh template


<%@include file="/WEB-INF/jsp/shared/common-taglibs.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"
	http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- Bootstrap -->
	<link href="<c:url value="/resources/css/bootstrap.min.css"/>"
	rel="stylesheet" media="screen">

<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
      <script src="../../assets/js/html5shiv.js"></script>
      <script src="../../assets/js/respond.min.js"></script>
    <![endif]-->

<decorator:head />
<title><decorator:title /></title>
</head>
<body>
<div class="container">
<div class="container">
<div id="header" class="col-lg-12">
				<page:applyDecorator name="dev-header">
					<page:param name="section">
						<decorator:getProperty property="body.id" />
					</page:param>
				</page:applyDecorator></div>
<div class="clearfix"></div>
<div class="col-lg-12">
				<decorator:body /></div>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
	<script src="<c:url value="/resources/js/jquery-1.10.2.min.js"/>"></script>
	<!-- Include all compiled plugins (below), or include individual files as needed -->
	<script src="<c:url value="/resources/js/bootstrap.min.js"/>"></script>
	<script src="<c:url value="/resources/js/bootstrap-triggers.js"/>"></script>
</body>
</html>

and our sitemesh header for dev panel


<%@include file="/WEB-INF/jsp/shared/common-taglibs.jsp"%>
<c:set var="activeTab"><decorator:getProperty property="section"/></c:set>
<nav class="navbar navbar-default">
<div class="navbar-header">
		<button type="button" class="navbar-toggle" data-toggle="collapse"
			data-target=".navbar-ex1-collapse">
			<span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span>
			<span class="icon-bar"></span> <span class="icon-bar"></span>
		</button>
		<a href="<c:url value="/dev/login"/>" class="navbar-brand">Developer Panel</a></div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
	<li ${activeTab eq 'login' ? 'class="active"' : '' }><a href="<c:url value="/dev/login"/>">Login</a></li>
	<li ${activeTab eq 'users' ? 'class="active"' : '' }><a href="<c:url value="/dev/users"/>">Users</a></li>
	<li ${activeTab eq 'crids' ? 'class="active"' : '' }><a href="<c:url value="/dev/crids"/>">CRIDs</a></li>
	<li ${activeTab eq 'permits' ? 'class="active"' : '' }><a href="<c:url value="/dev/permits"/>">Permits</a></li>
</ul>
</div>
<!-- /.navbar-collapse -->
</nav>

our common-taglibs file

<%-- common taglibs to include for jsps --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" 	pageEncoding="UTF-8" isErrorPage="false"%>
<%@ taglib uri="http://www.springframework.org/security/tags" 	prefix="sec"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@ taglib uri="http://www.opensymphony.com/sitemesh/decorator" 	prefix="decorator"%>
<%@ taglib uri="http://www.opensymphony.com/sitemesh/page" prefix="page"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>

and our custom bootstrap tags for text input

<%@tag
	description="Extended input tag to allow for sophisticated errors"
	pageEncoding="UTF-8"%>
<%@taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@attribute name="path" required="true" type="java.lang.String"%>
<%@attribute name="labelCssClass" required="false"
	type="java.lang.String"%>
<%@attribute name="formCssClass" required="false"
	type="java.lang.String"%>
<%@attribute name="errorCssClass" required="false"
	type="java.lang.String"%>
<%@attribute name="label" required="true" type="java.lang.String"%>
<%@attribute name="maxLength" required="true" type="java.lang.Long"%>
<%@attribute name="placeholder" required="false" type="java.lang.String"%>
<%@attribute name="required" required="false" type="java.lang.Boolean"%>
<%@attribute name="disabled" required="false" type="java.lang.Boolean"%>
<%@attribute name="readonly" required="false" type="java.lang.Boolean"%>
<c:set var="errors"><form:errors path="${path}"/></c:set>
<div class="form-group ${not empty errors ? 'has-error' : '' }">
	<label
		class="control-label ${empty labelCssClass ? 'col-lg-2' : labelCssClass}"
		for="${path}">${label}<c:if test="${required}">
			<span class="text-danger">*</span>
		</c:if></label>
<div class="${empty formCssClass ? 'col-lg-6' : formCssClass }">
		<form:input path="${path}" disabled="${disabled}" cssClass="form-control" maxlength="${maxLength}" readonly="${readonly}" placeholder="${empty placeholder ? label : placeholder}" /></div>
<c:if test="${not empty errors}">
		<span class="text-danger ${empty errorCssClass ? 'col-lg-4' : errorCssClass }">${errors}</span>
	</c:if></div>

Bootstrap is twitters browser framework that give us a grid for easily making layouts, lots of base css classes for styling, and some dynamic javascript with plugin support as well. I’ve created these custom tags to allow for simple reusable and consistent form on our page. I won’t go too much into boostrap here but all classes you see in the given views and tags are provided by boostrap. I’ve written 0 custom css and javascript so far.

Of course there are some obvious things missing that we will cover in the future such as testing each layer with unit and integration tests and all the necessary configuration to actually get the frameworks in place but the main goal is getting used to developing end to end functionality using mutliple frameworks to handle all the non business stuff for us.

Leave a comment