Develop a web application with Angular2

Angular2 promises many improvements over the previous version.

It’s simpler to learn thanks to easier and more concise concepts like component-based architecture.

The Angular2 module system makes it easier to develop complex and larger projects.

Angular2 is faster than its ancestor thanks to completely rewritten data binding and change detection.

Router module is improved by providing new features like sibling views and nested states.

The aim of this post is to discover some new Angular2 features by developing a web project that uses apart Angular2 other projects like Express, TypeScript and MongoDb.

The source code is provided on GitHub here. You can run the project at this link.

Project structure

The Angular2 sample project has this structure.

ng2-project
    |
    +-client
    |   +-app 
    |   +-assets (Directoy conatins images and css)
    |   +-index.html
    |   +-tsconfig.json
    |   \-typings.json
    |
    +-server 
    |   +-server.ts
    |   +-tsconfig.json
    |   \-typings.json
    |
    +-dist (Directory generated by gulp)
    |
    +-gulpfile.js
    \-pakage.json

You can explore the project structure on Github here

Project dependencies

The package.json file identifies npm package dependencies for the project.

{
  "name": "ng2-project",
  "version": "1.0.0",
  "main": "server/index.js",
  "license": "ISC",
  "scripts": {
    "start": "concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "postinstall": "gulp"
  },
  "dependencies": {
    "@angular/core": "2.0.0",
    "@angular/common": "2.0.0",
    "@angular/compiler": "2.0.0",
    "@angular/http": "2.0.0",
    "@angular/platform-browser": "2.0.0",
    "@angular/platform-browser-dynamic": "2.0.0",
    "@angular/router": "3.0.0",
    "@angular/upgrade": "2.0.0",
    "systemjs": "0.19.27",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.12",
    "zone.js": "^0.6.23",
    "angular2-in-memory-web-api": "0.0.20",
    "material-design-lite": "1.1.2",
    "express": "4.13.4",
    "mongodb": "2.1.18",
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "lite-server": "^2.1.0",
    "typescript": "1.8.10",
    "del": "2.2.0",
    "gulp": "3.9.1",
    "gulp-concat": "2.6.0",
    "gulp-sourcemaps": "1.6.0",
    "gulp-typescript": "2.13.4",
    "gulp-tsd": "0.1.1",
    "gulp-typings": "1.3.6",
    "run-sequence": "1.1.5"
  }
}

1- Server Side

We start with the server side implementation, and then we can test the rest API before using it in the client side code.

1.1 Add tsconfig.json file

The tsconfig.json file provides a TypeScript compiler configuration.

{
  "version": "1.8.10",
  "compilerOptions": {
    "target": "ES5",
    "module": "commonjs",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "files": [
    "typings/main.d.ts"
  ],
  "exclude": [
    "typings/browser",
    "typings/browser.d.ts"
  ]
}

1.1 Add typings file

TypeScript compiler doesn’t recognize natively some JavaScript libraries features and syntax.
Some of the included files that provides required description for TypeScript. Others don’t provide it and they will require to be configured in typings.js file.

{
  "version": false,
  "ambientDependencies": {
    "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "express-serve-static-core": "github:DefinitelyTyped/DefinitelyTyped/express-serve-static-core/express-serve-static-core.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "express": "github:DefinitelyTyped/DefinitelyTyped/express/express.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "serve-static": "github:DefinitelyTyped/DefinitelyTyped/serve-static/serve-static.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "mime": "github:DefinitelyTyped/DefinitelyTyped/mime/mime.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34"
  }
}

1.2 – Install and create database

To install MongoDB you can download and install it by following instructions in this link https://docs.mongodb.com/manual/installation.
Next we will need to initialize database data. you can use the code bellow:

use mydata

db.users.insert([
{"id" : "1" , "name" : "Steve" , "mail" : "steve@apple.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "2" , "name" : "Narco" , "mail" : "narco@gamil.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "3" , "name" : "Bombasto" , "mail" : "bombasto@gmail.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "4" , "name" : "Celeritas" , "mail" : "celeritas@gmail.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "5" , "name" : "Magneta" , "mail" : "magneta@gmail.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "6" , "name" : "RubberMan" , "mail" : "rubber.man@gmail.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"}
]);

You can use MongoDb as a service provided by mLab. All what you have to do is to sign up, create a database and add a user to access the database by using a simple web user interface.

1.3 – Implement the Rest API

In this project we use Express.js to implement the server side API.

Express.js is a web application framework for Node.js, It is designed for building web applications and APIs.

import express = require('express');
import path = require('path');
var port: number = process.env.PORT || 3000;
var app = express();
var MongoClient = require('mongodb').MongoClient;
var database;

app.use('/assets', express.static(path.resolve(__dirname, 'assets')));
app.use('/app', express.static(path.resolve(__dirname, 'app')));
app.use('/libs', express.static(path.resolve(__dirname, 'libs')));

var server = app.listen(port, function() {
    var port = server.address().port;
    console.log("This express app is listening on port " + port);
});

MongoClient.connect('mongodb://test:test@ds021761.mlab.com:21761/mydata', 
    function(err, db) {
    if (err) {
        throw err;
    }
    database = db;
});

app.get("/api/users", (req, res) => {
    // Get all registrations
    database.collection('users').find().toArray(function(err, result) {
        if (err) {
            console.log("Can't get users from database : " + err);
            throw err;
        }
        console.log("/api/users result size = " + result.length);
        res.send(result);
    });
});

app.get("/api/users/:id", (req, res) => {
    database.collection('users').findOne( {id : req.params.id} , function(err, item) {
        if (err) {
            console.log("Can't get users from database : " + err);
            throw err;
        }
        console.log("found user :" + JSON.stringify(item));
        res.send(item);
    });
});

var renderIndex = (req: express.Request, res: express.Response) => {
    res.sendFile(path.resolve(__dirname, 'index.html'));
}

app.get('/*', renderIndex);

1.4 Build and test server side code

To build a project we use Gulp that install typings and compiles TypeScript files.

Bellow the source code of gulpfile.js file

var gulp = require('gulp');
var sourcemaps = require('gulp-sourcemaps');
var ts = require('gulp-typescript');
var runSequence = require('run-sequence');
var gulpTypings = require("gulp-typings");

// TYPINGS
gulp.task("installTypings", function () {
    var stream = gulp.src(["./server/typings.json"])
        .pipe(gulpTypings());
    return stream;
});

// SERVER
gulp.task('buildServer', function () {
    var tsProject = ts.createProject('server/tsconfig.json');
    var tsResult = gulp.src('server/**/*.ts')
        .pipe(sourcemaps.init())
        .pipe(ts(tsProject))
    return tsResult.js
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('dist'))
});

gulp.task('build', function (callback) {
    runSequence('installTypings', 'buildServer', callback);
});

gulp.task('default', ['build']);

To build the project, Open a terminal window and enter this command:

npm install

Next, run the node server:

node dist/server.js

Then test the API in your browseron this url:
http://localhost:8080/api/users

If everything is well done, the browser display an array of users in JSON format.

2- Client side

2.1 Add typings file

Bellow the source code of client typings.json

{
  "ambientDependencies": {
    "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654"
  }
}

2.2 Add typescript configuration file

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
  ]
}

