Hello Ofbiz

01 February 2017

Just recently, I was tasked to support Apache Ofbiz (Open for Business) something not very exciting. The application is built on release 4 making Google search less efficient. The current release 16 is not that popular either let alone legacy versions. To have a better understanding of the framework, I'm going to follow this OFBiz Tutorial.

Just like any JVM application, you will need a JDK to run Ofbiz. It's build tool is ant being Apache. Newer releases has maven, ivy and gradle as build tool.

Downloading ofbiz

$ svn co http://svn.apache.org/repos/asf/ofbiz/branches/release15.12 ofbiz.15.12
...
U   ofbiz.15.12
Checked out revision 1781334.

Ofbiz first run

$ ./ant load-demo start
|OFBiz-batch-3        |DatabaseUtil                  |I| Searching in 868 tables for primary key fields ...
|OFBiz-batch-3        |DatabaseUtil                  |I| Reviewed 1698 primary key fields from database.

You will hear your CPU fan noise while Ofbiz is pulling its dependencies. Once successful, you should be able to browse https://localhost:8443/webtools.

Ofbiz is MVC

The whole Ofbiz are organised as component. You can treat component as folder. Your custom component goes to hot-deploy folder.

Creating my first component

$ ./ant create-component

create-component:
   [input] Component name: (e.g. mycomponent) [Mandatory]
pokedex
   [input] Component resource name: (e.g. MyComponent) [Mandatory]
Pokedex
   [input] Webapp name: (e.g. mycomponent) [Mandatory]
pokedex
   [input] Base permission: (e.g. MYCOMPONENT) [Mandatory]
POKEDEX
    [echo] The following hot-deploy component will be created:
    [echo]               Name: pokedex
    [echo]               Resource Name: pokedex
    [echo]               Webapp Name: pokedex
    [echo]               Base permission: POKEDEX
...
    [echo]             Restart OFBiz and then visit the URL: https://localhost:8443/pokedex
    [echo]         

BUILD SUCCESSFUL
Total time: 1 minute 10 seconds

That single line of ant command generated the following boiler-plate codes.

$ tree hot-deploy/pokedex
pokedex/
├── build
│   ├── classes
│   └── lib
│       └── pokedex.jar
├── build.xml
├── config
│   └── PokedexUiLabels.xml
├── data
│   ├── helpdata
│   │   └── HELP_Pokedex.xml
│   ├── PokedexDemoData.xml
│   ├── PokedexSecurityGroupDemoData.xml
│   ├── PokedexSecurityPermissionSeedData.xml
│   └── PokedexTypeData.xml
├── documents
│   └── Pokedex.xml
├── dtd
├── entitydef
│   └── entitymodel.xml
├── lib
├── ofbiz-component.xml
├── patches
│   ├── production
│   ├── qa
│   └── test
├── script
├── servicedef
│   └── services.xml
├── src
├── testdef
│   └── PokedexTests.xml
├── webapp
│   └── pokedex
│       ├── error
│       │   └── error.jsp
│       ├── index.jsp
│       └── WEB-INF
│           ├── actions
│           ├── controller.xml
│           └── web.xml
└── widget
    ├── CommonScreens.xml
    ├── PokedexForms.xml
    ├── PokedexMenus.xml
    └── PokedexScreens.xml

24 directories, 21 files

Model

Paste this XML to pokedex/entitydef/entitymodel.xml. Seems overrated to me since entity and model are synonyms on MVC's tiered architecture.

<?xml version="1.0" encoding="UTF-8"?>
<entitymodel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/entitymodel.xsd">
    <!-- ========================================================= -->
    <!-- ======================== Defaults ======================= -->
    <!-- ========================================================= -->
    <title>Entity of Pokedex Component</title>
    <description>Pokedex Emulation</description>
    <copyright>Niantic Labs</copyright>
    <version>1.0</version>

    <entity entity-name="PokemonType" package-name="com.prototype.pocket.monster" title="Pokemon Type Entity">
        <field name="pokemonTypeId" type="id-ne">
            <description>primary sequenced ID</description>
        </field>
        <field name="type" type="name"></field>
        <prim-key field="pokemonTypeId"/>
    </entity>

    <entity entity-name="Pokedex" package-name="com.prototype.pocket.monster" title="Pokedex Entity">
        <field name="pokedexId" type="id-ne">
            <description>primary sequenced ID</description>
        </field>
        <field name="pokemonTypeId" type="id-ne"></field>
        <field name="monster" type="name"></field>
        <field name="seen" type="numeric"></field>
        <field name="caught" type="numeric"></field>
        <field name="weight" type="fixed-point"/>
        <field name="height" type="fixed-point"/>
        <field name="description" type="description"/>
        <prim-key field="pokedexId"/>
        <relation type="one" fk-name="POKEMON_POKEMON_TYPEID" rel-entity-name="PokemonType">
            <key-map field-name="pokemonTypeId"/>
        </relation>
    </entity>

