draptik

mostly tech stuff

Installing Seafile on Raspberry Pi

With all the security issues in the past relating to privacy I’ve been wanting to install a private cloud service similar to Dropbox for some time now. So, here’s a post on how to install Seafile on the Raspberry Pi.

I choose Seafile over Owncloud because I have read multiple posts that (1) Owncloud is not very responsive an a Raspberry Pi and (2) Seafile has a better security model (see 1, 2, 3).

Using a Raspberry Pi for the server seems like a good choice, because it has very low power consumption, so you can have it running 24/7. Furthermore, the Rasperry Pi can be setup with Debian GNU/Linux (f. ex. Raspbian) running in server mode. As Debian is a widely used Linux distribution, most problems can be easily solved by searching the web.

Installing Seafile should be straightforward from following the instructions at the official Seafile Wiki. If you can read German, you can also follow the excellent instructions on Jan Karres’s blog Raspberry Pi: Owncloud-Alternative Seafile Server installieren.

As there are some pitfalls along the way, I’ll describe how I installed Seafile with SSL support.

Installation

Note: This is mainly a merge of Jan Karres’s blog post (in German) and the official wiki documentation from Seafile.

Demo values

1
2
3
4
5
domain: no-ip.org
sub-domain: mycloud
DDNS-domain: mycloud.no-ip.org
internal server name: mycloud
internal IP address of Raspberry Pi: 192.168.1.42

Prerequisites

I’ll assume you have a working Raspian installation on a Raspberry Pi.

If you want to reach your Seafile server from the internet and your ISP only provides you with a dynamic IP you will have to register with a DDNS provider such as Dyn or no-ip.com.

Step 0 Update

Update Raspbian:

$ sudo aptitude update

Step 1 Install dependencies

Install dependencies required by Seafile:

$ sudo aptitude -y install python2.7 python-setuptools python-simplejson python-imaging sqlite3

Step 2 Create seafile user

For security reasons we’ll create a separate user for running Seafile. The user will be called seafile and will not require a password, since we will never be accessing this user directly through SSH.

$ sudo adduser seafile --disabled-login (CHANGE 2016-09-14 the parameter --disable-password was a typo: Thanks to Raphi111 for the pointer)

Switch to being seafile user:

$ sudo su seafile

Change to seafile’s home directory:

$ cd

Step 3 Download and unpack

First we’ll create a new folder mycloud in the seafile user’s home directory:

$ mkdir mycloud && cd mycloud

Download and unpack Seafile Server for Raspberry Pi from http://www.seafile.com/en/download/.

1
2
3
4
wget https://bitbucket.org/haiwen/seafile/downloads/seafile-server_2.1.5_pi.tar.gz
tar -xvzf seafile-server_*
mkdir installed
mv seafile-server_* installed

You should have the following directory structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tree /home/seafile/mycloud/ -L 2
/home/seafile/mycloud/
├── installed
│   └── seafile-server_2.1.5_pi.tar.gz
└── seafile-server-2.1.5
    ├── reset-admin.sh
    ├── runtime
    ├── seaf-fuse.sh
    ├── seafile
    ├── seafile.sh
    ├── seahub
    ├── seahub.sh
    ├── setup-seafile-mysql.py
    ├── setup-seafile-mysql.sh
    ├── setup-seafile.sh
    └── upgrade

All config files are in the folder mycloud (currently there are no config files present yet). New versions can be installed side by side without having to change the config files. The install process will create a soft link seafile-server-latest pointing the the most current installation.

Step 4 Installation

$ cd seafile-server-2.1.5

Before you start the install process you can have a look at the options being configured during installation here.

For this example we’ll assume your DDNS domain is mycloud.no-ip.org and that we’ll use the default location for storing our data. Furthermore, we’ll use mycloud as our server name.

During the installation of Seahub (the web frontend for the Seafile server) you must enter an admin email address and provide a password (this password is your Seafile admin password and should not be the same as your email account password).

All other question can be answered by using the default values (press ENTER).

./setup-seafile.sh

Your directory tree should now look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
tree . -L 2
.
├── ccnet # <--------------------------- configuration files
│   ├── ccnet.conf
│   ├── ccnet.conf.lan
│   ├── ccnet.conf.wan
│   ├── ccnet.db
│   ├── GroupMgr
│   ├── misc
│   ├── mykey.peer
│   ├── OrgMgr
│   ├── PeerMgr
│   └── seafile.ini
├── conf
│   └── seafdav.conf
├── installed
│   └── seafile-server_2.1.5_pi.tar.gz
├── logs
│   ├── ccnet.log
│   ├── controller.log
│   ├── http.log
│   ├── seafile.log
│   ├── seahub_django_request.log
│   └── seahub.log
├── seafile-server-2.1.5
│   ├── reset-admin.sh
│   ├── runtime
│   ├── seaf-fuse.sh
│   ├── seafile
│   ├── seafile.sh
│   ├── seahub
│   ├── seahub.sh
│   ├── setup-seafile-mysql.py
│   ├── setup-seafile-mysql.sh
│   ├── setup-seafile.sh
│   └── upgrade
├── seafile-server-latest -> seafile-server-2.1.5
├── seahub-data
│   └── avatars
├── seahub.db
├── seahub_settings.py
└── seahub_settings.pyc

Step 5 Update URLs for Seahub

nano /home/seafile/mycloud/ccnet/ccnet.conf

/home/seafile/mycloud/ccnet/ccnet.conf
1
SERVICE_URL = https://mycloud.no-ip.org:8001

Don’t forget replacing http with https

Also add a line to seahub_settings.py.

nano /home/seafile/mycloud/seahub_settings.py

/home/seafile/mycloud/seahub_settings.py
1
2
SECRET_KEY ...
HTTP_SERVER_ROOT = 'https://mycloud.no-ip.org:8001/seafhttp'

Step 6 (Re)start Seahub in FastCGI mode

/home/seafile/mycloud/seafile-server-latest/seahub.sh stop