2.3 Add SystemJs config file

SystemJs is a dynamic module loader. It loads ES6 modules, AMD, CommonJS and global scripts in the browser and NodeJS.
SystemJs will load libraries when they are required.

The configuration below tells SystemJs where to look for a module when component is imported.

(function(global) {

    // map tells the System loader where to look for things
    var map = {
        'app':      'app',        
        // angular bundles
        '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
        '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
        '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
        '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
        '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
        '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
        '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
        'rxjs':     'libs/rxjs',
        '@angular': 'libs/@angular'
    };

    // packages tells the System loader how to load when no filename and/or no extension
    var packages = {
        'app':      { main: './main.js',  defaultExtension: 'js' },
        'rxjs':     { defaultExtension: 'js' },
        'angular2-in-memory-web-api': {defaultExtension: 'js'}
    };

    var config = {
        paths: { // paths serve as alias
          'npm:': 'libs/'
        },
        map: map,
        packages: packages
    }

    System.config(config);
})(this);

2.4 Add index.html page

Here, we import style sheets, load libraries and configure SystemJs.

We provide to SystemJS the root module (app) of the application. Then SytemJs will look for all dependencies referenced by the app module.

<html lang="en">
<head>
    <base href="/"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <title>ng2-project</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&amp;lang=en">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="https://code.getmdl.io/1.1.2/material.indigo-pink.min.css">
    <link rel="stylesheet" href="assets/css/styles.css">

    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="libs/es6-shim/es6-shim.min.js"></script>
    <script src="libs/zone.js/dist/zone.js"></script>
    <script src="libs/reflect-metadata/Reflect.js"></script>
    <script src="libs/systemjs/dist/system.src.js"></script>

    <!-- 2. Configure SystemJS -->
    <script src="app/systemjs.config.js"></script>
    <script>
        System.import('app').catch(function (err) {
            console.error(err);
        });
    </script>

    <script src="libs/material-design-lite/dist/material.min.js"></script>

</head>

<!-- 3. Display the application -->
<body class="mdl-color--grey-100">
    <my-app>Loading...</my-app>
</body>
</html>

2.5 Add application module : app.module.ts

NgModule is a decorator function that describes the module. The most important properties are:

  • declarations – the view classes that belong to this module.
  • imports – other modules whose exported classes are needed by component templates declared in this module.
  • providers – creators of services that this module contributes to the global collection of services; they become accessible in all parts of the app.
  • bootstrap – the main application view, called the root component, that hosts all other app views. Only the root module should set this bootstrap property.

Here’s a root module of our application:

import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { HttpModule }           from '@angular/http';
import { AppComponent }         from './app.component';
import { routing, appRoutingProviders }  from './app.routing';
import {HomeComponent}          from './home/home.comonent';
import {UserListComponent}      from './users/user-list.component';
import {UserDetailComponent}    from './users/user-detail.component';

@NgModule({
  imports: [
    BrowserModule,
    routing,
    HttpModule
  ],
  declarations: [
    AppComponent,
    HomeComponent,
    UserListComponent,
    UserDetailComponent
  ],
  providers: [
    appRoutingProviders
  ],
  bootstrap: [ AppComponent ]
})

export class AppModule { }

2.6 Add main.ts file

Here we launch the application by bootstrapping its root module.

/// <reference path="../typings/browser.d.ts" />
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

2.7 Configure router : app.routing.ts

Here we configure the router that looks for a corresponding Route from which it can determine the component to display.

In the following example, we configure our application with three route definitions.

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomeComponent}         from './home/home.comonent';
import {HomeResolver}         from './home/home.resolver';
import {HomeService}         from './home/home.service';
import {UserListComponent}     from './users/user-list.component';
import {UserDetailComponent}   from './users/user-detail.component';
import {UserDetailResolver}   from './users/user-detail.resolver';
import {UserListResolver}   from './users/user-list.resolver';
import {UserService}   from './users/user.service';

export const appRoutes : Routes =[
    {path: '',  
     component: HomeComponent,
     resolve : {technologies: HomeResolver}
    },
    {path: 'users', 
     component: UserListComponent,
     resolve : {users: UserListResolver}
    },
    {path: 'user/:id', 
     component: UserDetailComponent, 
     resolve : {user: UserDetailResolver}
    }];

export const appRoutingProviders: any[] = [HomeResolver, UserListResolver, 
        UserDetailResolver, HomeService, UserService];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

To achieve a better user-experience when browsing between pages, we use resolver.

It avoids displaying pages before data finished loading.

