Skip to main content

Developing Content-Driven Web Apps with karma-jcr

September 29, 2005

{cs.r.title}









Contents
What is the karma framework?
What is a Java Content Repository?
Let's develop an application
System requirements
Let's create your first karma action
Conclusion
Resources

Everyone seems to be talking about making development simpler
these days, and for good reason: Look at some of the J2EE
technologies out there, like the EJB 2.1 specification. That's one
of those specifications that makes outsiders think J2EE is a platform primarily for academics only.
But the good news is there's a growing awareness of the hurdles of J2EE complexity, and for this reason
the JSR 220 expert group designed the new EJB 3
specification from the ground up with simplicity in mind.

Every day another Ruby on Rails Clone seems to pop
up somewhere. For those of you who are unaware, Ruby on Rails is the framework
known for its productivity. While Ruby is truly superior to most
frameworks in productivity, as a language it lacks industry support and will probably have a hard time
making its way into the big enterprises any time soon.

So what does that mean for our Java projects? They are getting
tougher every day, and schedules are tight. The J2EE world offers a great stack of frameworks, but
they don't come as easy and integrated as the
Rails stack. It takes a lot of preparation, experience, and most of
all smart architectural decisions to get things rolling.

What is the karma framework?

The karma framework
was created to tackle the problem of unnecessary complexity, to
reduce the amount of configuration needed to get simple things
working. The key is that it uses a set of conventions to find
stuff. This approach is called convention over
configuration
—an approach that isn't so unusual these days but was not widely implemented when I
started the framework about a year ago.

"http://www.karma-mvc.org/confluence/display/COMMON/Home">karma
mvc
is the core component of the framework. It is a fairly
universal interpretation of the Model View Controller (MVC) pattern
targeted mainly for web application development (Servlet/JSP), but
with some tweaking it could also be used in an EJB context,
as a stand-alone on a command-line, or even as a web service
framework.

Yes, in a sense karma mvc is just another web framework! It has a
little bit of Struts, a little bit of Webwork, and some pieces of
Spring. I tried to extract the best parts and ideas of each of them
and combine them into one simple-to-learn and easy-to-use MVC
framework. If you are familiar with the ideas and concepts of MVC
frameworks I'm certain you can learn karma mvc in an
hour. If you aren't familiar with it, don't worry. I will summarize
it for you.

All requests that need to go to your application are routed
through a central controller, which is where the framework
lives. Actually, you don't have much work to do here except to
tell the controller where to route the requests. You can do this in two ways: The first one is
to use the default URL pattern-based
method; this approach doesn't require any configuration. The second one is to use an XML-based configuration file.

The karma controller looks at the URL that is invoked by the
client. It splits the URL into so-called request elements, and
figures out what needs to be done. For example:

http://yourhost/app/do/Register/register

This URL pattern requires karma to call the
Register action and then dispatch to the
register view. If you think this may be a security
issue for your application, you can configure it in an XML file.
These configurations are called aliases. You can define an
alias called register for this workflow. The URL would
then be much smaller:

http://yourhost/app/do/register

Have a look at the "http://www.karma-mvc.org/confluence/homepage.action">karma
documentation
for more information.

This controller also invokes actions. Each action, a single use-case, is a simple Java class (a "http://en.wikipedia.org/wiki/Plain_Old_Java_Object">POJO) that
contains your business logic. The
whole idea is to divide your business logic into short,
easy-to-maintain, specialized action classes. This is where you
make use of your Application model composed of JavaBeans.

It is worth mentioning that actions do not have bindings to
the servlet container in which your application runs (great
conditions for unit testing). The karma-mvc framework creates a new
ActionContext object for each invocation of an action. This
generic ActionContext contains form values, parameters, session objects, and
application objects.

Last but not least, views—single JSP
files that should not contain any Java business logic—display the
processing results of your actions. Views simply
display the objects that your actions give them access to.

Interceptors and filters are
advanced topics that I won't go into detail about in this
article.

What is a Java Content Repository?

Most Java developers today have a basic need for CRUD (create,
retrieve, update, and delete) capabilities in their web
applications. They need to persist their model data quickly and easily to persistent
storage. That is where databases and
Object-Relational Mapping frameworks come into place. In the Java
world, you would use Hibernate or iBatis, for example. In Rails you
have Active Record, which is part of the core framework.

