Introduction

One of the main features of MD4J is that it allows you to work on your model iteratively, providing a complete J2EE application following your model changes. So, each time you edit your model and build your project, MD4J will generate high quality code from JSPs and Struts classes, to Session EJBs and DAOs, allowing you to test CRUD and Search functionality on top of your model right away.

This tutorial will help you get started quickly (hopefully in ten minutes or so) using Quickstarter, a Maven-based J2EE project template for MD4J. The template has MD4J code generation setup for you, plus all the plumbing you are used to in your Maven or Ant projects, such as XDoclet for your EJBs and Struts οr other web classes. XDoclet is also applied on EJB and Struts code generated by MD4J from your Hibernate mappings.

A Maven archetype based on the template will be available soon, while an Ant-based project template is also in the works. Stay tuned for EJB 3 and Struts 2 code generation support!

Prerequisites

To folow the tutorial you need the following:

  • A JDK installed. Version 1.4 and above will do just fine. Remember to set up your $JAVA_HOME (%JAVA_HOME% in windows).
  • An instance of your favourite J2EE container. This tutorial currently assumes JBoss, but you should be able to adjust for Geronimo, Jonas etc. If you do, paches for this document and the template project itself are welcome.
  • This is a Maven 2 based tutorial, so if you dont have it installed you need to download and follow the installation instructions.
  • The MD4J core and quickstarter archives; those can be found in our download page at Sourceforge (or check them out from CVS). Both will be available in Maven's primary repository along with the 0.2 release.
  • A database server. This tutorial assumes MySQL but you should be able to adjust the instructions for any Hibernate supported database with ease.
  • In case of trouble, please ask for help in the MD4J User Mailing list.

The Quickstarter multi-project

Because MD4J is not available in the Maven Repository yet, you need to manually install md4j-0.2.jar in your local Maven repository. Using your shell or command prompt, navigate to directory where the core jar has been downloaded and enter the commands below in a single line:
        mvn install:install-file -Dfile=md4j-0.2.jar -DgroupId=gr.abiss.md4j 
        -DartifactId=md4j -Dversion=0.2 -Dpackaging=jar
      
Now extract the Quickstarter project template somewhere in your filesystem. After extracting it you should see the following list of directories:
  • md4j-quickstarter-mvn: the root project, just a POM
  • md4j-quickstarter-ear: the EAR module
  • md4j-quickstarter-struts: the Struts WAR module
  • md4j-quickstarter-ejb: the EJB JAR module
  • md4j-quickstarter-hibernate: the Hibernate-based module, produces the domain POJOs JAR
Quickstarter is actually a multiproject composed of the hibernate, ear, ejb and struts subprojects. These subprojects, in Maven jargon, are called modules. Each module works as a standalone Maven project with it's own POM (pom.xml), src directory etc. Before working with the quickstarter modules, take a look in md4j-quickstarter-mvn/filter.properties. This file is used by all Quickstarter modules; every property-value pair in it will have efect throughout the multiproject.

Setting up the project and database

If you have a local MySQL installed, all you have to do is login and create the database:
  % mysql -u root -p 
  password: ***** 
  mysql> create database quickstarter; 
  mysql> quit 
      
Where "*****" is the password (default is empty for MySQL). You may want to create a user for the quickstarter database and grant related privileges, in any case update connection.username and connection.password in filter.properties accordingly. If you use another database you will need to update the related properties accordingly, including the hibernate.dialect.

Working with the Hibernate Domain Module

The Domain module contains the application Hibernate mappings (*.hbm.xml), in other words the domain model of your application. The artifact it produces is a JAR containing those mappings, the Hibernate runtime configuration (hibernate.cfg.xml) and the compiled POJOs generated from the mappings. The POJOs are generated by the Maven Hibernate3 Plugin. MD4J actually uses the Hibernate module mappingsbehind the schenes from the ejb and Web modules, as we will see later, to generate source code. For now, let's play a bit with this module by editing a mapping and creating our database. Navigate to the model/src/main/gr/abiss/md4j/sampledomain/ folder and add the following property to both City and Country hbm files: and add the following property to it:
  <property name="population" type="integer" not-null="true" />
      
Note: When working with Hibernate mappings keep in mind the requirements. Your City mapping should now look like City.hbm.xml. To build the Domain module simply run the following command within the domain directory:
  mvn clean install
      
Here is what we created in the Domain module:
  • A JAR artifact containing the Hibernate mappings and filtered configuration file, along with the compiled POJOs from the sources generated in domain/target/hibernate3/generated-sources
  • The database tables using the SQL DDL generated in domain/target/hibernate3/sql/schema.sql

