- Jhipster angular app in which I have opened an AJP port on 8008

This commit is contained in:
Mark Quinn
2015-11-08 19:22:18 +00:00
parent e6dc2dd0c2
commit 24ed72ba31
232 changed files with 12497 additions and 0 deletions

3
.bowerrc Normal file
View File

@@ -0,0 +1,3 @@
{
"directory": "src/main/webapp/bower_components"
}

24
.editorconfig Normal file
View File

@@ -0,0 +1,24 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 4
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

21
.gitattributes vendored Normal file
View File

@@ -0,0 +1,21 @@
# All text files should have the "lf" (Unix) line endings
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.java text
*.js text
*.css text
*.html text
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.jar binary
*.pdf binary
*.eot binary
*.ttf binary
*.gzip binary
*.gz binary
*.ai binary
*.eps binary

123
.gitignore vendored Normal file
View File

@@ -0,0 +1,123 @@
######################
# Project Specific
######################
/src/main/webapp/dist
######################
# Node
######################
/node/**
/node_modules/**
######################
# SASS
######################
.sass-cache/**
######################
# Eclipse
######################
*.pydevproject
.project
.metadata
/bin/**
/tmp/**
/tmp/**/*
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/**
.loadpath
/src/main/resources/rebel.xml
# External tool builders
.externalToolBuilders/**
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
######################
# Intellij
######################
.idea/**
*.iml
*.iws
*.ipr
*.ids
*.orig
######################
# Maven
######################
/log/**
/target/**
######################
# Gradle
######################
.gradle/**
######################
# Package Files
######################
*.jar
*.war
*.ear
*.db
######################
# Windows
######################
# Windows image file caches
Thumbs.db
# Folder config file
Desktop.ini
######################
# Mac OSX
######################
.DS_Store
.svn
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
######################
# Directories
######################
/build/**
/bin/**
/spring_loaded/**
/deploy/**
######################
# Logs
######################
*.log
######################
# Others
######################
*.class
*.*~
*~
.merge_file*
######################
# Gradle Wrapper
######################
!gradle/wrapper/gradle-wrapper.jar

22
.jshintrc Normal file
View File

@@ -0,0 +1,22 @@
{
"node": true,
"esnext": true,
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"indent": 4,
"latedef": true,
"newcap": true,
"noarg": true,
"quotmark": "single",
"regexp": true,
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"smarttabs": true,
"white": true,
"predef": ["angular"]
}

21
.yo-rc.json Normal file
View File

@@ -0,0 +1,21 @@
{
"generator-jhipster": {
"baseName": "openAjpPort",
"packageName": "mark.quinn",
"packageFolder": "mark/quinn",
"authenticationType": "session",
"hibernateCache": "no",
"clusteredHttpSession": "no",
"websocket": "no",
"databaseType": "sql",
"devDatabaseType": "h2Memory",
"prodDatabaseType": "mysql",
"searchEngine": "no",
"useCompass": false,
"buildTool": "maven",
"frontendBuilder": "grunt",
"javaVersion": "8",
"enableTranslation": false,
"rememberMeKey": "d9443054ecffc6ae41b0912a82deb97699482f86"
}
}

398
Gruntfile.js Normal file
View File

@@ -0,0 +1,398 @@
// Generated on 2015-11-08 using generator-jhipster 2.19.0
'use strict';
var fs = require('fs');
var parseString = require('xml2js').parseString;
// Returns the second occurence of the version number
var parseVersionFromPomXml = function() {
var version;
var pomXml = fs.readFileSync('pom.xml', "utf8");
parseString(pomXml, function (err, result){
version = result.project.version[0];
});
return version;
};
// usemin custom step
var useminAutoprefixer = {
name: 'autoprefixer',
createConfig: function(context, block) {
if(block.src.length === 0) {
return {};
} else {
return require('grunt-usemin/lib/config/cssmin').createConfig(context, block) // Reuse cssmins createConfig
}
}
};
module.exports = function (grunt) {
require('load-grunt-tasks')(grunt);
require('time-grunt')(grunt);
grunt.initConfig({
yeoman: {
// configurable paths
app: require('./bower.json').appPath || 'app',
dist: 'src/main/webapp/dist'
},
watch: {
bower: {
files: ['bower.json'],
tasks: ['wiredep']
},
ngconstant: {
files: ['Gruntfile.js', 'pom.xml'],
tasks: ['ngconstant:dev']
}
},
autoprefixer: {
// src and dest is configured in a subtask called "generated" by usemin
},
wiredep: {
app: {
src: ['src/main/webapp/index.html'],
exclude: [
/angular-i18n/ // localizations are loaded dynamically
]
},
test: {
src: 'src/test/javascript/karma.conf.js',
exclude: [/angular-i18n/, /angular-scenario/],
ignorePath: /\.\.\/\.\.\//, // remove ../../ from paths of injected javascripts
devDependencies: true,
fileTypes: {
js: {
block: /(([\s\t]*)\/\/\s*bower:*(\S*))(\n|\r|.)*?(\/\/\s*endbower)/gi,
detect: {
js: /'(.*\.js)'/gi
},
replace: {
js: '\'{{filePath}}\','
}
}
}
}
},
browserSync: {
dev: {
bsFiles: {
src : [
'src/main/webapp/**/*.html',
'src/main/webapp/**/*.json',
'src/main/webapp/assets/styles/**/*.css',
'src/main/webapp/scripts/**/*.js',
'src/main/webapp/assets/images/**/*.{png,jpg,jpeg,gif,webp,svg}',
'tmp/**/*.{css,js}'
]
}
},
options: {
watchTask: true,
proxy: "localhost:8080"
}
},
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/*',
'!<%= yeoman.dist %>/.git*'
]
}]
},
server: '.tmp'
},
jshint: {
options: {
jshintrc: '.jshintrc'
},
all: [
'Gruntfile.js',
'src/main/webapp/scripts/app.js',
'src/main/webapp/scripts/app/**/*.js',
'src/main/webapp/scripts/components/**/*.js'
]
},
concat: {
// src and dest is configured in a subtask called "generated" by usemin
},
uglifyjs: {
// src and dest is configured in a subtask called "generated" by usemin
},
rev: {
dist: {
files: {
src: [
'<%= yeoman.dist %>/scripts/**/*.js',
'<%= yeoman.dist %>/assets/styles/**/*.css',
'<%= yeoman.dist %>/assets/images/**/*.{png,jpg,jpeg,gif,webp,svg}',
'<%= yeoman.dist %>/assets/fonts/*'
]
}
}
},
useminPrepare: {
html: 'src/main/webapp/**/*.html',
options: {
dest: '<%= yeoman.dist %>',
flow: {
html: {
steps: {
js: ['concat', 'uglifyjs'],
css: ['cssmin', useminAutoprefixer] // Let cssmin concat files so it corrects relative paths to fonts and images
},
post: {}
}
}
}
},
usemin: {
html: ['<%= yeoman.dist %>/**/*.html'],
css: ['<%= yeoman.dist %>/assets/styles/**/*.css'],
js: ['<%= yeoman.dist %>/scripts/**/*.js'],
options: {
assetsDirs: ['<%= yeoman.dist %>', '<%= yeoman.dist %>/assets/styles', '<%= yeoman.dist %>/assets/images', '<%= yeoman.dist %>/assets/fonts'],
patterns: {
js: [
[/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images']
]
},
dirs: ['<%= yeoman.dist %>']
}
},
imagemin: {
dist: {
files: [{
expand: true,
cwd: 'src/main/webapp/assets/images',
src: '**/*.{jpg,jpeg}', // we don't optimize PNG files as it doesn't work on Linux. If you are not on Linux, feel free to use '**/*.{png,jpg,jpeg}'
dest: '<%= yeoman.dist %>/assets/images'
}]
}
},
svgmin: {
dist: {
files: [{
expand: true,
cwd: 'src/main/webapp/assets/images',
src: '**/*.svg',
dest: '<%= yeoman.dist %>/assets/images'
}]
}
},
cssmin: {
// src and dest is configured in a subtask called "generated" by usemin
},
ngtemplates: {
dist: {
cwd: 'src/main/webapp',
src: ['scripts/app/**/*.html', 'scripts/components/**/*.html',],
dest: '.tmp/templates/templates.js',
options: {
module: 'openajpportApp',
usemin: 'scripts/app.js',
htmlmin: '<%= htmlmin.dist.options %>'
}
}
},
htmlmin: {
dist: {
options: {
removeCommentsFromCDATA: true,
// https://github.com/yeoman/grunt-usemin/issues/44
collapseWhitespace: true,
collapseBooleanAttributes: true,
conservativeCollapse: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
keepClosingSlash: true
},
files: [{
expand: true,
cwd: '<%= yeoman.dist %>',
src: ['*.html'],
dest: '<%= yeoman.dist %>'
}]
}
},
// Put files not handled in other tasks here
copy: {
fonts: {
files: [{
expand: true,
dot: true,
flatten: true,
cwd: 'src/main/webapp',
dest: '<%= yeoman.dist %>/assets/fonts',
src: [
'bower_components/bootstrap/fonts/*.*'
]
}]
},
dist: {
files: [{
expand: true,
dot: true,
cwd: 'src/main/webapp',
dest: '<%= yeoman.dist %>',
src: [
'*.html',
'scripts/**/*.html',
'assets/images/**/*.{png,gif,webp,jpg,jpeg,svg}',
'assets/fonts/*'
]
}, {
expand: true,
cwd: '.tmp/assets/images',
dest: '<%= yeoman.dist %>/assets/images',
src: [
'generated/*'
]
}]
},
generateOpenshiftDirectory: {
expand: true,
dest: 'deploy/openshift',
src: [
'pom.xml',
'src/main/**'
]
}
},
karma: {
unit: {
configFile: 'src/test/javascript/karma.conf.js',
singleRun: true
}
},
ngAnnotate: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/scripts',
src: '*.js',
dest: '.tmp/concat/scripts'
}]
}
},
buildcontrol: {
options: {
commit: true,
push: false,
connectCommits: false,
message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%'
},
openshift: {
options: {
dir: 'deploy/openshift',
remote: 'openshift',
branch: 'master'
}
}
},
ngconstant: {
options: {
name: 'openajpportApp',
deps: false,
wrap: '"use strict";\n// DO NOT EDIT THIS FILE, EDIT THE GRUNT TASK NGCONSTANT SETTINGS INSTEAD WHICH GENERATES THIS FILE\n{%= __ngModule %}'
},
dev: {
options: {
dest: 'src/main/webapp/scripts/app/app.constants.js'
},
constants: {
ENV: 'dev',
VERSION: parseVersionFromPomXml()
}
},
prod: {
options: {
dest: '.tmp/scripts/app/app.constants.js'
},
constants: {
ENV: 'prod',
VERSION: parseVersionFromPomXml()
}
}
}
});
grunt.registerTask('serve', [
'clean:server',
'wiredep',
'ngconstant:dev',
'browserSync',
'watch'
]);
grunt.registerTask('server', function (target) {
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
grunt.task.run([target ? ('serve:' + target) : 'serve']);
});
grunt.registerTask('test', [
'clean:server',
'wiredep:test',
'ngconstant:dev',
'karma'
]);
grunt.registerTask('build', [
'clean:dist',
'wiredep:app',
'ngconstant:prod',
'useminPrepare',
'ngtemplates',
'imagemin',
'svgmin',
'concat',
'copy:fonts',
'copy:dist',
'ngAnnotate',
'cssmin',
'autoprefixer',
'uglify',
'rev',
'usemin',
'htmlmin'
]);
grunt.registerTask('appendSkipBower', 'Force skip of bower for Gradle', function () {
if (!grunt.file.exists(filepath)) {
// Assume this is a maven project
return true;
}
var fileContent = grunt.file.read(filepath);
var skipBowerIndex = fileContent.indexOf("skipBower=true");
if (skipBowerIndex != -1) {
return true;
}
grunt.file.write(filepath, fileContent + "\nskipBower=true\n");
});
grunt.registerTask('buildOpenshift', [
'test',
'build',
'copy:generateOpenshiftDirectory',
]);
grunt.registerTask('deployOpenshift', [
'test',
'build',
'copy:generateOpenshiftDirectory',
'buildcontrol:openshift'
]);
grunt.registerTask('default', [
'test',
'build'
]);
};

31
bower.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "openAjpPort",
"version": "0.0.0",
"appPath": "src/main/webapp",
"testPath": "src/test/javascript/spec",
"dependencies": {
"bootstrap": "3.3.4",
"modernizr": "2.8.3",
"jquery": "2.1.4",
"json3": "3.3.2",
"angular-bootstrap": "0.13.1",
"angular-ui-router": "0.2.15",
"angular": "1.4.3",
"angular-resource": "1.4.3",
"angular-cookies": "1.4.3",
"angular-sanitize": "1.4.3",
"angular-local-storage": "0.2.0",
"angular-cache-buster": "0.4.3",
"ngInfiniteScroll": "1.2.0",
"ng-file-upload": "5.0.9"
},
"devDependencies": {
"angular-mocks": "1.4.3",
"angular-scenario": "1.4.3"
},
"resolutions": {
"angular": "1.4.3",
"angular-cookies": "1.4.3",
"jquery": "2.1.4"
}
}

56
package.json Normal file
View File

@@ -0,0 +1,56 @@
{
"name": "openajpport",
"version": "0.0.0",
"description": "Description for openAjpPort",
"private": true,
"dependencies": {
},
"devDependencies": {
"grunt": "0.4.5",
"grunt-autoprefixer": "2.2.0",
"grunt-build-control": "0.3.0",
"grunt-wiredep": "2.0.0",
"grunt-browser-sync": "2.1.2",
"grunt-contrib-copy": "0.8.0",
"grunt-contrib-clean": "0.6.0",
"grunt-contrib-concat": "0.5.1",
"grunt-contrib-cssmin": "0.11.0",
"grunt-contrib-htmlmin": "0.4.0",
"grunt-contrib-imagemin": "0.9.3",
"grunt-contrib-jshint": "0.11.0",
"grunt-contrib-uglify": "0.8.0",
"grunt-contrib-watch": "0.6.1",
"grunt-modernizr": "0.6.0",
"grunt-ng-annotate": "0.10.0",
"grunt-ng-constant": "1.1.0",
"grunt-rev": "0.1.0",
"grunt-svgmin": "2.0.1",
"grunt-text-replace": "0.4.0",
"grunt-usemin": "3.0.0",
"grunt-angular-templates":"0.5.7",
"load-grunt-tasks": "3.1.0",
"grunt-karma": "0.11.0",
"time-grunt": "1.1.0",
"event-stream": "3.3.1",
"jshint-stylish": "1.0.1",
"karma-script-launcher": "0.1.0",
"karma-chrome-launcher": "0.1.12",
"karma-html2js-preprocessor": "0.1.0",
"karma-jasmine": "0.3.5",
"karma-requirejs": "0.2.2",
"karma-phantomjs-launcher": "0.2.0",
"phantomjs": "1.9.17",
"karma": "0.12.35",
"generator-jhipster": "2.19.0",
"lodash": "3.3.1",
"xml2js": "0.4.5",
"yo": ">=1.3.0",
"requirejs": "2.1",
"jasmine-core": "2.1.0",
"zeparser": "0.0.7",
"wiredep": "2.2.2"
},
"engines": {
"node": ">=0.12.0"
}
}

694
pom.xml Normal file
View File