/home/seafile/mycloud/seafile-server-latest/seahub.sh start-fastcgi

Step 7 Install nginx (as admin)

IMPORTANT: Do not run steps 7 to 11 as user seafile. Use the default pi user for admin stuff.

sudo aptitude install nginx

Patching nginx for Raspberry Pi:

1
2
3
sudo sed -i "s/worker_processes 4;/worker_processes 1;/g" /etc/nginx/nginx.conf
sudo sed -i "s/worker_connections 768;/worker_connections 128;/g" /etc/nginx/nginx.conf
sudo /etc/init.d/nginx start

Step 8 Create a self certified SSL certificate (as admin)

The following commands create a self certified SSL certificate. The second to last command is interactive and will ask a few questions. Provide Country Name (enter your two letter country code, i.e. DE for Germany, UK for United Kingdom) and Common Name. The later should be your DDNS name (mycloud.no-ip.org in this example).

1
2
3
4
5
sudo mkdir /etc/nginx/ssl
cd /etc/nginx/ssl
sudo openssl genrsa -out seahub.key 2048
sudo openssl req -new -key seahub.key -out seahub.csr
sudo openssl x509 -req -days 3650 -in seahub.csr -signkey seahub.key -out seahub.crt

Step 9 Create nginx Seahub site (as admin)

sudo nano /etc/nginx/sites-available/seahub

/etc/nginx/sites-available/seahub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
server {
    listen 8001; # <--------------------------------------- NGINX PORT
    ssl on; # <-------------------------------------------- SSL
    ssl_certificate /etc/nginx/ssl/seahub.crt; # <--------- SSL
    ssl_certificate_key /etc/nginx/ssl/seahub.key; # <----- SSL
    server_name mycloud.no-ip.org.tld; # <----------------- CHANGE THIS
    error_page 497  https://$host:$server_port$request_uri;

    location / {
        fastcgi_pass    127.0.0.1:8000;
        fastcgi_param   SCRIPT_FILENAME     $document_root$fastcgi_script_name;
        fastcgi_param   PATH_INFO           $fastcgi_script_name;

        fastcgi_param   SERVER_PROTOCOL $server_protocol;
        fastcgi_param   QUERY_STRING        $query_string;
        fastcgi_param   REQUEST_METHOD      $request_method;
        fastcgi_param   CONTENT_TYPE        $content_type;
        fastcgi_param   CONTENT_LENGTH      $content_length;
        fastcgi_param   SERVER_ADDR         $server_addr;
        fastcgi_param   SERVER_PORT         $server_port;
        fastcgi_param   SERVER_NAME         $server_name;
        fastcgi_param   HTTPS   on;
        fastcgi_param HTTP_SCHEME https;

        access_log      /var/log/nginx/seahub.access.log;
        error_log       /var/log/nginx/seahub.error.log;
    }
    location /seafhttp {
        rewrite ^/seafhttp(.*)$ $1 break;
        proxy_pass http://127.0.0.1:8082;
        client_max_body_size 0;
    }

    location /media {
        root /home/seafile/mycloud/seafile-server-latest/seahub; # <-- change: 2014-07-11
        # include /etc/nginx/mime.types; # <--- UNCOMMENT THIS IF CSS FILES AREN'T LOADED
    }
}

Step 10 Activate nginx Seahub site (as admin)

sudo ln -s /etc/nginx/sites-available/seahub /etc/nginx/sites-enabled/seahub

Step 11 Restart nginx (as admin)

sudo /etc/init.d/nginx restart

Step 12 Network: Setup port forwarding

Depending on your router, the naming might differ. Some routers call it “port mapping”, some call it “port forwarding”. For what we are doing, it’s all the same.

My ISP provided me with a combined dsl-modem/router called “Easybox 904 xDSL”, so YMMV:

Step 13 Test if everything works

LAN test using IP address

Test if Seafile/Seahub is reachable from within your LAN using the IP address of your Raspberry Pi. Assuming the Raspberry Pi has the internal IP address 192.168.1.42, entering https://192.168.1.42:8001 in your browser should render the Seahub page.

If this fails, go back and confirm that you don’t have any typos in your config files.

Internet test

Test if Seafile/Seahub is reachable from the internet. You must use a device which is not connected to your LAN. If you have a smartphone, you can deactivate your WiFi, and try connecting to your newly setup server at https://mycloud.no-ip.org:8001. The Seahub page should be rendered correctly in your ‘internet’ browser.

If this fails, go back and confirm that you don’t have any typos in your config files.

LAN test using DDNS

Try connecting to Seafile/Seahub from within the LAN using the DDNS name: https://mycloud.no-ip.org:8001.

If this works out of the box, you can skip the rest of this article.

Step 14

I should probably mention that I am not a trained network admin. So everything below falls under the category “works for my setup, sorry I can’t provide support”.

Here’s an image of what has to be accomplished:

Step 14a NAT loopback

If trying to reach the IP of your DDNS from within your LAN fails, you should try to setup your NAT loopback (see your router documentation for details).

Step 14b Workaround, in case NAT loopback doesn’t work

This is the tricky part. (Marko, thanks!)

BEWARE: This works with my setup, but it might ruin your setup.

  1. Rename internal LAN DNS zone to no-ip.org (replace with your domain)

  2. Rename internal LAN name (static DHCP lease) to mycloud (replace with your sub domain)

Link Collection #3

JavaScript stuff:

c# stuff:

  • This video by Roy Osherove shows how to introduce seams into brownfield projects (start at approx. 45min into the video if you want to skip the book recommendations and the intro to the SOLID principle). I learned how to introduce seams into static c# classes and methods using virtual…
  • C# psychology by Eric Lippert…
  • NHibernate turbo? (untested, but from the man himself: pimping NHibernate by Ayende)

Node.js Backend Providing REST

TL;DR

My AngularJS demo app has a new backend implementation using node.js.

After some reading I decided I’ll stick with node’s express module.

OK. Here is a ‘minimal’ setup for a node.js server:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
var express = require('express');
var app = express();

