Tuesday, January 30, 2007

Roller Weblog 3.0 on Weblogic Platform v8.1.5

One fine day, I came across a Java based weblogging tool called "Roller" that was quite cool and decided to port it to Weblogic. Only problem was there weren't any how-tos so I decided to roll-up my sleeve and port it myself. Below are the steps I took to do the porting. There maybe better ways of doing it so please let me know.  Cheers!

Lastly, all credits to Dave Johnson and his wonderful team for making Roller available to the community. Keep up the great works, guys!

Roller v3.0 on Weblogic Platform v8.1.5

---------------------------------------------------
Preparation.

1) Download source code for Roller v3.0.
2) Create a temp folder (we shall named it as {temp} from here onwards). Place downloaded source code in '{temp}\roller'.
3) Create a new Default application with a new Web Project in Workshop and renamed the Web Project as 'roller'. (We shall call this folder '{DefaultApp}\roller' from here onwards).
4) Rename '{DefaultApp}\roller\WEB-INF\web.xml' to '{DefaultApp}\roller\WEB-INF\web.bak'
5) Rename '{DefaultApp}\roller\WEB-INF\validator-rules.xml' to '{DefaultApp}\roller\WEB-INF\validator-rules.bak' (You can remove it otherwise).
6) Rename '{DefaultApp}\roller\index.jsp' to '{DefaultApp}\roller\index.bak'. (in case you want to keep it for later).

First compilation of roller.

1) Remember to copy over any dependant libraries into '{temp}\roller\tools'.
2) Either modify properties.xmlf or simply dump 'jazzy.jar' into '{temp}\roller\tools\hibernate-3.1\lib'.
3) Add 'RollerSpellCheck.java' to '{temp}\roller\src\org\apache\roller\model', it. See source code below.


   package org.apache.roller.model;

   import com.swabunga.spell.engine.SpellDictionary;
   import com.swabunga.spell.engine.SpellDictionaryHashMap;
   import com.swabunga.spell.event.SpellCheckEvent;
   import com.swabunga.spell.event.SpellCheckListener;
   import com.swabunga.spell.event.SpellChecker;
   import com.swabunga.spell.event.StringWordTokenizer;

   import org.apache.commons.logging.Log;
   import org.apache.commons.logging.LogFactory;
   import org.apache.roller.RollerException;

   import java.io.IOException;
   import java.io.InputStream;
   import java.io.InputStreamReader;
   import java.util.ArrayList;

   /**
    * Since this class cannot be thread-safe due to the
    * SpellCheckListener interface, first a "canonical"
    * SpellDictionary must be instantiated with the path
    * to the dictionary. Subsequently, call getInstance()
    * which will return a new instance of RollerSpellCheck
    * using the "precompiled" SpellDictionary, or will throw
    * a NullPointerException.  A better way can
    * probably be found.
    **/
   public class RollerSpellCheck implements SpellCheckListener
   {
      private static Log mLogger =
         LogFactory.getFactory().getInstance(RollerSpellCheck.class);

      private static SpellDictionary dictionary = null;

      private SpellChecker spellCheck = null;
      private ArrayList spellCheckEvents = new ArrayList();

      /**
       * Initializer which takes an InputStream for
       * /WEB-INF/english.0 to load the SpellDictionary.
       * Building the SpellDictionary, and thus the SpellChecker
       * is an expensive operation.
       * You can get this InputStream with ServletContext.getResource(
       * "/WEB-INF/english.0").  Throws a RollerException if
       * SpellDictionary cannot be instantiated.
       **/
      public static void init(InputStream in) throws RollerException
      {
         try {
            InputStreamReader inR = new InputStreamReader( in );
            RollerSpellCheck.dictionary = new SpellDictionaryHashMap( inR );
         } catch (IOException ioe) {
            mLogger.error("RollerSpellCheck unable to load SpellDictionary",
                ioe);
            throw new RollerException(ioe);
         }
       }

      /**
       * Private constructor taking a prebuilt SpellChecker
       * as an argument.
       **/
      private RollerSpellCheck()
      {
         spellCheck = new SpellChecker(dictionary);
         spellCheck.addSpellCheckListener(this);
      }

      /**
       * Returns a new instance of RollerSpellCheck, using
       * the (hopefully) prebuilt SpellChecker.
       **/
      public static RollerSpellCheck getInstance() throws RollerException
      {
         if (RollerSpellCheck.dictionary == null)
         {
            throw new RollerException(
               "RollerSpellCheck.SpellDictionary has not been defined");
         }

          return new RollerSpellCheck();
      }

      /**
       * Fulfills interface SpellCheckListener.
       * SpellCheckEvent is placed into the ArrayList
       * spellCheckEvents held by this RollerSpellCheck.
       **/
      public void spellingError(SpellCheckEvent event)
      {
          spellCheckEvents.add( event );
      }

      /**
       * This is the method to check spelling.
       * The submitted String is "parsed" by SpellChecker,
       * SpellCheckEvents are placed into an ArrayList, which
       * is returned to the caller.  A SpellCheckEvent contains
       * the "suspect" word, and a LinkedList of suggested replacements.
       */
      public ArrayList checkSpelling(String str) throws RollerException
      {
         spellCheck.checkSpelling( new StringWordTokenizer(str) );
         return spellCheckEvents;
      }

      /**
       * Convenience method. Creates a RollerSpellCheck object
       * and calls checkSpelling(str) on it, returning the ArrayList.
       */
      public static ArrayList getSpellingErrors(String str) throws RollerException
      {
         RollerSpellCheck rCheck = RollerSpellCheck.getInstance();
         return rCheck.checkSpelling(str);
      }
   }


