Developing Content-Driven Web Apps with karma-jcr Blog

Version 2



    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

    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 frameworkwas 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.

    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:


    This URL pattern requires karma to call theRegister action and then dispatch to theregister 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:


    Have a look at the karma documentation for more information.

    This controller also invokes actions. Each action, a single use-case, is a simple Java class (a 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 newActionContext 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 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 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 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 CVSwhen you start a new project, but for this article I have prepared a download archive to get you started instantly.

    System requirements

    Make sure you have J2SDK 1.4.2+, Maven 1.0.x, and Tomcat 5.0.x available on your system. Maven is a popular Java build system similar to Ant. Refer to the 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:

    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, andwebapp 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 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 targetfolder 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 theapplicationContext.xml file in the src/webapp/WEB-INFfolder. This is a Spring framework configuration file that is used internally by the karma framework. You need to look for thejaasConfigFile, configFile, andrepositoryDir properties.

    Specify the absolute paths to the jaas.config andrepository.xml files in your barebone config folder. You also need to enter an absolute path forrepositoryDir. 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 URLhttp://localhost:8080/karma-jcr-starter-barebone/.

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

    Web application welcome page
    Figure 3. Web application welcome page

    If you don't see this page, check the catalina.outlogfile 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 yourapplicationContext.xml configuration file. Rerun maven clean war, and redeploy the war file to your Tomcatwebapps 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 file. You can find it in the src/java/com/inceedo/karma/jcr/barebone/actions folder.
    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 processmethod 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 theapplicationContext.xml file to do so), karma-jcr creates adefault 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 thegetComponent method on the IActionContextobject.

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

    <definitions> <alias aliasName="init" actionName="Init" viewName="success.jsp" inputName="/index.jsp" /> </definitions> 

    karma expects all your classes to be beneath a base package. In this case it's the com.inceedo.karma.jcr.barebonepackage, which is defined in the web.xml (insrc/webapp/WEB-INF). All actions go into theactions package, all interceptors intointerceptors, and all filters intofilters 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 anAction 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 thesrc/webapp folder.

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

    There's nothing unexpected here—just an HTML form to submit username and password to theLogin action by calling the login alias. Create a file insrc/java/com/inceedo/karma/jcr/barebone/actions.
    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, anerrormessage 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:

    <definitions> <alias aliasName="login" actionName="Login" viewName="addnews.jsp" inputName="/login.jsp" /> </definitions> 

    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.

    <c:if test="${!empty message}"> <font color="red"> <b>${message}</b> </font><br /> </c:if> <form method="POST" action="${ctx}/do/addarticle"> <table border="0"> <tr> <td>Headline:</td> <td> <input type="text" name="headline" /> </td> </tr> <tr> <td>Author:</td> <td> <input type="text" name="author" /> </td> </tr> <tr> <td>Text:</td> <td> <textarea name="text" rows="8" cols="60"> </textarea> </td> </tr> <tr> <td> </td> <td> <input type="submit" /> </td> </tr> </table> </form> 

    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.
    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 AddArticleaction 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.
    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 defaultNodeRights 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:

    <definitions> <alias aliasName="addarticle" actionName="AddArticle" viewName="addnews.jsp" /> </definitions> 

    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 aShowArticle 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 theGetArticleNames action.
    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.

    <h2>available news articles:</h2> <c:if test="${!empty noarticles}"> <font color="red"> <b>No articles found.</b> </font> <br /> </c:if> <c:forEach var="article" items="${articles}"> <a href="${ctx}/do/showarticle?name=${article.key}"> ${article.key} </a> <br/> </c:forEach> 

    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.

    Articles listing
    Figure 4. List of all articles
    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", "<br/>")); 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.

    <h2>${article.headline}</h2> <small>from ${}</small> <p>${article.text}</p> 

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

    <alias aliasName="articles" actionName="GetArticleNames" viewName="articles.jsp" inputName="/index.jsp" /> <alias aliasName="showarticle" actionName="ShowArticle" viewName="showarticle.jsp" /> 

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

    Figure 5 shows the showarticle action in your browser.

    Article details
    Figure 5. Article details


    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.