//CORS middleware
var allowCrossDomain = function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, X-XSRF-TOKEN');
  next();
};

app.use(allowCrossDomain);

/* we'll use the same port as tomcat... */
var MY_PORT = 8080; // default: 4730


/* REST API =========================================== */
var baseUrl = '/ngdemo/web';

/* GET ALL -------------------------------------------- */
app.get(baseUrl + '/users', function(req, res) {
  res.json(userRepository.getAll());
});

/* GET Dummy ------------------------------------------ */
app.get(baseUrl + '/dummy', function(req, res) {
  res.json({id: 0, firstName: 'JonFromREST', lastName: 'DoeFromREST'});
});

/* GET By Id ------------------------------------------ */
app.get(baseUrl + '/users/:id', function(req, res) {
  console.log('trying to retrieve user with id: ' + req.params.id);
  var user = userRepository.getById(req.params.id);
  res.json(user);
});


/* POST Create ---------------------------------------- */
app.post(baseUrl + '/users', function(req, res) {
  if(!req.body.hasOwnProperty('firstName') || !req.body.hasOwnProperty('lastName')) {
    res.statusCode = 400;
    return res.send('Error 400: POST syntax incorrect.');
  }

  var newUser = userRepository.addNewUser(req.body.firstName, req.body.lastName);
  res.json(newUser);
});

/* PUT (Update) --------------------------------------- */
app.put(baseUrl + '/users/:id', function (req, res) {
  if(!req.body.hasOwnProperty('id') || !req.body.hasOwnProperty('firstName') || !req.body.hasOwnProperty('lastName')) {
    res.statusCode = 400;
    return res.send('Error 400: PUT syntax incorrect.');
  }
  var changedUser = userRepository.changeUser(req.params.id, req.body.firstName, req.body.lastName);
  res.json(changedUser);
});

/* DELETE --------------------------------------------- */
app.delete(baseUrl + '/users/:id', function(req, res) {
  console.log('trying to delete user with id: ' + req.params.id);
  userRepository.deleteUser(req.params.id);
  res.json(true);
});

/* ==================================================== */

app.listen(process.env.PORT || MY_PORT);

/* Mmmhh... how can I place the code below into a seperate file and load it here? */

function User(id, firstName, lastName) {
  this.id = id;
  this.firstName = firstName;
  this.lastName = lastName;
};


function UserRepository() {

  this.users = [];

  this.createUsers = function() {
    var numberOfUsers = 10;
    for (var i = 0; i < numberOfUsers; i++) {
      var id = i + 1;
      this.users.push(new User(id, 'Foo' + id, 'Bar' + id));
    };
    return this.users;
  };

  this.getMaxUserId = function() {
    return Math.max.apply(Math, this.users.map(function(user) {
      return user.id;
    }));
  };

  this.getNumberOfUsers = function() {
    return this.users.length;
  };

  this.getAll = function() {
    return this.users;
  };

  this.getById = function(id) {
    var foundUser = false;
    for (var i = 0; i < this.users.length; i++) {
      var user = this.users[i];
      console.log('...checking user.id ' + user.id);
      if (user.id == id) {
        foundUser = true;
        return user;
      };
    };
    if (!foundUser) {
      console.log('Could not find user with id: ' + id);
      return 'user with id ' + id + ' not found.';
    };
  };

  this.addNewUser = function(firstName, lastName) {
    var newUser = new User(this.getMaxUserId() + 1, firstName, lastName);
    this.users.push(newUser);
    return this.getById(newUser.id);
  };

  this.changeUser = function(id, firstName, lastName) {
    var user = this.getById(id);
    user.firstName = firstName;
    user.lastName = lastName;
    return user;
  };

  this.deleteUser = function(id) {
    // sorry, i'm tired and don't know javascript that well...
    var indexToDelete = -1;
    for (var i = 0; i < this.users.length; i++) {
      var user = this.users[i];
      if (user.id == id) {
        indexToDelete = i;
        break;
      };
    };

    if (indexToDelete >= 0) {
      this.users.splice(indexToDelete, 1);
    };
  };
};

As you can see I am just dabbling with JS…

But hey: It works! ;–)

Source code for this post

You can clone a copy of this project here: https://github.com/draptik/angulardemorestful.

To checkout the correct version for this demo, use the following code:

1
2
3
git clone git@github.com:draptik/angulardemorestful.git
cd angulardemorestful
git checkout -f step6-nodejs-backend

In case you are not using git you can also download the project as ZIP or tar.gz file here: https://github.com/draptik/angulardemorestful/releases/tag/step6-nodejs-backend

AngularJS and CORS

TL;DR

While splitting my AngularJS demo app into independent back- and frontend projects (running two different servers) I stumbled across cross domain issues during development. This post describes how to implement CORS on the server and/or client side of an application.

This post describes how I split the backend and frontend of my AngularJS demo app into separate applications.

Hopefully this will simplify switching the used backend technology in the future (i.e. replacing Java with .NET or Node.JS).

New directory structure

To begin with I created two new top level folders: backend/java-backend and frontend. Then I moved all Java code (including Java IDE settings, pom.xml, etc.) to the new java-backend folder. Since the frontend code (JS, CSS, HTML templates) was previously located in src/main/webapp I moved it to the new frontend folder.

The Java project folder webapp now only contains the following minimal setup:

1
2
3
4
backend/java-backend/src/main/webapp
├── index.jsp
└── WEB-INF
    └── web.xml

The frontend folder now has the following (simplified) structure:

1
2
3
4
5
6
7
8
9
10
11
12
frontend/
├── css
├── frontend-web-server.js
├── index.html
├── js
│   ├── app.js
│   ├── bootstrap
│   ├── controllers.js
│   ├── directives.js
│   ├── filters.js
│   ├── jquery
│   └── services.js

Frontend web server

Not knowing anything about node.js (yet), I just copied the simple web server from Google’s AngularJS demo application PhoneCat to frontend/frontend-web-server.js. For this server to run you will have to install node.js on your system. The frontend server will be running on port 8000.