Creating the Database Tables

Before you continue, execute the generated SQL (in domain/target/hibernate3/sql/schema.sql) against your quickstarter database using something like the MySQL Query Browser to generate the application tables that correspond to your Hibernate mappings. The next modules, ejb and web are far easier to deal with, so lets get going while we are still within the ten minute timeframe ;-)

Building the EJB module

This is where you develop your EJBs or other business tier code. MD4J also generates EJBs and DAOs here and places them in target/generated-sources/main/java/. The module structure:
  /ejb
  |-src
  |---main
  |-----java: EJBs and other business level code like DAOs etc.
  |-----resources: property files and othr resources
  |-target
  |-pom.xml: The EJB module POM 
      

To generate a JAR with EJBs and DAOs for the domain model entities you edited while working with the Domain module, all you have to do is execute the following from within the EJB module directory:

          mvn clean install
      

With the above command, MD4J will read the Hibernate mappings in the Domain module to generate EJBs and DAOs. For example, the CityManagerBean.java, a Stateless Session EJB created by MD4J using the from City.hbm.xml mapping as input:

/*
 * This file was automatically generated by MD4J. 
 * 
 * Licensed under the GNU General Public License, Version 2.0 (the "License") or above;
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * *http://www.gnu.org/licenses/gpl.txt
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package gr.abiss.md4j.sampledomain.business;

import java.io.Serializable;

import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;

import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.naming.InitialContext;

import org.apache.log4j.Logger;
import org.apache.commons.lang.math.NumberUtils;

import gr.abiss.md4j.dao.Page;
import gr.abiss.md4j.dao.Order;
import gr.abiss.md4j.business.BrokenConstraintsException;

import gr.abiss.md4j.sampledomain.Country;

import gr.abiss.md4j.sampledomain.City;
import gr.abiss.md4j.sampledomain.dao.CityDAO;
/**
 * @ejb.bean name="CityManager"
 *           display-name="CityManager"
 *           description="City EJB"
 *           jndi-name="ejb/CityManagerBeanHome"
 *           local-jndi-name="ejb/CityManagerBeanLocalHome"
 *           view-type="both"
 *           type="Stateless"
 * @ejb.home extends="javax.ejb.EJBHome" local-extends="javax.ejb.EJBLocalHome"
 * @ejb.transaction type="Required"
 *
 * @ejb.interface
 *
 */
public class CityManagerBean implements SessionBean{

    private static Logger log = Logger.getLogger(CityManagerBean.class);
 
    /**
     * @ejb.create-method
     */
    public void ejbCreate() throws CreateException {
    }

    public void ejbActivate() throws EJBException {
    }

    public void ejbPassivate() throws EJBException {
    }

    public void ejbRemove() throws EJBException {
    }

    public void setSessionContext(SessionContext newContext) throws EJBException {
    }

    /**
     * Get a page of results matching the given parameters
     * @param params the search parameters
     * @param pageNumber the results page number 
     * @param pageSize the results page size
     * @return a search results page constructed based on the given parameters
     * 
     * @ejb.interface-method view-type = "both"
     * @throws EJBException
     */
    public Page search(Map params, Order order, int pageNumber, int pageSize) throws EJBException {
        Page page = null;
        try {
            CityDAO dao = new CityDAO();
            page = dao.getPage(params, order, pageNumber, pageSize);
        } catch (Exception e) {
            throw new EJBException(e);
        }
        return page;
    }

    /**
     * Get a page of results matching the given parameters
     * @param params the search parameters
     * @param pageNumber the results page number 
     * @param pageSize the results page size
     * @return a search results page constructed based on the given parameters
     * 
     * @ejb.interface-method view-type = "both"
     * @throws EJBException
     */
    public Page search(Set projectionProps, Map params, Order order, int pageNumber, int pageSize) throws EJBException {
        Page page = null;
        try {
            CityDAO dao = new CityDAO();
            page = dao.getPage(projectionProps, params, order, pageNumber, pageSize);
        } catch (Exception e) {
            throw new EJBException(e);
        }
        return page;
    }