The karma framework addresses this need for persistence and comes with an
additional component called "http://www.karma-mvc.org/confluence/display/JCR/Home">karma-jcr, an Object Persistence framework for Java Content
Repositories. Repositories are well known in content management and
portals but are not particularly common in web application
development.

A Java Content Repository (JCR) is a standard infrastructure for
content storage specified in "http://jcp.org/aboutJava/communityprocess/final/jsr170/index.html">
JSR-170
(JCR 1.0). It is a "content database" that can consist
of several workspaces each with a tree of items (nodes). Each node
can have a number of properties of different types (primitives
types, xml data and binary files). The nature of a node (its set
of properties or subnodes) is defined by its node type, which
compares to classes in the Java world. Take a look at
Chapter 4 ("The Repository Model") of the JCR specification for
more information.

karma-jcr is able to persist your model classes to a JCR in a
very high-level way. Most of your standard JavaBeans can be used
with karma-jcr. Supported properties include all primitives,
"complex" JavaBeans, Arrays, and most Java Collection types.
karma-jcr comes with Content-, Search-, Login-, and Admin Managers,
which cover common tasks needed for working with a repository. The
Manager classes are service classes that are used for specific
tasks, and are the only classes in karma-jcr that you need to deal
with. Internally karma-jcr uses "http://incubator.apache.org/jackrabbit/">Jackrabbit from
Apache; this is the reference implementation of the JCR 1.0
standard for communicating with the repository. To summarize it,
karma-jcr is a very easy-to-use abstraction layer on top of
Jackrabbit and JCR.

Let's develop an application

Enough theory. Let's get our hands dirty.

We are going to develop a web application with a simple
back office to publish news articles that will be
stored in a Java Content Repository. We need a page to display
articles and a protected page with a login to enter news
articles.

There are different ways to solve this problem. We are going to
use a karma barebone starter application. The barebone
contains a complete project structure with configuration files,
a build file, and skeleton code. You should always use the latest
barebone from "https://karma.dev.java.net/source/browse/karma/starters/">CVS
when you start a new project, but for this article I have prepared
a "http://www.karma-mvc.org/maven/karma/distributions/barebone_karma_0.7_karma_jcr_0.4.zip">
download archive
to get you started instantly.

System requirements

Make sure you have "http://java.sun.com/j2se/1.4.2/">J2SDK 1.4.2+, "http://maven.apache.org/start/download.html">Maven 1.0.x, and
"http://jakarta.apache.org/site/downloads/downloads_tomcat-5.cgi">Tomcat
5.0.x
available on your system. Maven is a popular Java build
system similar to Ant. Refer to the "http://maven.apache.org/start/install.html">Maven user guide
for instructions on how to install it. You can also use Ant, but you'll
need to set up the project manually. If you prefer, you can use a different
servlet engine from Tomcat.

I assume you have your environment all set up now and you've fired up
your favorite IDE. You will also need to access the command
line.

Unzip the barebone archive to your workspace directory. You
should see something like Figure 1:

<br "Directory structure" />
Figure 1. Directory structure

The config directory contains the configuration files for
your Java Content Repository. In the src directory there are
two subdirectories: java for your Java source files, and
webapp for your web-related files like JSP and CSS. Note
that there are no jar files in the barebone folder because Maven fetches the needed jars from the remote Maven
Repositories specified in the project.properties file. For this to work properly, make sure your system is connected to
the Internet.

To test your barebone, open up a command-line shell and go to
the barebone folder. Enter maven war.
This maven command will create a war file artifact that will be used with
Tomcat later on. Figure 2 shows the results of this command.

Maven build
Figure 2. Maven build

On the first run of maven, a
lot of jars will be downloaded, so this could take a while. You should now have a war file
artifact, called karma-jcr-starter-barebone.war, in the target folder of your barebone.

Before deploying the war file to your Tomcat server, you need to
make some configuration adjustments, indicating where you want your
Java Content Repository to be. Open the
applicationContext.xml file in the src/webapp/WEB-INF
folder. This is a Spring framework configuration file that is used
internally by the karma framework. You need to look for the
jaasConfigFile, configFile, and
repositoryDir properties.

Specify the absolute paths to the jaas.config and
repository.xml files in your barebone config folder.
You also need to enter an absolute path for
repositoryDir. This is the actual filesystem location
of your Java Content Repository.

You then need to rerun Maven to apply these configuration
changes to your war file artifact. Enter