It also makes the controller’s code much cleaner in contrast to fetching data inside the controller.

import {Injectable} from '@angular/core';
import {Resolve, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import { User, UserService } from './user.service';

@Injectable()
export class UserListResolver implements Resolve<User[]> {
 constructor(
        private service: UserService
 ) {}
 resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<User[]> {
        return this.service.getUsers();
 }
}

2.7 Add components

2.6.1 Add application main component

import {Component} from '@angular/core';
import {MDL} from './directives/MaterialDesignLite';
import './rxjs-operators';

@Component({
    selector: 'my-app',
    templateUrl: 'app/app.component.html'
})

export class AppComponent { }

“rxjs-oppertors” references the “rxjs-oppertors.ts” file where we will imports all required rxjs statics and operators we need for this application.

// Statics
import 'rxjs/add/observable/throw';
// Operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/toPromise';

Next, we write the template html (app.component.html) code.

<div mdl class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
  <header class="mdl-layout__header">
    <div class="mdl-layout__header-row">
      <!-- Title -->
      <span class="mdl-layout-title">Title</span>
      <!-- Add spacer, to align navigation to the right -->
      <div class="mdl-layout-spacer"></div>
      <!-- Navigation. We hide it in small screens. -->
      <nav class="mdl-navigation mdl-layout--large-screen-only">
        <a class="mdl-navigation__link" [routerLink]="['/']" >Home</a>
        <a class="mdl-navigation__link" [routerLink]="['users']">Users</a>
      </nav>
    </div>
  </header>
  <div class="mdl-layout__drawer">
    <span class="mdl-layout-title">Title</span>
    <nav class="mdl-navigation">
      <a class="mdl-navigation__link" [routerLink]="['/']" >Home</a>
      <a class="mdl-navigation__link" [routerLink]="['/users']">Users</a>
    </nav>
  </div>
  <main class="mdl-layout__content">
       <router-outlet></router-outlet>
  </main>
</div>

2.6.2 Add users list component

This page calls a rest web service and display users list.

First, we develop service module.

import {Injectable} from '@angular/core';
import {Http, Headers, Response} from '@angular/http';
import {Observable}     from 'rxjs/Observable';

export class User {
    constructor(public id:number, public name:string, 
         public email:string, public phone:string, public adress:string) {
    }
}

@Injectable()
export class UserService {
    constructor (private http: Http) {}
    private _usersUrl = 'api/users';

    private extractData(res: Response) {
        if (res.status < 200 || res.status >= 300) {
            throw new Error('Bad response status: ' + res.status);
        }
        return res.json();
    }

    private handleError (error: any) {
        let errMsg = error.message || 'Server error';
        console.error(errMsg); // log to console instead
        return Observable.throw(errMsg);
    }

    getUsers() : Observable<User[]> {
        return this.http.get(this._usersUrl)
            .map(this.extractData)
            .catch(this.handleError);
    }

    getUser(id:number | string) : Observable<User> {
        return this.http.get(this._usersUrl + "/" + id)
            .map(this.extractData)
            .catch(this.handleError);
    }
}

Next, we develop the UsersList component.

import {Component, OnInit}   from '@angular/core';
import {Router, ActivatedRoute} from '@angular/router';
import {User}   from './user.service';


@Component({
    templateUrl: 'app/users/user-list.component.html'
})
export class UserListComponent implements OnInit {
    users:User[];
    errorMessage:string;
    private selectedId:number;

    constructor(private route: ActivatedRoute,
                private router:Router) {
    }

    ngOnInit() {
        this.users = this.route.snapshot.data['users'];
    }

    isSelected(user:User) {
        return user.id === this.selectedId;
    }

    onSelect(user:User) {
        this.router.navigate(['/user', user.id]);
    }
}

The source code bellow presents the template html code

<div class="page-content mdl-grid">
    <div class="mdl-grid--no-spacing mdl-card mdl-shadow--4dp mdl-cell mdl-cell--12-col">
    <div class="users-card-wide">
        <div class="mdl-card__title mdl-color--purple-700">
            <h1 class="mdl-card__title-text">Users</h1>
        </div>
        <div class="mdl-list">
          <div class="mdl-list__item mdl-list__item--two-line" *ngFor="let user of users"
            [class.selected]="isSelected(user)"
            (click)="onSelect(user)">
            <span class="mdl-list__item-primary-content">
              <img class="user-list-avatar mdl-list__item-avatar" 
                   src="assets/img/users/user-{{user.id}}.jpg">
              <span>{{user.name}}</span>
              <span class="mdl-list__item-sub-title">
                {{user.mail}}
              </span>
            </span>
            <a class="mdl-list__item-secondary-action" href="#">
                <i class="material-icons">star</i>
            </a>
          </div>
        </div>
    </div>
</div>

3. Build project

To build the client side and the server side code of our project, we update gulpfile.js.

var gulp = require('gulp');
var path = require('path');
var sourcemaps = require('gulp-sourcemaps');
var ts = require('gulp-typescript');
var del = require('del');
var runSequence = require('run-sequence');
var gulpTypings = require("gulp-typings");


gulp.task('clean', function () {
    return del('dist')
});


// TYPINGS
gulp.task("installTypings", function () {
    var stream = gulp.src(["./client/typings.json", "./server/typings.json"])
        .pipe(gulpTypings());
    return stream;
});

gulp.task("deleteDupTypings", function () {
    return del(["./client/typings/main.d.ts",
        "./client/typings/main",
        "./server/typings/browser.d.ts",
        "./server/typings/browser"]);
});


// SERVER
gulp.task('buildServer', function () {
    // code provided previded in the previous section
});

// CLIENT
/*
 jsNPMDependencies, sometimes order matters here! so becareful!
 */
var jsNPMDependencies = [
    'material-design-lite/dist/material.min.js',
    'es6-shim/es6-shim.min.js',
    'zone.js/dist/zone.js',
    'reflect-metadata/Reflect.js',
    'systemjs/dist/system.src.js',
    'rxjs/**/*.js',
    '@angular/**/*.js'
];

gulp.task('buildIndex', function () {
    var mappedPaths = jsNPMDependencies.map(function (file) {
        return path.resolve('node_modules', file)
    });

    //Let's copy our head dependencies into a dist/libs
    var copyJsNPMDependencies = gulp.src(mappedPaths, {base: 'node_modules'})
        .pipe(gulp.dest('dist/libs'));

    //Let's copy html and js to dist
    var copyIndex = gulp.src(['client/**/*.html', 'client/**/*.js'])
        .pipe(gulp.dest('dist'));

    var copyAsserts = gulp.src('client/assets/**/*')
        .pipe(gulp.dest('dist/assets'));

    return [copyJsNPMDependencies, copyIndex, copyAsserts];
});

gulp.task('buildClient', function () {
    var tsProject = ts.createProject('client/tsconfig.json');
    var tsResult = gulp.src('client/**/*.ts')
        .pipe(sourcemaps.init())
        .pipe(ts(tsProject));
    return tsResult.js
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('dist'));
});