4) Do an initial compilation in '{temp}\roller' (Assuming you already got Ant just type "build all").
5) Copy all files under '{temp}\roller\build\webapp\' to '{DefaultApp}\roller'. (If using Windows Explorer, reply "Yes to All" when prompt whether to override existing files/folders).

Setup WebLogic roller

As WebLogic v8.1 is a servlet 2.3 container, we need to replace some jar files
1) Copy and replace the following files.   
   1.1) Rename or remove the following files in '{DefaultApp}\roller\WEB-INF\lib'
         i) jstl.jar
         ii) standard.jar
   1.2) Copy the jar files 'jstl-1.0.6.jar' and 'standard-1.0.6.jar' to '{DefaultApp}\roller\WEB-INF\lib'.
2) Copy 'jazzy.jar' to '{DefaultApp}\roller\WEB-INF\lib' if it does not exists.
   
3) We need to make changes to some jsp files.    
   3.1) Copy '{DefaultApp}\roller\WEB-INF\classes\META-INF\tlds\roller.tld' to '{DefaultApp}\roller\WEB-INF'.
   3.2) Open up {DefaultApp}\roller\taglibs.jsp' and change the taglib declarations to use jstl v1.0. Example, change taglib URI "http://java.sun.com/jsp/jstl/core" to "http://java.sun.com/jstl/core". Also uncomment the following "<%@ page language="java" contentType="text/html; charset=UTF-8" %>" to <%--@ page language="java" contentType="text/html; charset=UTF-8" --%>
   3.3) Open up '{DefaultApp}\roller\jsps\admin\rollerProperties.jsp' and change taglib declarations as per 3.2.
   3.4) Open up '{DefaultApp}\roller\jsps\tiles\search.jsp', under line 37, change the special character '»' to '&raquo;'.
   3.5) Copy 'struts-bean.tld' to '{DefaultApp}\roller\WEB-INF\'.
       
