How to install Orbeon OPS as a WebApp in Tomcat and have your own WebApp forward requests to it

Leigh Klotz <Leigh.Klotz@Xerox.com>

What is this document about?

This document explains how to modify an existing installation of Orbeon OPS as a WebApp in Tomcat so that another WebApp can forward XHTML+XForms documents to Orbeon OPS, without changing the URL of the original WebApp.

Status of this document

This document is intended to help estimate the level of effort required to put these changes into Orbeon OPS.

These changes:

Why would you want to do this?

OPS has over 40 JAR files in it, including old and variant versions of commonly used packages. Putting OPS in its own WebApp gives Orbeon OPS its own Java ClassLoader and isolates other WebApps using it from Orbeon's dependencies, which might otherwise have conflicts.

Acknowledgements

I got the following from Adrian Baker:

The rest I figured out myself, by consulting the Tomcat documentation and the Orbeon OPS Source.

Does it work?

Almost. There are a couple of unsolved problems:

Throughout the instructions below

How would you do this?

  1. Fetch ops-cvs-20070126-0000.tar.gz and unzip it to c:/path/to/Orbeon/ops-cvs-2007-01-2.
  2. Apply the following changes.
  3. Rebuild.

Or, instead of rebuild, do this:

  1. These changes are in files that are eventually wrapped up into two JAR files.
  2. For the two changed Java files, you can make the changes to the source code and just recompile those files as follows:
    $ javac -g -cp c:/path/to/tomcat/webapps/ops/WEB-INF/lib/ops.jar c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/src/java/org/orbeon/oxf/xforms/processor/handlers/XHTMLHeadHandler.java 
    $ javac -g -cp c:/path/to/tomcat/webapps/ops/WEB-INF/lib/ops.jar\;c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/lib/log4j-1.3alpha0.jar\;c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/lib/servlet-2_3-4_0_4.jar\;c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/lib/commons-fileupload-1.0.jar c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/src/java/org/orbeon/oxf/servlet/ServletExternalContext.java 
    $ mkdir -p c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/classes/org/orbeon/oxf/servlet/
    $ cp c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/src/java/org/orbeon/oxf/servlet/ServletExternalContext*.class c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/classes/org/orbeon/oxf/servlet/
    $ mkdir -p c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/classes/org/orbeon/oxf/xforms/processor/handlers
    $ cp c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/src/java/org/orbeon/oxf/xforms/processor/handlers/XHTMLHeadHandler*.class c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/classes/org/orbeon/oxf/xforms/processor/handlers
    $ cd c:/path/to/Orbeon/ops-cvs-2007-01-26/orbeon/classes
    $ jar uvf c:/path/to/tomcat/webapps/ops/WEB-INF/lib/ops.jar .
    
  3. For the two changed JavaScript files xforms.js and xforms-min.js, apply the changes and copy them to a new directory structure and incorporate them into ops-resources-public.jar as follows:
    $ cd c:/path/to/patched/resources/
    $ jar uvf c:/path/to/tomcat/WEB-INF/lib/ops-resources-public.jar ops/javascript/xforms-min.js
    $ jar uvf c:/path/to/tomcat/WEB-INF/lib/ops-resources-public.jar ops/javascript/xforms.js
    

The changes

In xforms.js add a WEBAPP_PREFIX var which lets this file know what indirect URL it's loaded under. This example is hard-coded, but a real solution would work with multiple locations. The same change must be made in xforms-min.js but I assume that's autogenerated. If it's not, you'll have to make it by hand.
*** orig/ops-cvs-2007-01-26/orbeon/src/examples/web/ops/javascript/xforms.js	Thu Nov  9 12:15:46 2006
--- ops-cvs-2007-01-26/orbeon/src/examples/web/ops/javascript/xforms.js	Wed Jan 31 14:32:07 2007
! var PATH_TO_JAVASCRIPT = "/ops/javascript/xforms.js";
------

! var WEBAPP_PREFIX="/MYWEBAPP/MYSERVLET/ops";
! var PATH_TO_JAVASCRIPT = WEBAPP_PREFIX + "/ops/javascript/xforms.js";

In ServletExternalContext.java, put in the same string.
--- ops-cvs-2007-01-26/orbeon/src/java/org/orbeon/oxf/servlet/ServletExternalContext.java	Tue Jan 30 16:50:00 2007
*** orig/ops-cvs-2007-01-26/orbeon/src/java/org/orbeon/oxf/servlet/ServletExternalContext.java	Mon Dec 11 17:27:16 2006
          public String getContextPath() {
!             return nativeRequest.getContextPath();
          }
