((( Draft )))

Getting Started with JCR (Content Repository for Java, JSR-170) Page: 4

Page 1 | Page 2 | Page 3 | Page 4 |Page 5

Please Note: all of the DOJO Tree information has been moved into a new article. I've noticed so many search results bringing people here with DOJO questions so, I decided to give it its own area. The new article goes into more depth whereas this was intended to be used in a very narrow scope. I hope the new one proves to be useful.
--Paul

Initializing the Repository

I like the idea of using classpath like references for node paths in the repository. This seems like a good way to prevent object intersection. So, for the menu I want to start the menu nodes with a base path com/willcde4beer/dmo/menu/. To do this, we can either just create the nodes and add childs nodes or, import an XML file into the repository. I am going to choose the second because, the code to build the nodes would be throw away, and we've already covered it in the Hello World.

So, lets start with the repository XML file:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <sv:node sv: name="com" xmlns: rep="internal"
  3.   xmlns: jcr="http://www.jcp.org/jcr/1.0"
  4.   xmlns: sv="http://www.jcp.org/jcr/sv/1.0">
  5.   <sv:property sv: name="jcr:primaryType" sv: type="Name">
  6.     <sv:value>nt:unstructured</sv:value>
  7.   </sv:property>
  8.   <sv:node sv: name="com">
  9.     <sv:property sv: name="jcr:primaryType" sv: type="Name">
  10.       <sv:value>nt:unstructured</sv:value>
  11.     </sv:property>
  12.     <sv:node sv: name="willcode4beer">
  13.       <sv:property sv: name="jcr:primaryType" sv: type="Name">
  14.         <sv:value>nt:unstructured</sv:value>
  15.       </sv:property>
  16.       <sv:node sv: name="demo">
  17.         <sv:property sv: name="jcr:primaryType" sv: type="Name">
  18.           <sv:value>nt:unstructured</sv:value>
  19.         </sv:property>
  20.         <sv:node sv: name="menu">
  21.           <sv:property sv: name="jcr:primaryType"
  22.             sv: type="Name">
  23.             <sv:value>nt:unstructured</sv:value>
  24.           </sv:property>
  25.         </sv:node>
  26.       </sv:node>
  27.     </sv:node>
  28.   </sv:node>
  29. </sv:node>

If you follow the nodes, you will notice the sv:node tags with a name that corresponds to the part of the path. Their relationship is expressed through the nesting of the sv:node tags.