frontend-web-server.js
1
2
3
// ...
var DEFAULT_PORT = 8000; // <-- frontend port
// ...

We can start/stop the frontend web server using the scripts start_frontend_server.sh and stop_frontend_server.sh.

Backend web server

The backend server is Tomcat. We can start/stop the backend web server using the scripts start_java_backend.sh and stop_java_backend.sh. The backend server will be running on port 8080.

CORS configuration

Cross-origin resource sharing (CORS) allows Javascript to make requests to other domains. Compared to JSONP which only allows the GET HTTP verb, CORS allows all HTTP verbs (GET, POST, PUT, DELETE), making it an ideal candidate for RESTful services. The only drawback: CORS requires a modern browser (see Wikipedia for details).

2014-01-03 CORS can be configured on the server and/or the client side. CORS must be configured on the server and the client side (Thanks to Richard for the pointer!). The following example demonstrates both approaches this.

Server side configuration example (Java)

The basic idea is to add additional header information to the different Access-Control-Allow-* properties of the HTTP response.

ResponseCorsFilter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package ngdemo.web.rest;

import com.google.inject.Singleton;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/*
    Allow CORS requests.
 */
@Singleton
public class ResponseCorsFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException { }

    @Override
    public void destroy() { }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if (servletResponse instanceof HttpServletResponse) {
            HttpServletResponse alteredResponse = ((HttpServletResponse) servletResponse);
            addHeadersFor200Response(alteredResponse);
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    private void addHeadersFor200Response(HttpServletResponse response) {
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Allow-Methods", "Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With");
        response.addHeader("Access-Control-Allow-Headers", "GET, PUT, OPTIONS, X-XSRF-TOKEN");
    }
}

The above filter is used in the Guice configuration via the filter(...).through(...) method:

NgDemoApplicationSetup.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package ngdemo.infrastructure;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Scopes;
import com.google.inject.servlet.GuiceServletContextListener;
import com.google.inject.servlet.ServletModule;
import com.sun.jersey.api.core.PackagesResourceConfig;
import com.sun.jersey.api.core.ResourceConfig;
import com.sun.jersey.guice.spi.container.servlet.GuiceContainer;
import ngdemo.web.rest.ResponseCorsFilter;
import org.codehaus.jackson.jaxrs.JacksonJsonProvider;

public class NgDemoApplicationSetup extends GuiceServletContextListener {

    @Override
    protected Injector getInjector() {
        return Guice.createInjector(new ServletModule() {

            @Override
            protected void configureServlets() {
                super.configureServlets();
                ResourceConfig resourceConfig = new PackagesResourceConfig("ngdemo/web");
                for (Class<?> resource : resourceConfig.getClasses()) {
                    bind(resource);
                }
                bind(JacksonJsonProvider.class).in(Scopes.SINGLETON);
                serve("/web/*").with(GuiceContainer.class);

                // CORS filter:
                filter("/web/*").through(ResponseCorsFilter.class);
            }
        });
    }
}

Client side configuration example (Javascript)

The opposite approach, configuring the client instead of the server, works by (1) setting the useXDomain property to true and (2) removing header properties.

app.js
1
2
3
4
5
6
7
8
9
10
11
'use strict';

angular.module('ngdemo')
        .config(['$httpProvider', function ($httpProvider) {
        // ...

        // delete header from client:
        // http://stackoverflow.com/questions/17289195/angularjs-post-data-to-external-rest-api
        $httpProvider.defaults.useXDomain = true;
        delete $httpProvider.defaults.headers.common['X-Requested-With'];
    }]);

Source code for this post

You can clone a copy of this project here: https://github.com/draptik/angulardemorestful.

To checkout the correct version for this demo, use the following code:

1
2
3
git clone git@github.com:draptik/angulardemorestful.git
cd angulardemorestful
git checkout -f step5-split-frontend-backend-cors

In case you are not using git you can also download the project as ZIP or tar.gz file here: https://github.com/draptik/angulardemorestful/releases/tag/step5-split-frontend-backend-cors

Link Collection #1

When I try to learn new technologies I stumble across many sites. If a site is useful to the problem at hand, I normally add a comment in my source code referencing the URL.

But if I have the same question again a year (or a week) later, I forgot where I put the URL. Then I start to go through my browser’s bookmarks and history. Then I look through the multitude of ‘read-it-later’ services, such as Pocket, Evernote, etc. Sometimes I’m lucky and I actually find what I’m looking for. Most of the time I end up repeating the same search procedure again (which is a waste of time).

So here’s my first link collection. Themed AngularJS…

RESTful CRUD With AngularJS

This post will show how to perform typical CRUD (create, read, update and delete) operations in AngularJS when consuming a RESTful web service.

A prerequisite for this demo is a working RESTful web service. For a basic introduction on creating a Java based RESTful web service, see my introduction on how to consume a RESTful web service with AngularJS created by a Java backend. For completeness sake I’ve added a Java based sample at the end of this post.

Frontend (AngularJS)

Views (Partials)

We will create three views.

The first view will display all users (user-list.html):

The view also provides links to edit (ng-click="editUser(user.id)") and delete (ng-click="deleteUser(user.id)") specific users as well as a link to create a new user (ng-click="createUser()").

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="span6">
    <table class="table table-striped table-condensed">
        <thead>
        <tr>
            <th style="min-width: 80px;">First name</th>
            <th style="min-width: 80px;">Last name</th>
            <th style="width:20px;"> </th>
            <th style="width:20px;"> </th>
        </tr>
        </thead>
        <tbody>
        <tr ng-repeat="user in users">
            <td>{{ user.firstName }}</td>
            <td>{{ user.lastName }}</td>
            <td><a ng-click="editUser(user.id)" class="btn btn-small btn-primary">edit</a></td>
            <td><a ng-click="deleteUser(user.id)" class="btn btn-small btn-danger">delete</a></td>
        </tr>
        </tbody>
    </table>
    <a ng-click="createNewUser()" class="btn btn-small">create new user</a>
