Java Maven AngularJS Seed Project
Join the DZone community and get the full member experience.
Join For FreeIn this article I'll try to find the answer to a couple of basic questions like:
- Is there a marriage possible between AngularJS, Java and Maven?
- Can it be done?
- Should it be done?
- Must it be done?
Here I'll describe the whole process of making a Java Maven AngularJS seed project. This process will include the mistakes I made and the lessons learned. The whole goal is to create a setup that can be build by a Java developer, with no specific front-end skills.
Goals
- Maven as the build tool
- Java EE 7 as the back-end language
- CDI
- AngularJS as the frond-end framework
- Frond-end testing must also be done during the maven build
- Bower as the web package manager.
- Use Git as version control
- Bootstrap als css standard
- Javascript in src/main/javascript
- Minifying javascript
- Jasmine as javascript unit test tool integrated with maven
Prerequisites
- Firefox or Chrome
- npm
- nodejs
- JDK
- IDE
- bower
- PhantomJs or brew install phantomjs
- Application Server or brew install glassfish
The project is hosted here
Basic maven project
mvn archetype:generate -DgroupId=nl.ivonet -DartifactId=java-angularjs-seed -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false
After generating I've got this:
./pom.xml ./src ./src/main ./src/main/resources ./src/main/webapp ./src/main/webapp/index.jsp ./src/main/webapp/WEB-INF ./src/main/webapp/WEB-INF/web.xml
Now we start configuring it.
First create a test space and remove all unnecessary files:
Open a terminal in the root of the project. (from now on all commands are assumed to be executed from the root of the project)
mkdir -p src/main/java mkdir -p src/test/java mkdir -p src/test/javascript/unit mkdir -p src/test/javascript/e2e mkdir -p src/test/resources rm -f ./src/main/webapp/WEB-INF/web.xml rm -f ./src/main/webapp/index.jsp mkdir -p ./src/main/webapp/css touch ./src/main/webapp/css/specific.css mkdir -p ./src/main/webapp/js touch ./src/main/webapp/js/app.js touch ./src/main/webapp/js/controllers.js touch ./src/main/webapp/js/routes.js touch ./src/main/webapp/js/services.js touch ./src/main/webapp/js/filters.js touch ./src/main/webapp/js/services.js mkdir -p ./src/main/webapp/vendor mkdir -p ./src/main/webapp/partials mkdir -p ./src/main/webapp/img touch README.md touch .bowerrc
Initialize npm
As I want to use npm lets initialize it.
npm init This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sane defaults. See `npm help json` for definitive documentation on these fields and exactly what they do. Use `npm install --save` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. name: (java-angularjs-seed) version: (0.0.0) description: A starter project for AngularJS combined with java and maven entry point: (index.js) test command: karma start test/resources/karma.conf.js git repository: https://github.com/ivonet/java-angular-seed keywords: author: Ivo Woltring license: (ISC) Apache 2.0 About to write to /Users/ivonet/dev/ordina/LabTime/java-angularjs-seed/package.json: { "name": "java-angularjs-seed", "version": "0.0.0", "description": "A starter project for AngularJS combined with java and maven", "main": "index.js", "scripts": { "test": "karma start test/resources/karma.conf.js" }, "repository": { "type": "git", "url": "https://github.com/ivonet/java-angular-seed" }, "author": "Ivo Woltring", "license": "Apache 2.0", "bugs": { "url": "https://github.com/ivonet/java-angular-seed/issues" }, "homepage": "https://github.com/ivonet/java-angular-seed" } Is this ok? (yes)
adjust the ./package.json file. It needs a bit more tweaking:
{ "name": "java-angular-seed", "private": true, "version": "0.0.0", "description": "A starter project for AngularJS combined with java and maven", "repository": "https://github.com/ivonet/java-angular-seed", "license": "Apache 2.0", "devDependencies": { "bower": "^1.3.1", "http-server": "^0.6.1", "karma": "~0.12", "karma-chrome-launcher": "^0.1.4", "karma-firefox-launcher": "^0.1.3", "karma-jasmine": "^0.1.5", "karma-junit-reporter": "^0.2.2", "protractor": "~0.20.1", "shelljs": "^0.2.6" }, "scripts": { "postinstall": "bower install", "prestart": "npm install", "start": "http-server src/main/webapp -a localhost -p 8000", "pretest": "npm install", "test": "karma start src/test/javascript/karma.conf.js", "test-single-run": "karma start src/test/javascript/karma.conf.js --single-run", "preupdate-webdriver": "npm install", "update-webdriver": "webdriver-manager update", "preprotractor": "npm run update-webdriver", "protractor": "protractor src/test/javascript/protractor-conf.js", "update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + cat('src/main/webapp/vendor/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'src/main/webapp/index.html');\"" } }
Add Bower
I want to use bower as the the web package manager.
The default place bower will install its dependencies is ./bower-components but as I want them to conform to the maven structure I will add an instruction to the .bowerrc file we just touched.
Information hiding dictates that I won't tell users that I manage stuff with bower.
{ "directory": "src/main/webapp/vendor" }
This will tell bower to copy the downloaded packages to that place.
Add dependencies (by hand)
bower install angular#1.3.0-beta.14 bower install angular-route#1.3.0-beta.14 bower install angular-animate#1.3.0-beta.14 bower install angular-mocks#1.3.0-beta.14 bower install angular-loader#1.3.0-beta.14 bower install bootstrap
This should be enough for a first basic setup. You might want to change the versions.
Because I want to have an easy setup I want to eliminate all these manual steps so now I want to have a config file that helps me to do this automagically.
The following command will help create a setup script for bower.
bower init [?] name: java-angularjs-seed [?] version: 0.0.0 [?] description: A java / maven / angularjs seed project [?] main file: src/main/webapp/index.html [?] what types of modules does this package expose? [?] keywords: java,maven,angularjs,seed [?] authors: IvoNet [?] license: Apache 2.0 [?] homepage: http://ivonet.nl [?] set currently installed components as dependencies? Yes [?] add commonly ignored files to ignore list? Yes [?] would you like to mark this package as private which prevents it from being accidentally pub[?] would you like to mark this package as private which prevents it from being accidentally published to the registry? Yes ... [?] Looks good? (Y/n) Y
The resulting ./bower.json file should look something like:
{ "name": "java-angularjs-seed", "version": "0.0.0", "authors": [ "IvoNet <webmaster@ivonet.nl>" ], "description": "A java / maven / angularjs seed project", "keywords": [ "java", "maven", "angularjs", "seed" ], "license": "Apache 2.0", "homepage": "http://ivonet.nl", "private": true, "ignore": [ "**/.*", "node_modules", "bower_components", "src/main/webapp/vendor", "test", "tests" ], "dependencies": { "angular": "1.3.0-beta.14", "angular-loader": "1.3.0-beta.14", "angular-mocks": "1.3.0-beta.14", "angular-route": "1.3.0-beta.14", "bootstrap": "3.2.0" }, "main": "src/main/webapp/index.html" }
Now that I have this config file I should be able to add the dependencies with a command. But first to test it.
We need to remove the manually added dependencies.
rm -rf ./src/main/webapp/vendor
Now to test and try out the setup:
npm install
It should recreate it all again :-)
It will also add a folder node_modules to the project.
These folders should be excluded from version control, but that will be done later
Add Karma
I want to use Karma as the testrunner for javascript.
Create the ./src/test/javascript/karma.conf.js containing:
module.exports = function(config){ config.set({ basePath : '../../../', files : [ 'src/main/webapp/vendor/angular**/**.min.js', 'src/main/webapp/vendor/angular-mocks/angular-mocks.js', 'src/main/webapp/js/**/*.js', 'src/test/javascript/unit/**/*.js' ], autoWatch : true, frameworks: ['jasmine'], browsers : ['Chrome'], plugins : [ 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-jasmine', 'karma-junit-reporter' ], junitReporter : { outputFile: 'target/test_out/unit.xml', suite: 'src/test/javascript/unit' } }); };
Now we can run the unit tests by using the command npm test
Add Protractor
I want to use protractor as the end-2-end tester.
Create the ./src/test/javascript/protractor-conf.js:
'use strict'; exports.config = { allScriptsTimeout: 11000, specs: [ 'e2e/*.js' ], capabilities: { 'browserName': 'chrome' }, baseUrl: 'http://localhost:8000/', framework: 'jasmine', jasmineNodeOpts: { defaultTimeoutInterval: 30000 } };
Now we can run the integration tests by using the command npm run protractor
Add html
index.html:
<!doctype html> <html lang="en" data-ng-app="app"> <head> <meta charset="utf-8"> <title>Seed App</title> <link rel="stylesheet" href="vendor/bootstrap/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="css/specific.css"> </head> <body> <div ng-view></div> <script src="vendor/angular/angular.min.js"></script> <script src="vendor/angular-route/angular-route.min.js"></script> <script src="js/app.js"></script> <script src="js/controllers.js"></script> <script src="js/filters.js"></script> <script src="js/routes.js"></script> <script src="js/services.js"></script> </body> </html>
partials/home.html:
<p>{{title}}</p>
Add hello world AngularJs
js/apps.js:
'use strict'; var app = angular.module('app', [ 'ngRoute', 'controllers' ]); app.config(['$routeProvider', function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'partials/home.html', controller: 'HomeController' }) .otherwise({ redirectTo: '/' }); }]);
js/controlers.js:
'use strict'; var controllers = angular.module("controllers", []); controllers.controller("HomeController", ['$scope', function ($scope) { $scope.title = 'Hello world!'; }]);
Add Jasmine tests
src/test/javascript/unit/controllersSpec.js:
'use strict'; /* jasmine specs for controllers go here */ describe('Controller tests', function () { describe('HomeController', function () { var scope, ctrl; beforeEach(module('app')); beforeEach(inject(function ($rootScope, $controller) { scope = $rootScope.$new(); ctrl = $controller('HomeController', {$scope: scope}); })); it('should contain hello world', function () { expect(scope.title).toBe('Hello world!'); }); }); });
Add end-2-end tests
src/test/javascript/e2e/scenarios.js:
'use strict'; /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ describe('App integration tests', function () { beforeEach(function () { browser.get('/index.html'); }); it('should redirect index.html to index.html#/', function () { browser.getLocationAbsUrl().then(function (url) { expect(url.split('#')[1]).toBe('/'); }); }); });
Test basic setup
For this we need two terminal windows opened in the root of the project
In one of the terminals start the test server
npm start
Go here http://localhost:8000/ in a browser. You should see "Hello world!"
In the other terminal try out:
npm run protractor
This should result in an executed e2e test with no errors.
Lets try it out:
npm test
This should result in a working unit test and it should keep on monitoring for test changes. If you change the test in controllersSpec.js to a wrong text you should see this almost immediately in the terminal window.
Add Java EE 7
I want to be able to use the EE 7 stack.
Adjust the pom.xml
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>nl.ivonet</groupId> <artifactId>java-angularjs-seed</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>java-angularjs-seed Maven Webapp</name> <url>http://ivonet.nl</url> <properties> <artifact.name>app</artifact.name> <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.5</version> <scope>test</scope> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>${artifact.name}</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> <compilerArguments> <endorseddirs>${endorsed.dir}</endorseddirs> </compilerArguments> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.4</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.6</version> <executions> <execution> <phase>validate</phase> <goals> <goal>copy</goal> </goals> <configuration> <outputDirectory>${endorsed.dir}</outputDirectory> <silent>true</silent> <artifactItems> <artifactItem> <groupId>javax</groupId> <artifactId>javaee-endorsed-api</artifactId> <version>7.0</version> <type>jar</type> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
CDI
Now we want to have Content and Dependency Injection so we need this file ./src/main/webapp/WEB-INF/beans.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans 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/beans_1_1.xsd" bean-discovery-mode="annotated"> </beans>
We now have a basic maven project that can be build by the standard maven commands.
Intermezzo
So now I have a working project that can contain java and angular with all its functionality. The trouble is there is no integration yet, no version control and no maven build to do it all.
Can it be done?
Goals met:
- AngularJS as the frond-end framework
- Bower as the web package manager.
Goals partially met:
- Maven as the build tool, but no javascript tests yet
- Java EE 7 as the back-end language, but no example yet
- CDI, but no example yet
- bootstrap
- Javascript unit testing
Goals to be met:
- Maven as the build tool
- Java EE 7 as the back-end language - with example
- Frond-end testing must also be able done during the maven build
- Use Git as version control
- Maven integration for javascript testing
- Maven for minifying javascript
- Javascript jslint
- Javascript as resource
Add Version Controll
I use git for this.
add a .gitignore file to the root of the project containing at least the following lines:
.idea/ .DS_Store node_modules/
add a .gitignore file to the src/main/webapp of the project containing:
vendor/
now create the git repository from the root of the project:
git init git add . git commit -m "initial import"
Add Java example
In order to make it possible to create RESTful services we need a root context for these services. As I want to talk json I also define a json provider.
package nl.ivonet.application; import nl.ivonet.controler.HomeControler; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; import java.util.Set; /** * Basic JAX-RS application. * * @author Ivo Woltring */ @ApplicationPath("service") public class SeedApplication extends Application { @Override public Set<Class<?>> getClasses() { final Set<Class<?>> resources = new java.util.HashSet<>(); // following code can be used to customize Jersey 2.0 JSON provider: try { final Class jsonProvider = Class.forName("org.glassfish.jersey.jackson.JacksonFeature"); resources.add(jsonProvider); } catch (final ClassNotFoundException ex) { java.util.logging.Logger.getLogger(getClass().getName()) .log(java.util.logging.Level.SEVERE, null, ex); } addRestResourceClasses(resources); return resources; } /** * Add your own resources here. */ private void addRestResourceClasses(final Set<Class<?>> resources) { resources.add(HomeControler.class); } }
Now for a Hello world service...
package nl.ivonet.controler; import nl.ivonet.model.Hello; import javax.enterprise.context.RequestScoped; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; /** * Hello world man. * @author Ivo Woltring */ @Path("/home") @RequestScoped public class HomeControler { @GET @Produces(MediaType.APPLICATION_JSON) public Hello get() { return new Hello("world"); } }
Change the HomeController.js
'use strict'; var controllers = angular.module("controllers", []); controllers.controller("HomeController", ['$scope', '$http', function ($scope, $http) { $scope.debug = true; $scope.title = 'Hello '; $http.get("service/home").success(function (data) { $scope.data = data; $scope.title += $scope.data.message; }); $scope.toggleDebug = function () { $scope.debug = !$scope.debug; }; }]);
And the home.html
<p>{{title}}</p> <div> <pre data-ng-model="debug" data-ng-show="debug">{{ data | json }}</pre> </div>
For this all to work we need a Java EE Container and Node.js just won't do :-)
I work with Glassfish 4.1 for my test environment and deployed it.
And now we get:
Refactoring...
I stared a bit at the code and concluded that I'm not happy with the current state of affairs. Right now the javascript code resides in src/main/webapp/js and it shouldn't be there. According to the maven rulez it should be in src/main/javascript just like java and other languages. I also want to have a minified javascript in the artifact and not the origional.
So I added the folder and told maven it needed to recognize it as a resource folder.
Add the folling config snippet to the <build> tag in the pom.xml
<resources> <resource> <directory>src/main/javascript</directory> <filtering>true</filtering> </resource> </resources>
I moved all files in the js folder of the webapp to that folder and removed the js folder in webapp. As I wanted the minifacation to work and the jslint. I used a maven plugin for that. After some trial and errors the following config seems to work the best.
<plugin> <groupId>net.alchim31.maven</groupId> <artifactId>yuicompressor-maven-plugin</artifactId> <version>1.1</version> <executions> <execution> <id>compress-js</id> <goals> <goal>jslint</goal> <goal>compress</goal> </goals> </execution> </executions> <configuration> <failOnWarning>true</failOnWarning> <outputDirectory>target/app/js/</outputDirectory> <nosuffix>true</nosuffix> <excludes> <exclude>vendor/**</exclude> <exclude>**/*min.css</exclude> <exclude>**/*min.js</exclude> </excludes> </configuration> </plugin>
and a small change to the karma.conf.js. Change src/main/webapp/js/**/*.js to src/main/javascript/**/*.js. now the npm test should work again.
The mvn clean package command failed because my app.js didn't get through the jslint step and that is just what I wanted. Fixed it and voila.
Intermezzo
Goals met:
- AngularJS as the frond-end framework
- Bower as the web package manager.
- Javascript jslint
- Javascript as resource
- Javascript testing with Karma / Jasmine
- Maven for minifying javascript
- Maven as the build tool
- Java EE 7 as the back-end language
- bootstrap
- CDI
- Use Git as version control
Goals to be met:
- Frond-end testing must also be able done during the maven build
- Maven integration for javascript testing
So I guess that my main goal now is to get jasmine working with maven...
Add jasmine maven plugin
At first I removed all jasmine pom stuff to start from scratch. Then I added the most basic jasmine config to it. But that didn't work at all. Which was actually logical but I had to do some reading to understand that :-). I had to tell (in the correct order) what the plugin already needs to know before it can test my personal javascript code. At this time this means that I have to tell it to load JQuery and Angular and the Angular mocks. That seemed to do a bit more but now it didn't find any *Spec.js files and I had to tell the plugin where to find that and the sources. This seemed to work but I got terrible stacktraces. After some research I found out that it had nothing to do with my configuration but with htmlunit, which is used by default by the plugin. So I had to install phantomjs and configure it as the driver.
brew install phantomjs
Or install it according to the these instructions.
The plugin configuration below finally worked.
<plugin> <groupId>com.github.searls</groupId> <artifactId>jasmine-maven-plugin</artifactId> <version>1.3.1.5</version> <executions> <execution> <goals> <goal>test</goal> <goal>bdd</goal> </goals> </execution> </executions> <configuration> <webDriverClassName>org.openqa.selenium.phantomjs.PhantomJSDriver</webDriverClassName> <preloadSources> <source>${vendor.loc}/jquery/dist/jquery.js</source> <source>${vendor.loc}/angular/angular.min.js</source> <source>${vendor.loc}/angular-route/angular-route.min.js</source> <source>${vendor.loc}/angular-mocks/angular-mocks.js</source> </preloadSources> <jsSrcDir>src/main/javascript</jsSrcDir> <jsTestSrcDir>src/test/javascript/unit</jsTestSrcDir> <specIncludes> <include>*Spec.js</include> </specIncludes> </configuration> </plugin>
Add bower to maven
As I want to have the integration as complete as possible I want to have the initial bower install also be part of maven. Again some research and for me the maven-exec-plugin seems a good solution:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.3.2</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>exec</goal> </goals> </execution> </executions> <configuration> <executable>bower</executable> <arguments> <argument>install</argument> </arguments> <workingDirectory>${basedir}</workingDirectory> </configuration> </plugin>
Now during the generate-source phase it will run bower and put all the javascript dependencies into src/main/webapp/vendor because that has been configured in .bowerrc and bower.json.
So it can be done! ;-)
Clean
As the vendor folder in webapp is not part of our version control and cleanup is also part of the fun I added the maven clean plugin with some extra configuration.
<plugin> <artifactId>maven-clean-plugin</artifactId> <version>2.5</version> <configuration> <filesets> <fileset> <directory>src/main/webapp/vendor</directory> </fileset> <fileset> <directory>node_modules</directory> </fileset> </filesets> </configuration> </plugin>
Removed protractor
As I tend to use Fitnesse and Selenium (in comination) to do my end-to-end testing I decided to remove protractor from the project. I want to test against the real server with all stuff working before running e2e tests. (I'm not sure this is a good choice but I made it)
I had to refactor some stuff like the path to the javascript unit tests as I moved the other scripts to src/test/javascript and removing references to protractor.
Conclusion
I committed the whole seed project to github and except for the end to end tests it all seems to work.
It is a learning process and to say that I'm completely happy with the configuration is to lie.
In this seed project it was all about integration. Integrating the different worlds of front-end and back-end with all it's tools.
The question remains if that should be the way to go? I don't know yet.
Decoupling these worlds might be better.
I tend to think that these worlds should only meet through services and the front-end and back-end should be build separately. But as I was writing this a colleague of mine told me that he really wanted a setup like this because he wanted the java developers to be able to build the whole project with a simple command like mvn clean install, because it cuts down on the learning curve.
Final conclusion: Who am I to judge?!
Cheerz,
Ivo.
Related topics
Ivo Woltring is a senior consultant with Ordina. Ordina is the largest independent services provider in the field of consulting, solutions and IT in the Benelux. We focus on the financial services sector, public sector, healthcare sector and a number of specific segments in the industry sector. Ivo helps customers with building enterprise applications and is coach and teacher for talented young professionals. He has worked on various projects since 1998, as a developer, team lead, Scrum Master, architect, teacher, coach and mentor. You can read more on his personal blog IvoNet.nl
Published at DZone with permission of Ivo Woltring. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
How To Use Pandas and Matplotlib To Perform EDA In Python
-
Scaling Site Reliability Engineering (SRE) Teams the Right Way
-
RBAC With API Gateway and Open Policy Agent (OPA)
-
Extending Java APIs: Add Missing Features Without the Hassle
Comments