Maven Struts 2 enterprise application based on Java EE 7 and Glassfish 4

This tutorial will show you how to create a Maven Struts 2 enterprise application based on Tiles 3, Java EE 7 and Glassfish 4. It will also show you how to make use of EJBs and JPA on Glassfish. Furthermore you will learn how to use i18n in your Struts 2 application as well. I will create a multi-module Maven project. Our ear module will contain an EJB module and two war modules of which one only contains our RESTful services and the other one contains the actual Struts 2 application. We will use jQuery to make an ajax call for retrieving data from the (PostgreSQL) database through a Stateless EJB and JPA 2.1. Our RESTful service will produce both JSON and XML without implementing custom serialization to JSON/XML. We will let Jersey and Jackson handle all the JSON and XML serialization / de-serialization stuff for us, so it's just a matter of configuration.

The IDE I used is Netbeans 7.3.1, but you could also use any other IDE of your choice. I have tested this tutorial with Maven 3.0.5 and Glassfish 4, my favorite Java EE 7 certified application server. I did not test with Maven 3.1 at all. At the time I wrote this tutorial the latest version of Struts 2 was 2.3.15.1.


Table of contents:

  1. Maven parent pom (pom.xml)
  2. Maven EJB module
    1. Implementing the Message JPA Entity class (Message.java)
    2. Implementing the MessageServiceBean (EJB)
  3. Maven Service module (war)
    1. Defining web.xml and glassfish-web.xml
    2. REST configuration (ApplicationConfig.java and MyJacksonJsonProvider.java)
    3. Implementing the RESTful web service (MessageService.java)
  4. Maven Struts 2 module
    1. Defining web.xml and glassfish-web.xml
    2. Defining struts.xml and struts.properties
    3. Internationalization / i18n
    4. Implementing the action class (MessageAction.java)
    5. Defining tiles.xml
    6. Implementing the default layout (jsp, jQuery, CDN)
    7. Implementing the Tiles fragments (jsp)
  5. Maven EAR module
  6. Adding JDBC resource to Glassfish 4
  7. Running the application
    1. Add JDBC resource to Glassfish 4
    2. Create the ear with Maven
    3. Deploy ear to Glassfish 4 with asadmin command
    4. Run application in your browser
  8. Additional hints
  9. Download Source Code (Maven project)

The application we will develop is very simple. Our application allows to enter a message which will be stored into a database. You can also see a list of all available messages in the database. You can download the Maven project to try it yourself. The final screens which we will develop in this tutorial look like this:


Adding new messages and displaying the result:
Screen 1: Entering new Messages Screen 2: Message saved

As you can see this is a very detailed tutorial and creating it meant a lot of effort for me. I hope it will help others. Any feedback is welcome - feel free to leave a comment (see below). If you have any questions do not hesitate to contact me. For helping me to maintain my tutorials any donation is welcome.


1. Maven parent pom (pom.xml)

As already mentioned, we will create a multi-module Maven project. For more details about Maven multi-module projects please check Maven by Example, Chapter 6. A Multi-module Project. Below you can see the pom.xml of our parent project. It contains a lot of stuff you actually do not need. To give you one example: Struts 2 sometimes has dependencies to versions of libraries which are not the latest versions. I have chosen to use newer versions by using Maven's dependencyManagement. If you do stuff like this you have to be very careful, because this might have some critical side effects. Please choose what ever you want and what not by adapting the pom.

        <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

            <modelVersion>4.0.0</modelVersion>
            <groupId>com.nabisoft.tutorials</groupId>
            <artifactId>struts2-multi-module-demo</artifactId>
            <version>1.0-SNAPSHOT</version>
            <name>struts2-multi-module-demo</name>
            <packaging>pom</packaging>

            <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
                <version.javaee.api>7.0</version.javaee.api>
                <version.eclipselink>2.5.0</version.eclipselink>
                <version.struts>2.3.15.1</version.struts>
                <version.log4j>1.2.17</version.log4j>
                <version.slf4j>1.7.5</version.slf4j>
                <version.jdk>1.7</version.jdk>
                <version.mvn.compiler>3.1</version.mvn.compiler>
                <version.jersey>2.0</version.jersey>
            </properties>

            <modules>
                <module>struts2-module-ejb</module>
                <module>struts2-module-service</module>
                <module>struts2-module-war</module>
                <module>struts2-module-ear</module>
            </modules>

            <repositories>

                <repository>
                    <id>java.net-Public</id>
                    <name>Maven Java Net Snapshots and Releases</name>
                    <url>https://maven.java.net/content/groups/public/</url>
                    <layout>default</layout>
                </repository>

                <repository>
                    <id>Central</id>
                    <name>Maven Repository</name>
                    <url>http://repo1.maven.org/maven2</url>
                    <layout>default</layout>
                </repository>

                <repository>
                    <id>central</id>
                    <name>Central Repository</name>
                    <url>http://repo.maven.apache.org/maven2</url>
                    <layout>default</layout>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                </repository>

                <repository>
                    <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
                    <id>eclipselink</id>
                    <layout>default</layout>
                    <name>Repository for library EclipseLink (JPA 2.0)</name>
                </repository>

            </repositories>

            <dependencyManagement>
                <dependencies>

                    <!-- Java EE -->
                    <dependency>
                        <groupId>javax</groupId>
                        <artifactId>javaee-web-api</artifactId>
                        <version>${version.javaee.api}</version>
                        <scope>provided</scope>
                    </dependency>

                    <dependency>
                        <groupId>javax</groupId>
                        <artifactId>javaee-api</artifactId>
                        <version>${version.javaee.api}</version>
                        <scope>provided</scope>
                    </dependency>

                    <!-- Jersey -->
                    <dependency>
                        <groupId>org.glassfish.jersey.containers</groupId>
                        <artifactId>jersey-container-servlet</artifactId>
                        <version>${version.jersey}</version>
                        <scope>provided</scope>
                    </dependency>
                    <dependency>
                        <groupId>org.glassfish.jersey.media</groupId>
                        <artifactId>jersey-media-json-jettison</artifactId>
                        <version>${version.jersey}</version>
                        <scope>provided</scope>
                    </dependency>
                    <dependency>
                        <groupId>org.glassfish.jersey.media</groupId>
                        <artifactId>jersey-media-json-jackson</artifactId>
                        <version>${version.jersey}</version>
                        <scope>provided</scope>
                    </dependency>
                    <dependency>
                        <groupId>org.glassfish.jersey.media</groupId>
                        <artifactId>jersey-media-json-processing</artifactId>
                        <version>${version.jersey}</version>
                        <scope>provided</scope>
                    </dependency>
                    <dependency>
                        <groupId>org.glassfish.jersey.media</groupId>
                        <artifactId>jersey-media-multipart</artifactId>
                        <version>${version.jersey}</version>
                        <scope>provided</scope>
                    </dependency>
                    <dependency>
                        <groupId>org.glassfish.jersey.media</groupId>
                        <artifactId>jersey-media-sse</artifactId>
                        <version>${version.jersey}</version>
                        <scope>provided</scope>
                    </dependency>
                    <!-- if you are using Jersey client specific features -->
                    <dependency>
                        <groupId>org.glassfish.jersey.core</groupId>
                        <artifactId>jersey-client</artifactId>
                        <version>${version.jersey}</version>
                        <scope>provided</scope>
                    </dependency>

                    <!-- modules -->
                    <!--  EJBs -->
                    <dependency>
                        <groupId>com.nabisoft.tutorials</groupId>
                        <artifactId>struts2-module-ejb</artifactId>
                        <version>${project.version}</version>
                        <type>ejb</type>
                    </dependency>

                    <!--  RESTful Web Services -->
                    <dependency>
                        <groupId>com.nabisoft.tutorials</groupId>
                        <artifactId>struts2-module-service</artifactId>
                        <version>${project.version}</version>
                        <type>war</type>
                    </dependency>

                    <!--  The Struts 2 web application -->
                    <dependency>
                        <groupId>com.nabisoft.tutorials</groupId>
                        <artifactId>struts2-module-war</artifactId>
                        <version>${project.version}</version>
                        <type>war</type>
                    </dependency>

                    <!--  EAR that contains it all -->
                    <dependency>
                        <groupId>com.nabisoft.tutorials</groupId>
                        <artifactId>struts2-module-ear</artifactId>
                        <version>${project.version}</version>
                    </dependency>

                    <!-- 3rd party -->

                    <!-- need this version because of struts/tiles and commons-validator -->

                    <!-- used by struts, but they use different groupId -->
                    <dependency>
                        <groupId>org.javassist</groupId>
                        <artifactId>javassist</artifactId>
                        <version>3.17.1-GA</version>
                    </dependency>

                    <dependency>
                        <groupId>commons-digester</groupId>
                        <artifactId>commons-digester</artifactId>
                        <version>2.1</version>
                    </dependency>

                    <dependency>
                        <groupId>commons-io</groupId>
                        <artifactId>commons-io</artifactId>
                        <version>2.4</version>
                    </dependency>

                    <dependency>
                        <groupId>commons-lang</groupId>
                        <artifactId>commons-lang</artifactId>
                        <version>2.6</version>
                    </dependency>

                    <dependency>
                        <groupId>org.apache.commons</groupId>
                        <artifactId>commons-lang3</artifactId>
                        <version>3.1</version>
                    </dependency>

                    <dependency>
                        <groupId>commons-validator</groupId>
                        <artifactId>commons-validator</artifactId>
                        <version>1.4.0</version>
                    </dependency>

                </dependencies>
            </dependencyManagement>

            <dependencies>

                <!-- Java EE -->
                <!-- we don't want to do this globally -->
                <!-- 
                <dependency>
                    <groupId>javax</groupId>
                    <artifactId>javaee-api</artifactId>
                    <version>${version.javaee.api}</version>
                    <scope>provided</scope>
                </dependency>
                -->

                <!-- Logging -->      
                <dependency>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                    <version>${version.slf4j}</version>
                </dependency>

                <!-- Apache Commons -->
                <dependency>
                    <groupId>commons-io</groupId>
                    <artifactId>commons-io</artifactId>
                </dependency>

                <dependency>
                    <groupId>commons-lang</groupId>
                    <artifactId>commons-lang</artifactId>
                </dependency>

                <dependency>
                    <groupId>org.apache.commons</groupId>
                    <artifactId>commons-lang3</artifactId>
                </dependency>

                <dependency>
                    <groupId>commons-validator</groupId>
                    <artifactId>commons-validator</artifactId>
                </dependency>

                <!-- TEST -->
                <dependency>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                    <version>3.8.1</version>
                    <scope>test</scope>
                </dependency>

            </dependencies>

            <build>
                <pluginManagement>
                    <plugins>
                        <plugin>
                            <groupId>org.apache.maven.plugins</groupId>
                            <artifactId>maven-compiler-plugin</artifactId>
                            <version>${version.mvn.compiler}</version>
                            <configuration>
                                <source>${version.jdk}</source>
                                <target>${version.jdk}</target>
                            </configuration>
                        </plugin>
                        <plugin>
                            <groupId>org.apache.maven.plugins</groupId>
                            <artifactId>maven-war-plugin</artifactId>
                            <version>2.3</version>
                            <configuration>
                                <failOnMissingWebXml>true</failOnMissingWebXml>
                                <archive>
                                    <addMavenDescriptor>false</addMavenDescriptor>
                                </archive>
                            </configuration>
                        </plugin>

                    </plugins>
                </pluginManagement>
            </build>

        </project>
    