@@ -0,0 +1,694 @@
<?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>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>1.2.5.RELEASE</version>
<relativePath />
</parent>
<groupId>mark.quinn</groupId>
<artifactId>openajpport</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>openAjpPort</name>
<prerequisites>
<maven>3.0.0</maven>
</prerequisites>
<properties>
<HikariCP.version>2.3.7</HikariCP.version>
<assertj-core.version>3.1.0</assertj-core.version>
<awaitility.version>1.4.0</awaitility.version>
<commons-io.version>2.4</commons-io.version>
<commons-lang.version>2.6</commons-lang.version>
<gatling-maven-plugin.version>2.1.6</gatling-maven-plugin.version>
<gatling.version>2.1.6</gatling.version>
<hibernate.version>4.3.6.Final</hibernate.version>
<java.version>1.8</java.version>
<javax.inject.version>1</javax.inject.version>
<joda-time-hibernate.version>1.3</joda-time-hibernate.version>
<json-path.version>0.9.1</json-path.version>
<liquibase-hibernate4.version>3.5</liquibase-hibernate4.version>
<liquibase-slf4j.version>1.2.1</liquibase-slf4j.version>
<liquibase.version>3.3.2</liquibase.version>
<mapstruct.version>1.0.0.CR1</mapstruct.version>
<maven-enforcer-plugin.version>1.3.1</maven-enforcer-plugin.version>
<maven-sortpom-plugin.version>2.3.0</maven-sortpom-plugin.version>
<maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<metrics-spark-reporter.version>1.2</metrics-spark-reporter.version>
<metrics-spring.version>3.0.4</metrics-spring.version>
<run.addResources>false</run.addResources>
<sonar-maven-plugin.version>2.3</sonar-maven-plugin.version>
<spring-security.version>4.0.1.RELEASE</spring-security.version>
<springfox.version>2.0.3</springfox.version>
<usertype-core.version>4.0.0.GA</usertype-core.version>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate4</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hppc</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-json-org</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${json-path.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.mattbertolini</groupId>
<artifactId>liquibase-slf4j</artifactId>
<version>${liquibase-slf4j.version}</version>
</dependency>
<dependency>
<groupId>com.ryantenney.metrics</groupId>
<artifactId>metrics-spring</artifactId>
<version>${metrics-spring.version}</version>
<exclusions>
<exclusion>
<artifactId>metrics-annotation</artifactId>
<groupId>com.codahale.metrics</groupId>
</exclusion>
<exclusion>
<artifactId>metrics-core</artifactId>
<groupId>com.codahale.metrics</groupId>
</exclusion>
<exclusion>
<artifactId>metrics-healthchecks</artifactId>
<groupId>com.codahale.metrics</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${HikariCP.version}</version>
<exclusions>
<exclusion>
<artifactId>tools</artifactId>
<groupId>com.sun</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- The HikariCP Java Agent is disabled by default, as it is experimental
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP-agent</artifactId>
<version>${HikariCP.version}</version>
</dependency>
-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<!-- reporting -->
<dependency>
<groupId>fr.ippon.spark.metrics</groupId>
<artifactId>metrics-spark-reporter</artifactId>
<version>${metrics-spark-reporter.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-annotation</artifactId>
<version>${dropwizard-metrics.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-healthchecks</artifactId>
<version>${dropwizard-metrics.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId>
<version>${dropwizard-metrics.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-jvm</artifactId>
<version>${dropwizard-metrics.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-servlet</artifactId>
<version>${dropwizard-metrics.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-servlets</artifactId>
<exclusions>
<exclusion>
<artifactId>metrics-healthchecks</artifactId>
<groupId>io.dropwizard.metrics</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>${gatling.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.version}</version>
<exclusions>
<exclusion>
<artifactId>mapstruct</artifactId>
<groupId>org.mapstruct</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>${javax.inject.version}</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time-hibernate</artifactId>
<version>${joda-time-hibernate.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>${usertype-core.version}</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase.version}</version>
<exclusions>
<exclusion>
<artifactId>jetty-servlet</artifactId>
<groupId>org.eclipse.jetty</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader-tools</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-connector</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-localconfig-connector</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-spring-service-connector</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-data</artifactId>
<version>${spring-security.version}</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<filtering>false</filtering>
<directory>src/main/resources</directory>
<excludes>
<exclude>**/*.xml</exclude>
</excludes>
</resource>
</resources>
<pluginManagement>
<plugins>
<!--This plugin's configuration is used to store Eclipse m2e settings
only. It has no influence on the Maven build itself.-->
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<versionRange>[1.0,)</versionRange>
<goals>
<goal>copy</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute />
</action>
</pluginExecution>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<versionRange>[1.0,)</versionRange>
<goals>
<goal>enforce</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute />
</action>
</pluginExecution>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<versionRange>[1.2.1,)</versionRange>
<goals>
<goal>java</goal>
<goal>exec</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore />
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>com.google.code.sortpom</groupId>
<artifactId>maven-sortpom-plugin</artifactId>
<version>${maven-sortpom-plugin.version}</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sort</goal>
</goals>
</execution>
</executions>
<configuration>
<sortProperties>true</sortProperties>
<nrOfIndentSpace>4</nrOfIndentSpace>
<sortDependencies>groupId,artifactId</sortDependencies>
<sortPlugins>groupId,artifactId</sortPlugins>
<keepBlankLines>true</keepBlankLines>
<expandEmptyElements>false</expandEmptyElements>
</configuration>
</plugin>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>${gatling-maven-plugin.version}</version>
<configuration>
<configFolder>src/test/gatling/conf</configFolder>
<dataFolder>src/test/gatling/data</dataFolder>
<resultsFolder>target/gatling/results</resultsFolder>
<bodiesFolder>src/test/gatling/bodies</bodiesFolder>
<simulationsFolder>src/test/gatling/simulations</simulationsFolder>
<!-- This will force Gatling to ask which simulation to run
This is useful when you have multiple simulations -->
<simulationClass>*</simulationClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${maven-enforcer-plugin.version}</version>
<executions>
<execution>
<id>enforce-versions</id>
<goals>
<goal>enforce</goal>
</goals>
</execution>
</executions>
<configuration>
<rules>
<requireMavenVersion>
<message>You are running an older version of Maven. JHipster requires at least Maven 3.0</message>
<version>[3.0.0,)</version>
</requireMavenVersion>
<requireJavaVersion>
<message>You are running an older version of Java. JHipster requires at least JDK ${java.version}</message>
<version>[${java.version}.0,)</version>
</requireJavaVersion>
</rules>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-Xmx256m</argLine>
<!-- Force alphabetical order to have a reproducible build -->
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<packagingExcludes>WEB-INF/lib/tomcat-*.jar</packagingExcludes>
</configuration>
</plugin>
<plugin>
<groupId>org.bsc.maven</groupId>
<artifactId>maven-processor-plugin</artifactId>
<version>2.2.4</version>
<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>${mapstruct.version}</version>
</dependency>
</dependencies>
<configuration>
<defaultOutputDirectory>${project.build.directory}/generated-sources</defaultOutputDirectory>
<processors>
<processor>org.mapstruct.ap.MappingProcessor</processor>
</processors>
<options>
<mapstruct.suppressGeneratorTimestamp>true</mapstruct.suppressGeneratorTimestamp>
<mapstruct.defaultComponentModel>spring</mapstruct.defaultComponentModel>
</options>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>${sonar-maven-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>${liquibase.version}</version>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.18.2-GA</version>
</dependency>
<dependency>
<groupId>org.liquibase.ext</groupId>
<artifactId>liquibase-hibernate4</artifactId>
<version>${liquibase-hibernate4.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
<configuration>
<changeLogFile>src/main/resources/config/liquibase/master.xml</changeLogFile>
<diffChangeLogFile>src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml</diffChangeLogFile>
<driver />
<url />
<defaultSchemaName />
<username />
<password />
<referenceUrl>hibernate:spring:mark.quinn.domain?dialect=</referenceUrl>
<verbose>true</verbose>
<logging>debug</logging>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005</jvmArguments>
<arguments>
<argument>--spring.profiles.active=dev</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<!-- log configuration -->
<logback.loglevel>DEBUG</logback.loglevel>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
</dependencies>
</profile>
<profile>
<id>fast</id>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<arguments>
<argument>--spring.profiles.active=dev,fast</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<!-- log configuration -->
<logback.loglevel>DEBUG</logback.loglevel>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
</dependencies>
</profile>
<profile>
<id>prod</id>
<build>
<plugins>
<plugin>
<groupId>com.github.trecloux</groupId>
<artifactId>yeoman-maven-plugin</artifactId>
<version>0.4</version>
<executions>
<execution>
<id>run-frontend-build</id>
<phase>generate-resources</phase>
<goals>
<goal>build</goal>
</goals>
<configuration>
<buildTool>grunt</buildTool>
<buildArgs>build --force --no-color</buildArgs>
</configuration>
</execution>
</executions>
<configuration>
<yeomanProjectDirectory>${project.basedir}</yeomanProjectDirectory>
</configuration>
</plugin>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.5</version>
<configuration>
<filesets>
<fileset>
<directory>src/main/webapp/dist</directory>
</fileset>
<fileset>
<directory>.tmp</directory>
</fileset>
<fileset>
<directory>node_modules</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<arguments>
<argument>--spring.profiles.active=prod</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<!-- log configuration -->
<logback.loglevel>INFO</logback.loglevel>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,143 @@
package mark.quinn;
import org.apache.catalina.connector.Connector;
import mark.quinn.config.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.env.Environment;
import org.springframework.core.env.SimpleCommandLinePropertySource;
import com.google.common.base.Joiner;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
@ComponentScan
@EnableAutoConfiguration(exclude = {MetricFilterAutoConfiguration.class, MetricRepositoryAutoConfiguration.class})
public class Application {
private static final Logger log = LoggerFactory.getLogger(Application.class);
@Inject
private Environment env;
@Value("${tomcatAjp.protocol}")
String ajpProtocol;
@Value("${tomcatAjp.port}")
String ajpPort;
@Value("${tomcatAjp.enabled}")
String ajpEnabled;
@Value("${tomcatAjp.scheme}")
String ajpScheme;
/**
* Initializes openAjpPort.
* <p/>
* Spring profiles can be configured with a program arguments --spring.profiles.active=your-active-profile
* <p/>
* <p>
* You can find more information on how profiles work with JHipster on <a href="http://jhipster.github.io/profiles.html">http://jhipster.github.io/profiles.html</a>.
* </p>
*/
@PostConstruct
public void initApplication() throws IOException {
if (env.getActiveProfiles().length == 0) {
log.warn("No Spring profile configured, running with default configuration");
} else {
log.info("Running with Spring profile(s) : {}", Arrays.toString(env.getActiveProfiles()));
Collection<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
if (activeProfiles.contains("dev") && activeProfiles.contains("prod")) {
log.error("You have misconfigured your application! " +
"It should not run with both the 'dev' and 'prod' profiles at the same time.");
}
if (activeProfiles.contains("prod") && activeProfiles.contains("fast")) {
log.error("You have misconfigured your application! " +
"It should not run with both the 'prod' and 'fast' profiles at the same time.");
}
if (activeProfiles.contains("dev") && activeProfiles.contains("cloud")) {
log.error("You have misconfigured your application! " +
"It should not run with both the 'dev' and 'cloud' profiles at the same time.");
}
}
}
/**
* Main method, used to run the application.
*/
public static void main(String[] args) throws UnknownHostException {
SpringApplication app = new SpringApplication(Application.class);
app.setShowBanner(false);
SimpleCommandLinePropertySource source = new SimpleCommandLinePropertySource(args);
addDefaultProfile(app, source);
addLiquibaseScanPackages();
Environment env = app.run(args).getEnvironment();
log.info("Access URLs:\n----------------------------------------------------------\n\t" +
"Local: \t\thttp://127.0.0.1:{}\n\t" +
"External: \thttp://{}:{}\n----------------------------------------------------------",
env.getProperty("server.port"),
InetAddress.getLocalHost().getHostAddress(),
env.getProperty("server.port"));
}
@Bean
public EmbeddedServletContainerFactory servletContainer() {
Integer ajpPortInt = Integer.parseInt(ajpPort);
Boolean ajpEnabledBool = Boolean.valueOf(ajpEnabled);
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
if (ajpEnabledBool)
{
Connector ajpConnector = new Connector(ajpProtocol);
ajpConnector.setProtocol(ajpProtocol);
ajpConnector.setPort(ajpPortInt);
ajpConnector.setSecure(false);
ajpConnector.setAllowTrace(false);
ajpConnector.setScheme(ajpScheme);
tomcat.addAdditionalTomcatConnectors(ajpConnector);
}
return tomcat;
}
/**
* If no profile has been configured, set by default the "dev" profile.
*/
private static void addDefaultProfile(SpringApplication app, SimpleCommandLinePropertySource source) {
if (!source.containsProperty("spring.profiles.active") &&
!System.getenv().containsKey("SPRING_PROFILES_ACTIVE")) {
app.setAdditionalProfiles(Constants.SPRING_PROFILE_DEVELOPMENT);
}
}
/**
* Set the liquibases.scan.packages to avoid an exception from ServiceLocator.
*/
private static void addLiquibaseScanPackages() {
System.setProperty("liquibase.scan.packages", Joiner.on(",").join(
"liquibase.change", "liquibase.database", "liquibase.parser",
"liquibase.precondition", "liquibase.datatype",
"liquibase.serializer", "liquibase.sqlgenerator", "liquibase.executor",
"liquibase.snapshot", "liquibase.logging", "liquibase.diff",
"liquibase.structure", "liquibase.structurecompare", "liquibase.lockservice",
"liquibase.ext", "liquibase.changelog"));
}
}

View File

@@ -0,0 +1,40 @@
package mark.quinn;
import mark.quinn.config.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
/**
* This is a helper Java class that provides an alternative to creating a web.xml.
*/
public class ApplicationWebXml extends SpringBootServletInitializer {
private final Logger log = LoggerFactory.getLogger(ApplicationWebXml.class);
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.profiles(addDefaultProfile())
.showBanner(false)
.sources(Application.class);
}
/**
* Set a default profile if it has not been set.
* <p/>
* <p>
* Please use -Dspring.profiles.active=dev
* </p>
*/
private String addDefaultProfile() {
String profile = System.getProperty("spring.profiles.active");
if (profile != null) {
log.info("Running with Spring profile(s) : {}", profile);
return profile;
}
log.warn("No Spring profile configured, running with default configuration");
return Constants.SPRING_PROFILE_DEVELOPMENT;
}
}

View File

@@ -0,0 +1,62 @@
package mark.quinn.aop.logging;
import mark.quinn.config.Constants;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import javax.inject.Inject;
import java.util.Arrays;
/**
* Aspect for logging execution of service and repository Spring components.
*/
@Aspect
public class LoggingAspect {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Inject
private Environment env;
@Pointcut("within(mark.quinn.repository..*) || within(mark.quinn.service..*) || within(mark.quinn.web.rest..*)")
public void loggingPointcut() {}
@AfterThrowing(pointcut = "loggingPointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
if (env.acceptsProfiles(Constants.SPRING_PROFILE_DEVELOPMENT)) {
log.error("Exception in {}.{}() with cause = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), e.getCause(), e);
} else {
log.error("Exception in {}.{}() with cause = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), e.getCause());
}
}
@Around("loggingPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
if (log.isDebugEnabled()) {
log.debug("Enter: {}.{}() with argument[s] = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs()));
}
try {
Object result = joinPoint.proceed();
if (log.isDebugEnabled()) {
log.debug("Exit: {}.{}() with result = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), result);
}
return result;
} catch (IllegalArgumentException e) {
log.error("Illegal argument: {} in {}.{}()", Arrays.toString(joinPoint.getArgs()),
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
throw e;
}
}
}

View File

@@ -0,0 +1,83 @@
package mark.quinn.async;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.task.AsyncTaskExecutor;
public class ExceptionHandlingAsyncTaskExecutor implements AsyncTaskExecutor,
InitializingBean, DisposableBean {
private final Logger log = LoggerFactory.getLogger(ExceptionHandlingAsyncTaskExecutor.class);
private final AsyncTaskExecutor executor;
public ExceptionHandlingAsyncTaskExecutor(AsyncTaskExecutor executor) {
this.executor = executor;
}
@Override
public void execute(Runnable task) {
executor.execute(task);
}
@Override
public void execute(Runnable task, long startTimeout) {
executor.execute(createWrappedRunnable(task), startTimeout);
}
private <T> Callable<T> createCallable(final Callable<T> task) {
return () -> {
try {
return task.call();
} catch (Exception e) {
handle(e);
throw e;
}
};
}
private Runnable createWrappedRunnable(final Runnable task) {
return () -> {
try {
task.run();
} catch (Exception e) {
handle(e);
}
};
}
protected void handle(Exception e) {
log.error("Caught async exception", e);
}
@Override
public Future<?> submit(Runnable task) {
return executor.submit(createWrappedRunnable(task));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return executor.submit(createCallable(task));
}
@Override
public void destroy() throws Exception {
if (executor instanceof DisposableBean) {
DisposableBean bean = (DisposableBean) executor;
bean.destroy();
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (executor instanceof InitializingBean) {
InitializingBean bean = (InitializingBean) executor;
bean.afterPropertiesSet();
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Async helpers.
*/
package mark.quinn.async;

View File

@@ -0,0 +1,53 @@
package mark.quinn.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import mark.quinn.async.ExceptionHandlingAsyncTaskExecutor;
@Configuration
@EnableAsync
@EnableScheduling
@Profile("!" + Constants.SPRING_PROFILE_FAST)
public class AsyncConfiguration implements AsyncConfigurer, EnvironmentAware {
private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class);
private RelaxedPropertyResolver propertyResolver;
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, "async.");
}
@Override
@Bean
public Executor getAsyncExecutor() {
log.debug("Creating Async Task Executor");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(propertyResolver.getProperty("corePoolSize", Integer.class, 2));
executor.setMaxPoolSize(propertyResolver.getProperty("maxPoolSize", Integer.class, 50));
executor.setQueueCapacity(propertyResolver.getProperty("queueCapacity", Integer.class, 10000));
executor.setThreadNamePrefix("openajpport-Executor-");
return new ExceptionHandlingAsyncTaskExecutor(executor);
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}

View File

@@ -0,0 +1,37 @@
package mark.quinn.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.cache.support.NoOpCacheManager;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
@Configuration
@EnableCaching
@AutoConfigureAfter(value = {MetricsConfiguration.class, DatabaseConfiguration.class})
@Profile("!" + Constants.SPRING_PROFILE_FAST)
public class CacheConfiguration {
private final Logger log = LoggerFactory.getLogger(CacheConfiguration.class);
private CacheManager cacheManager;
@PreDestroy
public void destroy() {
log.info("Closing Cache Manager");
}
@Bean
public CacheManager cacheManager() {
log.debug("No cache");
cacheManager = new NoOpCacheManager();
return cacheManager;
}
}

View File

@@ -0,0 +1,23 @@
package mark.quinn.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.config.java.AbstractCloudConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import javax.sql.DataSource;
@Configuration
@Profile(Constants.SPRING_PROFILE_CLOUD)
public class CloudDatabaseConfiguration extends AbstractCloudConfig {
private final Logger log = LoggerFactory.getLogger(CloudDatabaseConfiguration.class);
@Bean
public DataSource dataSource() {
log.info("Configuring JDBC datasource from a cloud provider");
return connectionFactory().dataSource();
}
}

View File

@@ -0,0 +1,21 @@
package mark.quinn.config;
/**
* Application constants.
*/
public final class Constants {
// Spring profile for development, production and "fast", see http://jhipster.github.io/profiles.html
public static final String SPRING_PROFILE_DEVELOPMENT = "dev";
public static final String SPRING_PROFILE_PRODUCTION = "prod";
public static final String SPRING_PROFILE_FAST = "fast";
// Spring profile used when deploying with Spring Cloud (used when deploying to CloudFoundry)
public static final String SPRING_PROFILE_CLOUD = "cloud";
// Spring profile used when deploying to Heroku
public static final String SPRING_PROFILE_HEROKU = "heroku";
public static final String SYSTEM_ACCOUNT = "system";
private Constants() {
}
}

View File

@@ -0,0 +1,109 @@
package mark.quinn.config;
import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import liquibase.integration.spring.SpringLiquibase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.util.Arrays;
@Configuration
@EnableJpaRepositories("mark.quinn.repository")
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
public class DatabaseConfiguration implements EnvironmentAware {
private final Logger log = LoggerFactory.getLogger(DatabaseConfiguration.class);
private RelaxedPropertyResolver dataSourcePropertyResolver;
private RelaxedPropertyResolver liquiBasePropertyResolver;
private Environment env;
@Autowired(required = false)
private MetricRegistry metricRegistry;
@Override
public void setEnvironment(Environment env) {
this.env = env;
this.dataSourcePropertyResolver = new RelaxedPropertyResolver(env, "spring.datasource.");
this.liquiBasePropertyResolver = new RelaxedPropertyResolver(env, "liquiBase.");
}
@Bean(destroyMethod = "shutdown")
@ConditionalOnExpression("#{!environment.acceptsProfiles('cloud') && !environment.acceptsProfiles('heroku')}")
public DataSource dataSource() {
log.debug("Configuring Datasource");
if (dataSourcePropertyResolver.getProperty("url") == null && dataSourcePropertyResolver.getProperty("databaseName") == null) {
log.error("Your database connection pool configuration is incorrect! The application" +
" cannot start. Please check your Spring profile, current profiles are: {}",
Arrays.toString(env.getActiveProfiles()));
throw new ApplicationContextException("Database connection pool is not configured correctly");
}
HikariConfig config = new HikariConfig();
config.setDataSourceClassName(dataSourcePropertyResolver.getProperty("dataSourceClassName"));
if(StringUtils.isEmpty(dataSourcePropertyResolver.getProperty("url"))) {
config.addDataSourceProperty("databaseName", dataSourcePropertyResolver.getProperty("databaseName"));
config.addDataSourceProperty("serverName", dataSourcePropertyResolver.getProperty("serverName"));
} else {
config.addDataSourceProperty("url", dataSourcePropertyResolver.getProperty("url"));
}
config.addDataSourceProperty("user", dataSourcePropertyResolver.getProperty("username"));
config.addDataSourceProperty("password", dataSourcePropertyResolver.getProperty("password"));
//MySQL optimizations, see https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
if ("com.mysql.jdbc.jdbc2.optional.MysqlDataSource".equals(dataSourcePropertyResolver.getProperty("dataSourceClassName"))) {
config.addDataSourceProperty("cachePrepStmts", dataSourcePropertyResolver.getProperty("cachePrepStmts", "true"));
config.addDataSourceProperty("prepStmtCacheSize", dataSourcePropertyResolver.getProperty("prepStmtCacheSize", "250"));
config.addDataSourceProperty("prepStmtCacheSqlLimit", dataSourcePropertyResolver.getProperty("prepStmtCacheSqlLimit", "2048"));
}
if (metricRegistry != null) {
config.setMetricRegistry(metricRegistry);
}
return new HikariDataSource(config);
}
@Bean
public SpringLiquibase liquibase(DataSource dataSource) {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDataSource(dataSource);
liquibase.setChangeLog("classpath:config/liquibase/master.xml");
liquibase.setContexts(liquiBasePropertyResolver.getProperty("context"));
if (env.acceptsProfiles(Constants.SPRING_PROFILE_FAST)) {
if ("org.h2.jdbcx.JdbcDataSource".equals(dataSourcePropertyResolver.getProperty("dataSourceClassName"))) {
liquibase.setShouldRun(true);
log.warn("Using '{}' profile with H2 database in memory is not optimal, you should consider switching to" +
" MySQL or Postgresql to avoid rebuilding your database upon each start.", Constants.SPRING_PROFILE_FAST);
} else {
liquibase.setShouldRun(false);
}
} else {
log.debug("Configuring Liquibase");
}
return liquibase;
}
@Bean
public Hibernate4Module hibernate4Module() {
return new Hibernate4Module();
}
}

View File

@@ -0,0 +1,26 @@
package mark.quinn.config;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import mark.quinn.domain.util.CustomDateTimeDeserializer;
import mark.quinn.domain.util.CustomDateTimeSerializer;
import mark.quinn.domain.util.CustomLocalDateSerializer;
import mark.quinn.domain.util.ISO8601LocalDateDeserializer;
import com.fasterxml.jackson.datatype.joda.JodaModule;
@Configuration
public class JacksonConfiguration {
@Bean
public JodaModule jacksonJodaModule() {
JodaModule module = new JodaModule();
module.addSerializer(DateTime.class, new CustomDateTimeSerializer());
module.addDeserializer(DateTime.class, new CustomDateTimeDeserializer());
module.addSerializer(LocalDate.class, new CustomLocalDateSerializer());
module.addDeserializer(LocalDate.class, new ISO8601LocalDateDeserializer());
return module;
}
}

View File

@@ -0,0 +1,48 @@
package mark.quinn.config;
import mark.quinn.config.locale.AngularCookieLocaleResolver;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.env.Environment;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
@Configuration
public class LocaleConfiguration extends WebMvcConfigurerAdapter implements EnvironmentAware {
private RelaxedPropertyResolver propertyResolver;
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, "spring.messageSource.");
}
@Bean(name = "localeResolver")
public LocaleResolver localeResolver() {
AngularCookieLocaleResolver cookieLocaleResolver = new AngularCookieLocaleResolver();
cookieLocaleResolver.setCookieName("NG_TRANSLATE_LANG_KEY");
return cookieLocaleResolver;
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:/i18n/messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(propertyResolver.getProperty("cacheSeconds", Integer.class, 1));
return messageSource;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("language");
registry.addInterceptor(localeChangeInterceptor);
}
}

View File

@@ -0,0 +1,18 @@
package mark.quinn.config;
import mark.quinn.aop.logging.LoggingAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Profile;
@Configuration
@EnableAspectJAutoProxy
public class LoggingAspectConfiguration {
@Bean
@Profile(Constants.SPRING_PROFILE_DEVELOPMENT)
public LoggingAspect loggingAspect() {
return new LoggingAspect();
}
}

View File

@@ -0,0 +1,70 @@
package mark.quinn.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties;
@Configuration
public class MailConfiguration implements EnvironmentAware {
private static final String ENV_SPRING_MAIL = "mail.";
private static final String DEFAULT_HOST = "127.0.0.1";
private static final String PROP_HOST = "host";
private static final String DEFAULT_PROP_HOST = "localhost";
private static final String PROP_PORT = "port";
private static final String PROP_USER = "username";
private static final String PROP_PASSWORD = "password";
private static final String PROP_PROTO = "protocol";
private static final String PROP_TLS = "tls";
private static final String PROP_AUTH = "auth";
private static final String PROP_SMTP_AUTH = "mail.smtp.auth";
private static final String PROP_STARTTLS = "mail.smtp.starttls.enable";
private static final String PROP_TRANSPORT_PROTO = "mail.transport.protocol";
private final Logger log = LoggerFactory.getLogger(MailConfiguration.class);
private RelaxedPropertyResolver propertyResolver;
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_SPRING_MAIL);
}
@Bean
public JavaMailSenderImpl javaMailSender() {
log.debug("Configuring mail server");
String host = propertyResolver.getProperty(PROP_HOST, DEFAULT_PROP_HOST);
int port = propertyResolver.getProperty(PROP_PORT, Integer.class, 0);
String user = propertyResolver.getProperty(PROP_USER);
String password = propertyResolver.getProperty(PROP_PASSWORD);
String protocol = propertyResolver.getProperty(PROP_PROTO);
Boolean tls = propertyResolver.getProperty(PROP_TLS, Boolean.class, false);
Boolean auth = propertyResolver.getProperty(PROP_AUTH, Boolean.class, false);
JavaMailSenderImpl sender = new JavaMailSenderImpl();
if (host != null && !host.isEmpty()) {
sender.setHost(host);
} else {
log.warn("Warning! Your SMTP server is not configured. We will try to use one on localhost.");
log.debug("Did you configure your SMTP settings in your application.yml?");
sender.setHost(DEFAULT_HOST);
}
sender.setPort(port);
sender.setUsername(user);
sender.setPassword(password);
Properties sendProperties = new Properties();
sendProperties.setProperty(PROP_SMTP_AUTH, auth.toString());
sendProperties.setProperty(PROP_STARTTLS, tls.toString());
sendProperties.setProperty(PROP_TRANSPORT_PROTO, protocol);
sender.setJavaMailProperties(sendProperties);
return sender;
}
}

View File

@@ -0,0 +1,159 @@
package mark.quinn.config;
import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.graphite.Graphite;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.codahale.metrics.health.HealthCheckRegistry;
import com.codahale.metrics.jvm.*;
import com.ryantenney.metrics.spring.config.annotation.EnableMetrics;
import com.ryantenney.metrics.spring.config.annotation.MetricsConfigurerAdapter;
import fr.ippon.spark.metrics.SparkReporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableMetrics(proxyTargetClass = true)
@Profile("!" + Constants.SPRING_PROFILE_FAST)
public class MetricsConfiguration extends MetricsConfigurerAdapter implements EnvironmentAware {
private static final String ENV_METRICS = "metrics.";
private static final String ENV_METRICS_GRAPHITE = "metrics.graphite.";
private static final String ENV_METRICS_SPARK = "metrics.spark.";
private static final String PROP_JMX_ENABLED = "jmx.enabled";
private static final String PROP_GRAPHITE_ENABLED = "enabled";
private static final String PROP_SPARK_ENABLED = "enabled";
private static final String PROP_GRAPHITE_PREFIX = "";
private static final String PROP_PORT = "port";
private static final String PROP_HOST = "host";
private static final String PROP_METRIC_REG_JVM_MEMORY = "jvm.memory";
private static final String PROP_METRIC_REG_JVM_GARBAGE = "jvm.garbage";
private static final String PROP_METRIC_REG_JVM_THREADS = "jvm.threads";
private static final String PROP_METRIC_REG_JVM_FILES = "jvm.files";
private static final String PROP_METRIC_REG_JVM_BUFFERS = "jvm.buffers";
private final Logger log = LoggerFactory.getLogger(MetricsConfiguration.class);
private MetricRegistry metricRegistry = new MetricRegistry();
private HealthCheckRegistry healthCheckRegistry = new HealthCheckRegistry();
private RelaxedPropertyResolver propertyResolver;
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_METRICS);
}
@Override
@Bean
public MetricRegistry getMetricRegistry() {
return metricRegistry;
}
@Override
@Bean
public HealthCheckRegistry getHealthCheckRegistry() {
return healthCheckRegistry;
}
@PostConstruct
public void init() {
log.debug("Registering JVM gauges");
metricRegistry.register(PROP_METRIC_REG_JVM_MEMORY, new MemoryUsageGaugeSet());
metricRegistry.register(PROP_METRIC_REG_JVM_GARBAGE, new GarbageCollectorMetricSet());
metricRegistry.register(PROP_METRIC_REG_JVM_THREADS, new ThreadStatesGaugeSet());
metricRegistry.register(PROP_METRIC_REG_JVM_FILES, new FileDescriptorRatioGauge());
metricRegistry.register(PROP_METRIC_REG_JVM_BUFFERS, new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()));
if (propertyResolver.getProperty(PROP_JMX_ENABLED, Boolean.class, false)) {
log.info("Initializing Metrics JMX reporting");
JmxReporter jmxReporter = JmxReporter.forRegistry(metricRegistry).build();
jmxReporter.start();
}
}
@Configuration
@ConditionalOnClass(Graphite.class)
@Profile("!" + Constants.SPRING_PROFILE_FAST)
public static class GraphiteRegistry implements EnvironmentAware {
private final Logger log = LoggerFactory.getLogger(GraphiteRegistry.class);
@Inject
private MetricRegistry metricRegistry;
private RelaxedPropertyResolver propertyResolver;
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_METRICS_GRAPHITE);
}
@PostConstruct
private void init() {
Boolean graphiteEnabled = propertyResolver.getProperty(PROP_GRAPHITE_ENABLED, Boolean.class, false);
if (graphiteEnabled) {
log.info("Initializing Metrics Graphite reporting");
String graphiteHost = propertyResolver.getRequiredProperty(PROP_HOST);
Integer graphitePort = propertyResolver.getRequiredProperty(PROP_PORT, Integer.class);
String graphitePrefix = propertyResolver.getProperty(PROP_GRAPHITE_PREFIX, String.class, "");
Graphite graphite = new Graphite(new InetSocketAddress(graphiteHost, graphitePort));
GraphiteReporter graphiteReporter = GraphiteReporter.forRegistry(metricRegistry)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.prefixedWith(graphitePrefix)
.build(graphite);
graphiteReporter.start(1, TimeUnit.MINUTES);
}
}
}
@Configuration
@ConditionalOnClass(SparkReporter.class)
@Profile("!" + Constants.SPRING_PROFILE_FAST)
public static class SparkRegistry implements EnvironmentAware {
private final Logger log = LoggerFactory.getLogger(SparkRegistry.class);
@Inject
private MetricRegistry metricRegistry;
private RelaxedPropertyResolver propertyResolver;
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_METRICS_SPARK);
}
@PostConstruct
private void init() {
Boolean sparkEnabled = propertyResolver.getProperty(PROP_SPARK_ENABLED, Boolean.class, false);
if (sparkEnabled) {
log.info("Initializing Metrics Spark reporting");
String sparkHost = propertyResolver.getRequiredProperty(PROP_HOST);
Integer sparkPort = propertyResolver.getRequiredProperty(PROP_PORT, Integer.class);
SparkReporter sparkReporter = SparkReporter.forRegistry(metricRegistry)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build(sparkHost, sparkPort);
sparkReporter.start(1, TimeUnit.MINUTES);
}
}
}
}

View File

@@ -0,0 +1,146 @@
package mark.quinn.config;
import mark.quinn.security.*;
import mark.quinn.web.filter.CsrfCookieGeneratorFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.repository.query.spi.EvaluationContextExtension;
import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.csrf.CsrfFilter;
import javax.inject.Inject;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Inject
private Environment env;
@Inject
private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler;
@Inject
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
@Inject
private AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler;
@Inject
private Http401UnauthorizedEntryPoint authenticationEntryPoint;
@Inject
private UserDetailsService userDetailsService;
@Inject
private RememberMeServices rememberMeServices;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/scripts/**/*.{js,html}")
.antMatchers("/bower_components/**")
.antMatchers("/i18n/**")
.antMatchers("/assets/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/test/**")
.antMatchers("/console/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.ignoringAntMatchers("/websocket/**")
.and()
.addFilterAfter(new CsrfCookieGeneratorFilter(), CsrfFilter.class)
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.rememberMe()
.rememberMeServices(rememberMeServices)
.rememberMeParameter("remember-me")
.key(env.getProperty("jhipster.security.rememberme.key"))
.and()
.formLogin()
.loginProcessingUrl("/api/authentication")
.successHandler(ajaxAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.usernameParameter("j_username")
.passwordParameter("j_password")
.permitAll()
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler)
.deleteCookies("JSESSIONID")
.permitAll()
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/account/reset_password/init").permitAll()
.antMatchers("/api/account/reset_password/finish").permitAll()
.antMatchers("/api/logs/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/audits/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/**").authenticated()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/metrics/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/health/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/dump/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/shutdown/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/beans/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/configprops/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/info/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/autoconfig/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/env/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/configuration/security").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/configuration/ui").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/swagger-ui.html").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/protected/**").authenticated();
}
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
}

View File

@@ -0,0 +1,39 @@
package mark.quinn.config;
import org.apache.commons.lang.CharEncoding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
@Configuration
public class ThymeleafConfiguration {
private final Logger log = LoggerFactory.getLogger(ThymeleafConfiguration.class);
@Bean
@Description("Thymeleaf template resolver serving HTML 5 emails")
public ClassLoaderTemplateResolver emailTemplateResolver() {
ClassLoaderTemplateResolver emailTemplateResolver = new ClassLoaderTemplateResolver();
emailTemplateResolver.setPrefix("mails/");
emailTemplateResolver.setSuffix(".html");
emailTemplateResolver.setTemplateMode("HTML5");
emailTemplateResolver.setCharacterEncoding(CharEncoding.UTF_8);
emailTemplateResolver.setOrder(1);
return emailTemplateResolver;
}
@Bean
@Description("Spring mail message resolver")
public MessageSource emailMessageSource() {
log.info("loading non-reloadable mail messages resources");
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:/mails/messages/messages");
messageSource.setDefaultEncoding(CharEncoding.UTF_8);
return messageSource;
}
}

View File

@@ -0,0 +1,161 @@
package mark.quinn.config;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.servlet.InstrumentedFilter;
import com.codahale.metrics.servlets.MetricsServlet;
import mark.quinn.web.filter.CachingHttpHeadersFilter;
import mark.quinn.web.filter.StaticResourcesProductionFilter;
import mark.quinn.web.filter.gzip.GZipServletFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.MimeMappings;
import org.springframework.boot.context.embedded.ServletContextInitializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import javax.inject.Inject;
import javax.servlet.*;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* Configuration of web application with Servlet 3.0 APIs.
*/
@Configuration
@AutoConfigureAfter(CacheConfiguration.class)
public class WebConfigurer implements ServletContextInitializer, EmbeddedServletContainerCustomizer {
private final Logger log = LoggerFactory.getLogger(WebConfigurer.class);
@Inject
private Environment env;
@Autowired(required = false)
private MetricRegistry metricRegistry;
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
log.info("Web application configuration, using profiles: {}", Arrays.toString(env.getActiveProfiles()));
EnumSet<DispatcherType> disps = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC);
if (!env.acceptsProfiles(Constants.SPRING_PROFILE_FAST)) {
initMetrics(servletContext, disps);
}
if (env.acceptsProfiles(Constants.SPRING_PROFILE_PRODUCTION)) {
initCachingHttpHeadersFilter(servletContext, disps);
initStaticResourcesProductionFilter(servletContext, disps);
initGzipFilter(servletContext, disps);
}
if (env.acceptsProfiles(Constants.SPRING_PROFILE_DEVELOPMENT)) {
initH2Console(servletContext);
}
log.info("Web application fully configured");
}
/**
* Set up Mime types.
*/
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT);
// IE issue, see https://github.com/jhipster/generator-jhipster/pull/711
mappings.add("html", "text/html;charset=utf-8");
// CloudFoundry issue, see https://github.com/cloudfoundry/gorouter/issues/64
mappings.add("json", "text/html;charset=utf-8");
container.setMimeMappings(mappings);
}
/**
* Initializes the GZip filter.
*/
private void initGzipFilter(ServletContext servletContext, EnumSet<DispatcherType> disps) {
log.debug("Registering GZip Filter");
FilterRegistration.Dynamic compressingFilter = servletContext.addFilter("gzipFilter", new GZipServletFilter());
Map<String, String> parameters = new HashMap<>();
compressingFilter.setInitParameters(parameters);
compressingFilter.addMappingForUrlPatterns(disps, true, "*.css");
compressingFilter.addMappingForUrlPatterns(disps, true, "*.json");
compressingFilter.addMappingForUrlPatterns(disps, true, "*.html");
compressingFilter.addMappingForUrlPatterns(disps, true, "*.js");
compressingFilter.addMappingForUrlPatterns(disps, true, "*.svg");
compressingFilter.addMappingForUrlPatterns(disps, true, "*.ttf");
compressingFilter.addMappingForUrlPatterns(disps, true, "/api/*");
compressingFilter.addMappingForUrlPatterns(disps, true, "/metrics/*");
compressingFilter.setAsyncSupported(true);
}
/**
* Initializes the static resources production Filter.
*/
private void initStaticResourcesProductionFilter(ServletContext servletContext,
EnumSet<DispatcherType> disps) {
log.debug("Registering static resources production Filter");
FilterRegistration.Dynamic staticResourcesProductionFilter =
servletContext.addFilter("staticResourcesProductionFilter",
new StaticResourcesProductionFilter());
staticResourcesProductionFilter.addMappingForUrlPatterns(disps, true, "/");
staticResourcesProductionFilter.addMappingForUrlPatterns(disps, true, "/index.html");
staticResourcesProductionFilter.addMappingForUrlPatterns(disps, true, "/assets/*");
staticResourcesProductionFilter.addMappingForUrlPatterns(disps, true, "/scripts/*");
staticResourcesProductionFilter.setAsyncSupported(true);
}
/**
* Initializes the caching HTTP Headers Filter.
*/
private void initCachingHttpHeadersFilter(ServletContext servletContext,
EnumSet<DispatcherType> disps) {
log.debug("Registering Caching HTTP Headers Filter");
FilterRegistration.Dynamic cachingHttpHeadersFilter =
servletContext.addFilter("cachingHttpHeadersFilter",
new CachingHttpHeadersFilter(env));
cachingHttpHeadersFilter.addMappingForUrlPatterns(disps, true, "/assets/*");
cachingHttpHeadersFilter.addMappingForUrlPatterns(disps, true, "/scripts/*");
cachingHttpHeadersFilter.setAsyncSupported(true);
}
/**
* Initializes Metrics.
*/
private void initMetrics(ServletContext servletContext, EnumSet<DispatcherType> disps) {
log.debug("Initializing Metrics registries");
servletContext.setAttribute(InstrumentedFilter.REGISTRY_ATTRIBUTE,
metricRegistry);
servletContext.setAttribute(MetricsServlet.METRICS_REGISTRY,
metricRegistry);
log.debug("Registering Metrics Filter");
FilterRegistration.Dynamic metricsFilter = servletContext.addFilter("webappMetricsFilter",
new InstrumentedFilter());
metricsFilter.addMappingForUrlPatterns(disps, true, "/*");
metricsFilter.setAsyncSupported(true);
log.debug("Registering Metrics Servlet");
ServletRegistration.Dynamic metricsAdminServlet =
servletContext.addServlet("metricsServlet", new MetricsServlet());
metricsAdminServlet.addMapping("/metrics/metrics/*");
metricsAdminServlet.setAsyncSupported(true);
metricsAdminServlet.setLoadOnStartup(2);
}
/**
* Initializes H2 console
*/
private void initH2Console(ServletContext servletContext) {
log.debug("Initialize H2 console");
ServletRegistration.Dynamic h2ConsoleServlet = servletContext.addServlet("H2Console", new org.h2.server.web.WebServlet());
h2ConsoleServlet.addMapping("/console/*");
h2ConsoleServlet.setInitParameter("-properties", "src/main/resources");
h2ConsoleServlet.setLoadOnStartup(1);
}
}