</entitymodel>

Test data

Paste this XML to pokedex/data/PokedexDemoData.xml

<?xml version="1.0" encoding="UTF-8"?>
<entity-engine-xml>
    <Pokedex pokedexId="1" pokemonTypeId="1" monster="BULBASAUR" seen="20" caught="18" weight="20.5" height="1.35" description="Bulbasaur can be seen napping in bright sunlight. There is a seed on its back. By soaking up the sun's ray, the seed grows progressively larger"/>
    <Pokedex pokedexId="2" pokemonTypeId="1" monster="IVYSAUR" seen="2" caught="2" weight="30.5" height="1.58" description="An evolved Bulbasaur"/>
    <Pokedex pokedexId="3" pokemonTypeId="1" monster="VENUSAUR" seen="1" caught="1" weight="32.5" height="2.03" description="An evolved Ivysaur"/>
</entity-engine-xml>

and to pokedex/data/PokedexTypeData.xml.

<?xml version="1.0" encoding="UTF-8"?>
<entity-engine-xml>
    <PokemonType pokemonTypeId="1" type="GRASS"/>
    <PokemonType pokemonTypeId="2" type="ELECTRIC"/>
    <PokemonType pokemonTypeId="3" type="WATER"/>
    <PokemonType pokemonTypeId="4" type="FIRE"/>
    <PokemonType pokemonTypeId="5" type="PSYCHIC"/>
    <PokemonType pokemonTypeId="6" type="FAIRY"/>
    <PokemonType pokemonTypeId="7" type="STEEL"/>
    <PokemonType pokemonTypeId="8" type="GROUND"/>
    <PokemonType pokemonTypeId="9" type="NORMAL"/>
</entity-engine-xml>

Views

Paste this XML to pokedex/widget/PokedexForms.xml. I'm not sure about the Ofbiz definition of form here but I'm certain this is not just for HTML forms.

<?xml version="1.0" encoding="UTF-8"?>
<forms xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/widget-form.xsd">

    <form name="addPokedexForm" type="single" target="addToPokedex">
        <auto-fields-service service-name="addToPokedex"/>
        <field name="pokemonTypeId" title="${uiLabelMap.CommonType}">
            <drop-down allow-empty="false" current-description="">
                <entity-options description="${type}" key-field-name="pokemonTypeId" entity-name="PokemonType">
                    <entity-order-by field-name="type"/>
                </entity-options>
            </drop-down>
        </field>
        <field name="submitButton" title="${uiLabelMap.CommonAdd}">
            <submit button-type="button"/>
        </field>
    </form>

    <form name="findPokemonForm" type="single" target="findPokemon" default-entity-name="Pokedex">
        <field name="noConditionFind">
            <hidden value="Y"/> <!-- if this isn't there then with all fields empty no query will be done -->
        </field>
        <field name="monster" title="${uiLabelMap.PokedexMonster}">
            <text-find/>
        </field>
        <field name="pokemonTypeId" title="${uiLabelMap.PokemonType}">
            <drop-down allow-empty="true" current-description="">
                <entity-options description="${type}" key-field-name="pokemonTypeId" entity-name="PokemonType">
                    <entity-order-by field-name="type"/>
                </entity-options>
            </drop-down>
        </field>
        <field name="searchButton" title="${uiLabelMap.CommonFind}" widget-style="smallSubmit">
            <submit button-type="button" image-location="/images/icons/magnifier.png"/>
        </field>
    </form>

    <form name="pokemonListForm" type="list" list-name="listIt" paginate-target="findPokemon"
          default-entity-name="Pokedex" separate-columns="true"
          odd-row-style="alternate-row" header-row-style="header-row-2" default-table-style="basic-table hover-bar">
        <actions>
            <!-- Preparing search results for user query by using OFBiz stock service to perform find operations on a single entity or view entity -->
            <service service-name="performFind" result-map="result" result-map-list="listIt">
                <field-map field-name="inputFields" from-field="pokemonContext"/>
                <field-map field-name="entityName" value="Pokedex"/>
                <field-map field-name="orderBy" from-field="parameters.sortField"/>
                <field-map field-name="viewIndex" from-field="viewIndex"/>
                <field-map field-name="viewSize" from-field="viewSize"/>
            </service>
        </actions>
        <field name="pokedexId" title="${uiLabelMap.PokedexId}">
            <display/>
        </field>
        <field name="description" title="${uiLabelMap.PokedexDescription}">
            <display/>
        </field>
        <field name="monster" title="${uiLabelMap.PokedexMonster}" sort-field="true">
            <display/>
        </field>
        <field name="seen" title="${uiLabelMap.PokedexSeen}" sort-field="true">
            <display/>
        </field>
        <field name="caught" title="${uiLabelMap.PokedexCaught}">
            <display/>
        </field>
        <field name="pokemonTypeId" title="${uiLabelMap.PokemonType}">
            <display-entity entity-name="PokemonType" key-field-name="pokemonTypeId" description="${type}"/>
        </field>
    </form>