2. Maven EJB module

Our EJB module is very simple. It contains the JPA 2.1 Entity classes and the EJBs (we have only one of each). Our JPA 2.1 Entity class and our EJB is discussed below, but first please have a look at the pom.xml:

        <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

            <modelVersion>4.0.0</modelVersion>
            <artifactId>struts2-module-ejb</artifactId>
            <packaging>ejb</packaging>
            <name>struts2-module-ejb</name>
            <parent>
                <groupId>com.nabisoft.tutorials</groupId>
                <artifactId>struts2-multi-module-demo</artifactId>
                <version>1.0-SNAPSHOT</version>
            </parent>
            <dependencies>
                <!-- Java EE 7 API -->
                <dependency>
                    <groupId>javax</groupId>
                    <artifactId>javaee-api</artifactId>
                </dependency>

                <!-- JDBC drivers-->
                <dependency>
                    <groupId>postgresql</groupId>
                    <artifactId>postgresql</artifactId>
                    <version>8.4-702.jdbc4</version>
                    <scope>provided</scope>
                </dependency>

            </dependencies>

            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-ejb-plugin</artifactId>
                        <version>2.3</version>
                        <configuration>
                            <ejbVersion>3.2</ejbVersion>
                            <archive>
                                <manifest>
                                    <addClasspath>true</addClasspath>
                                </manifest>
                            </archive> 
                        </configuration>
                    </plugin>
                </plugins>
            </build>

        </project>
    

As you can see from the pom.xml we have defined a dependency to a postgresql JDBC library. This is needed because the database we use is a PostgreSQL database. Below you can see the persistence.xml which we will use for our EJB module:

        <?xml version="1.0" encoding="UTF-8"?>
        <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
          <persistence-unit name="strutsPU" transaction-type="JTA">
            <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
            <jta-data-source>jdbc/struts</jta-data-source>
            <properties>
              <property name="eclipselink.ddl-generation" value="create-tables"/>
            </properties>
          </persistence-unit>
        </persistence>
    

2.1 Implementing the Message JPA Entity class (Message.java)

Why do we need a database? Well, because we will store messages to and read messages from the database. Below you find both our JPA Entity class (Message.java) and our Stateless EJB (MessageServiceBean.java). Message.java is very simple as it only stores messages and a corresponding timestamp. Please keep in mind the @XmlRootElement annotation we added to Message.java. Our MessageServiceBean class is a simple Stateless EJB that allows to read Messages from and write Messages to the database. We will use these two classes later in our Struts 2 Action class.

        package com.nabisoft.tutorials.mavenstruts.entity;

        import java.io.Serializable;
        import java.util.Date;

        import javax.persistence.Cacheable;
        import javax.persistence.Column;
        import javax.persistence.Entity;
        import javax.persistence.GeneratedValue;
        import javax.persistence.GenerationType;
        import javax.persistence.Id;
        import javax.persistence.Table;
        import javax.persistence.Temporal;
        import javax.xml.bind.annotation.XmlRootElement;

        @Entity
        @Cacheable(false)
        @Table(name="MESSAGES")
        @XmlRootElement(name="message")
        public class Message implements Serializable {

            @Id
            @GeneratedValue (strategy=GenerationType.IDENTITY)
            private Long id;

            @Column(nullable=false, length=64)
            private String message;

            @Temporal(javax.persistence.TemporalType.TIMESTAMP)
            @Column(nullable=false)
            private Date createdOn;

            public Message() {
                super();
            }

            public Long getId() {
                return id;
            }

            public void setId(Long id) {
                this.id = id;
            }

            public String getMessage() {
                return message;
            }

            public void setMessage(String message) {
                this.message = message;
            }

            public Date getCreatedOn() {
                return createdOn;
            }

            public void setCreatedOn(Date createdOn) {
                this.createdOn = createdOn;
            }

        }
    