// WATCH
gulp.task('watch', ['buildServer'], function () {
    gulp.watch('server/*.ts', ['buildServer']);
    gulp.watch('client/**/*.ts', ['buildClient']);
});

// BUILD
gulp.task('build', function (callback) {
    runSequence('clean', 'installTypings', 'deleteDupTypings', 'buildServer', 
                  'buildIndex', 'buildClient', callback);
});

gulp.task('default', ['build']);

Next, Open a terminal window and enter this command:

npm install

4. Run the project

Finally, we launch the node server.

node dist/server.js

Open your browser and test the application at this URL http://localhost:8080.

angular2_sample_prinscreen

Conclusion

First impression after developing this sample project, Angular2 needs more investment than its ancestor from developer to start developing application. It requires learning Typescript and understanding other features like SystemJs and Typings.

Angular2 have the advantage to be modular and more structured than its previous version that make it a good choice for large applications development.

Advertisements

Social login with Spring-Social

spring-social

Social login has the advantage to simplify user account creation and to provide application with more accurate user information.

Implementing a social login with each social networking sites using an OAuth protocol could be a headache for a developer.

Spring provides social API and implements it for most of famous social networking site like LinkedIn, Facebook and Twitter. This API integrates easily to a Spring security project to provide authentication using OAuth2 protocol.

This post presents how to use Spring social to authenticate users for an Angular and Spring based project.

You can get project source code on GitHub and you can run the application here.

1 – Add maven dependencies

In order to use Spring Social you must add the necessary dependencies. For the sample we will add the following Spring Social dependencies:

<dependency>
   <groupId>org.springframework.social</groupId>
   <artifactId>spring-social-config</artifactId>
   <version>${spring-social-version}</version>
</dependency>

<dependency>
   <groupId>org.springframework.social</groupId>
   <artifactId>spring-social-security</artifactId>
   <version>${spring-social-version}</version>
</dependency>

<dependency>
   <groupId>org.springframework.social</groupId>
   <artifactId>spring-social-linkedin</artifactId>
   <version>1.0.0.RELEASE</version>
</dependency>

<!-- add more social dependencies like github, twitter, google ... -->

2 – Spring Security config

We have to setup a security filter, SocialAuthenticationFilter, to integrate it with Spring Security so that a social network provider can be activated when a user needs to be authenticated. The security filer will listen to URL’s that start with /auth and route incoming requests to the corresponding social network provider. A request sent to /auth/linkedin will be redirected to the LinkedIn provider. The security filter is configured by an SpringSocialConfigurer.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@ComponentScan(basePackages = {"com.mycompany.myproject.security"})
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    // Dependency injection code ...

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**", "/index.html", "/login.html",
                "/partials/**", "/template/**", "/", "/error/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .headers().disable()
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .antMatchers("/signup/**").permitAll()
                .antMatchers("/**").authenticated()
                .and()
            .exceptionHandling()
                // Exception Handling config ...
            .formLogin()
                // Login config ...
            .logout()
                // Logout config ...
            .rememberMe()
                // Remember me config ...
            .apply(new SpringSocialConfigurer()
                .postLoginUrl("/")
                .defaultFailureUrl("/#/login")
                .alwaysUsePostLoginUrl(true))
                .and()
    }

    @Bean
    public SocialUserDetailsService socialUsersDetailService() {
        return new SimpleSocialUsersDetailService(userDetailsService());
    }
}

Next, we implement a SocialUserDetailsService to provide user details for the SpringSocialConfigurer.

public class SimpleSocialUsersDetailService implements SocialUserDetailsService {

    private UserDetailsService userDetailsService;

    public SimpleSocialUsersDetailService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public SocialUserDetails loadUserByUserId(String userId) 
                           throws UsernameNotFoundException, DataAccessException {
        UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
        return new SocialUser(userDetails.getUsername(), 
                           userDetails.getPassword(), userDetails.getAuthorities());
    }

}

2 – Spring Social config

We can configure Spring Social by following these steps:

  • Implement the SocialConfigurer interface.
  • Add @EnableSocial annotation to have the Spring MVC configuration defined in SocialConfiguration imported.
  • Configure each provider with the standard OAuth Client Id and Client Secret properties. This is done in the class SocialConfigurer where we add a connection factory implementation for each social network service.
  • Implement getUserIdSource() method. The UserIdSource object returned by this method is responsible of determining the correct account id of the user. Because our case the username of the user as an account id, we implement this method by returning a new AuthenticationNameUserIdSource object.
  • Implement the getUsersConnectionRepository() method. In this application we use the provided Jdbc Spring Implementation.
@Configuration
@EnableSocial
@ComponentScan(basePackages = {"com.mycompany.myproject.social"})
public class SocialConfig implements SocialConfigurer {

