Monday, June 19, 2006

How to pre-compile JSP pages for Weblogic8 with Ant

It can be done in similar way as for Tomcat Application Server (see this link). The only difference is: we have another JSP compiler which takes different input parameters and expects JSP pages in well specified locations. It does not "like" JSP pages within WEB-INF folder, so you need to follow this rule. Otherwise you have to copy files with "wrong" locations into "right" locations.

1. Specify "project.classpath" for your project. It will include all jars required to compile or run your project:


<path id="project.classpath">
  ...
</path>


2. Specify "weblogic.jsp.classpath". You need to have Weblogic Web Server installed ar ${weblogic.home}. You also need to specify, where you have your implementation of logging system ("${repository.home}/log4j").


  <path id="weblogic.jsp.classpath">

    <!-- 1. You have to include jars from your project. -->

    <path refid="project.classpath"/>

    <!-- 2. Weblogic jsp compiler and dependent classes (including JavaEE/Servlet/JSP interface classes). -->

    <fileset dir="${weblogic.home}/server/lib">
      <include name="weblogic.jar"/>
    </fileset>

    <!-- 3. This library is required by Weblogic jsp compiler (not in weblogic installation!). -->

    <fileset dir="${repository.home}/saxpath">
      <include name="saxpath-1.0-FCS.jar"/>
    </fileset>

    <!-- 4. Weblogic jsp compiler internally uses Java compiler. -->

    <fileset dir="${java.home}/lib">
      <include name="tools.jar"/>
    </fileset>

    <fileset dir="${java.home}/../lib">
      <include name="tools.jar"/>
    </fileset>

    <!-- 5. Implementation of logging system (if it is not in "project.classpath" yet). -->

     <fileset dir="${global.repository.home}/log4j">
       <include name="log4j-1.2.8.jar"/>
     </fileset>
  </path>


3. Now we can generate Java sources for JSP files.


  <property name="jsp.src.dir" value="<the root for your JSP files>"/>
  <property name="jsp.package.name" value="<the package name for your JSPs, like com.mycompany.jsp>"/>

  <property name="build.dir" value="target/build"/>
  <property name="jsp.generated.src.dir" value="${build.dir}/jsp_sources"/>
  <property name="jsp.classes.dir" value="${build.dir}/jsp_classes"/>

  <target name="tomcat.jsp.generate">
    <mkdir dir="${jsp.generated.src.dir}"/>
    <mkdir dir="${jsp.classes.dir}"/>

    <java classname="org.apache.jasper.JspC" fork="yes">
      <classpath refid="tomcat.jsp.classpath" />

      <arg line="-uriroot ${jsp.src.dir} -d ${jsp.generated.src.dir} -p ${jsp.package.name} -webapp ${jsp.src.dir}" />
    </java>
  </target>

  <target name ="weblogic.jsp.generate">
    <mkdir dir="${jsp.generated.src.dir}"/>
    <mkdir dir="${jsp.classes.dir}"/>

    <java classname="weblogic.jspc" fork="yes">
      <classpath refid="weblogic.jsp.classpath" />

      <sysproperty key="weblogic.jsp.windows.caseSensitive" value="false"/>

      <arg line="-forceGeneration -keepgenerated -compileAll -webapp ${jsp.src.dir} -d ${jsp.generated.src.dir}"/>
    </java>
  </target>


It is very important to set "weblogic.jsp.windows.caseSensitive" to "false". At the same step Weblogic will compile Java sources into Java classes (-forceGeneration and -keepgenerated). Compiled JSP java classes will be located in same folders as JSP compiled classes. We will separate them on next step.

You don't have to specify package name for JSP pages. JSP compiler will assign default "jsp_servlet" package name for all JSP pages. Each folder will be converted with the "_" prefix plus original name, each jsp page into "__" prefix plus original name.

4. Now, it's better to separate generated Java sources from compiled files.


  <target name ="weblogic.jsp.compile" depends="weblogic.jsp.generate"
          description="Separates sources from classes">
    <move todir="${jsp.classes.dir}/jsp_servlet">
      <fileset dir="${jsp.generated.src.dir}/jsp_servlet">
        <include name="**/*.class"/>
      </fileset>
    </move>
  </target>


If, on this step you don't have compiler errors, your source code (JSP part) is not broken.

When you assemble war file, you should include everything from ${jsp.src.dir} folder except JSP files and their includes.

Friday, June 02, 2006

How to handle JSP Error Page

At first glance it looks like a very straightforward task.


1. Each JSP page should use "errorPage" attribute. For example:

<%-- /example.jsp --%>

<%@ page errorPage="/WEB-INF/templates/errorPage.jsp"%>