2.2 Implementing the MessageServiceBean (EJB)

        package com.nabisoft.tutorials.mavenstruts.ejb;

        import javax.ejb.Stateless;
        import javax.persistence.EntityManager;
        import javax.persistence.PersistenceContext;
        import javax.persistence.TypedQuery;

        import com.nabisoft.tutorials.mavenstruts.entity.Message;
        import java.util.List;

        @Stateless
        public class MessageServiceBean{

            @PersistenceContext
            private EntityManager em;


            public List<Message> findAllMessages() {
                TypedQuery<Message> query = em.createQuery("SELECT msg FROM Message msg", Message.class);
                return query.getResultList();
            }

            public Message find(Long id) {
                return em.find(Message.class, id);
            }

            public void save(Message msg) {
                em.persist(msg);        
            }

            public void remove(Long id) {
                Message message = find(id);
                if (message != null) {
                    em.remove(message);
                }
            }

            public void remove(Message message) {
                if (message != null && message.getId()!=null && em.contains(message)) {
                    em.remove(message);
                }
            }

        }
    

3. Maven Service module (war)

Our service module contains all of our RESTful web services. It uses our EJB. That's why it has a dependency to our EJB module (see the previous step). Please consider that the scope of that dependency is provided.

        <?xml version="1.0"?>
        <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

            <modelVersion>4.0.0</modelVersion>
            <artifactId>struts2-module-service</artifactId>
            <packaging>war</packaging>
            <name>struts2-module-service</name>
            <parent>
                <groupId>com.nabisoft.tutorials</groupId>
                <artifactId>struts2-multi-module-demo</artifactId>
                <version>1.0-SNAPSHOT</version>
            </parent>

            <properties>
            <!-- http://wiki.eclipse.org/M2E-WTP_FAQ -->
            <!-- <m2eclipse.wtp.contextRoot>/service</m2eclipse.wtp.contextRoot> -->
            </properties>

            <dependencies>

                <!-- Java EE 7 Web Api -->
                <dependency>
                    <groupId>javax</groupId>
                    <artifactId>javaee-web-api</artifactId>
                </dependency>

                <!-- Jersey (because we might use Jersey specific features) -->
                <dependency>
                    <groupId>org.glassfish.jersey.containers</groupId>
                    <artifactId>jersey-container-servlet</artifactId>
                </dependency>
                <dependency>
                    <groupId>org.glassfish.jersey.media</groupId>
                    <artifactId>jersey-media-json-jettison</artifactId>
                </dependency>
                <dependency>
                    <groupId>org.glassfish.jersey.media</groupId>
                    <artifactId>jersey-media-json-jackson</artifactId>
                </dependency>

                <!-- other -->
                <dependency>
                    <groupId>${project.groupId}</groupId>
                    <artifactId>struts2-module-ejb</artifactId>
                    <scope>provided</scope>
                    <type>ejb</type>
                </dependency>

            </dependencies>

            <build>
                <finalName>service</finalName>
                <plugins>
                    <!--
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-eclipse-plugin</artifactId>
                        <configuration>
                            <wtpContextName>service</wtpContextName>
                            <additionalProjectFacets>
                                <jst.web>3.0</jst.web>
                            </additionalProjectFacets>
                        </configuration>
                    </plugin>
                    -->          
                </plugins>
            </build>

        </project>
    

3.1 Defining web.xml and glassfish-web.xml

Often I configure Jersey in the web.xml, but this time I have chosen to configure our Jersey Application the "Java EE 7" way. Java EE 7 comes with JAX-RS 2.0 (JSR 339) and the default implementation is Jersey 2.0. If you prefer to stay with the web.xml configuration then make sure not to use com.sun.jersey.spi.container.servlet.ServletContainer because in Jersey 2.0 you have to use org.glassfish.jersey.servlet.ServletContainer (see hints in web.xml below)! For more information please see the official Jersey documentation.

        <?xml version="1.0" encoding="UTF-8"?>  
        <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">

          <!-- we will use the Java EE 7 REST specification instead of this: -->
          <!-- <servlet> -->
            <!-- <servlet-name>JerseyServlet</servlet-name> -->
            <!--  <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> -->   <!-- Glassfish 3 (Java EE 6) -->
            <!-- <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> -->                    <!-- Glassfish 4 (Java EE 7) -->
            <!-- <init-param> -->
                <!-- <param-name>jersey.config.server.provider.packages</param-name> -->
                <!-- <param-value>com.nabisoft.tutorials.mavenstruts.service</param-value> -->
            <!-- </init-param> -->
            <!-- <load-on-startup>1</load-on-startup> -->
          <!-- </servlet> -->

          <!--
          <servlet-mapping>
            <servlet-name>JerseyServlet</servlet-name>
            <url-pattern>/*</url-pattern>
          </servlet-mapping>
          -->

          <session-config>
            <session-timeout>30</session-timeout>
            <cookie-config>
              <name>SESSIONID</name>
            </cookie-config>
          </session-config>
        </web-app>
    

In the glassfish-web.xml file we can configure Glassfish specific stuff for our web application. We define the context-root as /service and we disable the xpoweredBy header because we don't want Glassfish to be too "chatty" (this is a little security concern, often referred to as "server obfuscation).

        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
        <glassfish-web-app error-url="">
          <context-root>/service</context-root>
          <class-loader delegate="true"/>
          <jsp-config>  
            <property name="keepgenerated" value="true">
              <description>Keep a copy of the generated servlet class java code.</description>
            </property>        
            <property name="xpoweredBy" value="false">
              <description>server obfuscation</description>
            </property>        
          </jsp-config>
        </glassfish-web-app>
    

3.2 REST configuration (ApplicationConfig.java and MyJacksonJsonProvider.java)

Next we want to configure our RESTful application because we didn't do it in the web.xml (see above). Our ApplicationConfig class configures everything we want. I have added some additional comments which should help you to understand the implementation of ApplicationConfig.java better:

        package com.nabisoft.jaxrs.application;

        import com.nabisoft.tutorials.mavenstruts.service.MessageService;
        import java.util.Collections;
        import java.util.HashMap;
        import java.util.Map;
        import java.util.Set;
        import javax.ws.rs.ApplicationPath;
        import javax.ws.rs.core.Application;
        import org.apache.log4j.Logger;

        @ApplicationPath("/")
        public class ApplicationConfig extends Application {

            private static final Logger LOGGER = Logger.getLogger(MessageService.class);

            @Override
            public Set<Class<?>> getClasses() {

                Set<Class<?>> resources = new java.util.HashSet<>();

                LOGGER.debug("REST configuration starting: getClasses()");            

                //features
                //this we will register Jackson JSON providers
                resources.add(org.glassfish.jersey.jackson.JacksonFeature.class);
                //but we could also register them manually:
                //resources.add(org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider.class);


                //we don't want everything added by netbeans.
                //unfortunately you cannot configure in the netbeans ide what you want and what not
                //addRestResourceClasses(resources);

                //instead let's do it manually:
                resources.add(com.nabisoft.jaxrs.provider.MyJacksonJsonProvider.class);
                resources.add(com.nabisoft.tutorials.mavenstruts.service.MessageService.class);

                LOGGER.debug("REST configuration ended successfully.");

                return resources;
            }

            @Override
            public Set<Object> getSingletons() {
                return Collections.emptySet();
            }

            @Override
            public Map<String, Object> getProperties() {
                Map<String, Object> properties = new HashMap<>();

                //in Jersey WADL generation is enabled by default, but we don't 
                //want to expose too much information about our apis.
                //therefore we want to disable wadl (http://localhost:8080/service/application.wadl should return http 404)
                //see https://jersey.java.net/nonav/documentation/latest/user-guide.html#d0e9020 for details
                properties.put("jersey.config.server.wadl.disableWadl", true);

                //we could also use something like this instead of adding each of our resources
                //explicitely in getClasses():
                //properties.put("jersey.config.server.provider.packages", "com.nabisoft.tutorials.mavenstruts.service");


                return properties;
            }

            /**
             * Do not modify addRestResourceClasses() method.
             * It is automatically re-generated by NetBeans REST support to populate
             * given list with all resources defined in the project.
             */
            private void addRestResourceClasses(Set<Class<?>> resources) {
                resources.add(com.nabisoft.jaxrs.provider.MyJacksonJsonProvider.class);
                resources.add(com.nabisoft.tutorials.mavenstruts.service.MessageService.class);
                resources.add(org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider.class);
                resources.add(org.codehaus.jackson.jaxrs.JacksonJsonProvider.class);
                resources.add(org.codehaus.jackson.jaxrs.JsonMappingExceptionMapper.class);
                resources.add(org.codehaus.jackson.jaxrs.JsonParseExceptionMapper.class);
                resources.add(org.glassfish.jersey.server.wadl.internal.WadlResource.class);
            }

        }
    