View File

@@ -0,0 +1,85 @@
package mark.quinn.config.apidoc;
import mark.quinn.config.Constants;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StopWatch;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import static springfox.documentation.builders.PathSelectors.regex;
/**
* Springfox Swagger configuration.
*
* Warning! When having a lot of REST endpoints, Springfox can become a performance issue. In that
* case, you can use a specific Spring profile for this class, so that only front-end developers
* have access to the Swagger view.
*/
@Configuration
@EnableSwagger2
@Profile("!"+Constants.SPRING_PROFILE_PRODUCTION)
public class SwaggerConfiguration implements EnvironmentAware {
private final Logger log = LoggerFactory.getLogger(SwaggerConfiguration.class);
public static final String DEFAULT_INCLUDE_PATTERN = "/api/.*";
private RelaxedPropertyResolver propertyResolver;
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, "swagger.");
}
/**
* Swagger Springfox configuration.
*/
@Bean
public Docket swaggerSpringfoxDocket() {
log.debug("Starting Swagger");
StopWatch watch = new StopWatch();
watch.start();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.genericModelSubstitutes(ResponseEntity.class)
.forCodeGeneration(true)
.genericModelSubstitutes(ResponseEntity.class)
.directModelSubstitute(org.joda.time.LocalDate.class, String.class)
.directModelSubstitute(org.joda.time.LocalDateTime.class, Date.class)
.directModelSubstitute(org.joda.time.DateTime.class, Date.class)
.directModelSubstitute(java.time.LocalDate.class, String.class)
.directModelSubstitute(java.time.ZonedDateTime.class, Date.class)
.directModelSubstitute(java.time.LocalDateTime.class, Date.class)
.select()
.paths(regex(DEFAULT_INCLUDE_PATTERN))
.build();
watch.stop();
log.debug("Started Swagger in {} ms", watch.getTotalTimeMillis());
return docket;
}
/**
* API Info as it appears on the swagger-ui page.
*/
private ApiInfo apiInfo() {
return new ApiInfo(
propertyResolver.getProperty("title"),
propertyResolver.getProperty("description"),
propertyResolver.getProperty("version"),
propertyResolver.getProperty("termsOfServiceUrl"),
propertyResolver.getProperty("contact"),
propertyResolver.getProperty("license"),
propertyResolver.getProperty("licenseUrl"));
}
}