<%-- some JSP content --%>


If you don't want to repeat this for each JSP page, use Tiles template or specify error page in "web.xml" file:

<web-app>
  ...
 
  <error-page>
    <exception-type>java.lang.Throwable</exception-type>
    <location>/WEB-INF/templates/errorPage.jsp</location>
  </error-page>

  ...
 
</web-app>



This description registers "errorPage.jsp" JSP page as error page for all JSP pages within web application.


2. You need to create error page, which displays runtime exceptions:

<%-- /WEB-INF/templates/errorPage.jsp --%>

<%@ page isErrorPage="true" %>

<%
  out.println("Exception: " + exception);
%>



By using "isErrorPage" attribute you declare this JSP page as error page. As a result, you have access to "exception" JSP variable (in addition to regular "application", "session",
"request", etc.).


This is basic. The problem happened, when your JSP page generates big output (bigger than page buffer) before the exception. In this case your error page will be appended to the current content instead of displaying in the new page.

To overcome the problem you can:


3.A. Play with "buffer" attribute. Say, you know, that the size of your page will never be more than 1MB. In this case add these attributes to the page:


<%-- /example2.jsp --%>

<%@ page errorPage="/WEB-INF/templates/errorPage.jsp"%>

<%@ page buffer="1024kb"%>
<%@ page autoFlush="false"%>

<%-- some JSP content --%>


This approach is not perfect, because you cannot guarantee the maximum page size. Another drawback is that page will be refreshed only after completing the operation. This could hurt user's perception.


3.B.1. Use custom javascript code to clear flushed already output. By using "document.getElementById()" function we canget reference to the body and rewrite it:


<%-- /WEB-INF/templates/errorPage.jsp, ver.1 --%>

<%@ page isErrorPage="true" %>

<html>
  <head>
    <script type="text/javascript">
      function newPage() {
        var exception = '<%= exception %>';

        var body = document.getElementById("body");

        body.innerHTML =
            "<html>" +
            " <body>" +
            " Exception: " + exception +
            " </body>" +
            "</html>";
        }
      }
    </script>
  </head>

  <body/>

</html>


This solution fits for simple error pages only. If you want to display full-featured error page, you have to look for another solution (see 3.B.2).



3.B.2. Redirect to another page.


This scenario is based on 3.B.1 solution. Now, instead of preparing the content to display, we'll submit the form, redirecting the flow to the requested page. In order to get the reference to javascript's "document" obect and clear it, we'll use document.open()" function:


<%-- /WEB-INF/templates/errorPage.jsp, ver.2 --%>

<%@ page isErrorPage="true" %>

<%
  // We want to have exception available on the redirected page.
  session.setAttribute("javax.servlet.error.exception",
                        request.getAttribute("javax.servlet.error.exception"));
%>

<html>
  <head>
    <script type="text/javascript">
      function newPage(action) {
        var newDoc = document.open("text/html", true);

        if(newDoc) {
          var errorFormName = 'errorForm';
          var txt =
            "<html>" +
            " <body>" +
            " <form id='" + errorFormName + "' method='get'/>" +
            " </body>" +
            "</html>";

          newDoc.write(txt);
          newDoc.close();

          var form = newDoc.getElementById(errorFormName);
          form.action = action;
          form.submit();

          return true;
        }

        return false;
      }

      newPage("someURL"); // direct execution of javascript custom function
    </script>
  </head>

  <body/>

</html>


This solution does not work properly yet. The problem is, that at the time of javascript execution the html page is not completely generated yet. As the result, "document.open()" call will return "null" object and the conten for the redirection will not be generated. See 3.B.3 for the solution.


3.B.3. Using timer object.


This scenario is based on 3.B.2 solution. It is exactly the same, the only difference is how we call "newPage()" function. Instead of direct call we'll do it indirectly from the timer:

<%-- /WEB-INF/templates/errorPage.jsp, ver.3 --%>

<%@ page isErrorPage="true" %>

<%
  // We want to have exception available on the redirected page.
  session.setAttribute("javax.servlet.error.exception",
                        request.getAttribute("javax.servlet.error.exception"));
%>

<html>
  <head>
    <script type="text/javascript">
      function newPage(action) { ... }

      var timer = new Timer("timer");
      timer.setScript("newPage('someURL')");

      timer.start();
    </script>
  </head>

  <body/>

</html>


Timer is the special custom javascript class that performs call to a given script periodically until the script returns "true" value. After this, the timer will stop execution.

You can download inplementation for the timer from here:

http://home.comcast.net/~shvets/blog/timer.js

Now JSP error page work properly.