Now we also want to fine-tune the behavior of Jackson. For more details see the official Jersey Documentation. Here is the source code of MyJacksonJsonProvider.java:

        package com.nabisoft.jaxrs.provider;

        import javax.inject.Singleton;
        import javax.ws.rs.Consumes;
        import javax.ws.rs.Produces;
        import javax.ws.rs.core.MediaType;
        import javax.ws.rs.ext.ContextResolver;
        import javax.ws.rs.ext.Provider;
        import org.apache.log4j.Logger;
        import org.codehaus.jackson.map.DeserializationConfig;

        import org.codehaus.jackson.map.ObjectMapper;
        import org.codehaus.jackson.map.annotate.JsonSerialize;

        /**
         * Jackson JSON processor could be controlled via providing a custom Jackson ObjectMapper instance. 
         * This could be handy if you need to redefine the default Jackson behavior and to fine-tune how 
         * your JSON data structures look like (copied from Jersey web site). * 
         * @see https://jersey.java.net/documentation/latest/media.html#d0e4799
         */

        @Provider
        @Produces(MediaType.APPLICATION_JSON)
        @Consumes(MediaType.APPLICATION_JSON)
        @Singleton
        public class MyJacksonJsonProvider implements ContextResolver<ObjectMapper> {

            private static final Logger LOGGER = Logger.getLogger(MyJacksonJsonProvider.class);
            private static final ObjectMapper MAPPER = new ObjectMapper();

            static {
              MAPPER.setSerializationInclusion(JsonSerialize.Inclusion.NON_EMPTY);
              MAPPER.disable(DeserializationConfig.Feature.USE_GETTERS_AS_SETTERS);
              //MAPPER.configure(DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
            }

            public MyJacksonJsonProvider() {
                LOGGER.debug("Instantiate MyJacksonJsonProvider");
            }

            @Override
            public ObjectMapper getContext(Class<?> type) {
                LOGGER.debug("MyJacksonProvider.getContext() called with type: "+type);
                return MAPPER;
            } 
        }
    

3.3 Implementing the RESTful web service (MessageService.java)

Let's implement some RESTful services now (see MessageService.java). The method getAllMessages() uses an injected instance of our EJB to read all the messages from the database. We implemented the EBJ in one of the steps above. We will call getAllMessages() later via jQuery (ajax call).

        package com.nabisoft.tutorials.mavenstruts.service;

        import com.nabisoft.tutorials.mavenstruts.ejb.MessageServiceBean;
        import com.nabisoft.tutorials.mavenstruts.entity.Message;
        import java.util.Date;
        import java.util.List;
        import javax.ejb.EJB;
        import org.apache.log4j.Logger;

        import javax.ejb.Stateless;
        import javax.ws.rs.GET;
        import javax.ws.rs.Path;
        import javax.ws.rs.Produces;
        import javax.ws.rs.core.MediaType;

        @Path("/message")
        @Stateless
        public class MessageService {

            @EJB
            private MessageServiceBean msgService;

            private static final Logger LOGGER = Logger.getLogger(MessageService.class);

            @GET
            @Path("ping")
            public String getServerTime() {
                LOGGER.debug("RESTful Service 'MessageService' is running ==> ping");
                return "received ping on "+new Date().toString();
            }

            @GET
            @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
            public List<Message> getAllMessages() throws Exception{
                List<Message> messages = null;

                //we could also use JNDI lookup of our EJB
                //MessageServiceBean msgService = (MessageServiceBean)new InitialContext().lookup("java:global/struts2-module-ear/struts2-module-ejb/MessageServiceBean");
                messages = msgService.findAllMessages();

                LOGGER.debug("getAllMessages(): found "+messages.size()+" message(s) on DB");

                return messages; //do not use Response object because this causes issues when generating XML automatically
            }

        }
    

4. Maven Struts 2 module

Our Struts 2 module uses Apache Tiles 3 as a templating framework for creating web pages with a consistent look and feel. It is also used for both page decorating and componentization. We will use the Tiles Plugin to make Struts 2 and Tiles 3 play together smoothly. For more information about Struts please refer to the Struts 2.x docs We don't want to enable all the Tiles 3 features, therefore we simply exclude what we don't want in the pom.xml (see below). Our Struts 2 module also uses the EJB module we implemented in the previous steps. Below you see the pom.xml of our Struts 2 module:

        <?xml version="1.0"?>
        <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

            <modelVersion>4.0.0</modelVersion>
            <artifactId>struts2-module-war</artifactId>
            <packaging>war</packaging>
            <name>struts2-module-war</name>
            <parent>
                <groupId>com.nabisoft.tutorials</groupId>
                <artifactId>struts2-multi-module-demo</artifactId>
                <version>1.0-SNAPSHOT</version>
            </parent>

            <properties>
                <!-- http://wiki.eclipse.org/M2E-WTP_FAQ -->
                <!-- <m2eclipse.wtp.contextRoot>/myroot</m2eclipse.wtp.contextRoot> -->
            </properties>

            <dependencies>

                <!-- Java EE 7 Web API -->
                <dependency>
                    <groupId>javax</groupId>
                    <artifactId>javaee-web-api</artifactId>
                </dependency>

                <!-- We will call EBJs in our sruts actions -->
                <dependency>
                    <groupId>${project.groupId}</groupId>
                    <artifactId>struts2-module-ejb</artifactId>
                    <scope>provided</scope>
                    <type>ejb</type>
                </dependency>

                <!-- Struts2 -->
                <dependency>
                    <groupId>org.apache.struts</groupId>
                    <artifactId>struts2-core</artifactId>
                    <version>${version.struts}</version>
                </dependency>

                <dependency>
                    <groupId>org.apache.struts</groupId>
                    <artifactId>struts2-convention-plugin</artifactId>
                    <version>${version.struts}</version>
                </dependency>

                <!-- Tiles 3 -->
                <dependency>
                    <groupId>org.apache.struts</groupId>
                    <artifactId>struts2-tiles3-plugin</artifactId>
                    <version>${version.struts}</version>
                    <exclusions>
                        <exclusion>
                            <groupId>org.slf4j</groupId>
                            <artifactId>jcl-over-slf4j</artifactId>
                        </exclusion>
                        <!-- Exclusions because not needed by SimpleTilesListener (see web.xml) -->
                        <exclusion>
                            <groupId>org.apache.tiles</groupId>
                            <artifactId>tiles-el</artifactId>
                        </exclusion>
                        <exclusion>
                            <groupId>org.apache.tiles</groupId>
                            <artifactId>tiles-mvel</artifactId>
                        </exclusion>
                        <exclusion>
                            <groupId>org.apache.tiles</groupId>
                            <artifactId>tiles-ognl</artifactId>
                        </exclusion>              
                        <exclusion>
                            <groupId>org.apache.tiles</groupId>
                            <artifactId>tiles-velocity</artifactId>
                        </exclusion>
                        <exclusion>
                            <groupId>org.apache.tiles</groupId>
                            <artifactId>tiles-freemarker</artifactId>
                        </exclusion>
                        <exclusion>
                            <groupId>org.apache.tiles</groupId>
                            <artifactId>tiles-request-mustache</artifactId>
                        </exclusion>              
                    </exclusions>
                </dependency>

            </dependencies>

            <build>
                <!-- sometimes this helps to set the contextRoot of webapp -->
                <!-- <finalName>/struts2-module-war</finalName>  -->
                <plugins>
                    <!-- 
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-eclipse-plugin</artifactId>
                        <configuration>
                            <wtpContextName>ROOT</wtpContextName>
                            <additionalProjectFacets>
                                <jst.web>3.0</jst.web>
                            </additionalProjectFacets>
                        </configuration>
                    </plugin>
                    -->

                    <!-- resources not copied via eclipse maven plugin for some reason, but with native maven build -->
                    <!-- 
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-war-plugin</artifactId>
                        <configuration>
                            <webResources>
                                <resource>                            
                                    <directory>src/main/i18n</directory>
                                    <targetPath>WEB-INF/classes</targetPath>
                                </resource>
                          </webResources>
                        </configuration>
                    </plugin>
                    -->

                </plugins>
            </build>

        </project>
    