 @Autowired
 private AccountConnectionSignUpService accountConnectionSignUpService;

 @Autowired
 private DataSource dataSource;

 @Override
 public void addConnectionFactories(ConnectionFactoryConfigurer cfc, Environment env) {
  cfc.addConnectionFactory(new LinkedInConnectionFactory(
   env.getProperty("spring.social.linkedin.appId"),
   env.getProperty("spring.social.linkedin.appSecret")));
  cfc.addConnectionFactory(new GitHubConnectionFactory(
   env.getProperty("spring.social.github.appId"),
   env.getProperty("spring.social.github.appSecret")));
  cfc.addConnectionFactory(new TwitterConnectionFactory(
   env.getProperty("spring.social.twitter.appId"),
   env.getProperty("spring.social.twitter.appSecret")));
  GoogleConnectionFactory gcf = new GoogleConnectionFactory(
   env.getProperty("spring.social.google.appId"),
   env.getProperty("spring.social.google.appSecret"));
  gcf.setScope("email");
  cfc.addConnectionFactory(gcf);
 }

 @Override
 public UserIdSource getUserIdSource() {
  return new AuthenticationNameUserIdSource();
 }

 @Override
 public UsersConnectionRepository
 getUsersConnectionRepository(ConnectionFactoryLocator cfl) {
  JdbcUsersConnectionRepository repository =
   new JdbcUsersConnectionRepository(dataSource, cfl, Encryptors.noOpText());
  repository.setConnectionSignUp(accountConnectionSignUpService);
  return repository;
 }
}

The JdbcUsersConnectionRepository implementation requires adding UserConnection table to the database schema.

We use the following SQL statement to create the table:

create table UserConnection (userId varchar(255) not null,
	providerId varchar(255) not null,
	providerUserId varchar(255),
	rank int not null,
	displayName varchar(255),
	profileUrl varchar(512),
	imageUrl varchar(512),
	accessToken varchar(512) not null,
	secret varchar(512),
	refreshToken varchar(512),
	expireTime bigint,
	primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);

Next, we implement our ConnectionSignUp implementation. After user social authentication, we store user profile information in the user repository.

@Component
public class AccountConnectionSignUpService implements ConnectionSignUp {

 @Autowired
 private UserRepo usersRepo;

 @Override
 public String execute(Connection < ? > connection) {
  Profile profile = new Profile();
  BeanUtils.copyProperties(connection.fetchUserProfile(), profile);
  String userId = UUID.randomUUID().toString();
  profile.setImageUrl(connection.getImageUrl());
  usersRepo.createUser(userId, profile);
  return userId;
 }
}

3 – Get OAuth application Id and secret

The links bellows allow you to get an applications ids and secrets:

These screenshots show steps to follow to get OAuth application Ids and secrets for LinkedIn and Google.

This slideshow requires JavaScript.

Conclusion

Spring-Social is not just for Social login, it provides APIs for most of known SaaS (Software as a Service) providers; for example Spring-Social provides an API for getting user connections or friends.

In this post, we provide a sample project to implement Social login for a web application. For more details, you can checkout the source code from GitHub and test online the application here.

Reference

Secure AngularJs application with Spring Security

Most of JEE developers are aware of web applications security requirements and are familiar with security frameworks like Spring-Security.

But when we have to secure a web application based on front-end framework like AngularJs, we will have to deal with some specific features and we will need to customize the server-side configuration.

For example, some commonly used features like Spring-Security taglib can’t be used because page is rendered client-side.

Photo credit: SLO Icon Design
In this post, I will present a sample project to deal with some common security features in an AngularJs project.
You can get project source code on GitHub here and you can run the application here.


1 – Back-end security management

This parts provides instructions on how to add Spring Security to an existing application using Spring java based config.

1.1 – Back-end dependencies

In order to use Spring Security you must add the necessary dependencies. For the sample we will add the following Spring Security dependencies:

      <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>4.0.3.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>4.0.3.RELEASE</version>
        </dependency>

1.2 – Spring security config

In this step, we create a Spring Security configuration.

@EnableWebSecurity
@Configuration
@ComponentScan(basePackages = {"com.mycompany.myproject.security"})
public class SecurityConfig extends WebSecurityConfigurerAdapter {

 public static final String REMEMBER_ME_KEY = "rememberme_key";

 @Autowired
 private RestUnauthorizedEntryPoint restAuthenticationEntryPoint;

 // Autowire other required beans 

 @Autowired
 public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
  auth.userDetailsService(userDetailsService);
 }

 @Override
 public void configure(WebSecurity web) throws Exception {
  web.ignoring().antMatchers("/resources/**", "/index.html", "/login.html",
   "/partials/**", "/", "/error/**");
 }

 @Override
 protected void configure(HttpSecurity http) throws Exception {
  http
   .headers().disable()
   .csrf().disable()
   .authorizeRequests()
    .antMatchers("/v2/api-docs").hasAnyAuthority("admin")
    .antMatchers("/users/**").hasAnyAuthority("admin")
    .anyRequest().authenticated()
    .and()
   .exceptionHandling()
    .authenticationEntryPoint(restAuthenticationEntryPoint)
    .accessDeniedHandler(restAccessDeniedHandler)
    .and()
   .formLogin()
    .loginProcessingUrl("/authenticate")
    .successHandler(restAuthenticationSuccessHandler)
    .failureHandler(restAuthenticationFailureHandler)
    .usernameParameter("username")
    .passwordParameter("password")
    .permitAll()
    .and()
   .logout()
    .logoutUrl("/logout")
    .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
    .deleteCookies("JSESSIONID")
    .permitAll()
    .and()
   .rememberMe()
    .rememberMeServices(rememberMeServices)
    .key(REMEMBER_ME_KEY)
    .and();
 }
}

1.2.1 – Authentication config