</forms>

Paste this XML to pokedex/widget/PokedexScreens.xml. You will notice above forms are loaded using this XML. The look-and-feel is described on decorator's metadata.

<?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" -->
            </actions>
            <widgets>
                <decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}">
                    <decorator-section name="body">
                        <screenlet title="Add To Pokedex">
                            <include-form name="addPokedexForm" location="component://pokedex/widget/PokedexForms.xml"/>
                        </screenlet>
                    </decorator-section>
                </decorator-screen>
            </widgets>
        </section>
    </screen>

    <screen name="findPokemon">
        <section>
            <actions>
                <set field="headerItem" value="findPokemon"/>
                <set field="titleProperty" value="PageTitleFindPokemon"/>
                <set field="pokemonContext" from-field="parameters"/>
            </actions>
            <widgets>
                <decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}">
                    <decorator-section name="body">
                        <section>
                            <condition>
                                <if-has-permission permission="POKEDEX" action="_VIEW"/>
                            </condition>
                            <widgets>
                                <decorator-screen name="FindScreenDecorator" location="component://common/widget/CommonScreens.xml">
                                    <decorator-section name="search-options">
                                        <include-form name="findPokemonForm" location="component://pokedex/widget/PokedexForms.xml"/>
                                    </decorator-section>
                                    <decorator-section name="search-results">
                                        <include-form name="pokemonListForm" location="component://pokedex/widget/PokedexForms.xml"/>
                                    </decorator-section>
                                </decorator-screen>
                            </widgets>
                            <fail-widgets>
                                <label style="h3">${uiLabelMap.PokedexViewPermissionError}</label>
                            </fail-widgets>
                        </section>
                    </decorator-section>
                </decorator-screen>
            </widgets>
        </section>
    </screen>``

    <screen name="AddToPokedexFtl">
        <section>
            <actions>
                <set field="titleProperty" value="Custom FTL"/>
                <set field="headerItem" value="addToPokedexFtl"/>
                <script location="component://pokedex/webapp/pokedex/WEB-INF/actions/ListPokedex.groovy"/>
            </actions>
            <widgets>
                <decorator-screen name="PokedexCommonDecorator" location="${parameters.mainDecoratorLocation}">
                    <decorator-section name="body">
                        <label style="h4" text="${uiLabelMap.PokedexList}"/>
                        <platform-specific>
                            <html><html-template location="component://pokedex/webapp/pokedex/crud/list.ftl"/></html>
                        </platform-specific>
                        <platform-specific>
                            <html><html-template location="component://pokedex/webapp/pokedex/crud/add.ftl"/></html>
                        </platform-specific>
                    </decorator-section>
                </decorator-screen>
            </widgets>
        </section>
    </screen>

</screens>

Paste this XML to pokedex/widget/CommonScreens.xml. This is the metadata the decorators for the above screens.