    /**
     * Save a new City instance based on the property-value pairs in the map  
     * @param map
     * @return the persisted City instance identifier 
     * 
     * @ejb.interface-method view-type = "both"
     * @throws javax.ejb.EJBException possibly wrapping a BrokenConstraintsException 
     * @ejb.transaction type="Required"
     */
    public Serializable save(Map map) throws EJBException {
        try {
            CityDAO dao = new CityDAO();
            // check for unique constraint violations
            Set brokens = dao.getBrokenUConstraints(map, null);
            log.info("Broken constaints: "+ brokens.size());
            if(brokens != null && brokens.size() > 0){
                throw new BrokenConstraintsException(brokens);
            }
            return dao.save(map);
        } catch (Exception e) {
            throw new EJBException(e);
        }
    }

    /**
     * Retreive the City matching the given identifier if any such match is found, <code>null</code> otherwise
     * @return the City matching the given identifier if any such match is found, <code>null</code> otherwise
     * 
     * @ejb.interface-method view-type = "both"
     * @throws EJBException
     */
    public Serializable get(Serializable identifier) throws EJBException {
        Serializable pojo = null;
        try {
            CityDAO dao = new CityDAO();
            pojo = (Serializable) dao.get(identifier);
        } catch (Exception e) {
            throw new EJBException(e);
        }
        return pojo;
    }

    /**
     * Retreive the City matching the given identifier as a map if any such match is found, <code>null</code> otherwise
     * @return the City matching the given identifier as a map if any such match is found, <code>null</code> otherwise
     * 
     * @ejb.interface-method view-type = "both"
     * @throws EJBException
     */
    public Map get(Serializable identifier, Set projectionProperties) throws EJBException {
        Map map = null;
        try {
            CityDAO dao = new CityDAO();
            map = dao.get(identifier, projectionProperties);
        } catch (Exception e) {
            throw new EJBException(e);
        }
        return map;
    }

    /**
     * Retreive the requested properties of the City matching the given identifier as a map that <b>includes parent options for the web tier<b>
     * The parent options are essentially the data needed to construct HTML select elements in views. 
     * @return the map containing the bunch if a match for the identifier is found, <code>null</code> otherwise
     * 
     * @ejb.interface-method view-type = "both"
     * @throws EJBException
     */
    public Map getWithOptions(Serializable identifier, Set projectionProperties) throws EJBException {
        Map map = null;
        try {
            CityDAO dao = new CityDAO();
            map = dao.get(identifier, projectionProperties);
            // add data for form drop downs refering to parent classes
            dao.addParentOptions(map);
        } catch (Exception e) {
            throw new EJBException(e);
        }
        return map;
    }

    /**
     * 
     * @ejb.interface-method view-type = "both"
     * @throws EJBException
     */
    public Map getParentOptions() throws EJBException {
        Map map = null;
        try {
            CityDAO dao = new CityDAO();
            // get data for form drop downs refering to parent classes
            map = dao.getParentOptions();
        } catch (Exception e) {
            throw new EJBException(e);
        }
        return map;
    }

    /**
     * 
     * @ejb.interface-method view-type = "both"
     * @throws EJBException
     * @ejb.transaction type="Required"
     */
    public void update(Map map) throws EJBException {
        try {
            CityDAO dao = new CityDAO();
            String sId = (String) map.get("id");
            if(sId != null){
               Long id = NumberUtils.createLong(sId);
                // check for unique constraint violations
                Set brokens = dao.getBrokenUConstraints(map, id);
                if(brokens != null && brokens.size() > 0){
                    throw new BrokenConstraintsException(brokens);
                }
                dao.update(map);
            }
            else{
                throw new Exception("Parameter id was null");
            }
        } catch (Exception e) {
            throw new EJBException(e);
        }
    }
    /**
     * 
     * @ejb.interface-method view-type = "both"
     * @throws EJBException
     * @ejb.transaction type="Required"
     */
    public void delete(Serializable pojo) throws EJBException {
        try {
            CityDAO dao = new CityDAO();
            dao.delete((Serializable) pojo);
        } catch (Exception e) {
            throw new EJBException(e);
        }
    }

} 
        

and the CityDAO.java, a Hibernate based Data Access Object also created by MD4J from the same Hibernate maping:

package gr.abiss.md4j.sampledomain.dao;
/*
 * This file was automatically generated by MD4J. 
 * 
 * Licensed under the GNU General Public License, Version 2.0 (the "License") or above;
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * *http://www.gnu.org/licenses/gpl.txt
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
import java.io.Serializable;

import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Date;

import org.apache.commons.lang.BooleanUtils;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;


import org.apache.log4j.Logger;
import org.apache.commons.lang.math.NumberUtils;

import gr.abiss.md4j.dao.Page;
import gr.abiss.md4j.dao.hibernate.Helper;
import gr.abiss.md4j.dao.DataAccessException;
import gr.abiss.md4j.util.Md4jGlobals;
import gr.abiss.md4j.util.ConvertUtils;

import gr.abiss.md4j.dao.hibernate.AbstractHbmDAO;

import gr.abiss.md4j.sampledomain.City;
import gr.abiss.md4j.sampledomain.Country;

/**
 * Data access operations for  City entities 
 */