Authentication entry point
We need a custom authenticationEntryPoint because default Spring-Security config will redirect to login page. In our case we need just a https status 401 and a json response.

@Component
public class RestUnauthorizedEntryPoint implements AuthenticationEntryPoint {

 @Override
 public void commence(HttpServletRequest request, HttpServletResponse response,
  AuthenticationException exception) throws IOException, ServletException {
  SecurityUtils.sendError(response, exception, HttpServletResponse.SC_UNAUTHORIZED,
   "Authentication failed");
 }
}

Login success handler
The login success handler returns http status 200 with user info in json format.

@Component
public class RestAuthenticationSuccessHandler 
extends SimpleUrlAuthenticationSuccessHandler {

 @Autowired
 private UserRepo userService;

 @Override
 public void onAuthenticationSuccess(HttpServletRequest request, 
  HttpServletResponse response, Authentication authentication)
 throws ServletException, IOException {
  User user = userService.findByLogin(authentication.getName());
  SecurityUtils.sendResponse(response, HttpServletResponse.SC_OK, user);
 }
}

Login fail handler
The login fail handler returns http status 401.

public class RestAuthenticationFailureHandler
extends SimpleUrlAuthenticationFailureHandler {

 @Override
 public void onAuthenticationFailure(HttpServletRequest request,
  HttpServletResponse response, AuthenticationException exception)
 throws IOException, ServletException {
  SecurityUtils.sendError(response, exception, HttpServletResponse.SC_UNAUTHORIZED,
   "Authentication failed");
 }
}

Logout success handler
When logout succeeds, we need to return ok status instead of login page redirection.
Spring security implements logout handler that returns ok status.

org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler

Account Rest web service
The account Rest web service returns the authenticated user account information if he’s already authenticated.

@RestController
public class SeurityController {

 @Autowired
 private UserRepo userRepo;

 @RequestMapping(value = "/security/account", method = RequestMethod.GET)
 public @ResponseBody
 User getUserAccount() {
  User user = userRepo.findByLogin(SecurityUtils.getCurrentLogin());
  user.setPassword(null);
  return user;
 }

}

1.2.2 – Authorization config

You can manage authorization by specifying authorized roles for each secured URL.

@Configuration
// @other annotations ..
public class SecurityConfig extends WebSecurityConfigurerAdapter {

// extra config code ...

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
    .antMatchers("/v2/api-docs").hasAnyAuthority("admin")
    .antMatchers("/users/**").hasAnyAuthority("admin")
    .anyRequest().authenticated()
    .and()
    // others config ...
}

}

Using Spring-Security annotations is another way to manage authorization.

First, we enable method security in the config class

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
// @other annotations ..
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  // config code ... 
}

Next, we add annotation for a method that must have access restriction.

@RestController
public class SeurityController {

    @PreAuthorize("hasAuthority('admin')")
    @RequestMapping(value = "/security/tokens", method = RequestMethod.GET)
    public @ResponseBody
    List<Token> getTokens () {
       // method code ...
    }
}

Access denied handler
The access denied handler returns http status 403.

@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {

 @Override
 public void handle(HttpServletRequest request, HttpServletResponse response,
  AccessDeniedException exception) throws IOException, ServletException {
  SecurityUtils.sendError(response, exception, HttpServletResponse.SC_FORBIDDEN, 
   "Not authorized resources");
 }
}

2 – Front-end security management

2.1 – Front-end dependencies

If you are using Bower, you need to add to your bower.json file the dependencies in the code bellow.

{
  "name": "example",
  "version": "0.0.1",
  "dependencies": {
    "jquery": "2.1.4",
    "angular": "~1.4",
    "angular-route": "~1.4",
    "angular-resource": "~1.4",
    "angular-sanitize": "~1.4",
    "angular-animate": "~1.4",
    "angular-spinkit": "0.3.3",
    "angular-http-auth": "1.2.2",
    // others dependencies
  }
}

2.2 – AngularJs config

The code source bellow presents the routeProvider configuration.

var myapp = angular
    .module('myApp', ['ngResource', 'ngRoute', 'http-auth-interceptor']);

myapp.config(function ($routeProvider, USER_ROLES) {

    $routeProvider.when("/home", {
        templateUrl: "partials/home.html"      
    }).when('/login', {
        templateUrl: 'partials/login.html',
        controller: 'LoginController'
    }).when("/error/:code", {
        templateUrl: "partials/error.html",
        controller: "ErrorController"
    }).
       // Other routes config ...
       // ......
    }).otherwise({
        redirectTo: '/error/404'
    });
});

2.3 – Authentication management

First, create services.


myapp.service('Session', function () {
    this.create = function (data) {
        this.id = data.id;
        this.login = data.login;
        this.firstName = data.firstName;
        this.lastName = data.familyName;
        this.email = data.email;
        this.userRoles = [];
        angular.forEach(data.authorities, function (value, key) {
            this.push(value.name);
        }, this.userRoles);
    };
    this.invalidate = function () {
        this.id = null;
        this.login = null;
        this.firstName = null;
        this.lastName = null;
        this.email = null;
        this.userRoles = null;
    };
    return this;
});

myapp.service('AuthSharedService', function($rootScope, $http, authService, Session) {
 return {
  login: function(userName, password, rememberMe) {
   var config = {
    params: {
     username: userName,
     password: password,
     rememberme: rememberMe
    },
    ignoreAuthModule: 'ignoreAuthModule'
   };
   $http.post('authenticate', '', config)
    .success(function(data, status, headers, config) {
     authService.loginConfirmed(data);
    }).error(function(data, status, headers, config) {
     $rootScope.authenticationError = true;
     Session.invalidate();
    });
  }
 };
});

Next, write a login controller.

myapp.controller('LoginController', function($rootScope, $scope, AuthSharedService) {
 $scope.rememberMe = true;
 $scope.login = function() {
  $rootScope.authenticationError = false;
  AuthSharedService.login($scope.username, $scope.password, $scope.rememberMe);
 }
});