4) Amend '{DefaultApp}\roller\jsps\authoring\spellcheck-entry.jsp' and add the following.
   (Hint: Paste after the </script> tag.)

   <%!
   public static String makeSelect(String word, List words)
   {
      StringBuffer buf = new StringBuffer("<select name=\"");
      buf.append("replacementWords\" style=\"font-size: 10px;\">");
      buf.append("<option selected=\"selected\" value=\"").append(word);
      buf.append("\">").append(word).append("</option>");
       if (words == null || words.size() < 1)
       {
          buf.append("<option value=\"").append(word);
          buf.append("\">No Suggestions</option>");
       }
       else
       {
          for (Iterator it2=words.iterator(); it2.hasNext();)
          {
              word = it2.next().toString();
              buf.append("<option value=\"").append(word);
              buf.append("\">").append(word).append("</option>");
          }
      }    
      buf.append("</select>");
      return buf.toString();
   }
   %>

   (Hint: Paste after the 'Use Spell Check Results' wordings.)

   <%
      String escapeText = StringUtils.replace( text, "<", "{" );
          escapeText = StringUtils.replace( escapeText, ">", "}" );
      StringBuffer newText = new StringBuffer(escapeText);
      ArrayList events = (ArrayList) session.getAttribute("spellCheckEvents");
      SpellCheckEvent event = null;
      String word = null;
      int start = -1;
      int end = -1;
      String select = null;
      for(ListIterator it=events.listIterator(events.size()); it.hasPrevious();)
      {
         event = (SpellCheckEvent)it.previous();
         word = event.getInvalidWord();
         start = event.getWordContextPosition();
         end = start + word.length();
         select = makeSelect(word, event.getSuggestions());

           newText.replace( start, end, select );
       }
       escapeText = StringUtils.replace( newText.toString(), "}", "&gt;" );
       escapeText = StringUtils.replace( escapeText, "{", "&lt;" );
    %>


5) Modify web.xml and comment out the <dispatcher> tag for <filter-mapping> tag. Alternatively, open up the new web.xml and the old web.bak in '{DefaultApp}\roller\WEB-INF\' and merge the 2 files taking note that the new web.xml is meant for servlet 2.4 container. If merging, remove the 'PageFlowJspFilter' filter and its filter mapping. You will also have to remove or comment out the 'PageFlowActionServlet'.
   5.1) Also uncomment the below. For some reasons, this cause Weblogic to load RollerContext twice and cause spring framework to complain about multiple ContextLoader definitions.

   <!--<listener>
      <listener-class>org.apache.roller.ui.core.RollerContext</listener-class>
   </listener>-->

6) Next we need to amend '{DefaultApp}\roller\WEB-INF\classes\hibernate.cfg.xml' with the following (See sample below, I'm using Oracle so modify for your DB).


       <!-- By default Roller uses a JNDI DataSource -->
       <property name="jndi.url">t3://localhost:7001</property>
       <property name="jndi.class">weblogic.jndi.WLInitialContextFactory</property>
       <property name="connection.datasource">jdbc/rollerdb</property>
       <property name="show_sql">false</property>
                       
       <property name="current_session_context_class">thread</property>
       <!-- select SQL dialect, MySQL 3.X or 4.X by default -->
       <property name="hibernate.dialect">org.hibernate.dialect.Oracle9Dialect</property>
       


7) Create the roller database tables using the DB scripts in '{DefaultApp}\roller\WEB-INF\dbscripts'
8) Create the necessary JDBC connection pool as well as a data source to the roller DB. Remember to name the JNDI name as 'jdbc/rollerdb'.
9) Changes to '{temp}\roller\src\org\apache\roller\ui\core\RollerContext.java', replace the line 'RollerConfig.setContextRealPath(mContext.getRealPath("/"));' as below.

       // To allow application to run from Weblogic Workshop 8.1. Damon, 8/12/2006.
       String realPath = mContext.getRealPath("/");
       if( -1==realPath.indexOf(".workshop\\output") ){
           RollerConfig.setContextRealPath(realPath);
        }
       else{
           realPath = realPath.replaceFirst(".workshop\\\\output\\\\","");
           RollerConfig.setContextRealPath(realPath);
        }


