Build, Deploy and Run in a Browser a Web App with Maven

The Problem

Every winter I teach a course in which students in their final semester work on a project to develop an e-commerce system. If you’d like to learn more about the project then contact me. As part of the learning materials for this course there are over 60 sample programs. This year I encountered a problem with my samples I had never seen before. Sample code that used a database took an inordinate amount of time from the start of a build, then on to testing, deployment and finally opening the web app in a browser. It made using the code in lectures difficult and my students found running these samples unfriendly. I needed to find a solution.

The Environment

Java 11

I used both Oracle’s and the AdoptOpenJDK’s distribution. There was no difference between the two as was expected.

Payara 5.194

I use version 5.194 as it works with Java 11. I switched from GlassFish to Payara a few years back when a problem with the time to run Arquillian tests was excessive. The team behind Arquillian had noted an issue on Windows and corrected the problem. As a side note I was in contact with the Arquillian team at Red Hat and they were unable to recreate my issue. Then one day they admitted to me that they did not have a single Windows OS system in their office.

MySQL 8

I like to use MySQL because it is an install and forget system. I advise my students to install it as a service as I do and as I have it installed in the college. Students can start and stop it in the Services app of Windows. I leave it running all the time on my personal systems.

NetBeans 11.2

I am a big fan of NetBeans and advocate for its use whenever I can. I find that in a school computer lab setting it needs the least technical support as compared to its bigger brothers Eclipse and IntelliJ. In moving from Oracle to the Apache Foundation the pace of releases now matches that of Java. There is a dedicated team working on it. There is a good possibility that the problem I encountered comes from a conflict between NetBeans and Payara. When I finish this blog I will post bug reports that I believe will lead to a solution.

The Development Environment

All my sample programs are Maven based. With so many samples it became difficult to make the yearly updates as new versions of dependencies and plug-ins came out to the 60 pom.xml files. To simplify this I created a project with a parent pm.xml file that every sample referred to in their own pom.xml file. The pom.xml file of each project identified the project and included any additional dependencies and plug-ins unique to that project.

While the goal of the project is to deploy it to a department server, we do our development in systems where all the components are running on the same computer. The last step in the course is to move it to a server.

This arrangement has served the course well until this year. I needed a fix now.

The Problem Details

Using NetBeans and when running a sample that used the JPA it took from the start of the build to completing the Arquillian tests approximately 25 seconds. I considered this an acceptable amount of time. The time from Arquillian to viewing the app in a browser took another 50 seconds. Although there was some difference in these times on slower systems, as long as you had at least 8 Gigs of RAM, 50 seconds was the best time possible. This was unacceptable.

What I Observed

NetBeans built and ran the tests, as I have already pointed out, in approx. 25 seconds. The creation of the deployable build took only a few more seconds. Most of the remaining 50 seconds were spent watching the message ‘Deploying’ in NetBeans.

As a test I manually deployed the war file into Payara’s autodeploy folder and it was ready to launch in seconds.  So, what was wrong? As I’ve already mentioned I will file reports with both Payara and NetBeans of which I guess this is the first report.

My Proposed Immediate Solution

I decided that I needed to remove deployment and running from NetBeans. I was inspired by the fact that Arquillian does that. Its framework deploys the test servlet it creates and captures the output for the unit testing. I needed to do something similar. As the projects were all Maven based I needed Maven to do the deploy and running.

Cargo – Deploying war files with Maven

The Cargo plugin is used specifically for deploying to servers from Maven. It can also start and stop the server. I needed to learn how to use Cargo.

What I wanted was a complete end to end example of deploying to a local server, not a remote server. I was sure getting one, local, to work would make getting the other, remote, easy to switch to.

Like all good programmers I turned to Google to search for examples. I knew there would be many, but I was confident that what ever appeared at the top of my searches was probably a well used and working example. I was disappointed.

What I found were mostly technical documents that assumed that I already knew the secret handshake. Not one started from nothing and moved to a complete working example. What I found were massive projects or blogs with fragments. I won’t bore you with the issues I came across from sloppy xml with errors to descriptions that used different terms for the same thing. There were projects with pom.xml files configured for every possibility with barely a comment to explain why. Blogs presented fragments that too often were buggy.

What I wanted from Cargo

I wanted the minimum amount of dependencies and plugins to run a Local Server. Or was that a Managed Server? I’m pretty sure I did not want a Remote Server or an Embedded Server. It took a few hours but this is what I came up with. First, in the pom.xml file of each project I added the following:

<!-- Global settings for the project. Settings can be accessed in the pom
by placing the tag name in ${...} -->
<properties>
    <!-- These properties must be updated to your particular setup -->
    <payara.home>C:/devapp/payara5</payara.home>
    <payara.domainName>domain1</payara.domainName>
    <payara.domainDir>${payara.home}/glassfish/domains</payara.domainDir>

    <skipTests>false</skipTests>
</properties>

I kept this in the individual pom.xml files so that my students could easily make changes to reflect whatever system they were using. I’m sure some of you are aghast at the skipTests properties but sometimes you want to just see if it will run. When I review my student’s work I always ensure that skipTests are false and that there are no @Ignore in the test classes.

In the parent pom.xml I added the following:

<plugin>
    <!-- The Cargo plugin handles undeploying and deploying war files
    It can also start and stop the server but start fails the build
    if the server is already started so I do not use this feature. -->
    <groupId>org.codehaus.cargo</groupId>
    <artifactId>cargo-maven2-plugin</artifactId>
    <version>1.7.9</version>
    <configuration>
        <container>
            <containerId>glassfish5x</containerId>
            <type>installed</type>
            <home>${payara.home}</home>
        </container>
        <configuration>
            <type>existing</type>
            <home>${payara.domainDir}</home>                 
            <properties>
                <cargo.glassfish.domain.name>${payara.domainName}</cargo.glassfish.domain.name>
            </properties>
        </configuration>
    </configuration>
    <!-- provides JSR88 client API to deploy on Payara -->
    <dependencies>
        <dependency>
            <groupId>org.glassfish.main.deployment</groupId>
            <artifactId>deployment-client</artifactId>
            <version>5.1.0</version>
        </dependency>
    </dependencies>
</plugin>    

I’d love to tell you where to go to get a line by line description of each line in the plug-in but I could not find one. Most are obvious but I’d hope that Cargo project would have a clear line by line break down. I know the information is there, but it’s spread across numerous documents. From the start of my research until I figured out that I needed to extract this from a much larger pom.xml and then getting it to work took about four hours. Luckily Tuesdays are my work at home days.

Not all my time was spent getting it to work. I needed to figure out what could go in the parent and individual child project pom.xml file. I also have no idea what a ‘JSR88 client API’ but I’m sure I could find out in a few more hours. What I cared about was that it worked and was repeatable.

One more piece was need in the parent pom.xml file and that was the default goals that appear at the beginning of the build section.

<build>
    <!-- Goals may be set in the IDE or the pom IDE or CLI goals override 
    the defaultGoal. The post-integration-test goal triggers the Antrun 
    plugin to start the browser with the deployed site. -->

    <defaultGoal>clean package cargo:redeploy post-integration-test</defaultGoal>

The goals clean and package were always there. You may ask why I have clean as it forces the deletion of the target folder and the build of everything. I discovered that without the clean goal changes to the pom.xml file did not trigger a complete rebuild in NetBeans. In the teaching environment doing a clean goal guarantees consistent builds.

The cargo:redeploy does what it says. The Cargo docs were misleading because they implied that cargo:deploy worked the same way. It did not. If the project has already been deployed then cargo:deploy declares an error and the build ends. The cargo:redeploy goal undeploys before it deploys and if there is nothing to undeploy it just goes straight to deploy.

The logic behind Cargo’s goals to start the server are for me illogical. The cargo:start goal will start the server if its not running otherwise it declares an error. I guess you can make the case for this but not for my purposes. I’d need to stop the server after every run and therefore add even more time to the build and deploy. To run Arquillin tests the server must be running. I’d like a goal like cargo:redeploy, call it cargo:restart, where if the server is already running all is well.

Time to Run The Browser

In Windows in a command/console window the command ‘start’ followed by a URL will run the default browser and open the URL. You can even choose the browser to use by adding a key word after start such as ‘start chrome’. I have no idea if this is supported on a Mac or Linux box but I should in the next day or two. I’ll add an addendum once I find out. I now knew how to start a browser process, I just needed to find out how to do it thru Maven

I came across two ways to run an external process. The first was to use the exec-maven-plugin plugin. I won’t bore you with what happened other than to say I thought this was the better plugin but I never got it to work.

The second plugin was maven-antrun-plugin plugin. It seems weird to use ant to run an external process. I didn’t care when I got it to work. Here is what I added to the parent pop.xml file.