4.1 Defining web.xml and glassfish-web.xml

The web.xml and glassfish-web.xml files are both self explaining (see my comments in the files). There are different ways to configure Tiles and I have chosen to use the the SimpleTilesListener. Actually, I wanted to change the path from where the tiles.xml is loaded. Unfortunately, I found a bug in the documented behavior and I reported the bug at https://issues.apache.org/jira/browse/TILES-570. That means we have to stick with the default location at /WEB-INF/tiles.xml. In the web.xml you can also see how we configure the Struts 2 framework. You could also use the web.xml file to disable the xpoweredBy header (I typically do this for server obfuscation reasons), but instead I decided to disable the xpoweredBy header in the glasfish-web.xml file becaue I believe this is cleaner (see my comments in web.xml).

        <?xml version="1.0" encoding="UTF-8"?>
        <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">

            <display-name>Maven multi-module Struts2 project</display-name>

            <welcome-file-list>
                <welcome-file>index.jsp</welcome-file>
            </welcome-file-list>

            <session-config>
                <session-timeout>30</session-timeout>
                <cookie-config>
                    <name>SESSIONID</name>
                </cookie-config>
            </session-config>

            <!-- tiles -->
            <!-- see see http://tiles.apache.org/framework/config-reference.html -->
            <!-- see http://tiles.apache.org/tutorial/configuration.html -->
            <!-- see http://tiles.apache.org/config-reference.html -->
            <listener>        
                <!-- <listener-class>org.apache.tiles.extras.complete.CompleteAutoloadTilesListener</listener-class> -->
                <!-- if using complete, then make sure to remove maven excludes -->

                <!-- 
                    - loads the "/WEB-INF/tiles.xml" file per default, be we will reconfigure to load /WEB-INF/classes/tiles.xml;
                    - allows support for JSP, Servlets and Portlets;
                    - no expression language is allowed;
                    - wildcard expressions can be used to declare definition names.
                -->
                <listener-class>org.apache.tiles.web.startup.simple.SimpleTilesListener</listener-class>
            </listener>

            <!-- this does not work at all, see bug report at https://issues.apache.org/jira/browse/TILES-570 -->
            <!--
            <context-param>        
                <param-name>org.apache.tiles.impl.BasicTilesContainer.DEFINITIONS_CONFIG</param-name>
                <param-name>org.apache.tiles.definition.DefinitionsFactory.DEFINITIONS_CONFIG</param-name>
                <param-value>/WEB-INF/classes/tiles.xml</param-value>        
            </context-param>
            -->



            <!-- Struts 2 -->
            <filter>
                <filter-name>struts2</filter-name>
                <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
            </filter>
            <filter-mapping>
                <filter-name>struts2</filter-name>
                <url-pattern>/*</url-pattern>
            </filter-mapping>

            <!-- we will do this in glassfish-web.xml -->
            <!--
            <servlet>
                <servlet-name>jsp</servlet-name>
                <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
                <init-param>
                    <param-name>xpoweredBy</param-name>
                    <param-value>false</param-value>
                </init-param>
                <init-param>
                    <param-name>keepgenerated</param-name>
                    <param-value>true</param-value>
                </init-param>
                <load-on-startup>3</load-on-startup>
            </servlet>
            -->

        </web-app>
    
        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
        <glassfish-web-app error-url="">  
          <context-root>/struts2-module-war</context-root>
          <class-loader delegate="true"/>
          <!-- see http://docs.oracle.com/cd/E18930_01/html/821-2417/beatx.html#scrolltoc -->
          <jsp-config>
            <property name="keepgenerated" value="true">
              <description>Keep a copy of the generated servlet class java code.</description>
            </property>        
            <property name="xpoweredBy" value="false">
              <description>server obfuscation</description>
            </property>
          </jsp-config>
        </glassfish-web-app>
    

4.2 Defining struts.xml and struts.properties

The struts.xml file has not much content. The devMode is false, but you could change it to true in order to receive some more information during development. I don't like the "*.do" extension that Struts applications typically use so I have chosen to disable the action extension. As you can see in the struts.xml we have introduced a new result-type called "tiles" which refers to org.apache.struts2.views.tiles.TilesResult. Furthermore, we have three tiles results: app.error, app.index and app.addMessage.

        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE struts PUBLIC
            "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
            "http://struts.apache.org/dtds/struts-2.3.dtd">

        <struts>

            <constant name="struts.devMode" value="false" />
            <constant name="struts.action.extension" value="," /> <!-- do not use empty string here! -->    

            <package name="my-default" extends="struts-default" abstract="true">

                <result-types>
                    <result-type name="tiles" class="org.apache.struts2.views.tiles.TilesResult" />
                </result-types>

                <interceptors>

                    <interceptor-stack name="my-defaultStack">
                        <interceptor-ref name="defaultStack">
                            <!-- see http://struts.apache.org/release/2.1.x/docs/i18n-interceptor.html -->
                            <param name="i18n.attributeName">org.apache.tiles.LOCALE</param>  <!-- make sure tiles also knows the locale -->
                        </interceptor-ref>
                    </interceptor-stack>

                </interceptors>

                <default-interceptor-ref name="my-defaultStack" />
                <default-action-ref name="404-not-found" />

                <!-- see http://www.packtpub.com/article/exceptions-and-logging-in-apache-struts2 -->
                <global-results>
                    <result name="error" type="tiles">app.error</result>
                </global-results>
                <global-exception-mappings>
                    <exception-mapping exception="java.lang.Throwable" result="error"></exception-mapping>
                </global-exception-mappings>

            </package>

            <package name="root" extends="my-default" namespace="/">
                <!--
                If no class attribute is specified the framework will assume success and
                render app.index
                If no name value for the result node is specified the success value is the default
                -->        
                <action name="">
                    <result type="tiles">app.index</result>
                </action>

                <!--
                If the URL is /addMessage then call the execute method of class MessageAction.
                If the result returned by the execute method is success then render app.addMessage
                -->
                <action name="addMessage" class="com.nabisoft.tutorials.mavenstruts.struts2.action.MessageAction" method="execute">
                    <result type="tiles" name="success">app.addMessage</result>
                    <result type="tiles" name="input">app.index</result>
                    <result type="tiles" name="error">app.index</result>
                </action>
            </package>


        </struts>
    

The struts.properties file is another configuration file where we can set some struts properties. See http://struts.apache.org/release/2.2.x/docs/strutsproperties.html for more information.

        struts.devMode=false
        #struts.locale=en
        #we don't want to show extensions, but we will configure this in struts.xml
        #struts.action.extension= ,
        struts.configuration.xml.reload=false
        struts.i18n.reload=true
        struts.custom.i18n.resources=global
        struts.enable.DynamicMethodInvocation=false
        #struts.enable.SlashesInActionNames=true
    

4.3 Internationalization / i18n

Internationalization is an easy job in Struts 2. We will simply create a global.properties file because we have defined it in our struts.properties (see above). global.properties is for the default language, for English we also create global_en.properties and for German global_de.properties files. Since I want English to be the default language global_en.properties will simply be empty. In this case you could also leave the file global_en.properties away but unfortunately I was facing some i18n issues. It seems that depending on the JRE locale struts chooses the corresponding "default"-locale. Offering an empty global_en.properties file solved my issue - it is enough for me as a workaround. All English descriptions will be in global.properties. This issue was available in previous versions of Struts 2 and I did not verify if it is still available in the latest version of Struts 2 (which is currently version 2.3.15.1).

        submit=submit
        your.message-label=Your message
        welcome=Welcome to Struts 2!
        error.enter.message=Please enter a Message!
        action.err.persist.message=Error saving message to DB
    

        submit=Absenden
        your.message-label=Ihre Nachricht
        welcome=Willkomen zu Struts 2!
        error.enter.message=Please enter a message!
        action.err.persist.message=Fehler beim persistieren der Nachricht
    

        #empty file
    

4.4 Implementing the action class (MessageAction.java)

Our one and only Action class is called MessageAction.java. It creates a new Message instance, looks up our MessageServiceBean via JNDI and uses it to save the new message to the database. I have also added some basic validation to remind you of the options you have regarding validation in Struts.

        package com.nabisoft.tutorials.mavenstruts.struts2.action;

        import java.util.Date;

        import javax.naming.InitialContext;

        import com.nabisoft.tutorials.mavenstruts.ejb.MessageServiceBean;
        import com.nabisoft.tutorials.mavenstruts.entity.Message;
        import com.opensymphony.xwork2.ActionSupport;
        import java.util.List;
        import org.apache.log4j.Logger;

        public class MessageAction extends ActionSupport {

            private static final Logger LOGGER = Logger.getLogger(MessageAction.class);

            private String message;
            private Date nowDate;
            private MessageServiceBean msgService;

            @Override
            public void validate(){
                LOGGER.debug("validate()...");
                if (message==null || message.length()==0){
                    addActionError(getText("error.enter.message"));
                    LOGGER.debug("vaidate(): vaidation failed because passed message is null/empty");
                }
            }

            @Override
            public String execute() throws Exception {
                LOGGER.debug("execute()...");

                this.nowDate = new Date();

                //JNDI lookup of our EJB
                msgService = (MessageServiceBean)new InitialContext().lookup("java:global/struts2-module-ear/struts2-module-ejb/MessageServiceBean");

                Message msg = new Message();
                msg.setCreatedOn(this.nowDate);
                msg.setMessage(this.message);

                msgService.save(msg);

                if (msg.getId() == null){
                    String errorMessage = getText("action.err.persist.message");
                    addActionError(errorMessage);
                    LOGGER.error(errorMessage+" (message was ==> "+this.message+")");
                    return ERROR;
                }        
                return SUCCESS;
            }

            public String getMessage() {
                return message;
            }

            public void setMessage(String message) {
                this.message = message;
            }

            public Date getNowDate() {
                return nowDate;
            }

            public List<Message> getMessages(){
                if (this.msgService != null){
                    return msgService.findAllMessages();
                }
                return null;
            }

        }
    

4.5 Defining tiles.xml

In the tiles.xml file we define our templates. In the tiles_de.xml file we make use of the i18n features of Tiles. I just added this file to remind you what Tiles can do for you. But to make i18n of Struts and Tiles work smoothly together we had to configure the i18n interceptor inside our struts.xml (see corresponding step above).

        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE tiles-definitions PUBLIC
            "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN"
            "http://tiles.apache.org/dtds/tiles-config_2_1.dtd">

        <tiles-definitions>

            <!-- default definition -->
            <!-- 3 rows, one column -->
            <definition name=".classicLayout" template="/WEB-INF/layouts/classic.jsp">
                <put-attribute name="title" value="Maven multi module Struts 2 project - Welcome Page" />
                <put-attribute name="header" value="/WEB-INF/headers/indexHeader.jsp" />
                <put-attribute name="content" value="/WEB-INF/contents/indexContent.jsp" />
                <put-attribute name="footer" value="/WEB-INF/footers/indexFooter.jsp" />
            </definition>

            <!-- global errors -->
            <definition name="app.error" extends=".classicLayout">
                <put-attribute name="title" value="Error" />
                <put-attribute name="content" value="/WEB-INF/contents/error.jsp" />
            </definition>

            <definition name="app.index" extends=".classicLayout">
            </definition>

            <definition name="app.addMessage" extends=".classicLayout">
                <put-attribute name="title" value="SUCCESS - Thank you for entering a message" />
                <put-attribute name="content" value="/WEB-INF/contents/messages.jsp" />
            </definition>

        </tiles-definitions>
    

        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN" "http://tiles.apache.org/dtds/tiles-config_2_1.dtd">
        <tiles-definitions>

            <!-- default definition -->
            <!-- 3 rows, one column -->
            <definition name=".classicLayout" template="/WEB-INF/layouts/classic.jsp">
                <put-attribute name="title" value="Maven multi module Struts 2 Projekt - Willkommen" />
                <put-attribute name="header" value="/WEB-INF/headers/indexHeader.jsp" />
                <put-attribute name="content" value="/WEB-INF/contents/indexContent.jsp" />
                <put-attribute name="footer" value="/WEB-INF/footers/indexFooter.jsp" />
            </definition>

        </tiles-definitions>
    

4.6 Implementing the default layout (jsp, jQuery, CDN)

In the tiles.xml we have defined our layout (classic.jsp). It uses Tiles to insert the header, content and footer. As I told you earlier we will use jQuery to execute ajax calls. That means we have to load jQuery. We will use jQuery 2.0.3, but we will fallback to jQuery 1.10.2 in case the client's browser is Internet Explorer version 8 or lower (i.e. IE 8, IE 7 and IE 6). jQuery 2.x does only support IE 9 and above and the 1.x branch supports IE 6, IE 7 and IE 8. For more information please refer to the jQuery 2.0 release notes. I added this snippet to show you how you can use jQuery and still support older IE versions. I have also added jQuery UI although we will not use it in this tutorial. It is always a good idea to load resources from a CDN (Content Delivery Network) when ever you can do that. That also means you should always have a fallback strategy in case the CDN is for some reason not accessible. Therefore I have added some code that tries to load the JavaScript libs mentioned above from a CDN and if that fails the libs will be loaded from the local server. I have added plenty of comments that should help you to understand the source code. Below you see the contents of classic.jsp:

        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false"%>
        <%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles"%>
        <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
        <%@ taglib prefix="s" uri="/struts-tags" %>
        <!DOCTYPE html>
        <html>
            <head>
                <!-- see from http://tiles.apache.org/tutorial/basic/pages.html for some nice explanations -->
                <title><tiles:getAsString name="title" /></title>

                <script>
                    //just in case we do not have a console ...
                    if (!window.console){
                        window.console = {};

                        window.console.log = function(msg){
                            alert(msg);
                        };

                        window.console.error = window.console.log;
                    }
                </script>

                <!--[if lt IE 9]>
                    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.js"></script>
                    <script>
                        //make sure jq is really loaded from google, else load it from our local server
                        if (!window.jQuery){
                            console.log("IE < IE 9: load jquery 1.10.2 from local server...");
                            document.write(unescape("%3Cscript src='<%=request.getContextPath() %>/js/jquery-1.10.2.js' type='text/javascript'%3E%3C/script%3E"));
                        }else{
                            console.log("IE < IE 9: we already have jquery "+$().jquery+" (jqQuery 1.10.2 will not be loaded from local server)");
                        }
                    </script>
                <![endif]-->

                <script>
                    //make sure jq is really loaded from google, else load it from our local server
                    if (!window.jQuery){
                        console.log("Try to load jquery 2.0.3 from Google CDN");
                        document.write(unescape("%3Cscript src='//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.js' type='text/javascript'%3E%3C/script%3E"));
                    } else{
                        console.log("we already have jquery "+$().jquery+" (jQuery 2.0.3 will not be loaded from Google CDN)");
                    }
                </script>

                <script>
                    //if it is still not loaded the load it from own server
                    if (!window.jQuery){
                        console.log("Try to load jquery 2.0.3 from local server");
                        document.write(unescape("%3Cscript src='<%=request.getContextPath() %>/js/jquery-2.0.3.js' type='text/javascript'%3E%3C/script%3E"));
                    } else{
                        console.log("we already have jquery "+$().jquery +" (jQuery 2.0.3 will not be loaded from local server)");
                    }
                </script>

                <!-- jquery ui integration from jquery cdn (content delivery network) -->
                <script src="//code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
                <link href="//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css"/>
                <script>
                    //if it is still not loaded the load it from own server
                    if (!window.jQuery.ui){
                        console.log("Try to load jquery ui 1.10.3 from local server");

                        //non-blocking loading of css from local server
                        var head = document.getElementsByTagName('head')[0];
                        var link = document.createElement('link');
                        link.href = '<%=request.getContextPath() %>/css/jquery-ui-1.10.3.css';
                        link.type = 'text/css';
                        link.rel = 'stylesheet';
                        head.appendChild(link);

                        document.write(unescape("%3Cscript src='<%=request.getContextPath() %>/js/jquery-ui-1.10.3.js' type='text/javascript'%3E%3C/script%3E"));
                    } else{
                        console.log("we already have jquery ui " + window.jQuery.ui.version +" (jQuery ui 1.10.3 will not be loaded from local server)");
                    }
                </script>

                <script src="<%=request.getContextPath() %>/js/json2.js"></script>

                <link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/styles.css" />

                <s:head/>

            </head>
            <body>
                <div class="body">

                    <div class="header">
                        <tiles:insertAttribute name="header" />
                    </div>

                    <div class="content">
                        <tiles:insertAttribute name="content" />
                    </div>

                    <div class="footer">
                        <tiles:insertAttribute name="footer" />
                    </div>

                </div>
            </body>
        </html>
    

4.7 Implementing the Tiles fragments (jsp)

We have a simple layout (see classic.jsp) which has a header, a footer and in between some content ("3 rows" template). Below you can see the Tiles footer and header fragments. Only the content should change, both header and footer stay the same on every page.

        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false"%>
        <div>
            This is the header content (indexHeader.jsp)
        </div>
    
        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false"%>
        <div>
            This is the footer content (indexFooter.jsp)
        </div>    
    

We have three different content fragments: indexContent.jsp, messages.jsp and error.jsp. Our indexContent.jsp fragment is for the initial screen. It allows you to enter a message and save it to the database. It also makes an ajax call to receive all the massages from the database and shows them in a simple list. The messages.jsp fragment is actually the success page which tells you that your message has been saved successfully to the database. It also displays the list of massages, but this time instead of making an ajax call we use an EJB inside our action class (MessageAction) to retrieve the list of all available messages from the database. The error.jsp fragment shows nothing but a simple error message. Below you see the sources of each of the three fragments:

        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false"%>
        <%@ taglib prefix="s" uri="/struts-tags"%>

        <h1>
            <s:text name="welcome" />
        </h1>

        <p>
            <s:url id="localeEN" namespace="/" action="">
                <s:param name="request_locale">en</s:param>
            </s:url>
            <s:url id="localeDE" namespace="/" action="">
                <s:param name="request_locale">de</s:param>
            </s:url>
            <s:a href="%{localeEN}">English</s:a>
            <s:a href="%{localeDE}">German</s:a>
        </p>

        <s:if test="hasActionErrors()">
            <div id="fieldErrors">
                <s:actionerror />
            </div>
        </s:if>

        <s:form action="addMessage" namespace="/" method="post" name="myForm" theme="xhtml">
            <s:textfield name="message" size="40" maxlength="40" required="true" key="your.message-label" />
            <s:submit key="submit" />
        </s:form>

        <h2>Messages already saved to DB:</h2>
        <ul id="messegaList"></ul>

        <script>
            $(function(){
                "use strict";

                var destinationUrl = "/service/message";

                $.ajax({
                    url: destinationUrl,
                    type: "GET",
                    cache: false,
                    dataType: "json",

                    success: function (data, textStatus, jqXHR){
                        var docfrag, ul, li, i;
                        console.log("message list retrieved from backend: OK");

                        ul = document.getElementById("messegaList");

                        docfrag = document.createDocumentFragment();
                        if ($.isArray(data) && data.length){
                            for (i=0; i < data.length; i++){
                                li = document.createElement("li");
                                li.textContent = data[i].message;
                                docfrag.appendChild(li);
                            }
                            ul.appendChild(docfrag);
                        }else{
                            li = document.createElement("li");
                            li.textContent = "No messages available";
                            ul.appendChild(li);
                        }

                    },

                    error: function (jqXHR, textStatus, errorThrown){
                        console.error("message list retrieved from backend: ERROR (url = "+destinationUrl+")");
                        console.error("HTTP STATUS: "+jqXHR.status +" for url = "+destinationUrl);
                    },

                    complete: function(jqXHR, textStatus){
                        //alert("complete");
                        //i.e. hide loading spinner
                    },

                    statusCode: {
                        404: function() {
                            console.error("404 ERROR: REST service not found: "+destinationUrl);
                        }
                    }
                });

                //event.preventDefault();
                return false;
            });
        </script>
    

        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false"%>
        <%@ taglib prefix="s" uri="/struts-tags" %>

        <h2>Thank you for your message on <s:property value="nowDate" /></h2>

        <p>Your Message was:</p>
        <p>
            <s:property value="message" />
        </p>

        <h2>Messages already saved to DB:</h2>
        <ul id="messegaList">
            <s:if test="%{getMessages().isEmpty()}">
                <li>No messages available</li>
            </s:if>
            <s:else>
                <s:iterator value="messages">
                    <li><s:property value="message"/></li>
                </s:iterator>
            </s:else>
        </ul>
    

        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false"%>
        <h2>An error has occurred</h2>
    

5. Maven EAR module

We will deploy our complete application as a Java EE ear file. Below you can see the content of the pom.xml file of our Maven ear module. Please consider how the maven-ear-plugin is configured, I have also added some comments for you. If you go back and check the pom.xml files of our other modules then you will see that some of them have dependencies with scope "provided". Together with the skinnyWars option of the maveb-ear-plugin we can make sure to include the libraries only once instead of having them in each and every compiled Maven artifact. This allows us to have much smaller struts2-module-ear.ear files in the end. Also keep in mind that we are referencing EJBs via portable JNDI names.

        <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

            <modelVersion>4.0.0</modelVersion>
            <artifactId>struts2-module-ear</artifactId>
            <packaging>ear</packaging>
            <name>struts2-module-ear</name>
            <parent>
                <groupId>com.nabisoft.tutorials</groupId>
                <artifactId>struts2-multi-module-demo</artifactId>
                <version>1.0-SNAPSHOT</version>
            </parent>

            <dependencies>

                <dependency>
                    <groupId>${project.groupId}</groupId>
                    <artifactId>struts2-module-ejb</artifactId>
                    <type>ejb</type>
                </dependency>

                <dependency>
                    <groupId>${project.groupId}</groupId>
                    <artifactId>struts2-module-service</artifactId>
                    <type>war</type>
                </dependency>

                <dependency>
                    <groupId>${project.groupId}</groupId>
                    <artifactId>struts2-module-war</artifactId>
                    <type>war</type>
                </dependency>

            </dependencies>

            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-ear-plugin</artifactId>
                        <version>2.8</version>
                        <configuration>
                            <!--<generateApplicationXml>true</generateApplicationXml>--> <!-- default: true -->
                            <applicationName>struts2-module-ear</applicationName>
                            <version>6</version>    <!-- 7 is not supported yet, but 6 should work as os now... -->
                            <initializeInOrder>true</initializeInOrder>
                            <defaultLibBundleDir>lib</defaultLibBundleDir>
                            <skinnyWars>true</skinnyWars>
                            <archive>
                                <manifest>
                                    <addClasspath>true</addClasspath>
                                </manifest>
                            </archive>
                            <modules>
                                <ejbModule>
                                    <groupId>com.nabisoft.tutorials</groupId>
                                    <artifactId>struts2-module-ejb</artifactId>
                                    <!-- IMPORTANT: must be in sync for JNDI references -->
                                    <bundleFileName>struts2-module-ejb.jar</bundleFileName>
                                </ejbModule>

                                <webModule>
                                    <groupId>com.nabisoft.tutorials</groupId>
                                    <artifactId>struts2-module-service</artifactId>
                                    <contextRoot>/service</contextRoot>
                                </webModule>

                                <webModule>
                                    <groupId>com.nabisoft.tutorials</groupId>
                                    <artifactId>struts2-module-war</artifactId>
                                    <contextRoot>/struts2-module-war</contextRoot>
                                </webModule>

                            </modules>
                        </configuration>
                    </plugin>
                </plugins>
            </build>

        </project>
    

But that's not all. I have placed a glassfish-resources.xml file inside our ear module. It contains all the resources we want to add to our Glassfish 4 server. In our case we only have a JDBC resource. We will add this resource file to Glassfish later by executing an asadmin command.

        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
        <resources>

            <jdbc-connection-pool 
                  name="struts-Pool"
                  allow-non-component-callers="false" 
                  associate-with-thread="false"
                  connection-creation-retry-attempts="0" 
                  connection-creation-retry-interval-in-seconds="10" 
                  connection-leak-reclaim="false" 
                  connection-leak-timeout-in-seconds="0" 
                  connection-validation-method="auto-commit" 
                  datasource-classname="org.postgresql.ds.PGSimpleDataSource" 
                  fail-all-connections="false" 
                  idle-timeout-in-seconds="300" 
                  is-connection-validation-required="false" 
                  is-isolation-level-guaranteed="true" 
                  lazy-connection-association="false" 
                  lazy-connection-enlistment="false" 
                  match-connections="false" 
                  max-connection-usage-count="0" 
                  max-pool-size="32" 
                  max-wait-time-in-millis="60000" 
                  non-transactional-connections="false" 
                  ping="false"
                  pool-resize-quantity="2" 
                  pooling="true" 
                  res-type="javax.sql.DataSource" 
                  statement-cache-size="0" 
                  statement-leak-reclaim="false" 
                  statement-leak-timeout-in-seconds="0" 
                  statement-timeout-in-seconds="-1" 
                  steady-pool-size="8" 
                  validate-atmost-once-period-in-seconds="0" 
                  wrap-jdbc-objects="false">
                <property name="serverName" value="localhost"/>
                <property name="portNumber" value="5432"/>
                <property name="databaseName" value="db_struts"/>
                <property name="User" value="struts"/>
                <property name="Password" value="struts"/>
                <property name="URL" value="jdbc:postgresql://localhost:5432/db_struts"/>
                <property name="driverClass" value="org.postgresql.Driver"/>
            </jdbc-connection-pool>
            <jdbc-resource enabled="true" jndi-name="jdbc/struts" object-type="user" pool-name="struts-Pool"/>

        </resources>
    

6. Adding JDBC resource to Glassfish 4

Now we want to tell Glassfish where our DB is, which credentials to use when connecting to the DB and so on. As I have mentioned earlier we want to connect to a PorstgreSQL database. For that purpose we added a glassfish-resources.xml file to our ear module in the previous step. Have a look at my other tutorial for a more detailed description about how to add JDBC resources to Glassfish. Below you can see the asadmin command you need to execute once before you deploy our little application.

        #you might not need "--secure"
        asadmin --secure add-resources glassfish-resources.xml

        #you can also specify the complete path to your glassfish-resources.xml file
        asadmin --secure add-resources /home/myuser/struts2-multi-module-demo/struts2-module-ear/src/main/setup/glassfish-resources.xml
    

7. Running the application

I expect you have already installed Glassfish 4, a PostgreSQL database and Maven. I also expect you have downloaded the Maven project. Please make sure you have un-zipped the zip archive, fired up your terminal and changed your directory to struts2-multi-module-demo (i.e. with "cd ~/struts2-multi-module-demo" in case you have extracted the zip archive to your home directory). The commands in the next steps assume you are in struts2-multi-module-demo.

7.1 Add JDBC resource to Glassfish 4

Before you deploy our application please make sure you have walked through the steps for adding JDBC resources to Glassfish. You only need to this once.

7.2 Create the ear with Maven

Use Maven to build everything:

        #this is our parent project
        cd ~/struts2-multi-module-demo 

        mvn clean install
    

7.3 Deploy ear to Glassfish 4 with asadmin command

Now use the following commands to deploy the ear file we just created to Glassfish:

        #start glassfish if not already started
        asadmin start-domain domain1

        #deploy now
        cd ~/struts2-multi-module-demo/
        asadmin deploy ./struts2-module-ear/target/struts2-module-ear-1.0-SNAPSHOT.ear

        #let's see what is currently deployed
        asadmin list-applications

        #this is how you could undeploy
        asadmin undeploy struts2-module-ear
    

7.4 Run application in your browser

To see the result of your application you need to enter one of the following URLs into a browser of your choice (see below). The ping service is just for testing purposes. The List all messages (RESTful) api returns either XML or JSON. If called from your browser you will typically get XML. If you want JSON then all you have to do is setting your accept http header to "application/json".


8. Additional hints

As you might have recognized our application uses log4j. I did not mention anything about configuration of log4j. Our log4j configuration is not perfect at all and you should adapt it the way you want it. I have used Netbeans 7.3.1 during development of this tutorial. At first I used Eclipse 4.3 (Kepler) but I found it horrible because the deployment features of Eclipse to Glassfish 4 were hindering my development progress heavily. The problem with Eclipse is that it deploys the ear whenever I press the save button, but unfortunately it deploys multiple times and for some reason it deploys the war files within our ear to context-roots which I did not want. A project clean helped sometimes, but not always. My feeling was that the Eclipse plugins do not consider the contextRoot definitions of the wars as specified for the maven-ear-plugin. Even the glassfish-web.xml seem not to be considers correctly in my case. I have checked that the maven-ear-plugin generates the correct application.xml for our ear, but I guess that the Eclipse Maven features do not always consider that application.xml for some reason. The funny part is that when triggering a re-deployment (i.e. by making a change and pressing the save button) the correct context-roots are used. This does not help me to be more productive, so I switched back to Netbeans (which I always liked). There is still one big issue which is not related to neither Netbeans nor Eclipse: If you try to follow this tutorial with Netbeans or Eclipse (or any other IDE) then your IDE will trigger a hot deployment whenever you make any changes and save them afterwards. If you change the content of jsp files everything works fine. But if you change our EJB (MessageServiceBean.java) and then trigger a re-deployment of the complete ear let's say 20-30 times in a row (in between you have to wait for the re-deployment to finish) the you will get a "java.lang.OutOfMemoryError: PermGen space" error. If you call the RESTful web service (http://localhost:8080/service/message/) in between the re-deployments then the error occurs much earlier. This is an error which has survived in Glassfish for so many years, I guess there is some Memory Leak. So the Memory Leak in Glassfish becomes visible when you deploy ear files again and again. I reported the bug just today because I believe with this tutorial it should be easy to reproduce it. For more details please see https://java.net/jira/browse/GLASSFISH-20732.


9. Download Source Code (Maven project)

You can download the complete Maven Project here:

Comments
  • If you like this tutorial help us to maintain it:
  • And don't forget to let others know about it:
Mavem Project
posted by Lucky
Tue Aug 08 13:51:41 UTC 2017
Please give me your sample project