maven clean
war
on the command line. You can now copy this file to your
Tomcat webapps folder and start Tomcat. Then open up your
favorite web browser, and go to the URL
http://localhost:8080/karma-jcr-starter-barebone/.

You should now see the welcome page shown in Figure 3.

<br "Web application welcome page" />
Figure 3. Web application welcome page

If you don't see this page, check the catalina.out
logfile of your Tomcat server.

The barebone application comes with a single action that you can
use to create a new Java Content Repository. Click the link.
You only need to do this on the first run of the application. It is for
convenience only; never use it in a production environment.

If things go wrong, doublecheck the paths in your
applicationContext.xml configuration file. Rerun
maven clean war, and redeploy the war file to your
Tomcat webapps folder.

It's now time to do some coding to get your news application
going!







Let's create your first karma action

This hasn't been too tough so far, has it? Let's get to the fun
part now.

First, you need to create a login page containing a simple HTML form with a username and
password textfield for your news
back office.

karma-jcr comes with the built-in concept that everything should be kept in
the repository. While initializing the repository, the standard
admin user with password password has already been created
for you. You will use the Login Manager to perform the
authentication. But first, have a look at the Init.java file. You can find it in the
src/java/com/inceedo/karma/jcr/barebone/actions folder.

Init.java

package com.inceedo.karma.jcr.barebone.actions;

// imports

public class Init implements IAction {

  private RepositoryAdminManager
                            repositoryAdminManager;

  public void process(IActionContext context) {
    try {
      getRepositoryAdminManager().initRepository();
    } catch (RepositoryInitializationException e) {
      e.printStackTrace();
      context.returnToInput();
    }
  }
  // getters and setters
}

The Init action is a simple Java class implementing
the IAction interface, which is required for all karma
actions. Init implements a single process method that
is invoked by the framework. Note that there are no
bindings to the servlet container, so you can easily create JUnit
tests for your actions.

The Admin Manager takes care of initializing the Java Content
Repository. It creates a repository layout for you where you can
store users, groups, and your content objects. It comes with a
domain concept. Domains are
logical subroots of your content. You can create different domains within your Java
Content Repository where you can separate your content. For example, you can create
domains like company_a and subdivision_b. If you
don't specify any other domains (use the
applicationContext.xml file to do so), karma-jcr creates a
default domain for you.

One more interesting aspect about the Init action
is how the RepositoryAdminManager is injected into the
action. When the web application is started, by default karma scans
all the action classes defined in the aliases, looking for dependencies. It
reflects on the class properties and compares them to beans managed
by the Spring container. It keeps a map of all dependencies and
injects the beans into the action at invocation time, so you don't
have to take care of that. Of course, you can manually retrieve
beans from the Spring container by invoking the
getComponent method on the IActionContext object.

The action alias init is used to call this action. Let's
look at the aliases.xml (in src/webapp/WEB-INF):

aliases.xml
&lt;definitions&gt;
  &lt;alias aliasName="init"
         actionName="Init"
         viewName="success.jsp"
         inputName="/index.jsp" /&gt;
&lt;/definitions&gt;

karma expects all your classes to be beneath a base package.
In this case it's the com.inceedo.karma.jcr.barebone
package, which is defined in the web.xml (in
src/webapp/WEB-INF). All actions go into the
actions package, all interceptors into
interceptors, and all filters into
filters beneath the base package, so you only have
to define this once.

Alias definitions are very straightforward. You have to define
an alias name (aliasName) that maps to an
Action class (actionName) whose result is
displayed in a view (viewName). In case something goes
wrong, it returns to the input view (inputName). Note
that actions are not supposed to make workflow decisions: the
workflow is always defined outside the action.

Next up is the Login action. First, you need to
create a simple login.jsp file that goes into the
src/webapp folder.

login.jsp
&lt;%@ page language="java"
    pageEncoding="UTF-8"%&gt;
&lt;%@ taglib uri="http://java.sun.com/jstl/core_rt"
    prefix="c" %&gt;
&lt;c:set var="ctx"
    value="${pageContext.request.contextPath}" 
    scope="request"/&gt;
    
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;Login&lt;/title&gt;
  &lt;/head&gt;
&lt;body&gt;
  &lt;c:if test="${!empty errormessage}"&gt;
    &lt;font color="red"&gt;
      &lt;b&gt;${errormessage}&lt;/b&gt;
    &lt;/font&gt;
    &lt;br /&gt;
  &lt;/c:if&gt;
  &lt;form method="POST" action="${ctx}/do/login"&gt;
    Username:
    &lt;input type="text" name="username" /&gt;
    &lt;br/&gt;
    Password:
    &lt;input type="password" name="password" /&gt;
    &lt;br/&gt;
    &lt;input type="submit" /&gt;
  &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;

There's nothing unexpected here—just an HTML form to submit
username and password to the
Login action by calling the login alias.
Create a Login.java file in
src/java/com/inceedo/karma/jcr/barebone/actions.

Login.java

package com.inceedo.karma.jcr.barebone.actions;

// imports

public class Login implements IAction {

  private RepositoryLoginManager
                        repositoryLoginManager;

  public void process(IActionContext context) {
    String username =
      context.getForm().getProperty("username");
    String password =
      context.getForm().getProperty("password");

    if (!getRepositoryLoginManager().domainLogin(
      "default", username, password)) {
      context.addToRequest("errormessage",
        "Login failed!");
      context.returnToInput();
    } else {
      context.addToSession("authenticatedUser",
        username);
    }
  }
  // getters and setters
}

The Login action uses the Login Manager to perform
a domain login. It retrieves the form values using the Form object
in the IActionContext. The Form object is created
dynamically by the karma framework. If the domain login fails, an
errormessage is created and added to the request. By setting
the returnToInput flag, karma knows that the action failed
and that the workflow needs to return to the input view, which is
the login.jsp in this case. If the domain login succeeds,
the username is added to the session for later reuse.

Next, add a new alias to your aliases.xml file:

aliases.xml
&lt;definitions&gt;
  &lt;alias aliasName="login"
         actionName="Login"
         viewName="addnews.jsp"
         inputName="/login.jsp" /&gt;
&lt;/definitions&gt;

After that you have to create an addnews.jsp file, as
defined in viewName, in the src/webapp/views folder.
karma expects all views that participate in workflows to be there.
You can, of course, create subfolders. login.jsp is outside
the views folder because it will be called directly.

addnews.jsp
&lt;c:if test="${!empty message}"&gt;
  &lt;font color="red"&gt;
    &lt;b&gt;${message}&lt;/b&gt;
  &lt;/font&gt;&lt;br /&gt;
&lt;/c:if&gt;

&lt;form method="POST"
      action="${ctx}/do/addarticle"&gt;
  &lt;table border="0"&gt;
    &lt;tr&gt;
      &lt;td&gt;Headline:&lt;/td&gt;
      &lt;td&gt;
       &lt;input type="text" name="headline" /&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Author:&lt;/td&gt;
      &lt;td&gt;
        &lt;input type="text" name="author" /&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Text:&lt;/td&gt;
      &lt;td&gt;
        &lt;textarea name="text" rows="8"
                  cols="60"&gt;
        &lt;/textarea&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&amp;nbsp;&lt;/td&gt;
      &lt;td&gt;
        &lt;input type="submit" /&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/table&gt;
&lt;/form&gt;

This is the JSP with an HTML form to add a news article. News
articles will consist of a headline, an author, and text. You
need to create a corresponding JavaBean class representing this
model.

Article.java

package com.inceedo.karma.jcr.barebone.model;

public class Article {

        private String headline;
        
        private String author;
        
        private String text;

    // getters and setters...
}

Now all you have to do is create an AddArticle
action that populates the Article JavaBean from the submitted form
values and saves the Article object to the repository.
To save objects you have to use the Content Manager. The repository has a tree structure, so you have to
determine the path at which you want to add the object.

AddArticle.java

package com.inceedo.karma.jcr.barebone.actions;

// imports

public class AddArticle implements IAction {
    
  private RepositoryContentManager
                          repositoryContentManager;
    
  public void process(IActionContext context) {
        
    Article article = new Article();
    article.setAuthor(
    context.getForm().getProperty("author"));
      article.setHeadline(
    context.getForm().getProperty("headline"));
      article.setText(
    context.getForm().getProperty("text"));
        
    try {
      getRepositoryContentManager().addObject(
        "default",
        "domains/default/objects/" 
          + article.getHeadline(),
        article,
        (String) 
        context.getFromSession("authenticatedUser"),
        new NodeRights());
            
        context.addToRequest("message",
          "Object created!");
            
    } catch (Exception e) {
      e.printStackTrace();
      context.addToRequest("message",
        "Object could not be created: " + 
        e.getMessage());
    }
  }
    