View File

@@ -0,0 +1,4 @@
/**
* Swagger api specific code.
*/
package mark.quinn.config.apidoc;

View File

@@ -0,0 +1,79 @@
package mark.quinn.config.audit;
import mark.quinn.domain.PersistentAuditEvent;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import java.util.*;
@Configuration
public class AuditEventConverter {
/**
* Convert a list of PersistentAuditEvent to a list of AuditEvent
* @param persistentAuditEvents the list to convert
* @return the converted list.
*/
public List<AuditEvent> convertToAuditEvent(Iterable<PersistentAuditEvent> persistentAuditEvents) {
if (persistentAuditEvents == null) {
return Collections.emptyList();
}
List<AuditEvent> auditEvents = new ArrayList<>();
for (PersistentAuditEvent persistentAuditEvent : persistentAuditEvents) {
AuditEvent auditEvent = new AuditEvent(persistentAuditEvent.getAuditEventDate().toDate(), persistentAuditEvent.getPrincipal(),
persistentAuditEvent.getAuditEventType(), convertDataToObjects(persistentAuditEvent.getData()));
auditEvents.add(auditEvent);
}
return auditEvents;
}
/**
* Internal conversion. This is needed to support the current SpringBoot actuator AuditEventRepository interface
*
* @param data the data to convert
* @return a map of String, Object
*/
public Map<String, Object> convertDataToObjects(Map<String, String> data) {
Map<String, Object> results = new HashMap<>();
if (data != null) {
for (String key : data.keySet()) {
results.put(key, data.get(key));
}
}
return results;
}
/**
* Internal conversion. This method will allow to save additional data.
* By default, it will save the object as string
*
* @param data the data to convert
* @return a map of String, String
*/
public Map<String, String> convertDataToStrings(Map<String, Object> data) {
Map<String, String> results = new HashMap<>();
if (data != null) {
for (String key : data.keySet()) {
Object object = data.get(key);
// Extract the data that will be saved.
if (object instanceof WebAuthenticationDetails) {
WebAuthenticationDetails authenticationDetails = (WebAuthenticationDetails) object;
results.put("remoteAddress", authenticationDetails.getRemoteAddress());
results.put("sessionId", authenticationDetails.getSessionId());
} else {
results.put(key, object.toString());
}
}
}
return results;
}
}

View File

@@ -0,0 +1,4 @@
/**
* Audit specific code.
*/
package mark.quinn.config.audit;

View File

@@ -0,0 +1,87 @@
package mark.quinn.config.locale;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
import java.util.TimeZone;
/**
* Angular cookie saved the locale with a double quote (%22en%22).
* So the default CookieLocaleResolver#StringUtils.parseLocaleString(localePart)
* is not able to parse the locale.
*
* This class will check if a double quote has been added, if so it will remove it.
*/
public class AngularCookieLocaleResolver extends CookieLocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
parseLocaleCookieIfNecessary(request);
return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
}
@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
parseLocaleCookieIfNecessary(request);
return new TimeZoneAwareLocaleContext() {
@Override
public Locale getLocale() {
return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
}
@Override
public TimeZone getTimeZone() {
return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME);
}
};
}
@Override
public void addCookie(HttpServletResponse response, String cookieValue) {
// Mandatory cookie modification for angular to support the locale switching on the server side.
cookieValue = "%22" + cookieValue + "%22";
super.addCookie(response, cookieValue);
}
private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
if (request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME) == null) {
// Retrieve and parse cookie value.
Cookie cookie = WebUtils.getCookie(request, getCookieName());
Locale locale = null;
TimeZone timeZone = null;
if (cookie != null) {
String value = cookie.getValue();
// Remove the double quote
value = StringUtils.replace(value, "%22", "");
String localePart = value;
String timeZonePart = null;
int spaceIndex = localePart.indexOf(' ');
if (spaceIndex != -1) {
localePart = value.substring(0, spaceIndex);
timeZonePart = value.substring(spaceIndex + 1);
}
locale = (!"-".equals(localePart) ? StringUtils.parseLocaleString(localePart.replace('-', '_')) : null);
if (timeZonePart != null) {
timeZone = StringUtils.parseTimeZoneString(timeZonePart);
}
if (logger.isTraceEnabled()) {
logger.trace("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale +
"'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : ""));
}
}
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
(locale != null ? locale: determineDefaultLocale(request)));
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
(timeZone != null ? timeZone : determineDefaultTimeZone(request)));
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Locale specific code.
*/
package mark.quinn.config.locale;

View File