</div>

The second and third view (user-detail.html and user-creation.html) both provide a form for entering the user properties.

They only differ in the actions provided. These actions (cancel(), updateUser(), createNewUser()) are invoked using ng-click:

user-[detail|creation].html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div class="container">
    <h1>User detail</h1>

    <form novalidate="novalidate" class="form-horizontal">
        <div class="control-group">
            <label class="control-label" for="inputFirstName">First name:</label>
            <div class="controls">
                <input type="text" id="inputFirstName" ng-model="user.firstName"/>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="inputLastName">Last name:</label>
            <div class="controls">
                <input type="text" id="inputLastName" ng-model="user.lastName"/>
            </div>
        </div>
        <div class="control-group">
            <div class="controls">
              <!-- user-detail.html: -->
                <a ng-click="cancel()" class="btn btn-small">cancel</a>
                <a ng-click="updateUser()" class="btn btn-small btn-primary">update user</a>

              <!-- user-creation.html: -->
                <a ng-click="createNewUser()" class="btn btn-small btn-primary">create new user</a>
            </div>
        </div>
    </form>
</div>

Controller

Next we will create three controllers corresponding to the three views.

UserListCtrl

UserListCtrl provides three functions editUser, deleteUser and createUser.

  • editUser and createUser merely redirect to a different partial view using AngularJs’s $location function.
  • deleteUser calls the UserFactory service method delete (which we will create shortly).

Furthermore the $scope.users is filled with the result from the UsersFactory.query() function.

Note that all required dependencies are injected into the controller’s signature (function ($scope, UsersFactory, UserFactory, $location)).

controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var app = angular.module('ngdemo.controllers', []);

app.controller('UserListCtrl', ['$scope', 'UsersFactory', 'UserFactory', '$location',
    function ($scope, UsersFactory, UserFactory, $location) {

        // callback for ng-click 'editUser':
        $scope.editUser = function (userId) {
            $location.path('/user-detail/' + userId);
        };

        // callback for ng-click 'deleteUser':
        $scope.deleteUser = function (userId) {
            UserFactory.delete({ id: userId });
            $scope.users = UsersFactory.query();
        };

        // callback for ng-click 'createUser':
        $scope.createNewUser = function () {
            $location.path('/user-creation');
        };

        $scope.users = UsersFactory.query();
    }]);
  /* ... */

UserDetailCtrl and UserCreationCtrl

UserDetailCtrl provides the function updateUser, which in turn invokes the service method UserFactory.update. The $scope.user is filled with the result from calling UserFactory.show. cancel is just a convenient link redirecting back to the user-list view.

UserCreationCtrl provides the function createNewUser, calling UsersFactory.create.

Again, both controllers use $location to redirect back to the user-list partial view.

controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* ... */
app.controller('UserDetailCtrl', ['$scope', '$routeParams', 'UserFactory', '$location',
    function ($scope, $routeParams, UserFactory, $location) {

        // callback for ng-click 'updateUser':
        $scope.updateUser = function () {
            UserFactory.update($scope.user);
            $location.path('/user-list');
        };

        // callback for ng-click 'cancel':
        $scope.cancel = function () {
            $location.path('/user-list');
        };

        $scope.user = UserFactory.show({id: $routeParams.id});
    }]);

app.controller('UserCreationCtrl', ['$scope', 'UsersFactory', '$location',
    function ($scope, UsersFactory, $location) {

        // callback for ng-click 'createNewUser':
        $scope.createNewUser = function () {
            UsersFactory.create($scope.user);
            $location.path('/user-list');
        }
    }]);

Don’t forget to map the views to the corresponding controllers in app.js using the $routeProvider:

app.js
1
2
3
4
5
6
7
angular.module('ngdemo', ['ngdemo.filters', 'ngdemo.services', 'ngdemo.directives', 'ngdemo.controllers']).
    config(['$routeProvider', function ($routeProvider) {
        $routeProvider.when('/user-list', {templateUrl: 'partials/user-list.html', controller: 'UserListCtrl'});
        $routeProvider.when('/user-detail/:id', {templateUrl: 'partials/user-detail.html', controller: 'UserDetailCtrl'});
        $routeProvider.when('/user-creation', {templateUrl: 'partials/user-creation.html', controller: 'UserCreationCtrl'});
        $routeProvider.otherwise({redirectTo: '/user-list'});
    }]);

Service

AngularJS can consume the web service using $resource. This module is injected via 'ngResource'.

We create two factories:

  • UsersFactory (note the plural s) calls the web service with methods not requiring an id (query and create).
  • UserFactory calls the web service with methods requiring a user id (show, update and delete).
services.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var services = angular.module('ngdemo.services', ['ngResource']);

services.factory('UsersFactory', function ($resource) {
    return $resource('/ngdemo/web/users', {}, {
        query: { method: 'GET', isArray: true },
        create: { method: 'POST' }
    })
});

services.factory('UserFactory', function ($resource) {
    return $resource('/ngdemo/web/users/:id', {}, {
        show: { method: 'GET' },
        update: { method: 'PUT', params: {id: '@id'} },
        delete: { method: 'DELETE', params: {id: '@id'} }
    })
});

Backend (Java)

Here is an example of a RESTful web service created with Java:

UserRestService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package ngdemo.web.rest;

import com.google.inject.Inject;
import ngdemo.domain.User;
import ngdemo.service.contract.UserService;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;

@Path("/users")
public class UserRestService {

    private final UserService userService;