  // getters and setters
}

Besides the object itself, you have to specify the creator of the
object and attach the rights to it. By creating a default
NodeRights object, you can use the default set of
rights, but you can also specify in detail who is going to be
able to access the object.

Next, add another alias to your aliases.xml file:

aliases.xml
&lt;definitions&gt;
  &lt;alias aliasName="addarticle"
         actionName="AddArticle"
         viewName="addnews.jsp" /&gt;
&lt;/definitions&gt;

In the final step, you are going to create a page to show the
available articles. You need a JSP to display all articles and a
ShowArticle action to load a single article from the
repository using the Content Manager. Then you need to create a
JSP to display this particular article. Let's first create the
GetArticleNames action.

GetArticleNames.java

package com.inceedo.karma.jcr.barebone.actions;

// imports

public class GetArticleNames implements IAction {
    
  private RepositorySearchManager
                        repositorySearchManager;
    
  public void process(IActionContext context) {
    try {
      HashMap map = (HashMap)
        getRepositorySearchManager().
        getContentObjectNamesFromPath(
        "domains/default/objects",
        true);

      context.addToRequest("articles", map);
            
      if (map == null || map.size() == 0) {
        context.addToRequest("noarticles", "true");
      }

      } catch (JcrRepositoryActionException rae) {
        rae.printStackTrace();
        context.returnToInput();
      }
  }

  // getters and setters
}

This action basically reads all object names at a given path and
then adds them to the request for the following JSP to display.

articles.jsp

&lt;h2&gt;available news articles:&lt;/h2&gt;

&lt;c:if test="${!empty noarticles}"&gt;
  &lt;font color="red"&gt;
    &lt;b&gt;No articles found.&lt;/b&gt;
  &lt;/font&gt;
  &lt;br /&gt;
&lt;/c:if&gt;

&lt;c:forEach var="article" items="${articles}"&gt;
  &lt;a href="${ctx}/do/showarticle?name=${article.key}"&gt;
   ${article.key}
  &lt;/a&gt;
  &lt;br/&gt;
&lt;/c:forEach&gt;

This JSP fragment for the articles.jsp shows how to
iterate the articles' HashMap using the JSTL core taglib.

Figure 4 shows what the article listing looks like.

<br "Articles listing" />
Figure 4. List of all articles

ShowArticle.java

package com.inceedo.karma.jcr.barebone.actions;

// imports

public class ShowArticle implements IAction {
    
  private RepositoryContentManager
                         repositoryContentManager;
    
  public void process(IActionContext context) {
        
    try {
      ContentBean contentBean =
        getRepositoryContentManager().getObject(
        "default",
        "domains/default/objects/"
        + context.getParameter("name"));
            
      Article article = (Article)
        contentBean.getObject();
      article.setText(
        article.getText().replaceAll("\n",
        "&lt;br/&gt;"));
            
      context.addToRequest("article", article);
            
      } catch (Exception e) {
        e.printStackTrace();
        context.returnToInput();
      }
  }

  // getters and setters
}

ShowArticle retrieves a single article from the
repository using the Content Manager.

showarticle.jsp

&lt;h2&gt;${article.headline}&lt;/h2&gt;
&lt;small&gt;from ${article.author}&lt;/small&gt;
&lt;p&gt;${article.text}&lt;/p&gt;

This JSP fragment for showarticle.jsp shows how to
display the article.

aliases.xml
&lt;alias
    aliasName="articles"
    actionName="GetArticleNames"
    viewName="articles.jsp"
    inputName="/index.jsp" /&gt;
    
&lt;alias
    aliasName="showarticle"
    actionName="ShowArticle"
    viewName="showarticle.jsp" /&gt;

Finally, you have to add some aliases to your
aliases.xml file.

Figure 5 shows the showarticle action in your
browser.

<br "Article details" />
Figure 5. Article details

Conclusion

This article showed you how to build a simple karma-based web
application using a Java Content Repository to store your content
objects. This is only an introduction, and the source code does
not suffice for a real-world project, but it should help you get
started on this topic. karma-jcr comes with easy-to-use abstract
Manager classes to perform basic tasks. It is a very young
project, as young as the JCR 1.0 standard itself, so feedback and
patches are very welcome.

Resources

width="1" height="1" border="0" alt=" " />
Oliver Kiessler is a senior software architect and technical consultant from Cologne, Germany.
Related Topics >> Programming   |   Web Development Tools   |