This document provides a brief overview of the
Barracuda Disc Rack application.
Similarities & Differences
Barracuda Disc Rack is similar in many ways to the original Enhydra Disc Rack application. This
is intentional, to make it as easy as possible for developers already familiar with the
Enhydra Base Presentation methodology to understand the Barracuda paradigm. For those not familiar
with Enhydra or the old Disc Rack application, you can ignore these comparisons and just concentrate
on the servlet standard .war aspects of BarracudaDiscRack. Similarities
include:
- we maintained the basic biz, data, and presentation package separation
- the classes in the biz and data layers are unchanged from the traditional DiscRack
application
- we were able to reuse much of the XMLC code that generated the actual HTML pages
Of course there are also differences:
- we no longer use the Enhydra PO structure
- we have instead adopted the Servlet 2.2 WAR format, with basic system definition
occuring in the web.xml file
- we use the Barracuda ApplicationGateway as the starting point to the system
- we do still instantiate an Enhydra Application object behind the scenes, since the DODS
generated data packages rely on this
- the presentation package has been broken into 3 distinct subpackages:
- events - events that define the actions this system is capable of responding to (NOTE:
as of the upgrade to Barracuda 1.0 Final, classes in this package are autogenerated using
Barracuda's EventBuilder taskdef)
- screens - the code responsible for handling events and generating HTML
- services - miscellaneous classes used by the various screens
- actions within the HTML pages are now mapped to real event objects
- we have added a forms / validation package to make it easier to parse and validate forms
Application Structure
Note that the Barracuda Disc Rack application is structured something like this:
- <root> - root content directory
- docs - documentation directory
- images - images that are used by the doc files
- javadocs - source code documentation (generated from Ant)
- src - contains all the Java source files, plus the necessary
ant scripts to build the system
- src_mockups - contains the markup files that act as a mockup of the
application
- WEB-INF - the Servlet 2.2 application directory
- classes - contains the actual classes used by the application
- db - contains the IDB database. This is where your driver must
point
- jars - contains 3rd party .jar files, some of which are needed only at
compile time, but also others that may be added to a global classloader outside the WebappClassLoader. The
build automatically copies the files needed at runtime to a generated lib directory, so this distinction is
somewhat moot.
- lib - a generated directory used by the servlet container to load libraries.
All required runtime jars are copied here by the build.
- lib-cvs - contains .jar files needed at runtime. These are copied to a generated
lib directory by the build.
- logs - default location for log files to be written. An alternative location can be
configured in the web.xml.
- tmp$ - a temp directory that is created during the build process.
Various Barracuda taskdefs will use this directory as a place to store generated source.
Application Flow
By by building on the Servlet API, we no longer need much of the Enhydra Application
functionality, since most of these features (eg. lifecycle services) are now provided by
the Servlet container. We do need to tell the Servlet container about the application,
however. Here's how the whole thing works.
Setup - the first thing we have to do is tell
the Servlet container how to identify URLs that represent events and route them to the
Barracuda event dispatching package. This happens by making an entry in the web.xml file that looks something like this:
<servlet>
<servlet-name>ApplicationGateway</servlet-name>
<servlet-class>org.enhydra.barracuda.discRack.DiscRackGateway</servlet-class>
<init-param>
<param-name>ConfFile</param-name>
<param-value>/WEB-INF/discRack.conf</param-value>
</init-param>
<init-param>
<param-name>ApplicationAssembler</param-name>
<param-value>org.enhydra.barracuda.core.event.DefaultApplicationAssembler</param-value>
</init-param>
<init-param>
<param-name>AssemblyDescriptor</param-name>
<param-value>/WEB-INF/event-gateway.xml</param-value>
</init-param>
<init-param>
<param-name>SAXParser</param-name>
<param-value>org.apache.xerces.parsers.SAXParser</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
This causes 3 things to occur:
- DiscRackGateway is registered as a valid Servlet and configured with the settings specified in the event-gateway.xml application assembler
- discRack.conf is passed to the servlet as a configuration parameter (for talking to the database)
all URLs that end in .event are mapped to this servlet
Initialization - When the Servlet
container starts, it will load the DiscRackGateway and initialize it. When this occurs we
do 2 things:
Instantiate the Enhydra Application object (again, this is necessary
because the DODS generated DiscRack classes depend on this class)
We initialize all known Event Gateways. An event gateway is just a class
that handles events. In the case of the BarracudaDiscRack example, there are 3 such
classes:
- LoginScreen
- RegistrationScreen
- DiscScreen
Registering event gateways gives each class a chance to specify which
events it is capable of handling.
Startup - When the user first goes to the
BarracudaDiscRack application, the URL they enter in the browser might look something like
this:
http://localhost:8080/BarracudaDiscRack/
This maps the default request to index.html in the main
content directory, which simply does an HTML redirect, firing GetLogin.event.
Event Processing - When an event
like GetLogin.event is fired, the DiscRackGateway receives the HTTP Request, maps it to
the appropriate event object, and then delivers it to the specified listener, which in
this case happens to be implemented in the LoginScreen class. The event
handler that processes this event does one of two things: it checks to see if the user
already has login credentials, and if so, attempts to automatically log them in by firing
a DoLogin.event; if not, it redirects processing to the rendering logic
by firing a RenderLogin.event.
Both of these events cause a response to be sent back to the client
browser. In the case of RenderLogin, we load the necessary XMLC objects to generate the
appropriate HTML view. Before rendering, however, we need to check for errors and
repopulate the data elements if possible. This illustrates an interesting characteristic
of web-apps -- the same form is often rendered differently depending on how it was
accessed:
If the user is coming to the form for the first time (ie.
GetLogin.event), there are no errors or previously entered data. All we need to do is
display a blank form, which the user can then proceed to fill out.
When the user actually submits a form (ie. DoLogin.event), we typically
process the form and either redirect based on whether or not the information is valid --
if they entered a correct user/pwd, we fire a GetCatalog.event; if not, we fire a
RenderLogin.event to redraw the login screen.
In this latter case, we need to show additional information: an error
message indicating the problem, and repopulated form fields (if there are many fields, the
user will not want to re-enter them all when only one field was at fault.) The way we
handle both of these needs is through form mapping & validation.
Form Mapping & Validation - When
we handle a DoLogin.event, we need to process the incoming HttpServletRequest and retrieve
the necessary form parameters. Barracuda provides an easy way to do this through form
mapping. First we define a "virtual" form that looks something like this:
class LoginForm extends DefaultFormMap {
//login form constants (these values correspond to the HTML param names)
static final String USER = "login";
static final String PASSWORD = "password";
HttpSession session = null;
public LoginForm(HttpSession isession) {
session = isession;
//define the elements
if (logger.isDebugEnabled()) logger.debug("Defining Login form elements");
this.defineElement(new DefaultFormElement(USER, FormType.STRING, null, new NotNullValidator("You must enter a Login.")));
this.defineElement(new DefaultFormElement(PASSWORD, FormType.STRING, null, new NotNullValidator("You must enter a Password.")));
//define a form validator
if (logger.isDebugEnabled()) logger.debug("Defining Registration form validator");
this.defineValidator(new LoginValidator(session));
}
/**
* we override this method because instead of using the default validation
* process (validate elements, then form), we don't want to validate at all
* if they are already logged in...we just assume they're valid and return.
*/
public FormMap validate(boolean deferExceptions) throws ValidationException {
//see if they are already logged in. If so, just consider them valid
if (logger.isDebugEnabled()) logger.debug("Checking to see if already logged in (prevalidated)");
if (LoginServices.isLoggedIn(session)) {
try {
Person p = LoginServices.getCurrentUser(session);
String fuser = this.getStringVal(LoginForm.USER);
String fpassword = this.getStringVal(LoginForm.PASSWORD);
if (fuser==null && fpassword==null) return this; //if user/pwd = null, just use the current settings
if (p.getLogin().equals(fuser) &&
p.getPassword().equals(fpassword)) return this; //if they're != null, make sure they match, otherwise, we'll want to force a complete revalidate
} catch (DiscRackBusinessException e) {}
}
//if not, go ahead and validate
return super.validate(deferExceptions);
}
}
This defines a form with two elements: USER, PASSWORD which map to
the HTML parameters "login", "password". Both are to be mapped to
String objects, and take a basic NotNullValidator (which ensures they are not null or
blanks). Notice that we also add a "form validator" to the form called
LoginValidator, which takes care of complex validation tasks, like making sure the user
name is on file and that the password entered matches.
To map an HttpServletRequest to the FormMap, we would do something like
this:
LoginForm lf = new LoginForm();
lf.map(req);
This causes the FormMap class to do it's thing and convert all
incoming parameters into real objects that can be easily accessed through the FormMap
convenience methods.
Validating a form is just as simple:
try {
lf.validate();
catch (ValidationException ve) {
...process accordingly...
}
To sum then, when we process the DoLogin.event, we
- map the incoming request to a virtual form map
- validate the form map and catch any corresponding validation exceptions
- save both the form and any validation exceptions in the event queue
- redirect flow by firing either a GetCatalog.event (if they succeeded in logging in) or a
RenderLogin.event (if they failed)
- in this latter case, the event handling code for the RenderLogin.event can retrieve the
errors and form from the event queue and use them to repopulate the page with appropriate
information.
The client screen is regenerated, and the process continues with the next
user "action".
Using Barracuda Components
- The DiscRack examples have recently been upgraded to take advantage of the Barracuda
Component model. The process is really quite straightforward. When we wish to render the
HTML page back to the user, rather than manipulating the DOM directly we do the following
instead.
First, we get a reference which we'd like to bind a BTemplate component to
it. Note that we need both a View and a Model when creating the component:
Node node = page.getDocument().getElementById("LoginForm");
BTemplate templateComp = new BTemplate(new LoginModel(context), new DefaultTemplateView(node));
The template component will treat the node it is bound too as a
template, parsing through it looking for commands or "directives" embedded
within that reference specific data elements in the model. A template component may have
multiple models.
Next, we render the component. This will cause the components to update
the DOM.
try {templateComp.render(new DefaultViewContext(context));}
catch (RenderException re) {Debug.println (this, 1, "Render err:"+re);}
Finally, we use a DOM writer to actually render the page.
new DefaultDOMWriter().write(page, context.getResponse());
Hmm. That was pretty simple. Let's take a look at what the actual
directives and model aspects look like. If we look in the Login.html template, we see
lines that looks something like this:
<tr>
<td align="right">Login: </td>
<td>
<input class="Dir::Get_Data.Login.Username" name="login" ...>
</td>
</tr>
<tr>
<td align="right">Password: </td>
<td>
<input class="Dir::Get_Data.Login.Password" type="password" ...>
</td>
</tr>
Here we can see simple directives embedded in the class attributes. Directives can also
be stored in a completely separate file, but we've left them in the template for this
example just to keep things simple.
The model looks something like this:
class LoginModel extends AbstractTemplateModel {
LoginForm fm = null;
ValidationException ve = null;
//constructor (extract form, exception from context)
public LoginModel(EventContext ec) {
fm = (LoginForm) ec.getState(LOGIN_FORM);
ve = (ValidationException) ec.getState(LOGIN_ERR);
}
//register the model by name
public String getName() {return "Login";}
//provide items by key
public Object getItem(String key) {
ViewContext vc = getViewContext();
if (key.equals("Username")) {
return (fm!=null ? fm.getStringVal(LoginForm.USER, "") : "");
} else if (key.equals("Password")) {
return (fm!=null ? fm.getStringVal(LoginForm.PASSWORD, "") : "");
} else if (key.equals("LoginButton")) {
BAction baComp = new BAction(new DoLogin());
baComp.setDisableBackButton(true);
return baComp;
} else if (key.equals("RegisterButton")) {
BAction baComp = new BAction(new GetRegister());
baComp.setDisableBackButton(true);
return baComp;
} else if (key.equals("ExceptionButton")) {
BAction baComp = new BAction(new GetLogin());
baComp.setParam("generateSampleError","true");
baComp.setDisableBackButton(true);
return baComp;
} else if (key.equals("Errors")) {
// if (ve==null) return "";
if (ve==null) return null;
List errlist = ve.getExceptionList();
StringBuffer sb = new StringBuffer(errlist.size()>1 ? "There were several errors:" : "");
Iterator it = errlist.iterator();
while (it.hasNext()) {
sb.append(" "+((ValidationException) it.next()).getMessage());
}
return sb.toString();
} else {
return super.getItem(key);
}
}
}
What happens is when the template component encounters directives requesting data, it
tries to match the directives with a known model (in this case the "Login"
model), and then attempts to look up data in the model based on the specified key (ie.
"Username", "Password", etc). In the example above, the model is
returning simple String data. It can also return a DOM node or even another Barracuda
component. Basically, what we are doing is returning data if we can find it in the form
(ie. the user already entered it), and if not, we just return blanks.
For more information about Barracuda components, try looking at the Barracuda Component Model Tutorial.
|