@@ -0,0 +1,73 @@
package mark.quinn.config.metrics;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
/**
* SpringBoot Actuator HealthIndicator check for the Database.
*/
public class DatabaseHealthIndicator extends AbstractHealthIndicator {
private JdbcTemplate jdbcTemplate;
private static Map<String, String> queries = new HashMap<>();
static {
queries.put("HSQL Database Engine",
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SYSTEM_USERS");
queries.put("Oracle", "SELECT 'Hello' from DUAL");
queries.put("Apache Derby", "SELECT 1 FROM SYSIBM.SYSDUMMY1");
queries.put("MySQL", "SELECT 1");
queries.put("PostgreSQL", "SELECT 1");
queries.put("Microsoft SQL Server", "SELECT 1");
}
private static String DEFAULT_QUERY = "SELECT 1";
private String query = null;
public DatabaseHealthIndicator(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
String product = getProduct();
builder.up().withDetail("database", product);
String query = detectQuery(product);
if (StringUtils.hasText(query)) {
try {
builder.withDetail("hello",
this.jdbcTemplate.queryForObject(query, Object.class));
} catch (Exception ex) {
builder.down(ex);
}
}
}
private String getProduct() {
return this.jdbcTemplate.execute((Connection connection) -> connection.getMetaData().getDatabaseProductName());
}
protected String detectQuery(String product) {
String query = this.query;
if (!StringUtils.hasText(query)) {
query = queries.get(product);
}
if (!StringUtils.hasText(query)) {
query = DEFAULT_QUERY;
}
return query;
}
public void setQuery(String query) {
this.query = query;
}
}

View File

@@ -0,0 +1,29 @@
package mark.quinn.config.metrics;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import javax.inject.Inject;
import javax.sql.DataSource;
@Configuration
public class JHipsterHealthIndicatorConfiguration {
@Inject
private JavaMailSenderImpl javaMailSender;
@Inject
private DataSource dataSource;
@Bean
public HealthIndicator dbHealthIndicator() {
return new DatabaseHealthIndicator(dataSource);
}
@Bean
public HealthIndicator mailHealthIndicator() {
return new JavaMailHealthIndicator(javaMailSender);
}
}

View File

@@ -0,0 +1,42 @@
package mark.quinn.config.metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.util.Assert;
import javax.mail.MessagingException;
/**
* SpringBoot Actuator HealthIndicator check for JavaMail.
*/
public class JavaMailHealthIndicator extends AbstractHealthIndicator {
private final Logger log = LoggerFactory.getLogger(JavaMailHealthIndicator.class);
private JavaMailSenderImpl javaMailSender;
public JavaMailHealthIndicator(JavaMailSenderImpl javaMailSender) {
Assert.notNull(javaMailSender, "javaMailSender must not be null");
this.javaMailSender = javaMailSender;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
log.debug("Initializing JavaMail health indicator");
try {
javaMailSender.getSession().getTransport().connect(javaMailSender.getHost(),
javaMailSender.getPort(),
javaMailSender.getUsername(),
javaMailSender.getPassword());
builder.up();
} catch (MessagingException e) {
log.debug("Cannot connect to e-mail server. Error: {}", e.getMessage());
builder.down(e);
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Health and Metrics specific code.
*/
package mark.quinn.config.metrics;

View File

@@ -0,0 +1,4 @@
/**
* Spring Framework configuration files.
*/
package mark.quinn.config;

View File

@@ -0,0 +1,82 @@
package mark.quinn.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.annotations.Type;
import org.hibernate.envers.Audited;
import org.joda.time.DateTime;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.validation.constraints.NotNull;
/**
* Base abstract class for entities which will hold definitions for created, last modified by and created,
* last modified by date.
*/
@MappedSuperclass
@Audited
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditingEntity {
@CreatedBy
@NotNull
@Column(name = "created_by", nullable = false, length = 50, updatable = false)
@JsonIgnore
private String createdBy;
@CreatedDate
@NotNull
@Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
@Column(name = "created_date", nullable = false)
@JsonIgnore
private DateTime createdDate = DateTime.now();
@LastModifiedBy
@Column(name = "last_modified_by", length = 50)
@JsonIgnore
private String lastModifiedBy;
@LastModifiedDate
@Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
@Column(name = "last_modified_date")
@JsonIgnore
private DateTime lastModifiedDate = DateTime.now();
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public DateTime getCreatedDate() {
return createdDate;
}
public void setCreatedDate(DateTime createdDate) {
this.createdDate = createdDate;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public DateTime getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(DateTime lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}

View File

@@ -0,0 +1,62 @@
package mark.quinn.domain;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Column;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* An authority (a security role) used by Spring Security.
*/
@Entity
@Table(name = "JHI_AUTHORITY")
public class Authority implements Serializable {
@NotNull
@Size(min = 0, max = 50)
@Id
@Column(length = 50)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Authority authority = (Authority) o;
if (name != null ? !name.equals(authority.name) : authority.name != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
@Override
public String toString() {
return "Authority{" +
"name='" + name + '\'' +
"}";
}
}

View File

@@ -0,0 +1,78 @@
package mark.quinn.domain;
import org.hibernate.annotations.Type;
import org.joda.time.LocalDateTime;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* Persist AuditEvent managed by the Spring Boot actuator
* @see org.springframework.boot.actuate.audit.AuditEvent
*/
@Entity
@Table(name = "JHI_PERSISTENT_AUDIT_EVENT")
public class PersistentAuditEvent {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "event_id")
private Long id;
@NotNull
@Column(nullable = false)
private String principal;
@Column(name = "event_date")
@Type(type = "org.jadira.usertype.dateandtime.joda.PersistentLocalDateTime")
private LocalDateTime auditEventDate;
@Column(name = "event_type")
private String auditEventType;
@ElementCollection
@MapKeyColumn(name="name")
@Column(name="value")
@CollectionTable(name="JHI_PERSISTENT_AUDIT_EVT_DATA", joinColumns=@JoinColumn(name="event_id"))
private Map<String, String> data = new HashMap<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPrincipal() {
return principal;
}
public void setPrincipal(String principal) {
this.principal = principal;
}
public LocalDateTime getAuditEventDate() {
return auditEventDate;
}
public void setAuditEventDate(LocalDateTime auditEventDate) {
this.auditEventDate = auditEventDate;
}
public String getAuditEventType() {
return auditEventType;
}
public void setAuditEventType(String auditEventType) {
this.auditEventType = auditEventType;
}
public Map<String, String> getData() {
return data;
}
public void setData(Map<String, String> data) {
this.data = data;
}
}

View File

@@ -0,0 +1,143 @@
package mark.quinn.domain;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.annotations.Type;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* Persistent tokens are used by Spring Security to automatically log in users.
*
* @see mark.quinn.security.CustomPersistentRememberMeServices
*/
@Entity
@Table(name = "JHI_PERSISTENT_TOKEN")
public class PersistentToken implements Serializable {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormat.forPattern("d MMMM yyyy");
private static final int MAX_USER_AGENT_LEN = 255;
@Id
private String series;
@JsonIgnore
@NotNull
@Column(name = "token_value", nullable = false)
private String tokenValue;
@JsonIgnore
@Column(name = "token_date")
@Type(type = "org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
private LocalDate tokenDate;
//an IPV6 address max length is 39 characters
@Size(min = 0, max = 39)
@Column(name = "ip_address", length = 39)
private String ipAddress;
@Column(name = "user_agent")
private String userAgent;
@JsonIgnore
@ManyToOne
private User user;
public String getSeries() {
return series;
}
public void setSeries(String series) {
this.series = series;
}
public String getTokenValue() {
return tokenValue;
}
public void setTokenValue(String tokenValue) {
this.tokenValue = tokenValue;
}
public LocalDate getTokenDate() {
return tokenDate;
}
public void setTokenDate(LocalDate tokenDate) {
this.tokenDate = tokenDate;
}
@JsonGetter
public String getFormattedTokenDate() {
return DATE_TIME_FORMATTER.print(this.tokenDate);
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
if (userAgent.length() >= MAX_USER_AGENT_LEN) {
this.userAgent = userAgent.substring(0, MAX_USER_AGENT_LEN - 1);
} else {
this.userAgent = userAgent;
}
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PersistentToken that = (PersistentToken) o;
if (!series.equals(that.series)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return series.hashCode();
}
@Override
public String toString() {
return "PersistentToken{" +
"series='" + series + '\'' +
", tokenValue='" + tokenValue + '\'' +
", tokenDate=" + tokenDate +
", ipAddress='" + ipAddress + '\'' +
", userAgent='" + userAgent + '\'' +
"}";
}
}

View File

@@ -0,0 +1,225 @@
package mark.quinn.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.validator.constraints.Email;
import javax.persistence.*;
import org.hibernate.annotations.Type;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import org.joda.time.DateTime;
/**
* A user.
*/
@Entity
@Table(name = "JHI_USER")
public class User extends AbstractAuditingEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
@Pattern(regexp = "^[a-z0-9]*$")
@Size(min = 1, max = 50)
@Column(length = 50, unique = true, nullable = false)
private String login;
@JsonIgnore
@NotNull
@Size(min = 60, max = 60)
@Column(length = 60)
private String password;
@Size(max = 50)
@Column(name = "first_name", length = 50)
private String firstName;
@Size(max = 50)
@Column(name = "last_name", length = 50)
private String lastName;
@Email
@Size(max = 100)
@Column(length = 100, unique = true)
private String email;
@Column(nullable = false)
private boolean activated = false;
@Size(min = 2, max = 5)
@Column(name = "lang_key", length = 5)
private String langKey;
@Size(max = 20)
@Column(name = "activation_key", length = 20)
@JsonIgnore
private String activationKey;
@Size(max = 20)
@Column(name = "reset_key", length = 20)
private String resetKey;
@Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
@Column(name = "reset_date", nullable = true)
private DateTime resetDate = null;
@JsonIgnore
@ManyToMany
@JoinTable(
name = "JHI_USER_AUTHORITY",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")})
private Set<Authority> authorities = new HashSet<>();
@JsonIgnore
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "user")
private Set<PersistentToken> persistentTokens = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
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;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public boolean getActivated() {
return activated;
}
public void setActivated(boolean activated) {
this.activated = activated;
}
public String getActivationKey() {
return activationKey;
}
public void setActivationKey(String activationKey) {
this.activationKey = activationKey;
}
public String getResetKey() {
return resetKey;
}
public void setResetKey(String resetKey) {
this.resetKey = resetKey;
}
public DateTime getResetDate() {
return resetDate;
}
public void setResetDate(DateTime resetDate) {
this.resetDate = resetDate;
}
public String getLangKey() {
return langKey;
}
public void setLangKey(String langKey) {
this.langKey = langKey;
}
public Set<Authority> getAuthorities() {
return authorities;
}
public void setAuthorities(Set<Authority> authorities) {
this.authorities = authorities;
}
public Set<PersistentToken> getPersistentTokens() {
return persistentTokens;
}
public void setPersistentTokens(Set<PersistentToken> persistentTokens) {
this.persistentTokens = persistentTokens;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User user = (User) o;
if (!login.equals(user.login)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return login.hashCode();
}
@Override
public String toString() {
return "User{" +
"login='" + login + '\'' +
", password='" + password + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", activated='" + activated + '\'' +
", langKey='" + langKey + '\'' +
", activationKey='" + activationKey + '\'' +
"}";
}
}

View File

@@ -0,0 +1,4 @@
/**
* JPA domain objects.
*/
package mark.quinn.domain;

View File

@@ -0,0 +1,31 @@
package mark.quinn.domain.util;
import java.io.IOException;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
/**
* Custom Jackson deserializer for transforming a JSON object to a Joda DateTime object.
*/
public class CustomDateTimeDeserializer extends JsonDeserializer<DateTime> {
@Override
public DateTime deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
JsonToken t = jp.getCurrentToken();
if (t == JsonToken.VALUE_STRING) {
String str = jp.getText().trim();
return ISODateTimeFormat.dateTimeParser().parseDateTime(str);
}
if (t == JsonToken.VALUE_NUMBER_INT) {
return new DateTime(jp.getLongValue());
}
throw ctxt.mappingException(handledType());
}
}

View File

@@ -0,0 +1,29 @@
package mark.quinn.domain.util;
import java.io.IOException;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
/**
* Custom Jackson serializer for transforming a Joda DateTime object to JSON.
*/
public class CustomDateTimeSerializer extends JsonSerializer<DateTime> {
private static DateTimeFormatter formatter = DateTimeFormat
.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
@Override
public void serialize(DateTime value, JsonGenerator generator,
SerializerProvider serializerProvider)
throws IOException {
generator.writeString(formatter.print(value.toDateTime(DateTimeZone.UTC)));
}
}

View File

@@ -0,0 +1,24 @@
package mark.quinn.domain.util;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.io.IOException;
/**
* Custom Jackson serializer for transforming a Joda LocalDate object to JSON.
*/
public class CustomLocalDateSerializer extends JsonSerializer<LocalDate> {
private static DateTimeFormatter formatter = DateTimeFormat
.forPattern("yyyy-MM-dd");
@Override
public void serialize(LocalDate value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(formatter.print(value));
}
}

View File

@@ -0,0 +1,12 @@
package mark.quinn.domain.util;
import java.sql.Types;
import org.hibernate.dialect.H2Dialect;
public class FixedH2Dialect extends H2Dialect {
public FixedH2Dialect() {
super();
registerColumnType( Types.FLOAT, "real" );
}
}

View File

@@ -0,0 +1,31 @@
package mark.quinn.domain.util;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.joda.time.LocalDate;
import org.joda.time.format.ISODateTimeFormat;
import java.io.IOException;
/**
* Custom Jackson deserializer for transforming a JSON object (using the ISO 8601 date format)
* to a Joda LocalDate object.
*/
public class ISO8601LocalDateDeserializer extends JsonDeserializer<LocalDate> {
@Override
public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
JsonToken t = jp.getCurrentToken();
if (t == JsonToken.VALUE_STRING) {
String str = jp.getText().trim();
return ISODateTimeFormat.dateTimeParser().parseDateTime(str).toLocalDate();
}
if (t == JsonToken.VALUE_NUMBER_INT) {
return new LocalDate(jp.getLongValue());
}
throw ctxt.mappingException(handledType());
}
}

View File

@@ -0,0 +1,10 @@
package mark.quinn.repository;
import mark.quinn.domain.Authority;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Spring Data JPA repository for the Authority entity.
*/
public interface AuthorityRepository extends JpaRepository<Authority, String> {
}

View File

@@ -0,0 +1,60 @@
package mark.quinn.repository;
import mark.quinn.config.audit.AuditEventConverter;
import mark.quinn.domain.PersistentAuditEvent;
import org.joda.time.LocalDateTime;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;
import java.util.Date;
import java.util.List;
/**
* Wraps an implementation of Spring Boot's AuditEventRepository.
*/
@Repository
public class CustomAuditEventRepository {
@Inject
private PersistenceAuditEventRepository persistenceAuditEventRepository;
@Bean
public AuditEventRepository auditEventRepository() {
return new AuditEventRepository() {
@Inject
private AuditEventConverter auditEventConverter;
@Override
public List<AuditEvent> find(String principal, Date after) {
Iterable<PersistentAuditEvent> persistentAuditEvents;
if (principal == null && after == null) {
persistentAuditEvents = persistenceAuditEventRepository.findAll();
} else if (after == null) {
persistentAuditEvents = persistenceAuditEventRepository.findByPrincipal(principal);
} else {
persistentAuditEvents =
persistenceAuditEventRepository.findByPrincipalAndAuditEventDateAfter(principal, new LocalDateTime(after));
}
return auditEventConverter.convertToAuditEvent(persistentAuditEvents);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void add(AuditEvent event) {
PersistentAuditEvent persistentAuditEvent = new PersistentAuditEvent();
persistentAuditEvent.setPrincipal(event.getPrincipal());
persistentAuditEvent.setAuditEventType(event.getType());
persistentAuditEvent.setAuditEventDate(new LocalDateTime(event.getTimestamp()));
persistentAuditEvent.setData(auditEventConverter.convertDataToStrings(event.getData()));
persistenceAuditEventRepository.save(persistentAuditEvent);
}
};
}
}

View File

@@ -0,0 +1,19 @@
package mark.quinn.repository;
import mark.quinn.domain.PersistentAuditEvent;
import org.joda.time.LocalDateTime;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* Spring Data JPA repository for the PersistentAuditEvent entity.
*/
public interface PersistenceAuditEventRepository extends JpaRepository<PersistentAuditEvent, String> {
List<PersistentAuditEvent> findByPrincipal(String principal);
List<PersistentAuditEvent> findByPrincipalAndAuditEventDateAfter(String principal, LocalDateTime after);
List<PersistentAuditEvent> findAllByAuditEventDateBetween(LocalDateTime fromDate, LocalDateTime toDate);
}

View File

@@ -0,0 +1,19 @@
package mark.quinn.repository;
import mark.quinn.domain.PersistentToken;
import mark.quinn.domain.User;
import org.joda.time.LocalDate;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* Spring Data JPA repository for the PersistentToken entity.
*/
public interface PersistentTokenRepository extends JpaRepository<PersistentToken, String> {
List<PersistentToken> findByUser(User user);
List<PersistentToken> findByTokenDateBefore(LocalDate localDate);
}

View File

@@ -0,0 +1,30 @@
package mark.quinn.repository;
import mark.quinn.domain.User;
import org.joda.time.DateTime;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
/**
* Spring Data JPA repository for the User entity.
*/
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findOneByActivationKey(String activationKey);
List<User> findAllByActivatedIsFalseAndCreatedDateBefore(DateTime dateTime);
Optional<User> findOneByResetKey(String resetKey);
Optional<User> findOneByEmail(String email);
Optional<User> findOneByLogin(String login);
@Override
void delete(User t);
}

View File

@@ -0,0 +1,4 @@
/**
* Spring Data JPA repositories.
*/
package mark.quinn.repository;

View File

@@ -0,0 +1,24 @@
package mark.quinn.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Returns a 401 error code (Unauthorized) to the client, when Ajax authentication fails.
*/
@Component
public class AjaxAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed");
}
}

View File

@@ -0,0 +1,25 @@
package mark.quinn.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Spring Security success handler, specialized for Ajax requests.
*/
@Component
public class AjaxAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
}

View File

@@ -0,0 +1,26 @@
package mark.quinn.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AbstractAuthenticationTargetUrlRequestHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Spring Security logout handler, specialized for Ajax requests.
*/
@Component
public class AjaxLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler
implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
}

View File

@@ -0,0 +1,16 @@
package mark.quinn.security;
/**
* Constants for Spring Security authorities.
*/
public final class AuthoritiesConstants {
private AuthoritiesConstants() {
}
public static final String ADMIN = "ROLE_ADMIN";
public static final String USER = "ROLE_USER";
public static final String ANONYMOUS = "ROLE_ANONYMOUS";
}

View File

@@ -0,0 +1,201 @@
package mark.quinn.security;
import mark.quinn.domain.PersistentToken;
import mark.quinn.domain.User;
import mark.quinn.repository.PersistentTokenRepository;
import mark.quinn.repository.UserRepository;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.security.web.authentication.rememberme.CookieTheftException;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* Custom implementation of Spring Security's RememberMeServices.
* <p/>
* Persistent tokens are used by Spring Security to automatically log in users.
* <p/>
* This is a specific implementation of Spring Security's remember-me authentication, but it is much
* more powerful than the standard implementations:
* <ul>
* <li>It allows a user to see the list of his currently opened sessions, and invalidate them</li>
* <li>It stores more information, such as the IP address and the user agent, for audit purposes<li>
* <li>When a user logs out, only his current session is invalidated, and not all of his sessions</li>
* </ul>
* <p/>
* This is inspired by:
* <ul>
* <li><a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved Persistent Login Cookie
* Best Practice</a></li>
* <li><a href="https://github.com/blog/1661-modeling-your-app-s-user-session">Github's "Modeling your App's User Session"</a></li></li>
* </ul>
* <p/>
* The main algorithm comes from Spring Security's PersistentTokenBasedRememberMeServices, but this class
* couldn't be cleanly extended.
* <p/>
*/
@Service
public class CustomPersistentRememberMeServices extends
AbstractRememberMeServices {
private final Logger log = LoggerFactory.getLogger(CustomPersistentRememberMeServices.class);
// Token is valid for one month
private static final int TOKEN_VALIDITY_DAYS = 31;
private static final int TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * TOKEN_VALIDITY_DAYS;
private static final int DEFAULT_SERIES_LENGTH = 16;
private static final int DEFAULT_TOKEN_LENGTH = 16;
private SecureRandom random;
@Inject
private PersistentTokenRepository persistentTokenRepository;
@Inject
private UserRepository userRepository;
@Inject
public CustomPersistentRememberMeServices(Environment env, org.springframework.security.core.userdetails.UserDetailsService userDetailsService) {
super(env.getProperty("jhipster.security.rememberme.key"), userDetailsService);
random = new SecureRandom();
}
@Override
@Transactional
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
PersistentToken token = getPersistentToken(cookieTokens);
String login = token.getUser().getLogin();
// Token also matches, so login is valid. Update the token value, keeping the *same* series number.
log.debug("Refreshing persistent login token for user '{}', series '{}'", login, token.getSeries());
token.setTokenDate(new LocalDate());
token.setTokenValue(generateTokenData());
token.setIpAddress(request.getRemoteAddr());
token.setUserAgent(request.getHeader("User-Agent"));
try {
persistentTokenRepository.saveAndFlush(token);
addCookie(token, request, response);
} catch (DataAccessException e) {
log.error("Failed to update token: ", e);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem", e);
}
return getUserDetailsService().loadUserByUsername(login);
}
@Override
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String login = successfulAuthentication.getName();
log.debug("Creating new persistent login for user {}", login);
PersistentToken token = userRepository.findOneByLogin(login).map(u -> {
PersistentToken t = new PersistentToken();
t.setSeries(generateSeriesData());
t.setUser(u);
t.setTokenValue(generateTokenData());
t.setTokenDate(new LocalDate());
t.setIpAddress(request.getRemoteAddr());
t.setUserAgent(request.getHeader("User-Agent"));
return t;
}).orElseThrow(() -> new UsernameNotFoundException("User " + login + " was not found in the database"));
try {
persistentTokenRepository.saveAndFlush(token);
addCookie(token, request, response);
} catch (DataAccessException e) {
log.error("Failed to save persistent token ", e);
}
}
/**
* When logout occurs, only invalidate the current token, and not all user sessions.
* <p/>
* The standard Spring Security implementations are too basic: they invalidate all tokens for the
* current user, so when he logs out from one browser, all his other sessions are destroyed.
*/
@Override
@Transactional
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie != null && rememberMeCookie.length() != 0) {
try {
String[] cookieTokens = decodeCookie(rememberMeCookie);
PersistentToken token = getPersistentToken(cookieTokens);
persistentTokenRepository.delete(token);
} catch (InvalidCookieException ice) {
log.info("Invalid cookie, no persistent token could be deleted");
} catch (RememberMeAuthenticationException rmae) {
log.debug("No persistent token found, so no token could be deleted");
}
}
super.logout(request, response, authentication);
}
/**
* Validate the token and return it.
*/
private PersistentToken getPersistentToken(String[] cookieTokens) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2 +
" tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
PersistentToken token = persistentTokenRepository.findOne(presentedSeries);
if (token == null) {
// No series match, so we can't authenticate using this cookie
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
}
// We have a match for this user/series combination
log.info("presentedToken={} / tokenValue={}", presentedToken, token.getTokenValue());
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete this session and throw an exception.
persistentTokenRepository.delete(token);
throw new CookieTheftException("Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.");
}
if (token.getTokenDate().plusDays(TOKEN_VALIDITY_DAYS).isBefore(LocalDate.now())) {
persistentTokenRepository.delete(token);
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
return token;
}
private String generateSeriesData() {
byte[] newSeries = new byte[DEFAULT_SERIES_LENGTH];
random.nextBytes(newSeries);
return new String(Base64.encode(newSeries));
}
private String generateTokenData() {
byte[] newToken = new byte[DEFAULT_TOKEN_LENGTH];
random.nextBytes(newToken);
return new String(Base64.encode(newToken));
}
private void addCookie(PersistentToken token, HttpServletRequest request, HttpServletResponse response) {
setCookie(
new String[]{token.getSeries(), token.getTokenValue()},
TOKEN_VALIDITY_SECONDS, request, response);
}
}