Finally, create login.html partial page.

<form>
    <div class="form-group"
         ng-class="{'has-error is-focused' : authenticationError}">
        <input id="login" ng-model="username" type="text" class="form-control" 
               required="required" placeholder="login"/>
        <span ng-show="authenticationError" class="help-block">
            Please check your credentials and try again.
        </span>
    </div>

    <div class="form-group">
        <input id="password" ng-model="password" type="password" class="form-control" 
               required="required" placeholder="password"/>
    </div>

    <input type="checkbox" ng-model="rememberMe"/><span> Remember me</span>

    <button ng-click="login()" >Login</button>
</form>

2.4 – Authorization management

In this part, we deal with server-side authorization error and with client-side authorization managing.

2.4.1 Manage server-side authorization

If the requested resource or service isn’t authorized for the authenticated User, the request will fail with response status 403.
The client-side logic must intercept the error and redirect the user to a 403 error page.

myapp.run(function($rootScope, $location, $http, AuthSharedService, Session,
 USER_ROLES, $q, $timeout) {
// Call when the 403 response is returned by the server
 $rootScope.$on('event:auth-forbidden', function(rejection) {
  $rootScope.$evalAsync(function() {
   $location.path('/error/403').replace();
  });
 });
}

2.4.2 – Client-side authorization management

It’s better to manage authorization client-side for better performance and to avoid servers-side errors.

We added an access property which details if the route requires the user to be logged in and what permissions the user must have to access the route.


myapp.constant('USER_ROLES', {
    all: '*',
    admin: 'admin',
    user: 'user'
});

myapp.config(function ($routeProvider, USER_ROLES) {

   $routeProvider.when("/home", {
        templateUrl: "partials/home.html",
        controller: 'HomeController',
        access: {
            loginRequired: true,
            authorizedRoles: [USER_ROLES.all]
        }
    }).when('/users', {
        templateUrl: 'partials/users.html',
        controller: 'UsersController',
        access: {
            loginRequired: true,
            authorizedRoles: [USER_ROLES.admin]
        }
    })
    // other routs config ...
    // ... 
    .otherwise({
        redirectTo: '/error/404',
        access: {
            loginRequired: false,
            authorizedRoles: [USER_ROLES.all]
        }
    });
}

Next, we add a service function to check if authenticated user has required roles to access the resource.

myapp.service('AuthSharedService', function (Session) {
    return {
        // other functions ...
        isAuthorized: function (authorizedRoles) {
            if (!angular.isArray(authorizedRoles)) {
                if (authorizedRoles == '*') {
                    return true;
                }
                authorizedRoles = [authorizedRoles];
            }
            var isAuthorized = false;
            angular.forEach(authorizedRoles, function (authorizedRole) {
                var authorized = (!!Session.login &&
                Session.userRoles.indexOf(authorizedRole) !== -1);
                if (authorized || authorizedRole == '*') {
                    isAuthorized = true;
                }
            });
            return isAuthorized;
        }
    };
});

Finally, we implement a listener on the $routeChangeStart event to track the next route navigation.
– If the user is not yet authenticated the function broadcast “event:auth-loginRequired”.
– If the user is not authorized the function broadcast “event:auth-loginRequired”.

myapp.run(function($rootScope, AuthSharedService, USER_ROLES) {

 $rootScope.$on('$routeChangeStart', function(event, next) {
 if (next.originalPath === "/login" && $rootScope.authenticated) {
   event.preventDefault();
  } else if (next.access && next.access.loginRequired && !$rootScope.authenticated) {
   event.preventDefault();
   $rootScope.$broadcast("event:auth-loginRequired", {});
  }else if(next.access && !AuthSharedService.isAuthorized(next.access.authorizedRoles)) {
   event.preventDefault();
   $rootScope.$broadcast("event:auth-forbidden", {});
  }
 });

}

2.4.3 – Manage already authenticated user

If the user is already authenticated and he has a valid remember me token he should access the application to the requested page.

When the application starts running, it requests account Rest web service. If the user is already authenticated the rest API will return user details.

We have to carry about account Rest web service response time. That’s why we redirect the user to loading page until getting the response to check authorities for accessing the requested page.

First, we update the AutehtSharedService with getAccount function.

myapp.service('AuthSharedService', function ($rootScope, $http, $resource, 
    authService, Session) {
    return {
        login: function (userName, password, rememberMe) {
            // login code ...
        },
        getAccount: function () {
            $rootScope.loadingAccount = true;
            $http.get('security/account')
                .then(function (response) {
                    authService.loginConfirmed(response.data);
                });
        },
        isAuthorized: function (authorizedRoles) {
            // isAuthorized code ..
        }
    };
});

Next, we add code to handle events and call getAccount function.

myapp.run(function($rootScope, $location, $http, AuthSharedService, Session,
 USER_ROLES, $q, $timeout) {

 $rootScope.$on('$routeChangeStart', function(event, next) {
  // route change start code ...
 });

 // Call when the the client is confirmed
 $rootScope.$on('event:auth-loginConfirmed', function(event, data) {
  $rootScope.loadingAccount = false;
  var nextLocation = ($rootScope.requestedUrl ? $rootScope.requestedUrl : "/home");
  var delay = ($location.path() === "/loading" ? 1500 : 0);

  $timeout(function() {
   Session.create(data);
   $rootScope.account = Session;
   $rootScope.authenticated = true;
   $location.path(nextLocation).replace();
  }, delay);

 });

 // Call when the 401 response is returned by the server
 $rootScope.$on('event:auth-loginRequired', function(event, data) {
  if ($rootScope.loadingAccount && data.status !== 401) {
   $rootScope.requestedUrl = $location.path()
   $location.path('/loading');
  } else {
   Session.invalidate();
   $rootScope.authenticated = false;
   $rootScope.loadingAccount = false;
   $location.path('/login');
  }
 });

 $rootScope.$on('event:auth-forbidden', function(rejection) {
  // auth-forbidden code ...
 });

 // Get already authenticated user account
 AuthSharedService.getAccount();

});

2.5 – Securing UI Elements

To secure UI elements, we will create a directive that accepts as attribute a comma-separated list of authorized roles.

myapp.directive('access', [
    'AuthSharedService',
    function (AuthSharedService) {
        return {
            restrict: 'A',
            link: function (scope, element, attrs) {
                var roles = attrs.access.split(',');
                if (roles.length > 0) {
                    if (AuthSharedService.isAuthorized(roles)) {
                        element.removeClass('hide');
                    } else {
                        element.addClass('hide');
                    }
                }
            }
        };
    }]);

Next, we specify authorized roles in the access property.


      <ul class="nav navbar-nav">
            <li><a href="#/home" class="mdi-action-home"></a></li>
            <li><a href="#/users">Users</a></li>
            <li><a href="#/apiDoc">API Doc.</a></li>
            <li><a href="#/tokens" access="admin">Sessions</a></li>
        </ul>

Conclusion

Even if security is managed client-side with all security features, it’s important to manage security server-side.
Client-side security is easy to hack; user can inspect code and change CSS and display hidden elements and it can display network requests and execute them out of the application.

I hope this post helped you to get an overview on securing an Angular web application with Spring Security. For more information you can read source code in Github and run a demo here.

Thanks for your comments 🙂

References

MapStruct

MapStruct is a serious alternative for Dozer bean mapping project. It’s a code generator which simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.

MapStruct solves some traditional mapping issues like a lack of performance, difficult mapping code debugging and late errors détection (usually in code execution).

This posts presents how to use MapStruct by dealing with bean mapping case.

Maven configuration

MapStruct is an annotation processor which is plugged into the Java compiler. That why we need to configure maven processor plugin to parse the mapping annotations and generate code.

    <dependencies>
      <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
      </dependency>
      <!-- other project dependencies -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.bsc.maven</groupId>
                <artifactId>maven-processor-plugin</artifactId>
                <version>2.2.4</version>
                <configuration>
                    <defaultOutputDirectory>
                        ${project.build.directory}/generated-sources
                    </defaultOutputDirectory>
                    <processors>
                        <processor>org.mapstruct.ap.MappingProcessor</processor>
                    </processors>
                </configuration>
                <executions>
                    <execution>
                        <id>process</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>process</goal>
                        </goals>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

Source classes

The source code bellow, presents the source class and its dependencies.

public class User {

    private String fullName;
    private Address address;
    private List<Authority> authorities;

    // Add getters and setters
}


public class Address {

    private String street;
    private String city;
    private String state;
    private String zipCode;

    @Override
    public String toString() {
        return street + ", " + city + ", " + state + ", " + zipCode;
    }
    
    // Add getters and setters
}

public class Authority {
    private String code;
    private String value;
    
    // Add getters and setters
}

Destination classes

The destination class has a String type address and a set of string for authorities values.

The mapper have to do three actions:

  1. Copy name field value.
  2. Convert Address object to String.
  3. Convert List to Set.

public class UserDto {

    private String name;
    private String address;
    private Set<String> authorities;

    // Add getters and setters
}

Mapping code

@Mapper
public abstract class UserMapping {
    public static UserMapping INSTANCE = Mappers.getMapper(UserMapping.class);

    @Mappings({
         @Mapping(source="fullName", target="name"),
         @Mapping(target="address", expression="java(user.getAddress().toString())")
    })
    public abstract UserDto userToDto (User user);

    @IterableMapping(elementTargetType = String.class)
    protected abstract Set<String> mapListToSet(List<Authority> value);

    protected String mapAuthorityToString(Authority authority) {
        return authority.getValue();
    }
}

Generated mapping code

Mapping code will be gerated in the generate-sources phase when executing Maven install goal.

If there is no mapping specification error, you can check mapping code under project-folder/target/generated-sources folder.

@Generated(value = "org.mapstruct.ap.MappingProcessor",  date = "today",
    comments = "version: 1.0.0.CR2, compiler: javac, 
    environment: Java 1.7.0 (Sun Microsystems Inc.)"
)
public class UserMappingImpl extends UserMapping {

    @Override
    public UserDto userToDto(User user) {
        if ( user == null ) {
            return null;
        }

        UserDto userDto = new UserDto();
        userDto.setName( user.getFullName() );
        userDto.setAuthorities( mapListToSet( user.getAuthorities() ) );
        userDto.setAddress( user.getAddress().toString());
        return userDto;
    }

    @Override
    protected Set<String> mapListToSet(List<Authority> value) {
        if ( value == null ) {
            return null;
        }
        Set<String> set_ = new HashSet<String>();
        for ( Authority authority : value ) {
            set_.add( mapAuthorityToString( authority ) );
        }
        return set_;
    }
}

Test beans mapping

public class MappingTest {
    @Test
    public void testMapping() {
        Address address = new Address( "street", "city", "state", "zipCode");
        User user = new User("name", address, Arrays.asList(new Authority("1", "admin"), new Authority("2", "user")));

        UserDto userDto = UserMapping.INSTANCE.userToDto(user);

        Assert.assertEquals("name", userDto.getName());
        Assert.assertEquals("street, city, state, zipCode", userDto.getAddress());
        Assert.assertTrue(userDto.getAuthorities().containsAll(Arrays.asList("admin", "user")));
    }

}

In this posts, we dealt with some MapStruct features like collections mapping and expressions. It has other mapping features that can fit your mapping needs like Decorators, Enum type mapping and customized mappings with Before/After mapping methods.

At the moment when I’m writing this article, Mapstruct is in RC2 version. I think soon we will have a release which is mature enough to be considered for production use.