10) There are some JNDI lookup which we need to change. In my case, I used a custom DB connection class called 'DBConnection'. Also define a mail session with JNDI name 'mail/Session' in the Weblogic server console.
   10.1) In '{temp}\roller\src\org\apache\roller\ui\core\RollerContext.java', modify the method 'upgradeDatabaseIfNeeded'.


          // Added to get DB connection. Damon
           import sp.DBConnection;

          try {
              DBConnection dbc = new DBConnection("jdbc/rollerdb");
              Connection con = dbc.openConnection();
              UpgradeDatabase.upgradeDatabase(con, mVersion);  
            
              dbc.closeConnection();
          } catch (NamingException e) {
              mLogger.warn("Unable to access DataSource", e);
          } catch (SQLException e) {
              mLogger.warn(e);
          }


           
   10.3) In '{temp}\roller\src\org\apache\roller\ui\rendering\servlets\CommentServlet.java' change the JNDI lookup.
   10.4) In '{temp}\roller\src\org\apache\roller\ui\rendering\servlets\PreviewServlet.java' change the line 'String mimeType = RollerContext.getServletContext().getMimeType(pageLink);' to below.

          // WebLogic throws an error here for some reason. Damon, 8/12/06.
          String mimeType = null;
              try{
              mimeType = RollerContext.getServletContext().getMimeType(pageLink);
          }
          catch( java.lang.NullPointerException npe ){
              //System.err.println("PreviewServlet: Encounter error when getting mime type.");
          }

   10.5) In '{temp}\roller\src\org\apache\roller\ui\authoring\struts\BookmarksAction.java' change line 142 to below.
          // Remove from parent collection. Damon, 14/12/2006.
           FolderData fd = pageModel.getFolder();
           Set bookmarkSet = fd.getBookmarks();

          for (int j = 0; j < bookmarks.length; j++)
          {
             bookmark = bmgr.getBookmark(bookmarks[j]);
             // Remove from parent collection. Damon, 14/12/2006.
             bookmarkSet.remove(bookmark);  
            
             bmgr.removeBookmark(bookmark);
          }


          Also line 375 for the method 'getFolder()' modify as below.

           /* Need to do this for WebLogic v8.1.5. If not bookmarks
              cannot be retrieved. Suspect cause of issue is due to
              WL using a child classloader for its JSP container.
              Damon, 13/12/06.
           */
           BookmarkManager bmgr = null;
           try{
               bmgr = RollerFactory.getRoller().getBookmarkManager();
               /* This forces lazy instantiation. For some reasons,
               if we don't force instantiation, the JSTL fails to get
               the object with Hibernate complaining that there is no
               session or session is closed. Suspect that WL JSP container
               actually loads its own copy of Hibernate during JSTL
               parsing, if anybody knows the real cause, i would appreciate
               if you can enlighten me on exactly what's
               happening. ;P
               Damon, 14/12/2006 Email: damonchong@aim.com
               */
               folder.getBookmarks().toString();
           }
           catch(Exception e){
               //e.printStackTrace();
               try{
                   /* For some unknown reasons, the folder instance
                   becomes stale especially when performing deletion of
                   bookmarks and bookmark folders. Thus, we need to refresh
                   it again. Damon, 14/12/2006.
                   */
                   folder = bmgr.getFolder(folder.getId());
               }
               catch(Exception ex){
                   ex.printStackTrace();
               }
           }
           return folder;


   10.6) In '{temp}\roller\src\org\apache\roller\ui\authoring\struts\InvitationsAction.java' modify from line 150 onwards, codes affecting JNDI lookup.   
   10.7) In '{temp}\roller\src\org\apache\roller\ui\authoring\struts\InviteMemberAction.java' modify from line 197 onwards, codes affecting JNDI lookup.

   
11) In '{temp}\roller\src\org\apache\roller\config\RollerRuntimeConfig.java', modify method 'isFrontPageWeblog' by adding after the line 'String frontPageHandle = getProperty("site.frontpage.weblog.handle");'.

          // Handle for Oracle DB, suspect null value for varchar2 is
          // not auto convert to empty string. Damon @ 8 Dec 2006
          if(null==frontPageHandle)
              frontPageHandle = "";


12) Update '{DefaultApp}\roller\WEB-INF\security.xml' change all instance of 'java:comp/env/jdbc/rollerdb' to 'jdbc/rollerdb'.
13) Do a compilation at '{temp}\roller' and copy 'roller-business.jar' and 'roller-web.jar' from '{temp}\roller\build\lib' to '{DefaultApp}\roller\WEB-INF\lib'.
14) Do a build of the 'DefaultApp' in Workshop and deploy.
15) Go to URL http://localhost:7001/roller to view Roller weblog. Enjoy!

1 comment:

Unknown said...

Hello.
I have some questions:
1. Roller use Lucene. If we deploy Roller to Cluster environment, where can i save lucene indexes ?
Saving it in the RAM is bad.
Saving it in DB - we have low performance. Share directory to all servers ?
Can you give me an advice?

P.S.: Sry for my bad english.