public class CityDAO extends AbstractHbmDAO {

    private static Logger log = Logger.getLogger(CityDAO.class);

    /**
     * The property names bound by uniqueness constraints
     */
    public static final Set uniConstrainedPropertyNames = new HashSet();
    static {
        uniConstrainedPropertyNames.add("id");
        uniConstrainedPropertyNames.add("name");
    }


    /**
     * Default Constructor 
     */
    public CityDAO() {
        super(City.class, "id");
    }

    /**
     * Constructor to be used by subclasses essentially bubbles up the object hierarchy 
     * up to the constructor of AbstractHbmDAO passing along the 
     * Class and Identifier namme for the Entity/POJO this DAO is about 
     * @see gr.abiss.md4j.hbm.AbstractHbmDAO#AbstractHbmDAO(java.lang.Class, java.io.Serializable) 
     */
    protected CityDAO(Class clazz, String identifier) {
        super(clazz, identifier);
    }

    /**
     * Creates a new City instance, using the map property-value 
     * pairs and persists it, then returns the identifier (PK). This method is <b>not</b> for updating 
     * @returns the identifier of the newly persisted POJO as Serializable that can be safely casted as Long 
     */
    public Serializable save(Map map) {
        Object pojo = this.createFromParams(map);
        return this.sf.getCurrentSession().save(pojo);
    }

    /**
     * Persists the given City instance  
     * This method is <b>not</b> for updating 
     * @returns the identifier of the newly persisted POJO as Serializable that can be safely casted as Long 
     */
    public Serializable save(City pojo) {
        return this.sf.getCurrentSession().save(pojo);
    }

    /**
     * Convert the identifier to the right class and call the generic load method
     * @see gr.abiss.md4j.dao.hibernate.AbstractHbmDAO#load(java.io.Serializable)
     */
    public Object load(Serializable identifier) {
        Long typedIdentifier = NumberUtils.createLong((String) identifier);
        return super.load(typedIdentifier);
    }

    /**
     * Convert the identifier to the right class and call the generic get method
     * @see gr.abiss.md4j.dao.hibernate.AbstractHbmDAO#get(java.io.Serializable)
     */
    public Object get(Serializable identifier) {
        Long typedIdentifier = NumberUtils.createLong((String) identifier);
        return super.get(typedIdentifier);
    }

    /**
     * Convert the identifier to the right class and call the generic get method
     * @see gr.abiss.md4j.dao.hibernate.AbstractHbmDAO#get(Serializable, Set)
     */
    public Map get(Serializable identifier, Set projectionProperties) {
        Long typedIdentifier = NumberUtils.createLong((String) identifier);
        return super.get(typedIdentifier, projectionProperties);
    }

    /**
     * Copy properties from the given map to the given City instance 
     * @param map The Map object to copy properties from 
     * @tparam obj The instance of City you wish to copy the properties to
     * @throws ClassCastException in case the given object is not an instance of City 
     * @see gr.abiss.md4j.hbm.AbstractHbmDAO#copyProperties(java.util.Map, java.lang.Object)
     */
    public void copyProperties(Map map, Object  obj)  {
        City pojo = (City) obj;
        if ( map.containsKey("id")) {
            String _stringid = (String) map.get("id");
            Long _id = NumberUtils.createLong(_stringid);
            pojo.setId(_id);
        }
        if ( map.containsKey("name")) {
            String _name = (String) map.get("name");
            pojo.setName(_name);
        }
        if ( map.containsKey("region")) {
            String _region = (String) map.get("region");
            pojo.setRegion(_region);
        }
        Serializable _countryIdentifier = (Serializable) map.get("country");
        if (_countryIdentifier !=  null) {
            Country _country = null;
            String countryId = (String)  _countryIdentifier;
            _country = (Country) this.sf.getCurrentSession().load(Country.class, countryId);
            pojo.setCountry(_country);
        }
    }