<?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-decorator">
        <section>
            <actions>
                <property-map resource="PokedexUiLabels" map-name="uiLabelMap" global="true"/>
                <property-map resource="CommonUiLabels" map-name="uiLabelMap" global="true"/>

                <set field="layoutSettings.companyName" from-field="uiLabelMap.PokedexCompanyName" global="true"/>
                <set field="layoutSettings.companySubtitle" from-field="uiLabelMap.PokedexCompanySubtitle" global="true"/>

                <set field="activeApp" value="pokedex" global="true"/>
                <set field="applicationMenuName" value="MainAppBar" global="true"/>
                <set field="applicationMenuLocation" value="component://pokedex/widget/PokedexMenus.xml" global="true"/>
                <set field="applicationTitle" value="${uiLabelMap.PokedexApplication}" global="true"/>
            </actions>
            <widgets>
                <include-screen name="GlobalDecorator" location="component://common/widget/CommonScreens.xml"/>
            </widgets>
        </section>
    </screen>

    <screen name="PokedexCommonDecorator">
        <section>
            <actions
                <property-map resource="PokedexUiLabels" map-name="uiLabelMap" global="true"/>
                <property-map resource="CommonUiLabels" map-name="uiLabelMap" global="true"/>
                <!-- Including custom CSS Styles that you want to use in your application view. [] in field can be used to
                 set the order of loading CSS files to load if there are multiple -->
                <set field="styleSheets[]" value="/pokedex/static/css/bootstrap.min.css"/>

                <!-- Including custom JS that you want to use in your application view. [] in field can be used to
                     set the order of loading of JS files to load if there are multiple -->
                <set field="javaScripts[+0]" value="/pokedex/static/js/bootstrap.min.js" global="true"/>
                <set field="layoutSettings.companyName" from-field="uiLabelMap.PokedexCompanyName" global="true"/>
            </actions>
            <widgets>
                <section>
                    <condition>
                        <if-has-permission permission="POKEDEX" action="_VIEW"/>
                    </condition>
                    <widgets>
                        <platform-specific>
                            <html>
                                <html-template location="component://pokedex/webapp/pokedex/includes/PreBody.ftl"/>
                            </html>
                        </platform-specific>
                        <decorator-section-include name="pre-body"/>
                        <decorator-section-include name="body"/>
                        <platform-specific>
                            <html>
                                <html-template location="component://pokedex/webapp/pokedex/includes/PostBody.ftl"/>
                            </html>
                        </platform-specific>
                    </widgets>
                    <fail-widgets>
                        <label style="h3">${uiLabelMap.OfbizDemoViewPermissionError}</label>
                    </fail-widgets>
                </section>
            </widgets>
        </section>
    </screen>
</screens>

Paste this XML to pokedex/widget/PokedexMenus.

<?xml version="1.0" encoding="UTF-8"?>
<menus xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/widget-menu.xsd">
    <menu name="MainAppBar" title="${uiLabelMap.PokedexApplication}" extends="CommonAppBarMenu" extends-resource="component://common/widget/CommonMenus.xml">
        <menu-item name="findPokemon" title="${uiLabelMap.PokemonFind}">
            <link target="findPokemon"/>
        </menu-item>
        <menu-item name="addToPokedexFtl" title="${uiLabelMap.AddToPokedexFtl}">
            <link target="addToPokedexFtl"/>
        </menu-item>
    </menu>
</menus>

Paste this HTML to pokedex/webapp/pokedex/includes/PreBody.ftl

<html>
<head>
    <title>${layoutSettings.companyName}</title>
    <meta name="viewport" content="width=device-width, user-scalable=no"/>
    <#if webSiteFaviconContent?has_content>
        <link rel="shortcut icon" href="">
    </#if>
    <#list styleSheets as styleSheet>
        <link rel="stylesheet" href="${StringUtil.wrapString(styleSheet)}" type="text/css"/>
    </#list>
    <#list javaScripts as javaScript>
        <script type="text/javascript" src="${StringUtil.wrapString(javaScript)}"/></script>
    </#list>
  </head>
  <body data-offset="125">
    <div class="container menus" id="container">
      <div class="row">
        <div class="col-sm-12">
          <ul id="page-title" class="breadcrumb">
            <li>
                <a href="<@ofbizUrl>main</@ofbizUrl>">Main</a>
            </li>
            <li class="active"><span class="flipper-title">${StringUtil.wrapString(uiLabelMap[titleProperty])}</span></li>
            <li class="pull-right">
              <a href="<@ofbizUrl>logout</@ofbizUrl>" title="${uiLabelMap.CommonLogout}">logout</i></a>
            </li>
          </ul>
        </div>
      </div>
      <div class="row">
        <div class="col-lg-12 header-col">
          <div id="main-content">

and to pokedex/webapp/pokedex/includes/PostBody.ftl.

          <#-- Close the tags opened in the PreBody section -->
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

The above HTML has styling and Javascript dependencies you have to download from Twitter's bootstrap. Then create two folders inside pokedex/webapp/pokedex/static, one for css and one for js. Place bootstrap.min.css and bootstrap.min.js to their respective folder. Afterwhich, expose the above static resources in your pokedex/webapp/pokedex/WEB-INF/web.xml by editing the allowedPaths.