    @Inject
    public UserRestService(UserService userService) {
        this.userService = userService;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<User> getAllUsersInJSON() {
        return userService.getAllUsers();
    }

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public User getUserById(@PathParam("id") int id) {
        return userService.getById(id);
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public User create(User user) {
        return userService.createNewUser(user);
    }

    @PUT
    @Path("{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public User update(User user) {
        return userService.update(user);
    }

    @DELETE
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public void remove(@PathParam("id") int id) {
        userService.remove(id);
    }
}

You can clone a copy of this project here: https://github.com/draptik/angulardemorestful.

To checkout the correct version for this demo, use the following code:

1
2
3
git clone git@github.com:draptik/angulardemorestful.git
cd angulardemorestful
git checkout -f step4-angularjs-crud

In case you are not using git you can also download the project as ZIP or tar.gz file here: https://github.com/draptik/angulardemorestful/releases/tag/step4-angularjs-crud

Unit Testing RESTful Services

In my two previous posts I gave an introduction on how to consume a RESTful web service with AngularJS created by a Java backend and use Guice in the Java backend.

In this post I will show how to create a unit test for this web service.

Most of this code is inspired by a blog post from Paulo Renato de Athaydes.

We will need to install some new dependencies:

  • jetty-maven-plugin
  • junit
  • jersey-client
  • jersey-grizzly2

Grizzly will be our web server for testing.

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<?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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <name>ngdemo Maven Webapp</name>
    <groupId>ngdemo</groupId>
    <artifactId>ngdemo</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jersey.version>1.17.1</jersey.version>
        <guice.version>3.0</guice.version>
    </properties>

    <build>
        <finalName>ngdemo</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.0</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>8.1.11.v20130520</version>
                <configuration>
                    <scanIntervalSeconds>10</scanIntervalSeconds>
                    <connectors>
                        <connector implementation="org.eclipse.jetty.nio.SelectChannelConnector">
                            <port>8080</port>
                            <maxIdleTime>60000</maxIdleTime>
                        </connector>
                    </connectors>
                    <stopKey/>
                    <stopPort/>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>

        <!-- javax: XML binding -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.1</version>
        </dependency>

        <!-- RESTful web service: Jersey ====================================== -->
        <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-server</artifactId>
            <version>${jersey.version}</version>
        </dependency>
        <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-servlet</artifactId>
            <version>${jersey.version}</version>
        </dependency>
        <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-json</artifactId>
            <version>${jersey.version}</version>
        </dependency>

        <!-- Guice ============================================================= -->
        <dependency>
            <groupId>com.google.inject</groupId>
            <artifactId>guice</artifactId>
            <version>${guice.version}</version>
        </dependency>

        <dependency>
            <groupId>com.google.inject.extensions</groupId>
            <artifactId>guice-servlet</artifactId>
            <version>${guice.version}</version>
        </dependency>

        <dependency>
            <groupId>com.sun.jersey.contribs</groupId>
            <artifactId>jersey-guice</artifactId>
            <version>${jersey.version}</version>
        </dependency>

        <!-- Required for bypassing web.xml via Guice. Used in TestServlet.java -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>


        <!-- Unit testing ====================================================== -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-client</artifactId>
            <version>${jersey.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-grizzly2</artifactId>
            <version>${jersey.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Our class under test is UserRestService.java:

UserRestService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package ngdemo.rest;

import com.google.inject.Inject;
import ngdemo.domain.User;
import ngdemo.service.contract.UserService;

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


@Path("/users")
public class UserRestService {

    private final UserService userService;

    @Inject
    public UserRestService(UserService userService) {
        this.userService = userService;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public User getDefaultUserInJSON() {
        return userService.getDefaultUser();
    }
}

Here is the corresponding unit test class UserRestServiceTest.java:

UserRestServiceTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package ngdemo.tests;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.ServletModule;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
import com.sun.jersey.api.core.PackagesResourceConfig;
import com.sun.jersey.api.core.ResourceConfig;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory;
import com.sun.jersey.guice.spi.container.GuiceComponentProviderFactory;
import ngdemo.repositories.contract.UserRepository;
import ngdemo.repositories.impl.UserRepositoryImpl;
import ngdemo.service.contract.UserService;
import ngdemo.service.impl.UserServiceImpl;
import org.glassfish.grizzly.http.server.HttpServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI;

import static junit.framework.Assert.assertEquals;

public class UserRestServiceTest {

    static final URI BASE_URI = getBaseURI();
    HttpServer server;

    private static URI getBaseURI() {
        return UriBuilder.fromUri("http://localhost/").port(9998).build();
    }

    @Before
    public void startServer() throws IOException {
        System.out.println("Starting grizzly...");

        Injector injector = Guice.createInjector(new ServletModule() {
            @Override
            protected void configureServlets() {
                bind(UserService.class).to(UserServiceImpl.class);
                bind(UserRepository.class).to(UserRepositoryImpl.class);
            }
        });

        ResourceConfig rc = new PackagesResourceConfig("ngdemo.rest");
        IoCComponentProviderFactory ioc = new GuiceComponentProviderFactory(rc, injector);
        server = GrizzlyServerFactory.createHttpServer(BASE_URI + "rest/", rc, ioc);

        System.out.println(String.format("Jersey app started with WADL available at "
                + "%srest/application.wadl\nTry out %sngdemo\nHit enter to stop it...",
                BASE_URI, BASE_URI));
    }

    @After
    public void stopServer() {
        server.stop();
    }

    @Test
    public void testGetDefaultUser() throws IOException {
        Client client = Client.create(new DefaultClientConfig());
        WebResource service = client.resource(getBaseURI());
        ClientResponse resp = service.path("rest").path("users")
                .accept(MediaType.APPLICATION_JSON)
                .get(ClientResponse.class);
        System.out.println("Got stuff: " + resp);
        String text = resp.getEntity(String.class);

        assertEquals(200, resp.getStatus());
        assertEquals("{\"firstName\":\"JonFromREST\",\"lastName\":\"DoeFromREST\"}", text);
    }
}

In the startServer method we create an injector for Guice, which we can then pass into the GuiceComponentProviderFactory to create the inversion of control (IoC) container.

Together with the ResourceConfig the IoC container is passed to Grizzly’s server factory to create the web server for testing.

Within the actual test method testGetDefaultUser we only have to setup the Jersey Client to retrieve the response (from the Grizzly server).

Here’s the test output from Maven:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ mvn test
...
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running ngdemo.tests.UserRestServiceTest
Starting grizzly...
Jul 19, 2013 1:50:35 PM com.sun.jersey.api.core.PackagesResourceConfig init
INFO: Scanning for root resource and provider classes in the packages:
  ngdemo.rest
Jul 19, 2013 1:50:35 PM com.sun.jersey.api.core.ScanningResourceConfig logClasses
INFO: Root resource classes found:
  class ngdemo.rest.UserRestService
Jul 19, 2013 1:50:35 PM com.sun.jersey.api.core.ScanningResourceConfig init
INFO: No provider classes found.
Jul 19, 2013 1:50:35 PM com.sun.jersey.server.impl.application.WebApplicationImpl _initiate
INFO: Initiating Jersey application, version 'Jersey: 1.17.1 02/28/2013 12:47 PM'
Jul 19, 2013 1:50:36 PM com.sun.jersey.guice.spi.container.GuiceComponentProviderFactory getComponentProvider
INFO: Binding ngdemo.rest.UserRestService to GuiceInstantiatedComponentProvider
Jul 19, 2013 1:50:37 PM org.glassfish.grizzly.http.server.NetworkListener start
INFO: Started listener bound to [localhost:9998]
Jul 19, 2013 1:50:37 PM org.glassfish.grizzly.http.server.HttpServer start
INFO: [HttpServer] Started.
Jersey app started with WADL available at http://localhost:9998/rest/application.wadl
Try out http://localhost:9998/ngdemo
Hit enter to stop it...
Got stuff: GET http://localhost:9998/rest/users returned a response status of 200 OK
Jul 19, 2013 1:50:37 PM org.glassfish.grizzly.http.server.NetworkListener stop
INFO: Stopped listener bound to [localhost:9998]
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.604 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.378s

Test time is 2.6 sec. Not bad considering we are starting a web server, deploying our app, creating a client, running the test and shutting down the web server.

Running this test from within IntelliJ takes: 0.009 sec…

You can clone a copy of this project here: https://github.com/draptik/angulardemorestful.

To checkout the correct version for this demo, use the following code:

1
2
3
git clone git@github.com:draptik/angulardemorestful.git
cd angulardemorestful
git checkout -f step3-backend-test

In case you are not using git you can also download the project as ZIP or tar.gz file here: https://github.com/draptik/angulardemorestful/releases/tag/step3-backend-test

Guice in Java Web Application

Google’s Guice framework promises to be a lightweight(!) Inversion-of-Control (IoC) container.

Advantages compared to Spring:

  • Spring is much more than an IoC container, and therefore overkill for many projects.
  • Configuration by code. NO XML.

Based on my previous post showing how to use AngularJS with a Java RESTful backend I extended the simple demo application to use Guice.

Let’s say we have a UserServiceImpl class which depends on a UserFactory interface. The UserFactory interface is injected into the constructor of the UserServiceImpl class.

The only thing we have to do is add the @Inject annotation to the constructor so that Guice can do its job.

UserServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package ngdemo.service.impl;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import ngdemo.domain.User;
import ngdemo.service.contract.UserFactory;
import ngdemo.service.contract.UserService;

import java.util.List;

public class UserServiceImpl implements UserService {

    private final UserFactory userFactory;

    @Inject
    public UserServiceImpl(UserFactory userFactory) {
        this.userFactory = userFactory;
    }

    @Override
    public List<User> getDefaultUsers() {
        return this.userFactory.createUsers();
    }

    @Override
    public User getDefaultUser() {
        return this.userFactory.createUser();
    }

}

For the IoC container to know which implementation to inject we have to create a Guice Module which derives from AbstractModule:

UserModule.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package ngdemo.infrastructure;

import com.google.inject.AbstractModule;
import ngdemo.service.contract.UserFactory;
import ngdemo.service.contract.UserService;
import ngdemo.service.impl.UserFactoryImpl;
import ngdemo.service.impl.UserServiceImpl;

public class UserModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(UserFactory.class).to(UserFactoryImpl.class);
        bind(UserService.class).to(UserServiceImpl.class);
    }
}

The UserModule class demonstrates the advantage of Guice vs. Spring: NO XML. When using Spring you normally would have to create Spring beans in an XML file like this:

applicationContext.xml (pseudo code)
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean id="userService" class="ngdemo.service.impl.UserServiceImpl">
      <constructor-arg ref="userFactory"/>
  </bean>
  
  <bean id="userFactory" class="ngdemo.service.impl.UserFactoryImpl" />
      
</beans>

Next we have to create a replacement for the servlets required by the servlet container:

ngdemo.infrastructure.NgDemoApplicationSetup.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package ngdemo.infrastructure;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
import com.google.inject.servlet.ServletModule;
import com.sun.jersey.api.core.PackagesResourceConfig;
import com.sun.jersey.api.core.ResourceConfig;
import com.sun.jersey.guice.spi.container.servlet.GuiceContainer;

public class NgDemoApplicationSetup extends GuiceServletContextListener {

    @Override
    protected Injector getInjector() {

        return Guice.createInjector(new ServletModule() {

            @Override
            protected void configureServlets() {

                super.configureServlets();

                // Configuring Jersey via Guice:
                ResourceConfig resourceConfig = new PackagesResourceConfig("ngdemo/rest");
                for (Class<?> resource : resourceConfig.getClasses()) {
                    bind(resource);
                }
                serve("/rest/*").with(GuiceContainer.class);
            }
        }, new UserModule()); // <-- Adding other Guice Dependency Injection Modules
    }
}

And finally the file web.xml:

web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app id="WebApp_ID" version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>Restful Web Application</display-name>

    <filter>
        <filter-name>guiceFilter</filter-name>
        <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>guiceFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>ngdemo.infrastructure.NgDemoApplicationSetup</listener-class>
    </listener>
</web-app>

The file web.xml is now free of any <servlet> tags. The only thing that has to be configured in XML is the <listener-class>. The value of the <listener-class> is our Java class NgDemoApplicationSetup, so all further configuration can be defined in a type safe manner.

You can clone a copy of this project here: https://github.com/draptik/angulardemorestful.

To checkout the correct version for this demo, use the following code:

1
2
3
git clone git@github.com:draptik/angulardemorestful.git
cd angulardemorestful
git checkout -f step2-guice

In case you are not using git you can also download the project as ZIP or tar.gz file here: https://github.com/draptik/angulardemorestful/releases/tag/step2-guice

AngularJS Example Using a Java RESTful Web Service

AngularJS is the current MVV-Whatever JavaScript framework by Google. Among other things, it provides bidirectional data binding.

Although I’m neither a Java nor a JavaScript expert, I choose the following scenario for my ‘Hello-World’ example:

  1. Java backend provides a RESTful web service.

  2. AngularJS consumes the web service.

That’s it.

Project structure

I intentionally put the backend and frontend code in the same project to simplify the example. In a real project you probably want to have seperate projects for front- and backend.

1
2
3
4
5
6
7
8
+---------------------------------------------------+
| demo project                                      |
|                                                   |
| +----------------+              +---------------+ |
| | backend (Java) | < -(REST)- > | frontend (JS) | |
| +----------------+              +---------------+ |
|                                                   |
+---------------------------------------------------+

Since the backend is Java based, I used a Maven default structure (maven-archetype-site-simple):

project structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
├── _documentation
│   └── readme.txt
├── ngdemo.iml
├── pom.xml
└── src
    └── main
        ├── java
        │   └── ngdemo
        │       ├── domain
        │       │   └── User.java
        │       ├── rest
        │       │   └── UserRestService.java
        │       └── service
        │           └── UserService.java
        └── webapp
            ├── css
            │   └── app.css
            ├── img
            ├── index-async.html
            ├── index.html
            ├── index.jsp
            ├── js
            │   ├── app.js
            │   ├── controllers.js
            │   ├── directives.js
            │   ├── filters.js
            │   └── services.js
            ├── lib
            │   └── angular
            │       ├── angular-cookies.js
            │       ├── angular-cookies.min.js
            │       ├── angular.js
            │       ├── angular-loader.js
            │       ├── angular-loader.min.js
            │       ├── angular.min.js
            │       ├── angular-resource.js
            │       ├── angular-resource.min.js
            │       ├── angular-sanitize.js
            │       ├── angular-sanitize.min.js
            │       └── version.txt
            ├── partials
            │   └── partial1.html
            └── WEB-INF
                └── web.xml

src/main/java is the backend.

src/main/webapp/js is the frontend.

src/main/webapp/ also includes a copy of angular-seed.

RESTful web service (backend)

Jersey is the Java reference implementation for providing REST.

Install the following dependencies in your pom.xml:

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- .. -->
<!-- RESTful web service: Jersey -->
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-server</artifactId>
    <version>1.17.1</version>
</dependency>
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-servlet</artifactId>
    <version>1.17.1</version>
</dependency>
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>1.17.1</version>
</dependency>
<!-- .. -->

Add the following servlet snippet to your web.xml:

web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- .. -->
<servlet>
    <servlet-name>jersey-serlvet</servlet-name>

    <servlet-class>
        com.sun.jersey.spi.container.servlet.ServletContainer
    </servlet-class>

    <init-param>
        <param-name>com.sun.jersey.config.property.packages</param-name>
        <param-value>ngdemo.rest</param-value>
    </init-param>

    <init-param>
        <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
        <param-value>true</param-value>
    </init-param>

    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>jersey-serlvet</servlet-name>
    <url-pattern>/rest/*</url-pattern>
</servlet-mapping>
<!-- .. -->

Enough configuration for now: Create a simple User object…

User.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package ngdemo.domain;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class User {

    private String firstName;
    private String lastName;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

…and a service class…

UserService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
package ngdemo.service;

import ngdemo.domain.User;

public class UserService {

    public User getDefaultUser() {
        User user = new User();
        user.setFirstName("JonFromREST");
        user.setLastName("DoeFromREST");
        return user;
    }
}

…and finally the RESTful Service… (Update 2015-08-07 small fix, thanks Jason):

UserRestService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package ngdemo.rest;

import ngdemo.domain.User;
import ngdemo.service.UserService;

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

@Path("/users")
public class UserRestService {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public User getDefaultUserInJSON() {
        UserService userService = new UserService();
        return userService.getDefaultUser();
    }
}

Converting the User object to JSON via @Produces(MediaType.APPLICATION_JSON) requires jersey-json in web.xml (POJOMappingFeature).

Consuming web service from AngularJS (frontend)

Don’t forget to add angular-resources.js to your index.html

Consuming the web service:

services.js
1
2
3
4
5
6
7
8
9
10
11
var services = angular.module('ngdemo.services', ['ngResource']);

services.factory('UserFactory', function ($resource) {
    return $resource('/ngdemo/rest/users', {}, {
        query: {
            method: 'GET',
            params: {},
            isArray: false
        }
    })
});

Usage in controller:

controller.js
1
2
3
4
5
6
7
var app = angular.module('ngdemo.controllers', []);

app.controller('MyCtrl1', ['$scope', 'UserFactory', function ($scope, UserFactory) {
    UserFactory.get({}, function (userFactory) {
        $scope.firstname = userFactory.firstName;
    })
}]);

Usage in view:

1
2
3
4
5
<div>
    <p>
        Result from RESTful service is: {{ firstname }}
    </p>
</div>

Et voila:

Update (2013-07-18):

You can clone a copy of this project here: https://github.com/draptik/angulardemorestful.

To checkout the correct version for this demo, use the following code:

1
2
3
git clone git@github.com:draptik/angulardemorestful.git
cd angulardemorestful
git checkout -f step1

In case you are not using git you can also download the project as ZIP or tar.gz file here: https://github.com/draptik/angulardemorestful/releases/tag/step1