    /**
     * Create the criteria for a City search using the given <code>params</code>, then bubble the  
     * operation to the superclass up to AbstractHbmDAO at witch point all <code>params</code>   
     * have bheen converted to criteria and the actual Criteria query can  be executed to return a Page of results. 
     * @see gr.abiss.md4j.hbm.AbstractHbmDAO#getPage(org.hibernate.Session, java.util.Set, java.util.Map, org.hibernate.Criteria, int, int)
     * @param sess The Hibernate Session object to use for the search
     * @param projectionProps The set of properties to return for each result
     * @param params The set of search criteria to use 
     * @param criteria The criteria to use for the search, produced by each DAO in the object hierarchy using the <code>params</code> map
     * @param pageNumber The page number to return
     * @param pageSize The page size
     * @return The resulting Page of results
     */
    protected void populateCriteria(Set projectionProps, Map map, Criteria criteria) {
           //criteriaFromParamsMap prosessing: id, id: searchType:Long, hbmtype: java.lang.Long, relatedClass: 
        if ( map.get("id") != null) {
            String _stringid = (String) map.get("id");
            Long _id = NumberUtils.createLong(_stringid);
            criteria.add(Restrictions.eq("id", _id));
        }
           //criteriaFromParamsMap prosessing: property, name: searchType:String, hbmtype: string, relatedClass: 
        if ( map.get("name") != null) {
            String _name = (String) map.get("name");
            criteria.add(Restrictions.like("name", "%" +  _name + "%"));
        }
           //criteriaFromParamsMap prosessing: property, region: searchType:String, hbmtype: string, relatedClass: 
        if ( map.get("region") != null) {
            String _region = (String) map.get("region");
            criteria.add(Restrictions.like("region", "%" +  _region + "%"));
        }
           //criteriaFromParamsMap prosessing: many-to-one, country: searchType:String, hbmtype: string, relatedClass: Country
        criteria.createAlias("country", "country");
        Serializable _countryIdentifier = (Serializable) map.get("country");
        if (_countryIdentifier !=  null) {
            String countryId = (String)  _countryIdentifier;
            criteria.add(Restrictions.eq("country.id", countryId));
        }
    }


    /**
     * @return The resulting Page of results
     */
    public void addParentOptions(Map map) {
        Session session = this.sf.getCurrentSession();
        map.put(Md4jGlobals.PARENT_OPTIONS+"#country", Helper.getQueryResultAsMap(session.createCriteria(Country.class).addOrder(Order.asc("id")), "id", "id"));
    }
    public Set getUniquePropertyNames(){ 
        return CityDAO.uniConstrainedPropertyNames; 
    }

}

        

in the mean time, Maven will use XDoclet to generate the EJB descriptors and package everything in target/md4j-quickstarter-ejb-0.2.jar.

Of course you can develop your own EJBs and other business-level code and place it in md4j-quickstarter-ejb/src/main/java. Maven will process that and package it in the EJB JAR, after XDoclet processing if neccessary.

Building the Web module

In this module you develop your web classes (Struts, Servlets, etc.) in src/main/java. JSPs and other web files like CSS and images are placed in src/main/wbapp. Md4J reads the Hibernate mappings from the Domain module to create Struts code in target/generated-sources/main/java/ and JSPs in target/md4j-quickstarter-war-0.2/

  /web
  |-src
  |---main
  |-----java: Java web sources (Struts, Servlets etc)
  |-----merge: Webdoclet merge files for web and Struts
  |-----webapp: JSPs, images, CSS etc
  |-target
  |-pom.xml: The module POM 
      

Similarly to the EJB module, to generate a JAR with your web files like Struts Actions and Forms, JSPs and everything else (including the Struts and JSP code MD4J will generate from your Domain mappings), all you have to do is execute the following from within the md4j-quickstarter-struts module directory:

  mvn clean install
      

This will create the WAR file as target/md4j-quickstarter-struts-0.2.war. Let's see what MD4J generated from our City mapping:

Imagine having to type all this code. Now lets deploy our EAR.

Deploying the EAR and Datasource

Lets build and deploy an EAR that bundles all the above to your local JBoss AS. To do this, yoo need to either have a JBOSS_HOME environment variable set or, in case you do not wish to use the environment variable, open the EAR module's pom.xml, then look for and edit the jbossHome element before executing the following:
  mvn clean install jboss:harddeploy
      
This will deploy the EAR in your local JBoss server. Now you need to deploy the datasource; simply copy the filtered ear/target/classes/md4j-quickstarter-ds.xml in your SJBOSS_HOME/server/default/deploy directory and you are good to go.

Test your Web Application

We haven't finished the sitemap generator yet, so you need to keep the following URL patterns in mind to test the web application. Supposing you want to test the pages for the entity/POJO named "City", here are the URLs you need to access: