天天看點

内容倉庫Apache JackRabbit

JSR-170把自己定義為一個能與内容倉庫互相通路的,獨立的,标準的方式。同時它也對内容倉庫做出了自己的定義,它認為内容倉庫是一個進階的資訊管理系統,該系統是是傳統的資料倉庫的擴充,它提供了諸如版本控制、全文檢索,通路控制,内容分類、通路控制、内容事件監視等内容服務。

Java Content Repository  API(JSR-170)試圖建立一套标準的API去通路内容倉庫。如果你對内容管理系統(CMS)不熟悉的話,你一定會對内容倉庫是什麼感到疑惑。你可以這樣去了解,把内容倉庫了解為一個用來存儲文本和二進制資料(圖檔,word文檔,PDF等等)的資料存儲應用程式。一個顯著的特點是你不用關心你真正的資料到底存儲在什麼地方,是關系資料庫?是檔案系統?還是XML?不僅僅是資料的存儲和讀取,大多數的内容倉庫還提供了更加進階的功能,例如通路控制,查找,版本控制,鎖定内容等等。

一段時間以來市場上出現了各個廠家開發的不同的CMS系統,這些系統都建立在他們各自的内容倉庫之上。
問題出現了,每個CMS開發商都提供了他們自己的API來通路内容倉庫。這對應用程式的開發者帶來了困擾,因為他們要學習不同的開發商提供的API,同時,他們的代碼也與這些特定的API産生了綁定。

JSR-170正是為解決這一問題而出現的,它提供了一套标準的API來通路任何資料倉庫。通過JSR-170,你開發代碼隻需要引用 javax.jcr.* 這些類和接口。它适用于任何相容JSR-170規範的内容倉庫。

我們将通過一個例子來逐漸了解JSR-170。

為什麼需要 Java Content Repository API

随着各個廠家各自的内容倉庫實作數量的增長,人們越來越需要一組通用的程式設計接口來使用這些内容倉庫,這就是JSR-170所要做的東西。它提供一組通用的程式設計接口來連接配接内容倉庫。你可以把JSR-170了解為和JDBC類似的API,這樣你可以不依賴任何具體的内容倉庫實作來開發你的程式。你可以直接使用支援JSR-170的内容倉庫;或者如果一些廠家的内容倉庫不支援JSR-170則可以通過這些廠家提供的JSR-170驅動來完成從JSR-170與廠家特定的内容倉庫的轉換。

下面這張圖描述了使用JSR-170開發的應用系統的結構。在該系統運作的時候,它可以操作内容倉庫1,2,3中的任意一個。在這些内容倉庫當中,隻有2是直接支援JSR-170的,剩下的兩個都需要JSR-170驅動來和應用系統互動。注意:你的應用系統完全不用關心你的資料是如何存儲的。1可能使用了關系資料庫來存儲,而2使用了檔案系統,至于上,它甚至更前衛的使用了XML。

repositorymodel1.gif

JSR-170 API對不同的人員提供了不同的好處。

●對于開發者無需了解廠家的倉庫特定的API,隻要相容JSR-170就可以通過JSR-170通路其倉庫。
●對于使用CMS的公司則無需花費資金用于在不同種類CMS的内容倉庫之間進行轉換。
●對于CMS廠家,無需自己開發内容倉庫,而專注于開發CMS應用。


JSR-170 是這樣定義内容倉庫的,内容倉庫由一組 workspace(工作空間)組成,這些workspace通常應該包含相似的内容。一個内容倉庫有一個到多個 workspace。每個workspace都是一個樹狀結構,都有一個唯一的樹根節點(root node)。樹上的item(元素)或者是個node(節點)或者是個property(屬性)。每個node都可以有零個到多個子節點和零個到多個子屬性。隻有根節點沒有父節點,其餘所有的節點都有一個父節點。property 也必須有一個父節點,但它沒有子節點或是子屬性,property 是葉子元素。property是真正存儲資料的元素。

下圖描述了一個blog應用程式的内容倉庫模型。每個root node(根節點)的子節點都代表了一個blog實體。與這個blog實體有關的資料都存儲在 bolgEntry 節點的屬性裡,其中一個 blogAttachment property 存儲了一個二進制圖檔檔案。
repositorymodel3.gif
根據内容倉庫實作的功能,JSR-170定義了三種級别:
Level 1:定義了一個隻讀的内容倉庫。功能包括讀取内容,将内容導出為XML和查找内容。
Level 2:定義了可寫的内容倉庫。Level 2是Level 1的擴充,新增的功能包括往内容倉庫裡寫入内容,和從XML導入資料到倉庫。
Advanced options:定義實作五種附加功能,版本控制、JTA、SQL查詢、清晰的内容鎖定和監視。

什麼是Apache JackRabbit?
Apache JackRabbit是一個開放源碼的JSR-170 實作,實作了Level 2,但它還有許多擴充的功能。詳細可以去它的官方網站。

下面我們決定用Apache JackRabbit來作為我們示例程式的内容倉庫。

如何配置Apache JackRabbit
JackRabbit需要兩個參數來配置一個内容倉庫執行個體。
1.内容倉庫主目錄:這個檔案目錄下通常包含了所有的内容,搜尋索引,内部配置檔案和其他持久化資訊。它的結構看起來會像下面這個樣子:
python 代碼

    c:/temp   
         |   
         |--Blogging   
                 |   
                 |-repository   
                 |       |   
                 |       |-index  
                 |       |-meta   
                 |       |-namespaces   
                 |       |-nodetypes                
                 |   
                 |-version   
                 |   
                 |-workspace   
                         |   
                         |--default   

  在上面的情況下,内容倉庫主目錄是c:/temp/Blogging.
2.内容倉庫配置檔案:一個典型的配置檔案如下:
xml 代碼

    <Repository>  
     <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">  
      <param name="path" value="${rep.home}/repository"/>  
     FileSystem>  
     <Security appName="Jackrabbit">  
      <AccessManager class="org.apache.jackrabbit.core.security.SimpleAccessManager"/>  
      <LoginModule class="org.apache.jackrabbit.core.security.SimpleLoginModule">  
        <param name="anonymousId" value="anonymous"/>  
      LoginModule>  
     Security>  
     <Workspaces rootPath="${rep.home}/workspaces" defaultWorkspace="default"/>  
     <Workspace name="${wsp.name}">  
      <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">  
       <param name="path" value="${wsp.home}"/>  
      FileSystem>  
      <PersistenceManager    
            class="org.apache.jackrabbit.core.state.db.DerbyPersistenceManager">  
       <param name="url" value="jdbc:derby:${wsp.home}/db;create=true"/>  
       <param name="schemaObjectPrefix" value="${wsp.name}_"/>  
      PersistenceManager>  
      <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">  
       <param name="path" value="${wsp.home}/index"/>  
      SearchIndex>  
     Workspace>  
     <Versioning rootPath="${rep.home}/version">  
      <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">  
       <param name="path" value="${rep.home}/version" />  
      FileSystem>  
      <PersistenceManager    
            class="org.apache.jackrabbit.core.state.db.DerbyPersistenceManager">  
       <param name="url" value="jdbc:derby:${rep.home}/version/db;create=true"/>  
       <param name="schemaObjectPrefix" value="version_"/>  
      PersistenceManager>  
      Versioning>  
      <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">  
       <param name="path" value="${rep.home}/repository/index"/>  
      SearchIndex>  
    Repository>   

 在這個配置檔案裡,元素是根元素,它包含了下面這些元素:
  a,: 該元素配置了内容倉庫的全局資料存儲位置,這些全局資料包括已注冊的命名空間,定制的節點類型等等。        JackRabbit 提供了幾種選擇,一種是像上面例子裡配置的存儲在本地檔案裡,LocalFileSystem. 如果你想把它們存儲在資料庫裡,你可以使用 DbFileSystem.
  b,:内容倉庫的安全配置,它有兩個子元素:和。配置的類用來判斷使用者有沒有權限來對特定資料執行特定的操作。
  c,:這個元素的配置對所有的workspace都通用。它的rootPath 屬性是所有workspace檔案夾的根目錄,在我們的例子裡它是c:/temp/Blogging/Workspace;defaultWorkspace 屬性則包含了workspace的預設名。
  d,:這個元素是所有workspace的預設配置模闆。去每個workspace檔案夾下你都會發現一個workspace.xml檔案,這個檔案和這個元素的配置一模一樣。三個子元素:,和這個workspace相關資料的存儲位置; ,這個workspace内容節點存儲政策;,可選,全文檢索。
  e,:配置一個版本相關的對象。其實JackRabbit也是把它作為節點來處理的。

這兩個參數可以通過兩種方式設定,一種是在倉庫執行個體建立時直接傳到Jackrabbit裡去,一種是間接的通過設定JNDI object factory。
你可以設定org.apache.jackrabbit.repository.home 這個系統屬性的值來指定你的内容倉庫主目錄;也可以設定
org.apache.jackrabbit.repository.conf 這個系統屬性的值來指定你的内容倉庫配置檔案repository.xml。如果你不設定這兩個
參數,Jackrabbit會把目前目錄作為内容倉庫主目錄,同時,它有一個預設的内容倉庫配置檔案。

開發我們的例子程式
jackrabbit已經配置好了,現在讓我們來建立我們的示例程式。這個例子程式将調用JCR-170 API。很顯然,我們需要做兩件事情:一個是作為背景的對資料進行增删改查(持久層),另一個是開發相對應的UI界面(WEB 層)。首先,讓我們定義一個DAO接口。這個接口BlogEntryDAO.java 如下:
java 代碼

    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;   
    }   

正如你看到的,這個接口提供了增删改查的方法,同時還提供了兩個方法來處理附件。接下來,我們需要一個DTO對象用來在兩個層之間傳遞資料。
java 代碼

    public class BlogEntryDTO {   
      
        private String userName;   
        private String title;   
        private String blogContent;   
        private Calendar creationTime;   
      
        //Getter and setter methods for each of these properties           
    }   