------  
          public String getContextPath() {
!             return "/MYWEBAPP/MYSERVLET" + nativeRequest.getContextPath();
          }
  

This adds a servlet for forwarding the request to, courtesy Adrian Baker. It goes in ops/WEB-INF/web.xml
*** web.xml~	Tue Jan 16 11:29:54 2007
--- web.xml	Thu Feb  1 10:01:59 2007
          <load-on-startup>3</load-on-startup>
      </servlet>

+ <!-- From Adrian Baker [adrian.baker@orionhealth.com] -->
+     <servlet>
+         <!--  Handles initial display of a form. -->
+         <servlet-name>ops-render-servlet</servlet-name>
+         <servlet-class>org.orbeon.oxf.servlet.OPSServlet</servlet-class>
+         <!-- Set main processor -->
+         <init-param>
+             <param-name>oxf.main-processor.name</param-name>
+             <param-value>{http://www.orbeon.com/oxf/processors}pipeline</param-value>
+         </init-param>
+         <init-param>
+             <param-name>oxf.main-processor.input.config</param-name>
+             <param-value>oxf:/render-form-from-request.xpl</param-value>
+         </init-param>
+     </servlet>
+ <!-- End of code from Adrian Baker [adrian.baker@orionhealth.com] -->

      <!-- Uncomment this for the SQL examples -->
      <!--
***************
      </servlet>-->
      <!-- End SQL examples -->

+     <servlet-mapping>
+         <servlet-name>ops-render-servlet</servlet-name>
+         <url-pattern>/ops-render-servlet</url-pattern>
+     </servlet-mapping>

      <servlet-mapping>
          <servlet-name>ops-main-servlet</servlet-name>


This file is the XPL from Adrian, referred to from the web.xml. It goes (as referred to above) in ops/WEB-INF/resources. I had to add non-empty instance and xforms-model input; even though it said they weren't used, if I left them out or defined them as empty it gave an error.
<?xml version="1.0"?>
<!-- From Adrian Baker [adrian.baker@orionhealth.com]
The OPSServlet forwarded to is configured to run a simple pipeline which uses the scope generator to extract the request attribute and pass it to the standard epilogue pipeline 
  request.setAttribute("xform", <XHTML+XForms DOM>);
  getServletConfig().getServletContext().getContext("/ops").getRequestDispatcher("/ops-render-servlet").forward(request, response)
-->

<p:config xmlns:p="http://www.orbeon.com/oxf/pipeline" xmlns:oxf="http://www.orbeon.com/oxf/processors">
    <p:processor name="oxf:scope-generator">
        <p:input name="config">
            <config>
                <key>xform</key>
                <scope>request</scope>
            </config>
        </p:input>
        <p:output name="data" id="extracted-xform"/>
    </p:processor>
    <p:processor name="oxf:pipeline">
        <p:input name="config" href="config/epilogue.xpl"/>
        <p:input name="data" href="#extracted-xform"/>
        <p:input name="instance"><x/></p:input>
        <p:input name="xforms-model"><x/></p:input>
    </p:processor>
</p:config>

Here's what you put the service() method of the servlet that you need to Orbeon-enable:
        if (isOpsRequest(request)) {
            String ops ="/ops";
            String srv=request.getPathInfo().substring(ops.length());
            ServletContext opsContext = getServletConfig().getServletContext().getContext(ops);
            if (opsContext == null) throw new RuntimeException("can't find Orbeon context; tomcat /MYWEBAPP webapp is missing ");
            RequestDispatcher dispatcher = opsContext.getRequestDispatcher(srv);
            if (dispatcher == null) throw new RuntimeException("can't find Orbeon request dispatcher");
            dispatcher.forward(request, response); // forward this back-channel directly.
            return;
        }
where
    private boolean isOpsRequest(HttpServletRequest request) {
        String path = request.getPathInfo();
        return (path != null && path.startsWith("/ops/"));
    }

Here's what you put in at the point of generation of an XHTML+XForms response that you want to send to Orbeon for it to send on to the HTML4 web user agent. It appears that domDocument can also be a DOM Node or a String.
                    String ops ="/ops";
                    String srv="/ops-render-servlet";
                    ServletContext opsContext = getServletConfig().getServletContext().getContext(ops);
                    if (opsContext == null) throw new RuntimeException("can't find Orbeon context; tomcat /MYWEBAPP webapp is missing <Context crossContext='true' />");
                    RequestDispatcher dispatcher = opsContext.getRequestDispatcher(srv);
                    if (dispatcher == null) throw new RuntimeException("can't find Orbeon request dispatcher");
                    
                    // From Adrian Baker [adrian.baker@orionhealth.com]
                    request.setAttribute("xform", domDocument);
                    dispatcher.forward(request, response);

If you deploy your own webapp without an explicit <server> and <host> and instead let tomcat figure it out, you have no place to hang the <Context crossContext=true>, so you have to edit your tomcat installations' toplevel server.xml file. Add this to tomcat/conf/server.xml. If you don't do this, your webapp will get null for RequestDispatcher.
       <!-- Define the default virtual host -->
       <Host name="localhost" debug="0" appBase="webapps" unpackWARs="true" autoDeploy="true">
+        <Context path="/MYWEBAPP" docBase="C:\path\to\tomcat\webapps\MYWEBAPP" crossContext="true"/>

Questionable or Untested Changes


For some reason I always get the portlet theme instead of the servlet theme. Just turn it off here with a constant true().
*** orig/ops-cvs-2007-01-26/orbeon/src/resources/config/epilogue-servlet.xpl	Fri Jan 12 07:12:10 2007
--- ops-cvs-2007-01-26/orbeon/src/resources/config/epilogue-servlet.xpl	Thu Feb  1 09:31:49 2007
              <!-- Apply theme -->
              <p:choose href="#request">
!                 <p:when test="starts-with(/request/request-path, '/doc/') or /request/parameters/parameter[name = 'orbeon-theme']/value = 'plain'">
                      <p:processor name="oxf:unsafe-xslt">
                          <p:input name="data" href="#xformed-data"/>
------
              <!-- Apply theme -->
              <p:choose href="#request">
!                 <p:when test="true() or starts-with(/request/request-path, '/doc/') or /request/parameters/parameter[name = 'orbeon-theme']/value = 'plain'">
                      <p:processor name="oxf:unsafe-xslt">
                          <p:input name="data" href="#xformed-data"/>

UNTESTED: I'm not sure this is necessary but I put the constant prefix into src and href attributes in theme-plain.xsl. I suspect URL-rewriting interacts with this.

*** orig/ops-cvs-2007-01-26/orbeon/src/resources/config/theme-plain.xsl	Fri Jan 12 07:12:10 2007
--- ops-cvs-2007-01-26/orbeon/src/resources/config/theme-plain.xsl	Thu Feb  1 09:32:19 2007
              ('xforms-help-image',
              (for $c in tokenize(@class, ' ') return if ($c = 'xforms-help') then () else $c))"/>
!         <xhtml:img alt="Help" title="" src="/ops/images/xforms/help.gif" class="{string-join($image-class, ' ')}"/>
          <xsl:copy>
              <xsl:apply-templates select="@*|node()"/>
------
              ('xforms-help-image',
              (for $c in tokenize(@class, ' ') return if ($c = 'xforms-help') then () else $c))"/>
!         <xhtml:img alt="Help" title="" src="/MYWEBAPP/ops/images/xforms/help.gif" class="{string-join($image-class, ' ')}"/>
          <xsl:copy>
              <xsl:apply-templates select="@*|node()"/>

UNTESTED: Same as above for xforms-to-xhtml.xsl.

*** orig/ops-cvs-2007-01-26/orbeon/src/resources/config/xforms-to-xhtml.xsl	Sun Nov 19 14:24:40 2006
--- ops-cvs-2007-01-26/orbeon/src/resources/config/xforms-to-xhtml.xsl	Thu Feb  1 09:32:51 2007
!         <xhtml:input type="image" class="calendar-button" src="/ops/images/xforms/calendar.gif" value="Date"
              onclick="showCalendar('', '{$id}'); return false;"/>
          <xsl:if test="not(preceding::xforms:input[@xxforms:type = '{http://www.w3.org/2001/XMLSchema}date'])">
------
!         <xhtml:input type="image" class="calendar-button" src="/MYWEBAPP/ops/images/xforms/calendar.gif" value="Date"
              onclick="showCalendar('', '{$id}'); return false;"/>
***************
!                     <xhtml:img src="/ops/images/xforms/help.gif" onclick="var w = window.open('', '_blank',
                          'height=150,width=250,status=no,toolbar=no,menubar=no,location=no,dependent=yes');
------
!                     <xhtml:img src="/MYWEBAPP/ops/images/xforms/help.gif" onclick="var w = window.open('', '_blank',
                          'height=150,width=250,status=no,toolbar=no,menubar=no,location=no,dependent=yes');