Overview
Why GWT? I am currently working on a client project that required a rich user interface experience. Developing the interactivity with manual javascript (prototype) coding was painfully slow and difficult to get working consistently across multiple browser versions.
After investigating the RIA options, GWT seemed like the best fit as it would work consistently across all browsers, required no virtual machine (like flash, silverlight or javafx) and had a friendly ofbiz compatible license.
My intention was to integrate GWT such that some forms requiring a richer UI could be developed with GWT whereas the standard ofbiz forms could still be used where required.
This document describes the steps I took to integrate GWT into ofbiz.
- Part 1 : create a basic GWT component
- Part 2 : make the GWT component talk to an ofbiz service
Caveat
1) Use these instructions at your own risk!
2) I still have a lot to learn about GWT so there maybe a better way of achieving what is described here.
3) The build scripts are not optimised.
4) I develop on a linux laptop. Some of the commands will be different if you are using Windows.
Pre-requisites
1) These instructions assume you are working on the latest ofbiz r924549 and only tested with the Tomahawk theme.
2) You are familiar with ofbiz development, if not read the tutorial at OFBiz Tutorial - A Beginners Development Guide
3) You are familiar with GWT, if not read the tutorials at http://code.google.com/webtoolkit/doc/latest/tutorial/index.html
Step 1 - setup GWT
Download GWT SDK (I used GWT 2.0.3).Unzip GWT into OFBIZ_HOME. My OFBIZ directory looks like this:
ofbiz +- applications +- bin +- debian +- framework +- gwt-2.0.3 +- doc +- samples +- hot-deploy +- runtime +- specialpurpose +- themes +- tools
Step 2 - create an ofbiz component
snowch@dl:~/workspace/ofbiz$ ./ant create-component Buildfile: build.xml Trying to override old definition of datatype javacc Trying to override old definition of datatype jjtree create-component: [input] Component name: (e.g. mycomponent) [Mandatory] gwtdemo [input] Component resource name: (e.g. MyComponent) [Mandatory] GwtDemo [input] Webapp name: (e.g. mycomponent) [Mandatory] gwtdemo [input] Base permission: (e.g. MYCOMPONENT) [Mandatory] GWTDEMO [echo] The following hot-deploy component will be created: [echo] Name: gwtdemo [echo] Resource Name: GwtDemo [echo] Webapp Name: gwtdemo [echo] Base permission: GWTDEMO [echo] Folder: /home/snowch/workspace/ofbiz/hot-deploy/gwtdemo [echo] [input] Confirm: (Y, [N], y, n) y
Step X - create GWT module descriptor
Create the gwt module descriptor xml file: src/org/example/gwtdemo.gwt.xml
<?xml version="1.0" encoding="UTF-8"?> <module rename-to='gwtdemo'> <inherits name='com.google.gwt.user.User'/> <inherits name='com.google.gwt.user.theme.standard.Standard'/> <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> --> <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/> --> <entry-point class='org.example.client.gwtdemo'/> <source path='client'/> <source path='shared'/> </module>
Take care to change the value of the values:
<module rename-to="gwtdemo">
and
<entry-point class="org.example.client.gwtdemo"/>
The module rename-to value should be the same as your component name.
We will create the entry-point class later in this tutorial.
Step x - Edit the component build.xml file
Add the following properties to your build.xml file below the <import file="../../common.xml"/> tag
<property name="gwt.args" value="-war webapp/gwtdemo/gwtpages" /> <property name="gwt.sdk" location="../../gwt-2.0.3" /> <property name="gwt.webapp.name" value="org.example.gwtdemo" />
gwt.args -war value should be webapp/<componentname>/gwtpages
gwt.webapp.name points to your GWT module descriptor file in package naming convention, with the .gwt.xml file extension removed (created above)
And the following just before the </project> tag:
<!-- ================================================================= --> <!-- GWT Targets --> <!-- ================================================================= --> <target name="libs" description="Copy libs to ${build}/lib"> <mkdir dir="${build.dir}/lib" /> <copy todir="${build.dir}/lib" file="${gwt.sdk}/gwt-servlet.jar" /> <!-- Add any additional server libs that need to be copied --> </target> <target name="javac" depends="libs" description="Compile java source"> <mkdir dir="${build.dir}/classes"/> <javac srcdir="src" includes="**" encoding="utf-8" destdir="${build.dir}/classes" source="1.5" target="1.5" nowarn="true" debug="true" debuglevel="lines,vars,source"> <classpath refid="local.class.path"/> </javac> <copy todir="${build.dir}/classes"> <fileset dir="src" excludes="**/*.java"/> </copy> </target> <target name="gwtc" depends="javac" description="GWT compile to JavaScript"> <java failonerror="true" fork="true" classname="com.google.gwt.dev.Compiler"> <classpath> <pathelement location="src"/> <path refid="local.class.path"/> </classpath> <!-- add jvmarg -Xss16M or similar if you see a StackOverflowError --> <jvmarg value="-Xmx256M"/> <!-- Additional arguments like -style PRETTY or -logLevel DEBUG --> <arg line="${gwt.args}"/> <arg value="${gwt.webapp.name}"/> </java> </target> <target name="jar" depends="javac, gwtc"> <jar jarfile="${build.dir}/lib/${name}.jar"> <fileset dir="${build.dir}/classes"/> </jar> </target> <target name="build" depends="gwtc, jar" description="Build this project" />
Step x - create the GWT entry point class
Create the following file: src/org/example/client/gwtdemo.java
package org.example.client; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.DialogBox; import com.google.gwt.user.client.ui.RootPanel; public class gwtdemo implements EntryPoint { public void onModuleLoad() { final Button helloButton = new Button("Say Hello"); helloButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent arg0) { DialogBox dialog = new DialogBox(); dialog.setAutoHideEnabled(true); dialog.setText("Hello World"); dialog.center(); dialog.show(); } }); RootPanel.get("helloButtonContainer").add(helloButton); } }
Step x - test that the ant build works
change to your component's top level folder and run ant
snowch@dl:~/workspace/ofbiz$ cd hot-deploy/gwtdemo/ snowch@dl:~/workspace/ofbiz/hot-deploy/gwtdemo$ ../../ant Buildfile: build.xml Trying to override old definition of datatype javacc Trying to override old definition of datatype jjtree libs: [mkdir] Created dir: /home/snowch/workspace/ofbiz/hot-deploy/gwtdemo/build/lib [copy] Copying 1 file to /home/snowch/workspace/ofbiz/hot-deploy/gwtdemo/build/lib javac: [mkdir] Created dir: /home/snowch/workspace/ofbiz/hot-deploy/gwtdemo/build/classes [javac] Compiling 1 source file to /home/snowch/workspace/ofbiz/hot-deploy/gwtdemo/build/classes [copy] Copying 1 file to /home/snowch/workspace/ofbiz/hot-deploy/gwtdemo/build/classes gwtc: [java] Compiling module org.example.gwtdemo [java] Compiling 6 permutations [java] Compiling permutation 0... [java] Compiling permutation 1... [java] Compiling permutation 2... [java] Compiling permutation 3... [java] Compiling permutation 4... [java] Compiling permutation 5... [java] Compile of permutations succeeded [java] Linking into /home/snowch/workspace/ofbiz/hot-deploy/gwtdemo/webapp/gwtdemo/gwtpages/gwtdemo. [java] Link succeeded [java] Compilation succeeded -- 33.599s jar: [jar] Building jar: /home/snowch/workspace/ofbiz/hot-deploy/gwtdemo/build/lib/ofbiz-gwtdemo.jar BUILD SUCCESSFUL
If all went well, you should see "BUILD SUCCESSFUL". Now check your webapp/gwtdemo/gwtdemo folder. It should contain a lot of files that were generated as part of the GWT compilation process.
The build process compiles the java gwtdemo.java class into javascript, html and images files and puts them in the webapp/gwtdemo/gwtpages/gwtdemo folder.
Grant web access to the gwtpages folder
Add the ":/gwtdemo.css:/gwtpages" to allowedPaths in web.xml:
<init-param> <param-name>allowedPaths</param-name> <param-value>/control:/select:/index.html:/index.jsp:/default.html:/default.jsp:/images:/includes/maincss.css:/gwtdemo.css:/gwtpages</param-value> </init-param>
Step x - add a page to the main screen
<?xml version="1.0" encoding="UTF-8"?> <screens xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/widget-screen.xsd"> <screen name="main"> <section> <actions> <set field="headerItem" value="main"/><!-- this highlights the selected menu-item with name = "main" --> <!-- ========================================================= --> <!-- load the gwt generated javascript and a custom stylesheet --> <!-- ========================================================= --> <set field="layoutSettings.javaScripts[+0]" value="/gwtdemo/gwtpages/gwtdemo/gwtdemo.nocache.js" global="true"/> <set field="layoutSettings.styleSheets[+0]" value="/gwtdemo/gwtdemo.css" global="true"/> </actions> <widgets> <decorator-screen name="GwtDemoCommonDecorator" location="${parameters.mainDecoratorLocation}"> <decorator-section name="body"> <!-- ===================================== --> <!-- Add a page to load the gwt javascript --> <!-- ===================================== --> <platform-specific> <html><html-template location="component://gwtdemo/webapp/gwtdemo/gwtdemo.ftl"/></html> </platform-specific> </decorator-section> </decorator-screen> </widgets> </section> </screen> </screens>
Step x - create the webapp/gwtdemo/gwtdemo.ftl page
Create the gwtdemo.ftl page that is used to load the gwt pages.
<!doctype html> <!-- OPTIONAL: include this if you want history support --> <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe> <!-- RECOMMENDED if your web app will not function without JavaScript enabled --> <noscript> <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif"> Your web browser must have JavaScript enabled in order for this application to display correctly. </div> </noscript> <h1>Web Application Starter Project</h1> <table align="center"> <tr> <td id="helloButtonContainer"></td> </tr> </table>
Step x - create the webapp/gwtdemo/gwtdemo.css page
/** Most GWT widgets already have a style name defined */ .gwt-DialogBox { width: 400px; } .dialogVPanel { margin: 5px; } .serverResponseLabelError { color: red; } /** Set ids using widget.getElement().setId("idOfElement") */ #closeButton { margin: 15px 6px 6px; }
Step x - Grant admin user access to your page
Either grant admin user GWTDEMO_VIEW permission, or comment out widget/CommonScreens.xml permission condition:
<screen name="GwtDemoCommonDecorator"> <section> <actions> </actions> <widgets> <decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}"> <decorator-section name="body"> <section> <!-- <condition> <if-has-permission permission="GWTDEMO" action="_VIEW"/> </condition> --> <widgets> <decorator-section-include name="body"/> </widgets> <fail-widgets> <label style="h3">${uiLabelMap.GwtDemoViewPermissionError}</label> </fail-widgets> </section> </decorator-section> </decorator-screen> </widgets> </section> </screen>
TODO
Single browser compilation:
<set-property name="user.agent" value="gecko"/>
GWT RPC with ofbiz with Authentication:
package cs.server; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javolution.util.FastMap; import org.ofbiz.base.util.Debug; import org.ofbiz.base.util.UtilMisc; import org.ofbiz.entity.DelegatorFactory; import org.ofbiz.entity.GenericDelegator; import org.ofbiz.entity.GenericValue; import org.ofbiz.service.GenericDispatcher; import org.ofbiz.service.GenericServiceException; import org.ofbiz.service.LocalDispatcher; import org.ofbiz.service.ServiceUtil; import com.google.gwt.user.server.rpc.RemoteServiceServlet; import cs.client.GreetingService; import cs.shared.AuthenticatorException; /** * The server side implementation of the RPC service. */ @SuppressWarnings("serial") public class GreetingServiceImpl extends RemoteServiceServlet implements GreetingService { public String greetServer(String input) throws AuthenticatorException { GenericDelegator delegator = (GenericDelegator) DelegatorFactory.getDelegator("default"); LocalDispatcher dispatcher = GenericDispatcher.getLocalDispatcher("default",delegator); HttpServletRequest request = this.getThreadLocalRequest(); HttpSession session = request.getSession(); if (session == null) { throw new AuthenticatorException("Session not found"); } GenericValue userLogin = (GenericValue) session.getAttribute("userLogin"); if (userLogin == null) { throw new AuthenticatorException("User login not found"); } Map<String, String> paramMap = UtilMisc.toMap( "userLogin", userLogin, "message", input ); Map<String, Object> result = FastMap.newInstance(); try { result = dispatcher.runSync("ping", paramMap); } catch (GenericServiceException e1) { Debug.logError(e1, GreetingServiceImpl.class.getName()); return e1.toString(); } if (ServiceUtil.isSuccess(result)) { return "RESPONSE: *** " + result.get("message") + " ***"; } if (ServiceUtil.isError(result) || ServiceUtil.isFailure(result)) { return ServiceUtil.getErrorMessage(result); } // shouldn't ever get here ... should we? throw new RuntimeException("Invalid "); } }