View File

@@ -0,0 +1,32 @@
package mark.quinn.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Returns a 401 error code (Unauthorized) to the client.
*/
@Component
public class Http401UnauthorizedEntryPoint implements AuthenticationEntryPoint {
private final Logger log = LoggerFactory.getLogger(Http401UnauthorizedEntryPoint.class);
/**
* Always returns a 401 error code to the client.
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException,
ServletException {
log.debug("Pre-authenticated entry point called. Rejecting access");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied");
}
}

View File

@@ -0,0 +1,72 @@
package mark.quinn.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* Utility class for Spring Security.
*/
public final class SecurityUtils {
private SecurityUtils() {
}
/**
* Get the login of the current user.
*/
public static String getCurrentLogin() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
UserDetails springSecurityUser = null;
String userName = null;
if(authentication != null) {
if (authentication.getPrincipal() instanceof UserDetails) {
springSecurityUser = (UserDetails) authentication.getPrincipal();
userName = springSecurityUser.getUsername();
} else if (authentication.getPrincipal() instanceof String) {
userName = (String) authentication.getPrincipal();
}
}
return userName;
}
/**
* Check if a user is authenticated.
*
* @return true if the user is authenticated, false otherwise
*/
public static boolean isAuthenticated() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Collection<? extends GrantedAuthority> authorities = securityContext.getAuthentication().getAuthorities();
if (authorities != null) {
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(AuthoritiesConstants.ANONYMOUS)) {
return false;
}
}
}
return true;
}
/**
* If the current user has a specific security role.
*/
public static boolean isUserInRole(String role) {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
if(authentication != null) {
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
return springSecurityUser.getAuthorities().contains(new SimpleGrantedAuthority(role));
}
}
return false;
}
}

View File

@@ -0,0 +1,18 @@
package mark.quinn.security;
import mark.quinn.config.Constants;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
/**
* Implementation of AuditorAware based on Spring Security.
*/
@Component
public class SpringSecurityAuditorAware implements AuditorAware<String> {
@Override
public String getCurrentAuditor() {
String userName = SecurityUtils.getCurrentLogin();
return (userName != null ? userName : Constants.SYSTEM_ACCOUNT);
}
}

View File

@@ -0,0 +1,52 @@
package mark.quinn.security;
import mark.quinn.domain.Authority;
import mark.quinn.domain.User;
import mark.quinn.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.Collections;
import java.util.List;
/**
* Authenticate a user from the database.
*/
@Component("userDetailsService")
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {
private final Logger log = LoggerFactory.getLogger(UserDetailsService.class);
@Inject
private UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(final String login) {
log.debug("Authenticating {}", login);
String lowercaseLogin = login.toLowerCase();
Optional<User> userFromDatabase = userRepository.findOneByLogin(lowercaseLogin);
return userFromDatabase.map(user -> {
if (!user.getActivated()) {
throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated");
}
List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(lowercaseLogin,
user.getPassword(),
grantedAuthorities);
}).orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the database"));
}
}

View File

@@ -0,0 +1,17 @@
package mark.quinn.security;
import org.springframework.security.core.AuthenticationException;
/**
* This exception is throw in case of a not activated user trying to authenticate.
*/
public class UserNotActivatedException extends AuthenticationException {
public UserNotActivatedException(String message) {
super(message);
}
public UserNotActivatedException(String message, Throwable t) {
super(message, t);
}
}

View File

@@ -0,0 +1,4 @@
/**
* Spring Security configuration.
*/
package mark.quinn.security;

View File

@@ -0,0 +1,41 @@
package mark.quinn.service;
import mark.quinn.config.audit.AuditEventConverter;
import mark.quinn.domain.PersistentAuditEvent;
import mark.quinn.repository.PersistenceAuditEventRepository;
import org.joda.time.LocalDateTime;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;
import java.util.List;
/**
* Service for managing audit events.
* <p/>
* <p>
* This is the default implementation to support SpringBoot Actuator AuditEventRepository
* </p>
*/
@Service
@Transactional
public class AuditEventService {
@Inject
private PersistenceAuditEventRepository persistenceAuditEventRepository;
@Inject
private AuditEventConverter auditEventConverter;
public List<AuditEvent> findAll() {
return auditEventConverter.convertToAuditEvent(persistenceAuditEventRepository.findAll());
}
public List<AuditEvent> findByDates(LocalDateTime fromDate, LocalDateTime toDate) {
List<PersistentAuditEvent> persistentAuditEvents =
persistenceAuditEventRepository.findAllByAuditEventDateBetween(fromDate, toDate);
return auditEventConverter.convertToAuditEvent(persistentAuditEvents);
}
}

View File

@@ -0,0 +1,98 @@
package mark.quinn.service;
import mark.quinn.domain.User;
import org.apache.commons.lang.CharEncoding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.core.env.Environment;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring4.SpringTemplateEngine;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.mail.internet.MimeMessage;
import java.util.Locale;
/**
* Service for sending e-mails.
* <p/>
* <p>
* We use the @Async annotation to send e-mails asynchronously.
* </p>
*/
@Service
public class MailService {
private final Logger log = LoggerFactory.getLogger(MailService.class);
@Inject
private Environment env;
@Inject
private JavaMailSenderImpl javaMailSender;
@Inject
private MessageSource messageSource;
@Inject
private SpringTemplateEngine templateEngine;
/**
* System default email address that sends the e-mails.
*/
private String from;
@PostConstruct
public void init() {
this.from = env.getProperty("mail.from");
}
@Async
public void sendEmail(String to, String subject, String content, boolean isMultipart, boolean isHtml) {
log.debug("Send e-mail[multipart '{}' and html '{}'] to '{}' with subject '{}' and content={}",
isMultipart, isHtml, to, subject, content);
// Prepare message using a Spring helper
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
try {
MimeMessageHelper message = new MimeMessageHelper(mimeMessage, isMultipart, CharEncoding.UTF_8);
message.setTo(to);
message.setFrom(from);
message.setSubject(subject);
message.setText(content, isHtml);
javaMailSender.send(mimeMessage);
log.debug("Sent e-mail to User '{}'", to);
} catch (Exception e) {
log.warn("E-mail could not be sent to user '{}', exception is: {}", to, e.getMessage());
}
}
@Async
public void sendActivationEmail(User user, String baseUrl) {
log.debug("Sending activation e-mail to '{}'", user.getEmail());
Locale locale = Locale.forLanguageTag(user.getLangKey());
Context context = new Context(locale);
context.setVariable("user", user);
context.setVariable("baseUrl", baseUrl);
String content = templateEngine.process("activationEmail", context);
String subject = messageSource.getMessage("email.activation.title", null, locale);
sendEmail(user.getEmail(), subject, content, false, true);
}
@Async
public void sendPasswordResetMail(User user, String baseUrl) {
log.debug("Sending password reset e-mail to '{}'", user.getEmail());
Locale locale = Locale.forLanguageTag(user.getLangKey());
Context context = new Context(locale);
context.setVariable("user", user);
context.setVariable("baseUrl", baseUrl);
String content = templateEngine.process("passwordResetEmail", context);
String subject = messageSource.getMessage("email.reset.title", null, locale);
sendEmail(user.getEmail(), subject, content, false, true);
}
}

View File

@@ -0,0 +1,176 @@
package mark.quinn.service;
import mark.quinn.domain.Authority;
import mark.quinn.domain.PersistentToken;
import mark.quinn.domain.User;
import mark.quinn.repository.AuthorityRepository;
import mark.quinn.repository.PersistentTokenRepository;
import mark.quinn.repository.UserRepository;
import mark.quinn.security.SecurityUtils;
import mark.quinn.service.util.RandomUtil;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Service class for managing users.
*/
@Service
@Transactional
public class UserService {
private final Logger log = LoggerFactory.getLogger(UserService.class);
@Inject
private PasswordEncoder passwordEncoder;
@Inject
private UserRepository userRepository;
@Inject
private PersistentTokenRepository persistentTokenRepository;
@Inject
private AuthorityRepository authorityRepository;
public Optional<User> activateRegistration(String key) {
log.debug("Activating user for activation key {}", key);
userRepository.findOneByActivationKey(key)
.map(user -> {
// activate given user for the registration key.
user.setActivated(true);
user.setActivationKey(null);
userRepository.save(user);
log.debug("Activated user: {}", user);
return user;
});
return Optional.empty();
}
public Optional<User> completePasswordReset(String newPassword, String key) {
log.debug("Reset user password for reset key {}", key);
return userRepository.findOneByResetKey(key)
.filter(user -> {
DateTime oneDayAgo = DateTime.now().minusHours(24);
return user.getResetDate().isAfter(oneDayAgo.toInstant().getMillis());
})
.map(user -> {
user.setPassword(passwordEncoder.encode(newPassword));
user.setResetKey(null);
user.setResetDate(null);
userRepository.save(user);
return user;
});
}
public Optional<User> requestPasswordReset(String mail) {
return userRepository.findOneByEmail(mail)
.filter(user -> user.getActivated() == true)
.map(user -> {
user.setResetKey(RandomUtil.generateResetKey());
user.setResetDate(DateTime.now());
userRepository.save(user);
return user;
});
}
public User createUserInformation(String login, String password, String firstName, String lastName, String email,
String langKey) {
User newUser = new User();
Authority authority = authorityRepository.findOne("ROLE_USER");
Set<Authority> authorities = new HashSet<>();
String encryptedPassword = passwordEncoder.encode(password);
newUser.setLogin(login);
// new user gets initially a generated password
newUser.setPassword(encryptedPassword);
newUser.setFirstName(firstName);
newUser.setLastName(lastName);
newUser.setEmail(email);
newUser.setLangKey(langKey);
// new user is not active
newUser.setActivated(false);
// new user gets registration key
newUser.setActivationKey(RandomUtil.generateActivationKey());
authorities.add(authority);
newUser.setAuthorities(authorities);
userRepository.save(newUser);
log.debug("Created Information for User: {}", newUser);
return newUser;
}
public void updateUserInformation(String firstName, String lastName, String email, String langKey) {
userRepository.findOneByLogin(SecurityUtils.getCurrentLogin()).ifPresent(u -> {
u.setFirstName(firstName);
u.setLastName(lastName);
u.setEmail(email);
u.setLangKey(langKey);
userRepository.save(u);
log.debug("Changed Information for User: {}", u);
});
}
public void changePassword(String password) {
userRepository.findOneByLogin(SecurityUtils.getCurrentLogin()).ifPresent(u-> {
String encryptedPassword = passwordEncoder.encode(password);
u.setPassword(encryptedPassword);
userRepository.save(u);
log.debug("Changed password for User: {}", u);
});
}
@Transactional(readOnly = true)
public User getUserWithAuthorities() {
User currentUser = userRepository.findOneByLogin(SecurityUtils.getCurrentLogin()).get();
currentUser.getAuthorities().size(); // eagerly load the association
return currentUser;
}
/**
* Persistent Token are used for providing automatic authentication, they should be automatically deleted after
* 30 days.
* <p/>
* <p>
* This is scheduled to get fired everyday, at midnight.
* </p>
*/
@Scheduled(cron = "0 0 0 * * ?")
public void removeOldPersistentTokens() {
LocalDate now = new LocalDate();
persistentTokenRepository.findByTokenDateBefore(now.minusMonths(1)).stream().forEach(token ->{
log.debug("Deleting token {}", token.getSeries());
User user = token.getUser();
user.getPersistentTokens().remove(token);
persistentTokenRepository.delete(token);
});
}
/**
* Not activated users should be automatically deleted after 3 days.
* <p/>
* <p>
* This is scheduled to get fired everyday, at 01:00 (am).
* </p>
*/
@Scheduled(cron = "0 0 1 * * ?")
public void removeNotActivatedUsers() {
DateTime now = new DateTime();
List<User> users = userRepository.findAllByActivatedIsFalseAndCreatedDateBefore(now.minusDays(3));
for (User user : users) {
log.debug("Deleting not activated user {}", user.getLogin());
userRepository.delete(user);
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Service layer beans.
*/
package mark.quinn.service;

View File

@@ -0,0 +1,41 @@
package mark.quinn.service.util;
import org.apache.commons.lang.RandomStringUtils;
/**
* Utility class for generating random Strings.
*/
public final class RandomUtil {
private static final int DEF_COUNT = 20;
private RandomUtil() {
}
/**
* Generates a password.
*
* @return the generated password
*/
public static String generatePassword() {
return RandomStringUtils.randomAlphanumeric(DEF_COUNT);
}
/**
* Generates an activation key.
*
* @return the generated activation key
*/
public static String generateActivationKey() {
return RandomStringUtils.randomNumeric(DEF_COUNT);
}
/**
* Generates a reset key.
*
* @return the generated reset key
*/
public static String generateResetKey() {
return RandomStringUtils.randomNumeric(DEF_COUNT);
}
}

View File

@@ -0,0 +1,52 @@
package mark.quinn.web.filter;
import org.springframework.core.env.Environment;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* This filter is used in production, to put HTTP cache headers with a long (1 month) expiration time.
* </p>
*/
public class CachingHttpHeadersFilter implements Filter {
// We consider the last modified date is the start up time of the server
private final static long LAST_MODIFIED = System.currentTimeMillis();
private long CACHE_TIME_TO_LIVE = TimeUnit.DAYS.toMillis(31L);
private Environment env;
public CachingHttpHeadersFilter(Environment env) {
this.env = env;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
CACHE_TIME_TO_LIVE = TimeUnit.DAYS.toMillis(env.getProperty("http.cache.timeToLiveInDays", Long.class, 31L));
}
@Override
public void destroy() {
// Nothing to destroy
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Cache-Control", "max-age=" + CACHE_TIME_TO_LIVE + ", public");
httpResponse.setHeader("Pragma", "cache");
// Setting Expires header, for proxy caching
httpResponse.setDateHeader("Expires", CACHE_TIME_TO_LIVE + System.currentTimeMillis());
// Setting the Last-Modified header, for browser caching
httpResponse.setDateHeader("Last-Modified", LAST_MODIFIED);
chain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,36 @@
package mark.quinn.web.filter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Filter used to put the CSRF token generated by Spring Security in a cookie for use by AngularJS.
*/
public class CsrfCookieGeneratorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Spring put the CSRF token in session attribute "_csrf"
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
// Send the cookie only if the token has changed
String actualToken = request.getHeader("X-CSRF-TOKEN");
if (actualToken == null || !actualToken.equals(csrfToken.getToken())) {
// Session cookie that will be used by AngularJS
String pCookieName = "CSRF-TOKEN";
Cookie cookie = new Cookie(pCookieName, csrfToken.getToken());
cookie.setMaxAge(-1);
cookie.setHttpOnly(false);
cookie.setPath("/");
response.addCookie(cookie);
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,42 @@
package mark.quinn.web.filter;
import org.apache.commons.lang.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* This filter is used in production, to serve static resources generated by "grunt build".
* <p/>
* <p>
* It is configured to serve resources from the "dist" directory, which is the Grunt
* destination directory.
* </p>
*/
public class StaticResourcesProductionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Nothing to initialize
}
@Override
public void destroy() {
// Nothing to destroy
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String contextPath = ((HttpServletRequest) request).getContextPath();
String requestURI = httpRequest.getRequestURI();
requestURI = StringUtils.substringAfter(requestURI, contextPath);
if (StringUtils.equals("/", requestURI)) {
requestURI = "/index.html";
}
String newURI = "/dist" + requestURI;
request.getRequestDispatcher(newURI).forward(request, response);
}
}

View File

@@ -0,0 +1,107 @@
package mark.quinn.web.filter.gzip;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public final class GZipResponseUtil {
private static final Logger LOG = LoggerFactory.getLogger(GZipResponseUtil.class);
/**
* Gzipping an empty file or stream always results in a 20 byte output
* This is in java or elsewhere.
* <p/>
* On a unix system to reproduce do <code>gzip -n empty_file</code>. -n tells gzip to not
* include the file name. The resulting file size is 20 bytes.
* <p/>
* Therefore 20 bytes can be used indicate that the gzip byte[] will be empty when ungzipped.
*/
private static final int EMPTY_GZIPPED_CONTENT_SIZE = 20;
/**
* Utility class. No public constructor.
*/
private GZipResponseUtil() {
// noop
}
/**
* Checks whether a gzipped body is actually empty and should just be zero.
* When the compressedBytes is {@link #EMPTY_GZIPPED_CONTENT_SIZE} it should be zero.
*
* @param compressedBytes the gzipped response body
* @param request the client HTTP request
* @return true if the response should be 0, even if it is isn't.
*/
public static boolean shouldGzippedBodyBeZero(byte[] compressedBytes, HttpServletRequest request) {
//Check for 0 length body
if (compressedBytes.length == EMPTY_GZIPPED_CONTENT_SIZE) {
if (LOG.isTraceEnabled()) {
LOG.trace("{} resulted in an empty response.", request.getRequestURL());
}
return true;
} else {
return false;
}
}
/**
* Performs a number of checks to ensure response saneness according to the rules of RFC2616:
* <ol>
* <li>If the response code is {@link javax.servlet.http.HttpServletResponse#SC_NO_CONTENT} then it is illegal for the body
* to contain anything. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
* <li>If the response code is {@link javax.servlet.http.HttpServletResponse#SC_NOT_MODIFIED} then it is illegal for the body
* to contain anything. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
* </ol>
*
* @param request the client HTTP request
* @param responseStatus the responseStatus
* @return true if the response should be 0, even if it is isn't.
*/
public static boolean shouldBodyBeZero(HttpServletRequest request, int responseStatus) {
//Check for NO_CONTENT
if (responseStatus == HttpServletResponse.SC_NO_CONTENT) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} resulted in a {} response. Removing message body in accordance with RFC2616.",
request.getRequestURL(), HttpServletResponse.SC_NO_CONTENT);
}
return true;
}
//Check for NOT_MODIFIED
if (responseStatus == HttpServletResponse.SC_NOT_MODIFIED) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} resulted in a {} response. Removing message body in accordance with RFC2616.",
request.getRequestURL(), HttpServletResponse.SC_NOT_MODIFIED);
}
return true;
}
return false;
}
/**
* Adds the gzip HTTP header to the response.
* <p/>
* <p>
* This is need when a gzipped body is returned so that browsers can properly decompress it.
* </p>
*
* @param response the response which will have a header added to it. I.e this method changes its parameter
* @throws GzipResponseHeadersNotModifiableException Either the response is committed or we were called using the include method
* from a {@link javax.servlet.RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
* method and the set header is ignored.
*/
public static void addGzipHeader(HttpServletResponse response) throws GzipResponseHeadersNotModifiableException {
response.setHeader("Content-Encoding", "gzip");
boolean containsEncoding = response.containsHeader("Content-Encoding");
if (!containsEncoding) {
throw new GzipResponseHeadersNotModifiableException("Failure when attempting to set "
+ "Content-Encoding: gzip");
}
}
}

View File

@@ -0,0 +1,111 @@
package mark.quinn.web.filter.gzip;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
public class GZipServletFilter implements Filter {
private Logger log = LoggerFactory.getLogger(GZipServletFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Nothing to initialize
}
@Override
public void destroy() {
// Nothing to destroy
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (!isIncluded(httpRequest) && acceptsGZipEncoding(httpRequest) && !response.isCommitted()) {
// Client accepts zipped content
if (log.isTraceEnabled()) {
log.trace("{} Written with gzip compression", httpRequest.getRequestURL());
}
// Create a gzip stream
final ByteArrayOutputStream compressed = new ByteArrayOutputStream();
final GZIPOutputStream gzout = new GZIPOutputStream(compressed);
// Handle the request
final GZipServletResponseWrapper wrapper = new GZipServletResponseWrapper(httpResponse, gzout);
wrapper.setDisableFlushBuffer(true);
chain.doFilter(request, wrapper);
wrapper.flush();
gzout.close();
// double check one more time before writing out
// repsonse might have been committed due to error
if (response.isCommitted()) {
return;
}
// return on these special cases when content is empty or unchanged
switch (wrapper.getStatus()) {
case HttpServletResponse.SC_NO_CONTENT:
case HttpServletResponse.SC_RESET_CONTENT:
case HttpServletResponse.SC_NOT_MODIFIED:
return;
default:
}
// Saneness checks
byte[] compressedBytes = compressed.toByteArray();
boolean shouldGzippedBodyBeZero = GZipResponseUtil.shouldGzippedBodyBeZero(compressedBytes, httpRequest);
boolean shouldBodyBeZero = GZipResponseUtil.shouldBodyBeZero(httpRequest, wrapper.getStatus());
if (shouldGzippedBodyBeZero || shouldBodyBeZero) {
// No reason to add GZIP headers or write body if no content was written or status code specifies no
// content
response.setContentLength(0);
return;
}
// Write the zipped body
GZipResponseUtil.addGzipHeader(httpResponse);
response.setContentLength(compressedBytes.length);
response.getOutputStream().write(compressedBytes);
} else {
// Client does not accept zipped content - don't bother zipping
if (log.isTraceEnabled()) {
log.trace("{} Written without gzip compression because the request does not accept gzip", httpRequest.getRequestURL());
}
chain.doFilter(request, response);
}
}
/**
* Checks if the request uri is an include. These cannot be gzipped.
*/
private boolean isIncluded(final HttpServletRequest request) {
String uri = (String) request.getAttribute("javax.servlet.include.request_uri");
boolean includeRequest = !(uri == null);
if (includeRequest && log.isDebugEnabled()) {
log.debug("{} resulted in an include request. This is unusable, because"
+ "the response will be assembled into the overrall response. Not gzipping.",
request.getRequestURL());
}
return includeRequest;
}
private boolean acceptsGZipEncoding(HttpServletRequest httpRequest) {
String acceptEncoding = httpRequest.getHeader("Accept-Encoding");
return acceptEncoding != null && acceptEncoding.contains("gzip");
}
}

View File

@@ -0,0 +1,51 @@
package mark.quinn.web.filter.gzip;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import java.io.IOException;
import java.io.OutputStream;
class GZipServletOutputStream extends ServletOutputStream {
private OutputStream stream;
public GZipServletOutputStream(OutputStream output)
throws IOException {
super();
this.stream = output;
}
@Override
public void close() throws IOException {
this.stream.close();
}
@Override
public void flush() throws IOException {
this.stream.flush();
}
@Override
public void write(byte b[]) throws IOException {
this.stream.write(b);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
this.stream.write(b, off, len);
}
@Override
public void write(int b) throws IOException {
this.stream.write(b);
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener listener) {
}
}

View File

@@ -0,0 +1,114 @@
package mark.quinn.web.filter.gzip;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;
class GZipServletResponseWrapper extends HttpServletResponseWrapper {
private GZipServletOutputStream gzipOutputStream = null;
private PrintWriter printWriter = null;
private boolean disableFlushBuffer = false;
public GZipServletResponseWrapper(HttpServletResponse response, GZIPOutputStream gzout)
throws IOException {
super(response);
gzipOutputStream = new GZipServletOutputStream(gzout);
}
public void close() throws IOException {
//PrintWriter.close does not throw exceptions. Thus, the call does not need
//be inside a try-catch block.
if (this.printWriter != null) {
this.printWriter.close();
}
if (this.gzipOutputStream != null) {
this.gzipOutputStream.close();
}
}
/**
* Flush OutputStream or PrintWriter
*
* @throws IOException
*/
@Override
public void flushBuffer() throws IOException {
//PrintWriter.flush() does not throw exception
if (this.printWriter != null) {
this.printWriter.flush();
}
if (this.gzipOutputStream != null) {
this.gzipOutputStream.flush();
}
// doing this might leads to response already committed exception
// when the PageInfo has not yet built but the buffer already flushed
// Happens in Weblogic when a servlet forward to a JSP page and the forward
// method trigger a flush before it forwarded to the JSP
// disableFlushBuffer for that purpose is 'true' by default
if (!disableFlushBuffer) {
super.flushBuffer();
}
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (this.printWriter != null) {
throw new IllegalStateException(
"PrintWriter obtained already - cannot get OutputStream");
}
return this.gzipOutputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if (this.printWriter == null) {
this.gzipOutputStream = new GZipServletOutputStream(
getResponse().getOutputStream());
this.printWriter = new PrintWriter(new OutputStreamWriter(
this.gzipOutputStream, getResponse().getCharacterEncoding()), true);
}
return this.printWriter;
}
@Override
public void setContentLength(int length) {
//ignore, since content length of zipped content
//does not match content length of unzipped content.
}
/**
* Flushes all the streams for this response.
*/
public void flush() throws IOException {
if (printWriter != null) {
printWriter.flush();
}
if (gzipOutputStream != null) {
gzipOutputStream.flush();
}
}
/**
* Set if the wrapped reponse's buffer flushing should be disabled.
*
* @param disableFlushBuffer true if the wrapped reponse's buffer flushing should be disabled
*/
public void setDisableFlushBuffer(boolean disableFlushBuffer) {
this.disableFlushBuffer = disableFlushBuffer;
}
}

View File