<plugin>
    <!-- This will run the system's default browser. Only tested on Windows OS -->
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.8</version>
    <executions>
        <execution>
            <id>Run URL in system browser.</id>
            <!-- Insufficient information to explain why this goal is required -->
            <!-- It was determined by trial and error --> 
            <phase>post-integration-test</phase>
            <configuration>
                <target>
                    <exec executable="start" vmlauncher="false">
                        <arg line="http://localhost:8080/${project.artifactId}/"/>
                    </exec>
                </target>
            </configuration>
            <goals>
                <!-- Insufficient information to explain why this 
                goal is required. It was determined by trial and error --> 
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>            

Initially I could not get this to work. The problem was that in the examples the value for phase did not work. I could not use the existing goals. Here is where I tried a goal that technically I wasn’t doing and that was the integration goal. There was pre-integration-test and post-integration-test. They both worked and I picked post-integration-test because I wasn’t really doing integration testing, I just needed the goal so it would use the antrun plugin. As for the goal ‘run’, your guess is as good as mind as to why this verb. It may look obvious but why not exec or execute. I couldn’t find a reason but neither did I spend a lot of time trying to figure it out.

I was also surprised that the property ${project.artifactId} worked inside the quotation marks of the attribute line. That it did eliminated my concern that I was going to need to find a way to concatenate “http://localhost:8080/” with ${project.artifactId}.

It’s Alive!

After more than six hours I had a parent pom.xml along with a small addition to each project pom.xml that appeared to work consistently. I say appeared as I have not tested all 60 programs yet. That will be my task for the next few days.

It now approx. 25 seconds from build to Arquillian tests but only 5 seconds more to deplaoy and open a browser. I went approx. 115 to 30 seconds. Not bad.

If you’d like to try out my code you will first need to clone: https://gitlab.com/omniprof/web_project_dependencies_1_2.git

You need to do a maven install. If you are using NetBeans then right click on the project, select Run Maven and select Install.

To run my reference app clone: https://gitlab.com/omniprof/kf_web_standard_project_1_2.git

You will need Java 11, Payara 5.194 and MySQL 8. The scripts for creating the database and tables with sample data are in Other Test Sources (src/test/resources) in the default package.

Be My Proof Reader

If you find any errors, omissions or some plain stupidity then please let me know.

For Your Amusement

The complete parent pom.xml from web_dependencies_project_1_2