Now, lets add a class with a main method to import the XML file into the repository. I'm going to recommend that the setup xml file be placed in the classpath. This way the main method only needs two parameters, and it limits the nuber of artifacts if you need to pass setup instructions to someone else. Something else to keep in mind, if you were going to provide your application to someone else for deployment, part of your deployment instructions would be how to run the class to set up the menu structure in the repository. Easy for them and easy for you. Anyway, onto the code.

  1. public class RepositorySetup {
  2.  
  3.   /*
  4.    * Location of the initialization config on the classpath.
  5.    */
  6.   private String configPath = "/setup.xml";
  7.  
  8.   /**
  9.    * Initializes the repository to contain the base node for the menu.
  10.    * com/willcode4beer/demo/menu
  11.    *
  12.    * @param args the repository config file and the repository home
  13.    */
  14.   public static void main(String[] args) throws Exception {
  15.     if (args.length < 2) {
  16.       System.err.println("Usage: java "
  17.           + RepositorySetup.class.getCanonicalName()
  18.           + " repositoryConfig repositoryHome");
  19.       System.exit(2);
  20.     }
  21.     RepositorySetup app = new RepositorySetup();
  22.     app.setpReposiory(args[0], args[1]);
  23.   }
  24.  
  25.   private void setpReposiory(String repositoryConfig, String repositoryHome)
  26.       throws Exception {
  27.     InputStream setupConfig = loadConfig();
  28.     Repository repository = new TransientRepository(repositoryConfig,
  29.         repositoryHome);
  30.     Session session = repository.login(new SimpleCredentials(
  31.         "testUserName", "testPassword".toCharArray()));
  32.     session.importXML("/", setupConfig,
  33.         ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
  34.     session.save();
  35.     session.logout();
  36.     setupConfig.close();
  37.   }
  38.  
  39.   private InputStream loadConfig() {
  40.     return this.getClass().getResourceAsStream(configPath);
  41.   }
  42. }

As you can see, only two parameters need to be passed to the main method, the location of the redpository configuration file, and the location of the repository home. If you have gone through the First Hops example on the Jackrabbit site, then this should look familiar. All the magic happens in the method call session.importXML (lines 32 and 33). The main method requires two parameters, the location of the repository config file and the location of the repository home. Running this (using the locations defined before) simply requires:

java com.willcode4beer.demo.jcr.menu.RepositorySetup "C:\\foo\\foorepository\\repository.xml" "C:\\foo\\foorepository\\repository"

Managing the JCR Session

You have seen in the First Hops and on the first page, that changes are commited to the repository when the save method is called on the Session object. Since we don't want to commit any changes until all of the menu changes have been made, we are going to have to store the Session object in the user's HttpSession.

To implement this, we can create a Session manager factory class. This will provide a reference to a JCR Session object on demand and maintain it in the user's HttpSession. The factory will use the decorator pattern to enhance the behavior of the Session. When the logout method is called, the session will be removed from the user's HttpSession.

  1. public class JcrSessionMgr {
  2.   final static String SESSION_KEY = JcrSessionMgr.class.getName()
  3.       + "@JcrSessionKey";
  4.  
  5.   private Repository repository;
  6.  
  7.   public Session getSession(HttpServletRequest request,
  8.       Credentials credentials) throws LoginException, RepositoryException {
  9.     Session session = (Session) request.getSession().getAttribute(
  10.         SESSION_KEY);
  11.     if (session == null) {
  12.       session = repository.login(credentials);
  13.     }
  14.     return new ManagedSession(session,request.getSession());
  15.   }
  16.  
  17.   public static void logut(HttpSession session){
  18.     session.removeAttribute(SESSION_KEY);
  19.   }
  20.  
  21.   public void setRepository(Repository repository) {
  22.     this.repository = repository;
  23.   }
  24. }

The class should be pretty clear. On lines 42 and 43 an attempt is made to retrieve a JCR Session from the user's HttpSession. If there isn't one there (line 44), then one is made by logging into the Repository (line 45). Finally, the session is wrapped (or decorated) and the wrapper object is returned.

The significant portion of the wrapper class ManagedSession follows:

  1. public class ManagedSession implements Session {
  2.  
  3.   private final Session jcrSession;
  4.  
  5.   private final HttpSession httpSession;
  6.  
  7.   public ManagedSession(Session jcrSession, HttpSession httpSession) {
  8.     this.jcrSession = jcrSession;
  9.     this.httpSession = httpSession;
  10.   }
  11.  
  12.   public void logout() {
  13.     this.jcrSession.logout();
  14.     JcrSessionMgr.logut(httpSession);
  15.   }

You can see that the wrapper holds a reference to the user's HttpSession (lines 38 and 42). When the overloaded method logout is called (line 45), the call is passed to the wrapped class and then the static logout method is called on the JcrSessionMgr.

The methods not displayed, simply pass the call onto the JCR Session object being wrapped. You may download the entire class to view if desired.

Releasing Resources

To be safe from holding the repository session forever (locking resurces), in case the user fails to commit or cancel, we will need to create an implementation of an HttpSessionListener. This will allow us to rollback the session with the repository if the user's session with the application server ends unexpectedly. If the user's HttpSession ends and there is still a reference to the JCR Session, then the methods refresh and logout will be called before removing the JCR Session from the HttpSession.

  1. public class JcrSessionListener implements HttpSessionListener {
  2.  
  3.   public void sessionDestroyed(HttpSessionEvent event) {
  4.     HttpSession httpSession = event.getSession();
  5.     JcrSessionMgr.logut(httpSession);
  6.   }
  7.  
  8.   public void sessionCreated(HttpSessionEvent event) {
  9.     // Nothing to do here
  10.   }
  11. }

Be sure to add a reference to the session listener in the web deployment descriptor (web.xml).

  1. <listener>
  2.   <listener-class>
  3.     com.willcode4beer.demo.jcr.JcrSessionListener
  4.   </listener-class>
  5. </listener>

Menu Editor Functions

Moving right along. So, now let's define what our menu is going to be. Let's say it will be a hierarchical menu with no limit on how deep the nodes can be nested. Let's also define a menu to contain the following properties: url, a caption (display text), and a description.

Now, about the menu editor. We have already become familiar with the tree widget. A tree seems a natural metaphor for a recursively nested structure like a menu. We will need to extend it a bit to hold the properties mentioned above. The tree will need to be created programatically from data in the content repository, and changes to the tree will need to be reflected in the repository.

Now, lets think about the types of actions that the tree and repository should have to edit the menu. From the context menu, an add and delete will be required. The delete should also cause the children of the deleted node to be deleted. Let's delay the add until the full set of properties have been declared by the user. Next, we should have the ability to move nodes around. Of course, we will also need the ability to edit the details of a given node. To ensure that the client is synchronized with the repository, lets rebuild the menu on the client after each operation. The user will need the ability to create top level menu items (there's nothing above the top level items to click to add a child to). Lastly, the user will need the ability to save/commit their changes or cancel them.

To do this, we'll keep with the Unix philosophy of small simple single purpose things. We will create a controller, like the hello world demo controller on the prvious page, for each function. We'll also ensure a unique URL mapping for each one. I'll cover the details of this in just a bit.

Linking the data to the tree

I'm going to start this by defining a MenuNode java class.

  1. public class MenuNode {
  2.   private String url;
  3.   private String caption;
  4.   private String description;
  5.   private List<MenuNode> children;
  6.   
  7.   public String getCaption() {
  8.     return caption;
  9.   }
  10.   public void setCaption(String caption) {
  11.     this.caption = caption;
  12.   }
  13.   public List<MenuNode> getChildren() {
  14.     return children;
  15.   }
  16.   public void addChild(MenuNode childNode){
  17.     if(this.children==null){
  18.       this.children = new ArrayList<MenuNode>();
  19.     }
  20.     this.children.add(childNode);
  21.   }
  22.   public void setChildren(List<MenuNode> children) {
  23.     this.children = children;
  24.   }
  25.   public String getDescription() {
  26.     return description;
  27.   }
  28.   public void setDescription(String description) {
  29.     this.description = description;
  30.   }
  31.   public String getUrl() {
  32.     return url;
  33.   }
  34.   public void setUrl(String url) {
  35.     this.url = url;
  36.   }
  37. }

Its basically a bean, just getters and setters with one exception. Lines 16-21 define an addChild method. It will create the children List if needed. This structure will be easy to represent in javascript using the JSON notation.

  1. {
  2.   url : "",
  3.   caption : "",
  4.   description : "",
  5.   children : []
  6. }

Its just a handful of properties and an array to hold more child elements. When passed to the client, we can use this in a recursive funcion to build the tree and its children.

Create the DAO

Now that everything is in place, lets define the interface for a data access object (DAO) to store and retrieve the MenuNode objects. Lets define what it needs to do. It should have a method to return the entire menu structure. The ability to save changes to a node. The ability to delete a node (and its child nodes). The ability to move a node.

init script in javascript

sending (fake) data to populate the tree

sending the tree object graph to the server

Now, we want to be able to serialize the state of the tree and send it to the server when it is time to save changes.

Testing with Selenium?

Ok, so you've poked the page with your browser, that was fun. But, testing web pages by hand is going to become very tiring very quickly. So, I think it's a good time to bring up testing with the Selenium framework. (Make Brian write this part?)

Do I leave this part out? The article will be pretty long and cover a lot of stuff by this point.

Getting the data

Optimizing - stop hitting the server so much

Use the client's CPU, its just being used to surf the web after all

receiving the object graph

storing the object graph in the repository

Communicating with the repository

Testing the connection with storage and retrieval

Store the root node for the menu

Putting it all together

Connecting the parts

Testing

Create an HttpUnit/jWebUnit test

Selenium test?


Feel free to send comments to: feedback@willcode4beer.com
Page 1 | Page 2 | Page 3 | Page 4 |Page 5


Sponsors:

About willCode4Beer