Tuesday, 13 September 2016

What is Java Content Repository Part-2

Developing a Blogging Application


With our Apache Jackrabbit installation built and configured, it's time to take the next step and build a sample application. In this section, we will develop a sample blogging application using the JCR-170 API. We need two things for developing this sample application: a backend to add, update, delete, and remove content in the content repository, and a client to provide a UI for performing these operations.

First we create clear-cut separation between these two parts by defining a DAO interface for the backend layer. So, create BlogEntryDAO.java interface like this

public interface BlogEntryDAO {
    public void insertBlogEntry(BlogEntryDTO blogEntryDTO)
        throws BlogApplicationException;
    public void updateBlogEntry(BlogEntryDTO blogEntryDTO)
        throws BlogApplicationException;
    public ArrayList getBlogList()
        throws BlogApplicationException;
    public BlogEntryDTO getBlogEntry(String blogTitle)
        throws BlogApplicationException;
    public void removeBlogEntry(String blogTitle)
        throws BlogApplicationException;
    public ArrayList searchBlogList(String userName)
        throws BlogApplicationException;
    public void attachFileToBlogEntry(String blogTitle, InputStream uploadInputStream)
        throws BlogApplicationException;
    public InputStream getAttachedFile(String blogTitle)
        throws BlogApplicationException;
}

As you can see, this class has methods for adding, updating, searching for blog entries, and two methods for dealing with binary content. Next, we need a DTO class that will be used for carrying data between the web layer and backend layer. Create the BlogEntryDTO class like this:

public class BlogEntryDTO {

    private String userName;
    private String title;
    private String blogContent;
    private Calendar creationTime;

    //Getter and setter methods for each of these properties      
}

Every blog entry will have four properties associated with it: userName, title, blogContent, and creationTime. With this interface between the UI layer and backend layer in place we can implement either layer. Because of space constraint, we have decided not to spend time talking about the UI layer; instead you can download sample code for this application from the resources section, where you'll find a sample Struts-based UI for BlogEntryDAO. The next section describes implementing the backend for the blogging application by implementing BlogEntryDAO.

Connecting to Jackrabbit


The first thing that we want to do in developing the backend is to write the component that gets a connection to Jackrabbit. To keep things simple, we will get the connection to Jackrabbit at application startup time and drop that connection when the application shuts down. Since we are developing a Struts application, we need to create our own PlugIn class that gets control at application startup and shutdown times, like this:

public class JackrabbitPlugin implements PlugIn{
    public static Session session;
    public void destroy() {
        session.logout();
    }
    public void init(ActionServlet actionServlet, ModuleConfig moduleConfig)
    throws ServletException {
        try {
            System.setProperty("org.apache.jackrabbit.repository.home",
                "c:/temp/Blogging");
            Repository repository = new TransientRepository();
            session = repository.login(new SimpleCredentials("username",
                    "password".toCharArray()));
        } catch (LoginException e) {
            throw new ServletException(e);
        } catch (IOException e) {
            throw new ServletException(e);
        } catch (RepositoryException e) {
            throw new ServletException(e);          
        }
    }
    public static Session getSession() {
        return session;
    }
}

The init() method of JackrabbitPlugin class will get called at application startup and destroy() method will get called at shutdown. The code inside the init() method is used for getting the connection to Jackrabbit. The first thing we do is set the org.apache.jackrabbit.repository.home system property to point to c:/temp/blogging, indicating where Jackrabbit should store its data. Next, create a new instance of TransientRepository. This is a class provided by Apache Jackrabbit, offering a proxy to the repository. It starts up the repository automatically when the first session is opened, and automatically stops the repository when the last session is closed.

Once you have a repository object, you can call its login() method to open a connection. login() takes an object of type Credential as an and argument; if this is null, it is assumed that authentication is handled by mechanism external to the repository itself (for example, the JAAS framework). Since we are not passing a workspace name argument to login(), Jackrabbit will create default a workspace and return a Session object for that particular workspace. The Session object encapsulates both the authorization settings of a particular user and a binding to the workspace specified by the workspaceName passed on login. Please note that there is a one-to-one mapping between session and workspace.

Add Content


With Apache Jackrabbit set up properly and our code to connect to it, we can implement the methods of BlogEntryDAO. The first method that we want to implement is insertBlogEntry(), which is used for adding new blogEntry nodes:

public void insertBlogEntry(BlogEntryDTO blogEntryDTO)
            throws BlogApplicationException {
        Session session = JackrabbitPlugin.getSession();
        Node rootNode = session.getRootNode();
        Node blogEntry = rootNode.addNode("blogEntry");
        blogEntry.setProperty("title", blogEntryDTO.getTitle());
        blogEntry.setProperty("blogContent", blogEntryDTO.getBlogContent());
        blogEntry.setProperty("creationTime", blogEntryDTO.getCreationTime());
        blogEntry.setProperty("userName", blogEntryDTO.getUserName());          
        session.save();
}

The first thing that we are doing in this method is getting an instance of the session object initialized in the JackrabbitPlugin class. After that, we call getRootNode() on the session object, which returns root node ("/") of the workspace. Once we have an object pointing to the root node, we can add a new child node to it by calling addNode() method on rootNode; this will create a new child node named blogEntry. After that, we can set the actual content of blogEntry as properties of the node. You might remember from the discussion on the repository model that properties are leaves and are used for storing actual content. In the case of blogEntery, every blogEntry will have four properties: title, blogContent, creationTime, and userName, each of which can be set by calling setProperty() on the newly created blogEntry node.

Notice the use of the method session.save() in the insertBlogEntry() method. This method is needed because changes made through methods of Session, Node, or Property are not immediately reflected in the persistent workspace. The changes are held in the transient storage associated with the Session object until they are either persisted using either session.save() or item.save(). Also, Session.save() validates changes and if this validation succeeds, it persists all pending changes currently stored in the Session object. Until this is done, changes made using one session are not made visible to other sessions. Conversely, Session.refresh(false) discards all pending changes currently stored in session. For more fine-grained control over which changes are persisted or discarded, the method Item.save() and Item.refresh() are also provided. Item.save() saves all pending changes in the Session that apply to a particular item or its subtree. Analogously, Item.refresh(false) discards all pending changes that apply to that item.

In the web UI you can post a new blog entry by going to the http://localhost:8080/<contentroot> page and clicking on "Add node" link, fill out that form, and click submit to invoke the insertBlogEntry() method.


Traversal


How do we test that node was actually added and persisted to content repository? Implement the getBlogList() method of BlogEntryDAO method, which returns a list of all child nodes of root node whose name is equal to blogEntry. The following code listing demonstrates how to do that:

public ArrayList getBlogList() throws BlogApplicationException {
    Session session = JackrabbitPlugin.getSession();
    ArrayList blogEntryList = new ArrayList();
    Node rootNode = session.getRootNode();
    NodeIterator blogEntryNodeIterator = rootNode.getNodes();

    while (blogEntryNodeIterator.hasNext()) {
        Node blogEntry = blogEntryNodeIterator.nextNode();
        if (blogEntry.getName().equals("blogEntry") == false)
            continue;
        String title = blogEntry.getProperty("title").getString();
        String blogContent = blogEntry.getProperty("blogContent").getString();
        Value creationTimeValue = (Value) blogEntry.getProperty(
                "creationTime").getValue();
        String userName = blogEntry.getProperty("userName").getString();
        BlogEntryDTO blogEntryDTO = new BlogEntryDTO(userName, title,
                blogContent, creationTimeValue.getDate());
        blogEntryList.add(blogEntryDTO);
    }
    return blogEntryList;
}

Once you have a root node object, you can call getNodes() on it to return all its child nodes. If the node does not have any children, then an empty NodeIterator is returned. We can iterate through NodeIterator to get a list of blogEntry nodes. You can call the node's getProperty() to read a property with a supplied name. getProperty() returns an instance of Value, whose implementation class depends on the type of property stored. Once you have this object you can call type-specific methods such as getString() for reading a string stored in the property, or getDate() for a stored date.

When you go to http://localhost:8080/<contextroot> in the web UI, the index page of the blog application will call the getBlogList() method and it will display all entries on index page.


Searching for Content (XPath)


JSR-170 defines two ways to search for content. One uses XPath syntax and the other uses SQL syntax. The specification mandates that every Level 1 compliant repository should provide support for XPath syntax, but support for SQL search is an optional feature that we will talk more about in the next part.

XPath is a search language originally designed for selecting elements from an XML document. Since a workspace, like an XML document, can be viewed as a tree structure, XPath provides a convenient syntax for searching workspace content.

Let's change our blogging application so that it allows you to search for all blog entries posted by a particular user (i.e., all blog entries where blogAuthor is some user name). We need two things to implement this: one is a change to the UI to accept the query userName and display results to the user. This feature can be seen in the sample application, which displays an input box named Blogger Name at the top of page, with a "Search" button. When you input text in this box and click search, control goes to SearchBlogEntriesAction.java, which calls the searchBlogList() method of BlogEntryDAO with the blogger name supplied by the user, and displays results as a list. So, the only thing that we have do is implement the searchBlogList() method of JackrabbitBlogEntryDAO class like this:

Session session = JackrabbitPlugin.getSession();
    Workspace workSpace = session.getWorkspace();
    QueryManager queryManager = workSpace.getQueryManager();

    StringBuffer queryStr = new StringBuffer(
            "//blogEntry[@"+PROP_BLOGAUTHOR +"= '");
    queryStr.append(userName);
    queryStr.append("']");
    Query query = queryManager.createQuery(queryStr.toString(),
            Query.XPATH);

    QueryResult queryResult = query.execute();

    NodeIterator queryResultNodeIterator = queryResult.getNodes();
    while (queryResultNodeIterator.hasNext()) {

        Node blogEntry = queryResultNodeIterator.nextNode();
        String title = blogEntry.getProperty(PROP_TITLE).getString();
        String blogContent = blogEntry.getProperty(PROP_BLOGCONTENT).getString();
        Value creationTimeValue = (Value) blogEntry.getProperty(
                PROP_CREATIONTIME).getValue();
        BlogEntryDTO blogEntryDTO = new BlogEntryDTO(userName, title,
                blogContent, creationTimeValue.getDate());
        blogEntryList.add(blogEntryDTO);
    }

Once we have the session object, call the getWorkspace() method on it to retrieve the workspace attached to the current session. Remember, there is a one-to-one mapping between workspace and session. This workspace object can be used to retrieve the QueryManager associated with this workspace. The QueryManager interface encapsulates methods for management of search queries. The next thing that we want to do is to create a query string, which in our case would be "//blogEntry[@blogAuthor='<bloggerName>'"]. This means "search all nodes with name equal to blogEntry and value of blogAuthor property equal to the <bloggerName> supplied by user". See the JSR-170 specification document for more details on the syntax of an XPath query.

You can create a new query object by calling the queryManager's createQuery() method, passing a query string and the name of the query language (XPath in our case). Once you have the Query object, you can call its execute() method to actually execute the query and return a QueryResult object. The results returned always respect the access restrictions of the current session. In other words, if the current session does not have read permission for a particular item, then that item will not be included in the result set, even if it would otherwise constitute a match. All queries are run against the peristent state of the workspace; pending changes stored in the session are not searched. Once you have results you can call getNodes() method on it to have an iterator go over nodes that match the query.

Two more methods that we want to implement are updateBlogEntry() and removeBlogEntry(), which are actually very simple. In both these methods, we retrieve the relevant node by using the title as the primary key. In the update case, set the new values for properties to update, and in the remove case, once you have have target node call the remove() method on it. Don't forgot to call session.save() method to make sure that your changes are persisted.

Handling Binary Bontent


One of the primary requirements of a content repository is that it should be able to handle binary content, such as image files. Now let's assume that we want to allow the user to attach an image to a particular blogEntry and we also want to add a retrieval method. To do that, we have added two links to every blog entry in its title bar: "attach file" for attaching an image to a blog entry and "display attached file," which displays the image attached to that blogEntry. To get this feature working we have to implement two methods from BlogEntryDAO: attachFileToBlogEntry() and getAttachedFile() :

public void attachFileToBlogEntry(String blogTitle,
  InputStream uploadInputStream) throws BlogApplicationException {
    Session session = JackrabbitPlugin.getSession();
    Node blogEntryNode = getBlogEntryNode(blogTitle, session);
    blogEntryNode.setProperty(PROP_ATTACHMENT, uploadInputStream);
    session.save();

}
public InputStream getAttachedFile(String blogTitle) throws BlogApplicationException {
    InputStream attachFileIS = null;
    Node blogEntryNode = getBlogEntryNode(blogTitle);
    Value attachFileValue = (Value) blogEntryNode.getProperty(PROP_ATTACHMENT).getValue();
    attachFileIS = attachFileValue.getStream();
  return attachFileIS;
}

As you can see from this code listing, the repository does not treat binary content any different from any other type of content. The only difference is you can add binary content by setting InputStream as the value of property; the same goes for retrieving a property with a binary value. Where this file is actually stored is determined by the value of the externalBLOBs attribute in your persistent manager. If its value is true, then this image file will be stored on filesystem, and if false it will be stored in the database as a BLOB. In our case this value is true, so the uploaded image file will be stored in the filesystem.