@@ -0,0 +1,10 @@
package mark.quinn.web.filter.gzip;
import javax.servlet.ServletException;
public class GzipResponseHeadersNotModifiableException extends ServletException {
public GzipResponseHeadersNotModifiableException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,4 @@
/**
* GZipping servlet filter.
*/
package mark.quinn.web.filter.gzip;

View File

@@ -0,0 +1,4 @@
/**
* Servlet filters.
*/
package mark.quinn.web.filter;

View File

@@ -0,0 +1,63 @@
package mark.quinn.web.propertyeditors;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.util.StringUtils;
import java.beans.PropertyEditorSupport;
import java.util.Date;
/**
* Custom PropertyEditorSupport to convert from String to
* Date using JodaTime (http://www.joda.org/joda-time/).
*/
public class LocaleDateTimeEditor extends PropertyEditorSupport {
private final DateTimeFormatter formatter;
private final boolean allowEmpty;
/**
* Create a new LocaleDateTimeEditor instance, using the given format for
* parsing and rendering.
* <p/>
* The "allowEmpty" parameter states if an empty String should be allowed
* for parsing, i.e. get interpreted as null value. Otherwise, an
* IllegalArgumentException gets thrown.
*
* @param dateFormat DateFormat to use for parsing and rendering
* @param allowEmpty if empty strings should be allowed
*/
public LocaleDateTimeEditor(String dateFormat, boolean allowEmpty) {
this.formatter = DateTimeFormat.forPattern(dateFormat);
this.allowEmpty = allowEmpty;
}
/**
* Format the YearMonthDay as String, using the specified format.
*
* @return DateTime formatted string
*/
@Override
public String getAsText() {
Date value = (Date) getValue();
return value != null ? new LocalDateTime(value).toString(formatter) : "";
}
/**
* Parse the value from the given text, using the specified format.
*
* @param text the text to format
* @throws IllegalArgumentException
*/
@Override
public void setAsText( String text ) throws IllegalArgumentException {
if ( allowEmpty && !StringUtils.hasText(text) ) {
// Treat empty String as null value.
setValue(null);
} else {
setValue(new LocalDateTime(formatter.parseDateTime(text)));
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Property Editors.
*/
package mark.quinn.web.propertyeditors;

View File

@@ -0,0 +1,236 @@
package mark.quinn.web.rest;
import com.codahale.metrics.annotation.Timed;
import mark.quinn.domain.Authority;
import mark.quinn.domain.PersistentToken;
import mark.quinn.domain.User;
import mark.quinn.repository.PersistentTokenRepository;
import mark.quinn.repository.UserRepository;
import mark.quinn.security.SecurityUtils;
import mark.quinn.service.MailService;
import mark.quinn.service.UserService;
import mark.quinn.web.rest.dto.UserDTO;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
import java.util.stream.Collectors;
/**
* REST controller for managing the current user's account.
*/
@RestController
@RequestMapping("/api")
public class AccountResource {
private final Logger log = LoggerFactory.getLogger(AccountResource.class);
@Inject
private UserRepository userRepository;
@Inject
private UserService userService;
@Inject
private PersistentTokenRepository persistentTokenRepository;
@Inject
private MailService mailService;
/**
* POST /register -> register the user.
*/
@RequestMapping(value = "/register",
method = RequestMethod.POST,
produces = MediaType.TEXT_PLAIN_VALUE)
@Timed
public ResponseEntity<?> registerAccount(@Valid @RequestBody UserDTO userDTO, HttpServletRequest request) {
return userRepository.findOneByLogin(userDTO.getLogin())
.map(user -> new ResponseEntity<>("login already in use", HttpStatus.BAD_REQUEST))
.orElseGet(() -> userRepository.findOneByEmail(userDTO.getEmail())
.map(user -> new ResponseEntity<>("e-mail address already in use", HttpStatus.BAD_REQUEST))
.orElseGet(() -> {
User user = userService.createUserInformation(userDTO.getLogin(), userDTO.getPassword(),
userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail().toLowerCase(),
userDTO.getLangKey());
String baseUrl = request.getScheme() + // "http"
"://" + // "://"
request.getServerName() + // "myhost"
":" + // ":"
request.getServerPort(); // "80"
mailService.sendActivationEmail(user, baseUrl);
return new ResponseEntity<>(HttpStatus.CREATED);
})
);
}
/**
* GET /activate -> activate the registered user.
*/
@RequestMapping(value = "/activate",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public ResponseEntity<String> activateAccount(@RequestParam(value = "key") String key) {
return Optional.ofNullable(userService.activateRegistration(key))
.map(user -> new ResponseEntity<String>(HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
/**
* GET /authenticate -> check if the user is authenticated, and return its login.
*/
@RequestMapping(value = "/authenticate",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public String isAuthenticated(HttpServletRequest request) {
log.debug("REST request to check if the current user is authenticated");
return request.getRemoteUser();
}
/**
* GET /account -> get the current user.
*/
@RequestMapping(value = "/account",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public ResponseEntity<UserDTO> getAccount() {
return Optional.ofNullable(userService.getUserWithAuthorities())
.map(user -> {
return new ResponseEntity<>(
new UserDTO(
user.getLogin(),
null,
user.getFirstName(),
user.getLastName(),
user.getEmail(),
user.getLangKey(),
user.getAuthorities().stream().map(Authority::getName)
.collect(Collectors.toList())),
HttpStatus.OK);
})
.orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
/**
* POST /account -> update the current user information.
*/
@RequestMapping(value = "/account",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public ResponseEntity<String> saveAccount(@RequestBody UserDTO userDTO) {
return userRepository
.findOneByLogin(userDTO.getLogin())
.filter(u -> u.getLogin().equals(SecurityUtils.getCurrentLogin()))
.map(u -> {
userService.updateUserInformation(userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail(),
userDTO.getLangKey());
return new ResponseEntity<String>(HttpStatus.OK);
})
.orElseGet(() -> new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
/**
* POST /change_password -> changes the current user's password
*/
@RequestMapping(value = "/account/change_password",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public ResponseEntity<?> changePassword(@RequestBody String password) {
if (!checkPasswordLength(password)) {
return new ResponseEntity<>("Incorrect password", HttpStatus.BAD_REQUEST);
}
userService.changePassword(password);
return new ResponseEntity<>(HttpStatus.OK);
}
/**
* GET /account/sessions -> get the current open sessions.
*/
@RequestMapping(value = "/account/sessions",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public ResponseEntity<List<PersistentToken>> getCurrentSessions() {
return userRepository.findOneByLogin(SecurityUtils.getCurrentLogin())
.map(user -> new ResponseEntity<>(
persistentTokenRepository.findByUser(user),
HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
/**
* DELETE /account/sessions?series={series} -> invalidate an existing session.
*
* - You can only delete your own sessions, not any other user's session
* - If you delete one of your existing sessions, and that you are currently logged in on that session, you will
* still be able to use that session, until you quit your browser: it does not work in real time (there is
* no API for that), it only removes the "remember me" cookie
* - This is also true if you invalidate your current session: you will still be able to use it until you close
* your browser or that the session times out. But automatic login (the "remember me" cookie) will not work
* anymore.
* There is an API to invalidate the current session, but there is no API to check which session uses which
* cookie.
*/
@RequestMapping(value = "/account/sessions/{series}",
method = RequestMethod.DELETE)
@Timed
public void invalidateSession(@PathVariable String series) throws UnsupportedEncodingException {
String decodedSeries = URLDecoder.decode(series, "UTF-8");
userRepository.findOneByLogin(SecurityUtils.getCurrentLogin()).ifPresent(u -> {
persistentTokenRepository.findByUser(u).stream()
.filter(persistentToken -> StringUtils.equals(persistentToken.getSeries(), decodedSeries))
.findAny().ifPresent(t -> persistentTokenRepository.delete(decodedSeries));
});
}
@RequestMapping(value = "/account/reset_password/init",
method = RequestMethod.POST,
produces = MediaType.TEXT_PLAIN_VALUE)
@Timed
public ResponseEntity<?> requestPasswordReset(@RequestBody String mail, HttpServletRequest request) {
return userService.requestPasswordReset(mail)
.map(user -> {
String baseUrl = request.getScheme() +
"://" +
request.getServerName() +
":" +
request.getServerPort();
mailService.sendPasswordResetMail(user, baseUrl);
return new ResponseEntity<>("e-mail was sent", HttpStatus.OK);
}).orElse(new ResponseEntity<>("e-mail address not registered", HttpStatus.BAD_REQUEST));
}
@RequestMapping(value = "/account/reset_password/finish",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public ResponseEntity<String> finishPasswordReset(@RequestParam(value = "key") String key, @RequestParam(value = "newPassword") String newPassword) {
if (!checkPasswordLength(newPassword)) {
return new ResponseEntity<>("Incorrect password", HttpStatus.BAD_REQUEST);
}
return userService.completePasswordReset(newPassword, key)
.map(user -> new ResponseEntity<String>(HttpStatus.OK)).orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
private boolean checkPasswordLength(String password) {
return (!StringUtils.isEmpty(password) && password.length() >= UserDTO.PASSWORD_MIN_LENGTH && password.length() <= UserDTO.PASSWORD_MAX_LENGTH);
}
}

View File

@@ -0,0 +1,44 @@
package mark.quinn.web.rest;
import mark.quinn.security.AuthoritiesConstants;
import mark.quinn.service.AuditEventService;
import mark.quinn.web.propertyeditors.LocaleDateTimeEditor;
import org.joda.time.LocalDateTime;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.http.MediaType;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import javax.inject.Inject;
import java.util.List;
/**
* REST controller for getting the audit events.
*/
@RestController
@RequestMapping("/api")
public class AuditResource {
@Inject
private AuditEventService auditEventService;
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(LocalDateTime.class, new LocaleDateTimeEditor("yyyy-MM-dd", false));
}
@RequestMapping(value = "/audits/all",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public List<AuditEvent> findAll() {
return auditEventService.findAll();
}
@RequestMapping(value = "/audits/byDates",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public List<AuditEvent> findByDates(@RequestParam(value = "fromDate") LocalDateTime fromDate,
@RequestParam(value = "toDate") LocalDateTime toDate) {
return auditEventService.findByDates(fromDate, toDate);
}
}

View File

@@ -0,0 +1,43 @@
package mark.quinn.web.rest;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import com.codahale.metrics.annotation.Timed;
import mark.quinn.web.rest.dto.LoggerDTO;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* Controller for view and managing Log Level at runtime.
*/
@RestController
@RequestMapping("/api")
public class LogsResource {
@RequestMapping(value = "/logs",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public List<LoggerDTO> getList() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
return context.getLoggerList()
.stream()
.map(LoggerDTO::new)
.collect(Collectors.toList());
}
@RequestMapping(value = "/logs",
method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.NO_CONTENT)
@Timed
public void changeLevel(@RequestBody LoggerDTO jsonLogger) {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.getLogger(jsonLogger.getName()).setLevel(Level.valueOf(jsonLogger.getLevel()));
}
}

View File

@@ -0,0 +1,57 @@
package mark.quinn.web.rest;
import com.codahale.metrics.annotation.Timed;
import mark.quinn.domain.User;
import mark.quinn.repository.UserRepository;
import mark.quinn.security.AuthoritiesConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.inject.Inject;
import java.util.List;
/**
* REST controller for managing users.
*/
@RestController
@RequestMapping("/api")
public class UserResource {
private final Logger log = LoggerFactory.getLogger(UserResource.class);
@Inject
private UserRepository userRepository;
/**
* GET /users -> get all users.
*/
@RequestMapping(value = "/users",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public List<User> getAll() {
log.debug("REST request to get all Users");
return userRepository.findAll();
}
/**
* GET /users/:login -> get the "login" user.
*/
@RequestMapping(value = "/users/{login}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
ResponseEntity<User> getUser(@PathVariable String login) {
log.debug("REST request to get User : {}", login);
return userRepository.findOneByLogin(login)
.map(user -> new ResponseEntity<>(user, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
}

View File

@@ -0,0 +1,44 @@
package mark.quinn.web.rest.dto;
import ch.qos.logback.classic.Logger;
import com.fasterxml.jackson.annotation.JsonCreator;
public class LoggerDTO {
private String name;
private String level;
public LoggerDTO(Logger logger) {
this.name = logger.getName();
this.level = logger.getEffectiveLevel().toString();
}
@JsonCreator
public LoggerDTO() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
@Override
public String toString() {
return "LoggerDTO{" +
"name='" + name + '\'' +
", level='" + level + '\'' +
'}';
}
}

View File

@@ -0,0 +1,93 @@
package mark.quinn.web.rest.dto;
import org.hibernate.validator.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.util.List;
public class UserDTO {
public static final int PASSWORD_MIN_LENGTH = 5;
public static final int PASSWORD_MAX_LENGTH = 100;
@Pattern(regexp = "^[a-z0-9]*$")
@NotNull
@Size(min = 1, max = 50)
private String login;
@NotNull
@Size(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH)
private String password;
@Size(max = 50)
private String firstName;
@Size(max = 50)
private String lastName;
@Email
@Size(min = 5, max = 100)
private String email;
@Size(min = 2, max = 5)
private String langKey;
private List<String> roles;
public UserDTO() {
}
public UserDTO(String login, String password, String firstName, String lastName, String email, String langKey,
List<String> roles) {
this.login = login;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.langKey = langKey;
this.roles = roles;
}
public String getPassword() {
return password;
}
public String getLogin() {
return login;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getEmail() {
return email;
}
public String getLangKey() {
return langKey;
}
public List<String> getRoles() {
return roles;
}
@Override
public String toString() {
return "UserDTO{" +
"login='" + login + '\'' +
", password='" + password + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", langKey='" + langKey + '\'' +
", roles=" + roles +
'}';
}
}

View File

@@ -0,0 +1,4 @@
/**
* Data Transfer Objects used by Spring MVC REST controllers.
*/
package mark.quinn.web.rest.dto;

View File

@@ -0,0 +1,34 @@
package mark.quinn.web.rest.errors;
/**
* Custom, parameterized exception, which can be translated on the client side.
* For example:
*
* <pre>
* throw new CustomParameterizedException(&quot;myCustomError&quot;, &quot;hello&quot;, &quot;world&quot;);
* </pre>
*
* Can be translated with:
*
* <pre>
* "error.myCustomError" : "The server says {{params[0]}} to {{params[1]}}"
* </pre>
*/
public class CustomParameterizedException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final String message;
private final String[] params;
public CustomParameterizedException(String message, String... params) {
super(message);
this.message = message;
this.params = params;
}
public ParameterizedErrorDTO getErrorDTO() {
return new ParameterizedErrorDTO(message, params);
}
}

View File

@@ -0,0 +1,13 @@
package mark.quinn.web.rest.errors;
public final class ErrorConstants {
public static final String ERR_CONCURRENCY_FAILURE = "error.concurrencyFailure";
public static final String ERR_ACCESS_DENIED = "error.accessDenied";
public static final String ERR_VALIDATION = "error.validation";
public static final String ERR_METHOD_NOT_SUPPORTED = "error.methodNotSupported";
private ErrorConstants() {
}
}

View File

@@ -0,0 +1,51 @@
package mark.quinn.web.rest.errors;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* DTO for transfering error message with a list of field errors.
*/
public class ErrorDTO implements Serializable {
private static final long serialVersionUID = 1L;
private final String message;
private final String description;
private List<FieldErrorDTO> fieldErrors;
ErrorDTO(String message) {
this(message, null);
}
ErrorDTO(String message, String description) {
this.message = message;
this.description = description;
}
ErrorDTO(String message, String description, List<FieldErrorDTO> fieldErrors) {
this.message = message;
this.description = description;
this.fieldErrors = fieldErrors;
}
public void add(String objectName, String field, String message) {
if (fieldErrors == null) {
fieldErrors = new ArrayList<>();
}
fieldErrors.add(new FieldErrorDTO(objectName, field, message));
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
public List<FieldErrorDTO> getFieldErrors() {
return fieldErrors;
}
}

View File

@@ -0,0 +1,70 @@
package mark.quinn.web.rest.errors;
import java.util.List;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* Controller advice to translate the server side exceptions to client-friendly json structures.
*/
@ControllerAdvice
public class ExceptionTranslator {
@ExceptionHandler(ConcurrencyFailureException.class)
@ResponseStatus(HttpStatus.CONFLICT)
@ResponseBody
public ErrorDTO processConcurencyError(ConcurrencyFailureException ex) {
return new ErrorDTO(ErrorConstants.ERR_CONCURRENCY_FAILURE);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorDTO processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
@ExceptionHandler(CustomParameterizedException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ParameterizedErrorDTO processParameterizedValidationError(CustomParameterizedException ex) {
return ex.getErrorDTO();
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@ResponseBody
public ErrorDTO processAccessDeniedExcpetion(AccessDeniedException e) {
return new ErrorDTO(ErrorConstants.ERR_ACCESS_DENIED, e.getMessage());
}
private ErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
ErrorDTO dto = new ErrorDTO(ErrorConstants.ERR_VALIDATION);
for (FieldError fieldError : fieldErrors) {
dto.add(fieldError.getObjectName(), fieldError.getField(), fieldError.getCode());
}
return dto;
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseBody
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorDTO processMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) {
return new ErrorDTO(ErrorConstants.ERR_METHOD_NOT_SUPPORTED, exception.getMessage());
}
}

View File

@@ -0,0 +1,33 @@
package mark.quinn.web.rest.errors;
import java.io.Serializable;
public class FieldErrorDTO implements Serializable {
private static final long serialVersionUID = 1L;
private final String objectName;
private final String field;
private final String message;
FieldErrorDTO(String dto, String field, String message) {
this.objectName = dto;
this.field = field;
this.message = message;
}
public String getObjectName() {
return objectName;
}
public String getField() {
return field;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,27 @@
package mark.quinn.web.rest.errors;
import java.io.Serializable;
/**
* DTO for sending a parameterized error message.
*/
public class ParameterizedErrorDTO implements Serializable {
private static final long serialVersionUID = 1L;
private final String message;
private final String[] params;
public ParameterizedErrorDTO(String message, String... params) {
this.message = message;
this.params = params;
}
public String getMessage() {
return message;
}
public String[] getParams() {
return params;
}
}

View File

@@ -0,0 +1,4 @@
/**
* Spring MVC REST controllers.
*/
package mark.quinn.web.rest;

View File

@@ -0,0 +1,30 @@
package mark.quinn.web.rest.util;
import org.springframework.http.HttpHeaders;
/**
* Utility class for http header creation.
*
*/
public class HeaderUtil {
public static HttpHeaders createAlert(String message, String param) {
HttpHeaders headers = new HttpHeaders();
headers.add("X-openajpportApp-alert", message);
headers.add("X-openajpportApp-params", param);
return headers;
}
public static HttpHeaders createEntityCreationAlert(String entityName, String param) {
return createAlert("openajpportApp." + entityName + ".created", param);
}
public static HttpHeaders createEntityUpdateAlert(String entityName, String param) {
return createAlert("openajpportApp." + entityName + ".updated", param);
}
public static HttpHeaders createEntityDeletionAlert(String entityName, String param) {
return createAlert("openajpportApp." + entityName + ".deleted", param);
}
}

View File

@@ -0,0 +1,66 @@
package mark.quinn.web.rest.util;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import java.net.URI;
import java.net.URISyntaxException;
/**
* Utility class for handling pagination.
*
* <p>
* Pagination uses the same principles as the <a href="https://developer.github.com/v3/#pagination">Github API</api>,
* and follow <a href="http://tools.ietf.org/html/rfc5988">RFC 5988 (Link header)</a>.
* </p>
*/
public class PaginationUtil {
public static final int DEFAULT_OFFSET = 1;
public static final int MIN_OFFSET = 1;
public static final int DEFAULT_LIMIT = 20;
public static final int MAX_LIMIT = 100;
public static Pageable generatePageRequest(Integer offset, Integer limit) {
if (offset == null || offset < MIN_OFFSET) {
offset = DEFAULT_OFFSET;
}
if (limit == null || limit > MAX_LIMIT) {
limit = DEFAULT_LIMIT;
}
return new PageRequest(offset - 1, limit);
}
public static HttpHeaders generatePaginationHttpHeaders(Page<?> page, String baseUrl, Integer offset, Integer limit)
throws URISyntaxException {
if (offset == null || offset < MIN_OFFSET) {
offset = DEFAULT_OFFSET;
}
if (limit == null || limit > MAX_LIMIT) {
limit = DEFAULT_LIMIT;
}
HttpHeaders headers = new HttpHeaders();
headers.add("X-Total-Count", "" + page.getTotalElements());
String link = "";
if (offset < page.getTotalPages()) {
link = "<" + (new URI(baseUrl +"?page=" + (offset + 1) + "&per_page=" + limit)).toString()
+ ">; rel=\"next\",";
}
if (offset > 1) {
link += "<" + (new URI(baseUrl +"?page=" + (offset - 1) + "&per_page=" + limit)).toString()
+ ">; rel=\"prev\",";
}
link += "<" + (new URI(baseUrl +"?page=" + page.getTotalPages() + "&per_page=" + limit)).toString()
+ ">; rel=\"last\"," +
"<" + (new URI(baseUrl +"?page=" + 1 + "&per_page=" + limit)).toString()
+ ">; rel=\"first\"";
headers.add(HttpHeaders.LINK, link);
return headers;
}
}

Some files were not shown because too many files have changed in this diff Show More