<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>

    <!-- This is an updated pom that has Maven deploy the web app to a local 
    instance of Payara and then runs the default browser to view the app. This 
    also makes it possible to use Maven at the command line rather than just 
    inside NetBeans but I have not tested this yet. There is just one requirment 
    and that is that Payara must be running first. -->
    
    <!-- Master or parent pom.xml -->
    <groupId>com.kfwebstandard</groupId>
    <artifactId>web_project_dependencies</artifactId>
    <version>1.2</version>
    <packaging>pom</packaging>

    <!-- Global settings for the project. Settings can be accessed in the pom
    by placing the tag name in ${...} -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <!-- Dependencies listed here are usually bom or bill of materials-->
    <!-- The bom lists all the child dependencies that could be used and -->
    <!-- lists the current version number for each -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.arquillian</groupId>
                <artifactId>arquillian-bom</artifactId>
                <version>1.5.0.Final</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- Dependencies are libraries that either must be included in the -->
    <!-- jar/war file or are expected to be found in the container such as -->
    <!-- GlassFish -->
    <dependencies>
        <!-- These dependencies are required to run the project on the server -->

        <!-- Jakarta EE 8.0 dependency -->
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>8.0.0</version>
            <scope>provided</scope>
        </dependency> 
        
        <!-- Java EE 8.0 dependency -->
        <!--        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>-->

        <!-- MySQL 8.0 dependency -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
            <scope>provided</scope>
        </dependency>

        <!-- MySQL 5.7 dependency -->
        <!--        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
            <scope>provided</scope>
        </dependency>-->

        <!-- EclipseLink dependency for the static metamodel generator -->
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
            <version>2.7.5</version>
            <scope>provided</scope>
        </dependency>

        <!-- These dependencies are required for testing only -->
        <dependency>
                <!-- Attempts to use a managed rather than remote dependency 
                fails with an error regarding a property glassFishHome or an 
                environmnet var GLASSFISH_HOME. Declaring a property 
                glassfishHome or glassfish.home does not satisfy the requirement 
                and using an environnment var means one more point of failure. 
                Therefore I continue to use the 3.1 remote plugin -->
                <groupId>org.jboss.arquillian.container</groupId>
                <artifactId>arquillian-glassfish-remote-3.1</artifactId>
                <version>1.0.2</version>
                <scope>test</scope>
            </dependency>
        
            <dependency>
                <!-- Resolves dependencies from the pom.xml when explicitly 
                referred to in the Arquillian deploy method -->
                <groupId>org.jboss.shrinkwrap.resolver</groupId>
                <artifactId>shrinkwrap-resolver-depchain</artifactId>
                <type>pom</type>
                <scope>test</scope>
            </dependency>

            <dependency>
                <!-- Connects Arquillian to JUnit -->
                <groupId>org.jboss.arquillian.junit</groupId>
                <artifactId>arquillian-junit-container</artifactId>
                <scope>test</scope>
            </dependency>
        
            <!-- JUnit dependency -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>

            <!-- Selenium dependencies -->
            <dependency>
                <groupId>org.seleniumhq.selenium</groupId>
                <artifactId>htmlunit-driver</artifactId>
                <version>2.36.0</version>
                <scope>test</scope>
            </dependency>

            <dependency>
                <!-- You will need a driver dependency for every browser you use
                in testing. You can find the maven dependencies at -->
                <groupId>org.seleniumhq.selenium</groupId>
                <artifactId>selenium-chrome-driver</artifactId>
                <version>3.141.59</version>
                <scope>test</scope>
            </dependency>

            <dependency>
                <!-- Normally you must download an exe for each browser. This 
                library will retrieve the the necessary file and place it in the 
                classpath for selenium to use -->
                <groupId>io.github.bonigarcia</groupId>
                <artifactId>webdrivermanager</artifactId>
                <version>3.7.1</version>
                <scope>test</scope>
            </dependency>

            <dependency>
                <!-- January 2018 Discovered that there is a guava conflict that 
                prevents Selenium from functioning unless this explicit 
                dependency is included for testing -->
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>23.0</version>
            </dependency>
            
            <dependency>
                <!-- Another dependency required -->
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>2.8.6</version>
            </dependency>        
            <dependency>
                <!-- A better way to write assertions -->
                <groupId>org.assertj</groupId>
                <artifactId>assertj-core</artifactId>
                <version>3.14.0</version>
                <scope>test</scope>
            </dependency>

            <dependency>
                <!-- Selenium uses SLF4J and log4j so these dependencies are 
                needed but they can also be used in logging in all my code -->
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.30</version>
                
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-slf4j-impl</artifactId>
                <version>2.13.0</version>
            </dependency>
        
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-web</artifactId>
                <version>2.13.0</version>
            </dependency>
    </dependencies>

    <!-- Information for compiling, testing and packaging are define here -->
    <build>
        <!-- Goals may be set in the IDE or the pom IDE or CLI goals override 
        the defaultGoal. The post-integration-test goal triggers the Antrun 
        plugin to start the browser with the deployed site. -->
        <defaultGoal>clean package cargo:redeploy post-integration-test</defaultGoal>
        
        <!-- Folders in the project required during testing -->
        <testResources>
            <!-- although a default maven location, this declaration is required by Arquillian -->
            <testResource>
                <directory>src/test/resources</directory>
            </testResource>
            <!-- location of the arquillian config file for connectiong to the server -->
            <testResource>
                <directory>src/test/resources-glassfish-remote</directory>
            </testResource>
        </testResources>

        <!-- This allows you to configure a plugin that children of this pom will use
        In this case we are configuring the default war plugin -->
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>3.2.3</version>
                </plugin>
            </plugins>
        </pluginManagement>
        
        <!-- Plugins are components that Maven uses for specific purposes beyond
        the basic tasks -->
        <plugins>
            <plugin>
                <!-- This will run the system's default browser. Only tested on Windows OS -->
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.8</version>
                <executions>
                    <execution>
                        <id>Run URL in system browser.</id>
                        <!-- Insufficient information to explain why this goal is required -->
                        <!-- It was determined by trial and error --> 
                        <phase>post-integration-test</phase>
                        <configuration>
                            <target>
                                <exec executable="start" vmlauncher="false">
                                    <arg line="http://localhost:8080/${project.artifactId}/"/>
                                </exec>
                            </target>
                        </configuration>
                        <goals>
                            <!-- Insufficient information to explain why this 
                            goal is required. It was determined by trial and error --> 
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>            
            
            <plugin>
                <!-- Executes JUnit tests and writes the results as an xml and
                text file. Maven requires test classes include one of the 
                following in their name: Test* *Test *TestCase -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <argLine>-Dfile.encoding=${project.build.sourceEncoding}</argLine>
                    <skipTests>${skipTests}</skipTests>
                </configuration>
            </plugin>

            <plugin>
                <!-- The Cargo plugin handles undeploying and deploying war files
                It can also start and stop the server but start fails the build
                if the server is already started so I do not use this feature. -->
                <groupId>org.codehaus.cargo</groupId>
                <artifactId>cargo-maven2-plugin</artifactId>
                <version>1.7.9</version>
                <configuration>
                    <container>
                        <containerId>glassfish5x</containerId>
                        <type>installed</type>
                        <home>${payara.home}</home>
                    </container>
                    <configuration>
                        <type>existing</type>
                        <home>${payara.domainDir}</home>                 
                        <properties>
                            <cargo.glassfish.domain.name>${payara.domainName}</cargo.glassfish.domain.name>
                        </properties>
                    </configuration>
                </configuration>
                <!-- provides JSR88 client API to deploy on Payara -->
                <dependencies>
                    <dependency>
                        <groupId>org.glassfish.main.deployment</groupId>
                        <artifactId>deployment-client</artifactId>
                        <version>5.1.0</version>
                    </dependency>
                </dependencies>
            </plugin>        

        </plugins>
    </build>
</project>

The complete project pom.xml from kf_web_standard_project_1_2

<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">
    <!-- Maven version of the xml document currently only 4.0.0 is valid -->
    <modelVersion>4.0.0</modelVersion>

    <!-- The GAV consists of an arbitrary descriptor that is usually in the
    form of a reverse domain name. -->
    <groupId>com.kfwebstandard</groupId>

    <!-- This is the name given to the packaged build -->
    <artifactId>kf_web_standard_project</artifactId>

    <!-- The version of the build. Any value is valid though a number and a
    string are common. SNAPSHOT means a project under development. FINAL is commonly
    used to refer to stable production version -->
    <version>1.2-SNAPSHOT</version>

    <!-- Default value is jar but may be war or ear -->
    <packaging>war</packaging>

    <!-- The name given to the project. Unlike groupId and artifactId a name
    may have spaces. This is the default but it is left in to remind you that it
    can have a different name -->
    <name>${project.artifactId}</name>

    <!-- A description of the program -->
    <description>Display the records from a database using JPA, CDI and JSF</description>

    <!-- Identifies the programmer or programmers who worked on the project -->
    <developers>
        <developer>
            <id>Enter your school id</id>
            <name>Enter your name</name>
            <email>Enter your email address</email>
        </developer>
    </developers>

    <!-- The company or organization that the programmer(s) work for -->
    <organization>
        <name>Enter school name</name>
    </organization>

    <!-- Global settings for the project. Settings can be accessed in the pom
    by placing the tag name in ${...} -->
    <properties>
        <!-- These properties must be updated to your particular setup -->
        <payara.home>C:/devapp/payara5</payara.home>
        <payara.domainName>domain1</payara.domainName>
        <payara.domainDir>${payara.home}/glassfish/domains</payara.domainDir>

        <skipTests>false</skipTests>
    </properties>

    <!-- All of the sample code shares the same dependencies and build         -->
    <!-- The parent project named web_project_dependencies must be loaded into -->
    <!-- the IDE and you must Run Maven with a goal of install:install         -->
    <!-- Now it can be the parent pom of all projects                          --> 
    <!-- https://gitlab.com/omniprof/web_project_dependencies.git              --> 
    <parent>
        <groupId>com.kfwebstandard</groupId>
        <artifactId>web_project_dependencies</artifactId>
        <version>1.2</version>
    </parent>

</project>

I hope that you have found this instructive. As I have already mentioned feel free to leave any comments or criticisms. Improvements and clarifications are also much appreciated.

About Ken Fogel

Java Champion, JCP Executive Committee Member and NetBeans Dream Team Member. Java One/Code One speaker. Organizer of DawsCon, Montreal's free one day software conference. Next conference is January 10, 2020. Faculty member of the Computer Science Technology Program at Dawson College, Montreal. Instructor at the Continuing Education Computer Institute of Concordia University, Montreal. Passionate about teaching and inspiring students to be better programmers.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.