這裡我們将僅僅讨論持久層。

連接配接jackrabbit
現在,第一件事情是開發一個元件,獲得一個到jackrabbit内容倉庫的連接配接。為了簡單,我們将在程式啟動的時候獲得這個連接配接,然後在程式停止的時候釋放這個連接配接。這裡我們使用了Struts ,是以我們需要開發一個PlugIn 類。如下:
java 代碼

    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;   
        }   
    }   

init()方法将會在程式啟動的時候調用,destroy()将會在程式停止的時候調用。我們在init()方法裡獲得了到jackrabbit内容倉庫的連接配接。看看代碼,我們做的第一件事是設定了org.apache.jackrabbit.repository.home這個系統屬性,在上篇文章裡提到,這個屬性是用來指向我們的内容倉庫主目錄。這裡我們設定它為c:/temp/blogging。接下來,我們建立了TransientRepository的一個執行個體。這是jackrabbit提供的類,它提供了一個到内容倉庫的代理。它在第一個session 打開的時候自動啟動内容倉庫,在最後一個session 關閉的時候自動關閉内容倉庫。
一旦我們獲得了一個内容倉庫對象,我們就可以調用它的login() 方法來打開一個連接配接。login() 方法需要一個Credential 對象作為參數。如果Credential 對象是NULL,jackrabbit會認為其他的機制做了這個驗證(比如JAAS)。login() 方法還可以傳入一個workspace名字作為參數,如果不傳入這個參數,jackrabbit會傳回一個session對象指向預設的workspace。注意workspace和session是一對一的,即一個session僅對應一個workspace。(注:如果不傳入Credential對象,傳回的session對workspace是隻讀的)

增加内容
連接配接已經建立起來了,下面讓我們實作BlogEntryDAO這個接口。第一個我們想實作的方法是插入資料 insertBlogEntry():
java 代碼

    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();   
    }   

首先獲得session 對象,即到内容倉庫特定workspace的連接配接。然後,我們在這個session 對象上調用getRootNode() 方法,獲得這個workspace的根節點,這個根節點的路徑是("/").一旦我們獲得這個根節點,我們就可以通過addNode()方法在這個根節點下增加新的子節點。新節點的名字是blogEntry. 通過setProperty() 方法我們把資料存儲到節點的property裡。正如我們先前說明的,真實的資料是存儲在property元素裡,property元素是葉子。
注意session.save() 這行代碼。這個方法是必須調用的,這個方法調用之前,任何 Node,Property的改變都被儲存在這個session的一個臨時區域裡,其他的和該session連接配接到相同workspace的session都看不到這些改變。當這個方法被調用并被成功執行後,這些Node,Property的改變才會被持久化到這個session關聯的workspace裡,同時所有與這個workspace關聯的session才可見這些變化。相對應的,Session.refresh(false)将會丢棄所有這些改變。item.save()和Item.refresh(false)作用相似,隻是影響範圍限定在單個Item上(注意,包括它的子節點)

獲得清單
在上一步中我們已經把資料儲存到了内容倉庫中,那我們如何确定資料确實儲存進去了呢?getBlogList() 這個方法将傳回根節點下所有名為blogEntry.的子節點。

java 代碼

    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;   
    }   

 一旦你獲得了根節點這個對象,你就可以通過調用getNodes()這個方法來擷取它所有的子節點。如果這個節點沒有子節點,将傳回一個空的NodeIterator 對象。我們可以周遊這個NodeIterator 對象來獲得名為blogEntry 的節點集合,然後通過getProperty()方法來獲得節點上的屬性,即我們儲存的真實資料。getProperty()方法傳回Value對象的一個執行個體。因為存儲資料類型的不同,是以傳回的Value對象執行個體是不同的。根據不同的資料類型,你應該調用特定的方法來擷取資料,比如getString()來擷取字元串,而getDate()獲得一個日期。

查找内容(用XPath的方式)
JSR-170定義了兩種方式來查找内容(也可以了解為查找節點)。一種使用XPath文法,另一種使用SQL文法。JSR-170要求Level 1必須實作XPath的方式,而SQL的方式則作為一個可選的功能。

XPath原本是一種設計用來查找XML元素的語言。因為我們的workspace是樹狀的結構,很像XML。是以XPath文法非常适合于在這裡查找内容。下面的代碼示範了通過作者名來查找節點。
java 代碼

    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);   
        }  
    java 代碼
        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;   
        }   
    正如你看到的那樣,我們的代碼在處理二進制内容和一般内容間并沒有什麼太大的差別。僅僅一點不同的是你要通過InputStream 對象來儲存和擷取二進制資料。在我們的配置檔案裡關于persistent manager會有一個externalBLOBs 屬性。把這個屬性設為true, 圖檔将會儲存在檔案裡,相反則會儲存在資料庫的blob字段裡。

    總結
    到這裡,我們對 JSR-170, Jackrabbit以及如何使用 JSR-170 API開發一個簡單的應用程式都有了大概的了解。我們的讨論更多的在于基礎。相信大家一定會對内容倉庫有個初步的認識。