<init-param>
     <param-name>allowedPaths</param-name>
     <param-value>/error:/control:/select:/index.html:/index.jsp:/default.html:/default.jsp:/images:/includes/maincss.css:/static</param-value>
</init-param>

Paste this to pokedex/webapp/pokedex/crud/add.ftl

<form method="post" action="<@ofbizUrl>addToPokedexEvent</@ofbizUrl>" name="addToPokedexEvent" class="form-horizontal">
    <div class="control-group">
        <label class="control-label" for="pokemonType">${uiLabelMap.PokemonType}</label>
        <div class="controls">
            <select id="pokemonTypeId" name="pokemonTypeId">
                <#list pokemonTypes as pokemonType>
                    <option value='${pokemonType.pokemonTypeId}'>${pokemonType.type}</option>
                </#list>
            </select>
        </div>
    </div>
    <div class="control-group">
        <label class="control-label" for="monster">${uiLabelMap.PokedexMonster}</label>
        <div class="controls">
            <input type="text" id="monster" name="monster" required>
        </div>
    </div>
    <div class="control-group">
        <label class="control-label" for="description">${uiLabelMap.PokedexDescription}</label>
        <div class="controls">
            <input type="text" id="description" name="description" required>
        </div>
    </div>
    <div class="control-group">
        <div class="controls">
            <button type="submit" class="btn">${uiLabelMap.CommonAdd}</button>
        </div>
    </div>
</form>

and to pokedex/webapp/pokedex/crud/list.ftl.

<table class="table table-bordered table-striped table-hover">
    <thead>
        <tr>
            <th>${uiLabelMap.PokedexId}</th>
            <th>${uiLabelMap.PokemonType}</th>
            <th>${uiLabelMap.PokedexMonster}</th>
            <th>${uiLabelMap.PokedexDescription}</th>
        </tr>
    </thead>
    <tbody>
    <#list pokedexList as pokedex>
        <tr>
            <td>${pokedex.pokedexId}</td>
            <td>${pokedex.getRelatedOne("PokemonType").get("type", locale)}</td>
            <td>${pokedex.monster}</td>
            <td>${pokedex.description}</td>
        </tr>
    </#list>
    </tbody>
</table>

Model attributes via Groovy

The above ftls has pokemonTypes and pokedexList attributes but the bottom service don't have them. To expose these attributes, paste this groovy on pokedex/webapp/pokedex/WEB-INF/actions/ListPokedex.groovy.

pokemonTypes = delegator.findList("PokemonType", null, null, null, null, false);
context.pokemonTypes = pokemonTypes;

pokedexList = delegator.findList("Pokedex", null, null, null, null, false);
context.pokedexList = pokedexList;

Controller

Paste this inside pokedex/webapp/pokedex/WEB-INF/controller.xml.

<?xml version="1.0" encoding="UTF-8"?>
<site-conf xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/site-conf.xsd">
    <!-- The controller elements that are common to all OFBiz components
         can be found in the following xml file. A component can override the
         elements found in the common-controller.xml file. -->
    <include location="component://common/webcommon/WEB-INF/common-controller.xml"/>

    <description>Pokedex Component Site Configuration File</description>

    <!-- Events to run on every request before security (chains exempt) -->
    <!--
    <preprocessor>
    </preprocessor>
    -->
    <!-- Events to run on every request after all other processing (chains exempt) -->
    <!--
    <postprocessor>
        <event name="test" type="java" path="org.ofbiz.webapp.event.TestEvent" invoke="test"/>
    </postprocessor>
    -->

    <!-- Request Mappings -->
    <request-map uri="main">
        <security https="true" auth="true"/>
        <response name="success" type="view" value="main"/>
    </request-map>
    <request-map uri="addToPokedexEvent">
        <security https="true" auth="true"/>
        <event type="java" path="com.prototype.pocket.monster.PokedexEvent" invoke="addToPokedex"/>
        <response name="success" type="view" value="addToPokedexFtl"/>
        <response name="error" type="view" value="addToPokedexFtl"/>
    </request-map>
    <request-map uri="findPokemon">
        <security https="true" auth="true"/>
        <response name="success" type="view" value="findPokemon"/>
    </request-map>
    <request-map uri="addToPokedexFtl">
        <security https="true" auth="true"/>
        <response name="success" type="view" value="addToPokedexFtl"/>
    </request-map>

    <!-- View Mappings -->
    <view-map name="main" type="screen" page="component://pokedex/widget/PokedexScreens.xml#main"/>
    <view-map name="findPokemon" type="screen" page="component://pokedex/widget/PokedexScreens.xml#findPokemon"/>
    <view-map name="addToPokedexFtl" type="screen" page="component://pokedex/widget/PokedexScreens.xml#AddToPokedexFtl"/>

</site-conf>

Service Layer

Paste this XML to pokedex/servicedef/services.xml.

<?xml version="1.0" encoding="UTF-8"?>
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/services.xsd">
    <description>Pokedex Services</description>
    <vendor></vendor>
    <version>1.0</version>

    <service name="addToPokedex" default-entity-name="Pokedex" engine="entity-auto" invoke="create" auth="true">
        <description>Create a pokedex entry</description>
        <auto-attributes include="pk" mode="OUT" optional="false"/>
        <auto-attributes include="nonpk" mode="IN" optional="true"/>
        <override name="comments" optional="true"/>
    </service>

    <service name="addToPokedexService" default-entity-name="Pokedex" engine="java"
             location="com.prototype.pocket.monster.PokedexService" invoke="addToPokedex" auth="false">
        <description>Add to Pokedex using a service</description>
        <auto-attributes include="pk" mode="OUT" optional="false"/>
        <auto-attributes include="nonpk" mode="IN" optional="true"/>
        <override name="comments" optional="true"/>
    </service>

</services>

Out-of-the-box (OOTB) Services

addToPokedex is using the entity-auto which is for simple one-to-many entity relationship. For multiple entities of complex relationship, you will need to write your custom implementation to your service.

findPokemon is using the generic service performFind which is efficient to use when you have to perform search on one entity or one view entity.

Event

You might notice that one of the controller mappings is mapped to addToPokedexEvent. Create this event on pokedex/com/prototype/pocket/monster/PokedexEvent.java.

package com.prototype.pocket.monster;

import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.service.GenericServiceException;
import org.ofbiz.service.LocalDispatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class PokedexEvent {

    public static final String module = PokedexEvent.class.getName();

    public static String addToPokedex(HttpServletRequest request, HttpServletResponse response) {

        LocalDispatcher dispatcher = (LocalDispatcher) request.getAttribute("dispatcher");

        String pokemonTypeId = request.getParameter("pokemonTypeId");
        String monster = request.getParameter("monster");
        String description = request.getParameter("description");

        if (UtilValidate.isEmpty(monster) || UtilValidate.isEmpty(description)) {
            request.setAttribute("_ERROR_MESSAGE_", "Monster and its description is required");
            return "error";
        }

        try {
            Debug.logInfo("Adding to pokedex using events", module);
            dispatcher.runSync("addToPokedexService", UtilMisc.toMap("pokemonTypeId", pokemonTypeId,
                    "monster", monster, "description", description));
        } catch (GenericServiceException e) {
            request.setAttribute("_ERROR_MESSAGE_", "Unable to add to pokedex: " + e.toString());
            return "error";
            request.setAttribute("_EVENT_MESSAGE_", "Successfully added to Pokedex");
        }

        return "success";
    }

}

The above event invokes addToPokedexService. Create this service on pokedex/src/com/prototype/pocket/monster/PokedexService.java.

package com.prototype.pocket.monster;

import org.ofbiz.base.util.Debug;
import org.ofbiz.entity.Delegator;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.service.DispatchContext;
import org.ofbiz.service.ServiceUtil;

import java.util.Map;

public class PokedexService{

    public static final String module = PokedexService.class.getName();

    public static Map<String, Object> addToPokedex(DispatchContext dispatchContext, Map<String, ? extends Object> context) {

        Map<String, Object> result = ServiceUtil.returnSuccess();
        Delegator delegator = dispatchContext.getDelegator();

        try {
            GenericValue pokedex = delegator.makeValue("Pokedex");
            pokedex.setNextSeqId();
            pokedex.setNonPKFields(context);
            pokedex = delegator.create(pokedex);
            result.put("pokedexId", pokedex.getString("pokedexId"));
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError("Error while adding to pokedex using " + module);
        }

        return result;
    }
}

Service vs Event

Criteria Service Event
Require Definition Yes No
Used to write business logic Yes No
Job Scheduling possible Yes No
Implementation possibilities Entity auto, Java, Simple (XML) & Groovy Simple (XML), Java & Groovy
Return Type Map String

Running pokedex

Now that the model, views, controller and service are wired using XMLs, you run ./ant clean-all build load-demo start to test your application.