diff --git a/README.md b/README.md
index e247eaf..f7a51ab 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ The project contains unit and integration test examples for standard components
The project illustrates the use of Spring Boot Actuator for application monitoring and management. The application demonstrates the recording of custom metrics and the creation of custom health checks. Also, custom Maven and Gradle project attributes are incorporated into the Actuator info endpoint.
#### API Documentation Generator
-The project includes [Springfox](http://springfox.github.io/springfox/) Swagger integration to automatically generate API docs for the RESTful web service endpoints. This feature may be activated using the *"docs"* Spring profile.
+The project includes [Spring REST Docs](https://spring.io/projects/spring-restdocs) integration to automatically generate API docs for the RESTful web service endpoints. This feature may be activated using the *"asciidoctor"* Gradle task.
#### Executable Jar
The Maven and Gradle builds produce a fully executable Spring Boot Jar file. The Jar file may be executed directly from the command line without the *"java -jar"* command and may be installed on servers as a Linux service.
@@ -279,6 +279,26 @@ java -jar build/libs/example-1.0.0.jar --spring.profiles.active=mysql,batch
./example-1.0.0.jar --spring.profiles.active=mysql,batch
```
+#### asciidoctor
+
+The `asciidoctor` Gradle task performs the following workflow steps:
+
+* compiles Java classes to the /build directory
+* copies all resources to the /build directory
+* executes the unit test suites
+* generates [Asciidoctor](https://asciidoctor.org/) snippets in the /build/generated-snippets directory
+* generates HTML API Docs in the /build/asciidoc/html5 directory
+
+The `asciidoctor` Gradle task generates API documentation using [Spring REST Docs](https://spring.io/projects/spring-restdocs).
+
+To execute the `asciidoctor` Gradle task, type the following command at a terminal prompt in the project base directory.
+
+```
+./gradlew clean asciidoctor
+```
+
+The API documentation is placed in the /build/asciidoc/html5 directory and the root document is named `index.html`.
+
#### encodePassword
The `encodePassword` Gradle task executes the `BCryptPasswordEncoderUtil` utility class to encode password values which may be included in the sample database scripts. The clear text password values are passed as a Gradle `-P` property arguments on the command line.
diff --git a/build.gradle b/build.gradle
index 081a996..55fe8fe 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,6 @@
plugins {
id 'org.springframework.boot' version '2.1.1.RELEASE'
+ id 'org.asciidoctor.convert' version '1.5.3'
}
apply plugin: 'io.spring.dependency-management'
@@ -12,14 +13,15 @@ apply plugin: 'project-report'
apply plugin: 'build-dashboard'
ext {
+ snippetsDir = file('build/generated-snippets')
+
jacocoVersion = '0.8.2'
checkstyleVersion = '8.14'
pmdVersion = '6.9.0'
- swaggerVersion = '2.7.0'
}
group = 'com.leanstacks'
-version = '2.2.0'
+version = '2.3.0'
sourceCompatibility = 11
targetCompatibility = 11
@@ -37,14 +39,16 @@ dependencies {
compile group: 'com.github.ben-manes.caffeine', name: 'caffeine'
compile group: 'org.liquibase', name: 'liquibase-core'
- compile group: 'io.springfox', name: 'springfox-swagger2', version: swaggerVersion
- compile group: 'io.springfox', name: 'springfox-swagger-ui', version: swaggerVersion
runtime group: 'org.hsqldb', name: 'hsqldb'
runtime group: 'mysql', name: 'mysql-connector-java'
testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test'
testCompile group: 'org.springframework.security', name: 'spring-security-test'
+ testCompile group: 'org.springframework.restdocs', name: 'spring-restdocs-mockmvc'
+ testCompile group: 'com.google.guava', name: 'guava', version: '27.0.1-jre'
+
+ asciidoctor group: 'org.springframework.restdocs', name: 'spring-restdocs-asciidoctor'
}
defaultTasks 'clean', 'build'
@@ -81,7 +85,15 @@ jacocoTestReport {
}
}
-test.finalizedBy jacocoTestReport
+test {
+ outputs.dir snippetsDir
+ finalizedBy jacocoTestReport
+}
+
+asciidoctor {
+ inputs.dir snippetsDir
+ dependsOn test
+}
checkstyle {
toolVersion = checkstyleVersion
@@ -90,7 +102,8 @@ checkstyle {
pmd {
toolVersion = pmdVersion
- ruleSetConfig = rootProject.resources.text.fromFile('etc/pmd/ruleset.xml')
+ ruleSets = []
+ ruleSetFiles = files('etc/pmd/ruleset.xml')
ignoreFailures = true
}
diff --git a/etc/pmd/ruleset.xml b/etc/pmd/ruleset.xml
index 3af8d85..0b5750e 100644
--- a/etc/pmd/ruleset.xml
+++ b/etc/pmd/ruleset.xml
@@ -1,78 +1,89 @@
-
+ xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.io/ruleset_2_0_0.xsd">
- This is the LeanStacks Official PMD ruleset.
-
+ This is the LeanStacks Official PMD ruleset.
+
-
-
+
-
-
-
-
-
-
-
-
-
+
+
-
+
+
-
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 0eab6b9..abc307e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.leanstacksskeleton-ws-spring-boot
- 2.2.0
+ 2.3.0Spring Boot Starter ProjectStarter application stack for RESTful web services using Spring Boot.
@@ -18,7 +18,6 @@
UTF-811
- 2.7.0
@@ -66,18 +65,6 @@
-
-
- io.springfox
- springfox-swagger2
- ${swagger.version}
-
-
- io.springfox
- springfox-swagger-ui
- ${swagger.version}
-
-
org.springframework.boot
@@ -89,6 +76,18 @@
spring-security-testtest
+
+ com.google.guava
+ guava
+ 27.0.1-jre
+ test
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+
diff --git a/src/docs/asciidoc/_includes/http-response-error.adoc b/src/docs/asciidoc/_includes/http-response-error.adoc
new file mode 100644
index 0000000..bba7511
--- /dev/null
+++ b/src/docs/asciidoc/_includes/http-response-error.adoc
@@ -0,0 +1,16 @@
+[source,http,options="nowrap"]
+----
+HTTP/1.1 404
+Content-Length: 213
+Content-Type: application/json;charset=UTF-8
+
+{
+ "timestamp": "2018-12-12T13:16:11.771539Z",
+ "method": "GET",
+ "path": "/api/...",
+ "status": 404,
+ "statusText": "Not Found",
+ "exceptionClass": "java.util.NoSuchElementException",
+ "exceptionMessage": "No value present"
+}
+----
\ No newline at end of file
diff --git a/src/docs/asciidoc/_includes/response-fields-error.adoc b/src/docs/asciidoc/_includes/response-fields-error.adoc
new file mode 100644
index 0000000..28f6c68
--- /dev/null
+++ b/src/docs/asciidoc/_includes/response-fields-error.adoc
@@ -0,0 +1,32 @@
+|===
+|Path|Type|Description
+
+|`+timestamp+`
+|`+String+`
+|The time the error occurred.
+
+|`+method+`
+|`+String+`
+|The HTTP method.
+
+|`+path+`
+|`+String+`
+|The request context path.
+
+|`+status+`
+|`+Number+`
+|The response HTTP status code.
+
+|`+statusText+`
+|`+String+`
+|The response HTTP status text.
+
+|`+exceptionClass+`
+|`+String+`
+|The exception class.
+
+|`+exceptionMessage+`
+|`+String+`
+|The exception message.
+
+|===
\ No newline at end of file
diff --git a/src/docs/asciidoc/create-greeting.adoc b/src/docs/asciidoc/create-greeting.adoc
new file mode 100644
index 0000000..689070f
--- /dev/null
+++ b/src/docs/asciidoc/create-greeting.adoc
@@ -0,0 +1,40 @@
+= API Endpoint - Create Greeting
+LeanStacks;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:includedir: _includes
+
+== Create a new greeting
+
+=== POST /api/greetings
+
+Create a new greeting.
+
+=== Request Body Parameters
+
+include::{snippets}/create-greeting/request-fields.adoc[]
+
+=== Response Body Parameters
+
+include::{snippets}/create-greeting/response-fields.adoc[]
+
+=== Example Request
+
+Using cURL:
+
+include::{snippets}/create-greeting/curl-request.adoc[]
+
+The HTTP request:
+
+include::{snippets}/create-greeting/http-request.adoc[]
+
+=== Example Response
+
+include::{snippets}/create-greeting/http-response.adoc[]
+
+=== Example Error Response
+
+include::{includedir}/http-response-error.adoc[]
+
+include::{includedir}/response-fields-error.adoc[]
diff --git a/src/docs/asciidoc/delete-greeting.adoc b/src/docs/asciidoc/delete-greeting.adoc
new file mode 100644
index 0000000..4895c16
--- /dev/null
+++ b/src/docs/asciidoc/delete-greeting.adoc
@@ -0,0 +1,32 @@
+= API Endpoint - Delete Greeting
+LeanStacks;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:includedir: _includes
+
+== Delete a greeting
+
+=== DELETE /api/greetings/{id}
+
+Delete all information about a specific greeting.
+
+=== Example Request
+
+Using cURL:
+
+include::{snippets}/delete-greeting/curl-request.adoc[]
+
+The HTTP request:
+
+include::{snippets}/delete-greeting/http-request.adoc[]
+
+=== Example Response
+
+include::{snippets}/delete-greeting/http-response.adoc[]
+
+=== Example Error Response
+
+include::{includedir}/http-response-error.adoc[]
+
+include::{includedir}/response-fields-error.adoc[]
diff --git a/src/docs/asciidoc/get-greeting.adoc b/src/docs/asciidoc/get-greeting.adoc
new file mode 100644
index 0000000..14444bd
--- /dev/null
+++ b/src/docs/asciidoc/get-greeting.adoc
@@ -0,0 +1,40 @@
+= API Endpoint - Get Greeting
+LeanStacks;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:includedir: _includes
+
+== Get a specific greeting
+
+=== GET /api/greetings/{id}
+
+Get the information about a specific greeting.
+
+=== Path Parameters
+
+include::{snippets}/get-greeting/path-parameters.adoc[]
+
+=== Response Body Parameters
+
+include::{snippets}/get-greeting/response-fields.adoc[]
+
+=== Example Request
+
+Using cURL:
+
+include::{snippets}/get-greeting/curl-request.adoc[]
+
+The HTTP request:
+
+include::{snippets}/get-greeting/http-request.adoc[]
+
+=== Example Response
+
+include::{snippets}/get-greeting/http-response.adoc[]
+
+=== Example Error Response
+
+include::{includedir}/http-response-error.adoc[]
+
+include::{includedir}/response-fields-error.adoc[]
diff --git a/src/docs/asciidoc/get-greetings.adoc b/src/docs/asciidoc/get-greetings.adoc
new file mode 100644
index 0000000..1d3e6db
--- /dev/null
+++ b/src/docs/asciidoc/get-greetings.adoc
@@ -0,0 +1,36 @@
+= API Endpoint - Get Greetings
+LeanStacks;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:includedir: _includes
+
+== Get a list of greetings
+
+=== GET /api/greetings
+
+Get a list of all greetings.
+
+=== Response Body Parameters
+
+include::{snippets}/get-greeting/response-fields.adoc[]
+
+=== Example Request
+
+Using cURL:
+
+include::{snippets}/get-greetings/curl-request.adoc[]
+
+The HTTP request:
+
+include::{snippets}/get-greetings/http-request.adoc[]
+
+=== Example Response
+
+include::{snippets}/get-greetings/http-response.adoc[]
+
+=== Example Error Response
+
+include::{includedir}/http-response-error.adoc[]
+
+include::{includedir}/response-fields-error.adoc[]
diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc
new file mode 100644
index 0000000..db1f63d
--- /dev/null
+++ b/src/docs/asciidoc/index.adoc
@@ -0,0 +1,27 @@
+= Overview
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+
+Review the available resources from the LeanStacks API. Click any endpoint for additional information.
+
+=== Greetings
+
+[cols="2,4,4",options="header"]
+|===
+
+| HTTP method | Endpoint | Function
+
+| GET | <> | Get a list of greetings
+
+| GET | <> | Get a specific greeting
+
+| POST | <> | Create a greeting
+
+| PUT | <> | Update a greeting
+
+| DELETE | <> | Delete a greeting
+
+| POST | <> | Send a greeting
+
+|===
diff --git a/src/docs/asciidoc/send-greeting.adoc b/src/docs/asciidoc/send-greeting.adoc
new file mode 100644
index 0000000..9a89133
--- /dev/null
+++ b/src/docs/asciidoc/send-greeting.adoc
@@ -0,0 +1,44 @@
+= API Endpoint - Send Greeting
+LeanStacks;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:includedir: _includes
+
+== Send a greeting
+
+=== POST /api/greetings/{id}/send
+
+Email a specific greeting synchronously or asynchronously.
+
+=== Path Parameters
+
+include::{snippets}/send-greeting/path-parameters.adoc[]
+
+=== Request Parameters
+
+include::{snippets}/send-greeting/request-parameters.adoc[]
+
+=== Response Body Parameters
+
+include::{snippets}/send-greeting/response-fields.adoc[]
+
+=== Example Request
+
+Using cURL:
+
+include::{snippets}/send-greeting/curl-request.adoc[]
+
+The HTTP request:
+
+include::{snippets}/send-greeting/http-request.adoc[]
+
+=== Example Response
+
+include::{snippets}/send-greeting/http-response.adoc[]
+
+=== Example Error Response
+
+include::{includedir}/http-response-error.adoc[]
+
+include::{includedir}/response-fields-error.adoc[]
diff --git a/src/docs/asciidoc/update-greeting.adoc b/src/docs/asciidoc/update-greeting.adoc
new file mode 100644
index 0000000..87f875c
--- /dev/null
+++ b/src/docs/asciidoc/update-greeting.adoc
@@ -0,0 +1,44 @@
+= API Endpoint - Update Greeting
+LeanStacks;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:includedir: _includes
+
+== Update a greeting
+
+=== PUT /api/greetings/{id}
+
+Update the information about a specific greeting.
+
+=== Path Parameters
+
+include::{snippets}/update-greeting/path-parameters.adoc[]
+
+=== Request Body Parameters
+
+include::{snippets}/update-greeting/request-fields.adoc[]
+
+=== Response Body Parameters
+
+include::{snippets}/update-greeting/response-fields.adoc[]
+
+=== Example Request
+
+Using cURL:
+
+include::{snippets}/update-greeting/curl-request.adoc[]
+
+The HTTP request:
+
+include::{snippets}/update-greeting/http-request.adoc[]
+
+=== Example Response
+
+include::{snippets}/update-greeting/http-response.adoc[]
+
+=== Example Error Response
+
+include::{includedir}/http-response-error.adoc[]
+
+include::{includedir}/response-fields-error.adoc[]
diff --git a/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java b/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java
deleted file mode 100644
index 096241f..0000000
--- a/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.leanstacks.ws;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Profile;
-
-import com.google.common.base.Predicate;
-
-import springfox.documentation.builders.ApiInfoBuilder;
-import springfox.documentation.builders.PathSelectors;
-import springfox.documentation.service.ApiInfo;
-import springfox.documentation.service.Contact;
-import springfox.documentation.spi.DocumentationType;
-import springfox.documentation.spring.web.plugins.Docket;
-import springfox.documentation.swagger2.annotations.EnableSwagger2;
-
-/**
- * The ApiDocsConfiguration class provides configuration beans for the Swagger API documentation generator.
- *
- * @author Matt Warman
- */
-@Profile("docs")
-@Configuration
-@EnableSwagger2
-public class ApiDocsConfiguration {
-
- /**
- * The project version.
- */
- public static final String PROJECT_VERSION = "2.2.0";
- /**
- * The project contact name.
- */
- public static final String PROJECT_CONTACT_NAME = "LeanStacks.com";
- /**
- * The project contact URL.
- */
- public static final String PROJECT_CONTACT_URL = "http://www.leanstacks.com/";
-
- /**
- * Create a Contact class to be used by Springfox's Swagger API Documentation framework.
- *
- * @return A Contact instance.
- */
- private Contact contact() {
- return new Contact(PROJECT_CONTACT_NAME, PROJECT_CONTACT_URL, null);
- }
-
- /**
- * Create an ApiInfo class to be used by Springfox's Swagger API Documentation framework.
- *
- * @return An ApiInfo instance.
- */
- private ApiInfo apiInfo() {
-
- // @formatter:off
- return new ApiInfoBuilder()
- .title("Project Skeleton for Spring Boot Web Services")
- .description("The Spring Boot web services starter project provides a foundation "
- + "to rapidly construct a RESTful web services application.")
- .contact(contact())
- .version(PROJECT_VERSION)
- .build();
- // @formatter:on
-
- }
-
- /**
- * Create a Docket class to be used by Springfox's Swagger API Documentation framework. See
- * http://springfox.github.io/springfox/ for more information.
- *
- * @return A Docket instance.
- */
- @Bean
- public Docket docket() {
- final Predicate paths = PathSelectors.ant("/api/**");
-
- // @formatter:off
- return new Docket(DocumentationType.SWAGGER_2)
- .apiInfo(apiInfo())
- .select()
- .paths(paths)
- .build();
- // @formatter:on
-
- }
-
-}
diff --git a/src/main/java/com/leanstacks/ws/batch/GreetingBatchBean.java b/src/main/java/com/leanstacks/ws/batch/GreetingBatchBean.java
index a7de569..9c07c1c 100644
--- a/src/main/java/com/leanstacks/ws/batch/GreetingBatchBean.java
+++ b/src/main/java/com/leanstacks/ws/batch/GreetingBatchBean.java
@@ -30,19 +30,36 @@ public class GreetingBatchBean {
*/
private static final Logger logger = LoggerFactory.getLogger(GreetingBatchBean.class);
+ /**
+ * Format for printed messages.
+ */
private static final String MESSAGE_FORMAT = "There are {} greetings in the data store.";
- // Metric Counters
+ /**
+ * Metric Counter for cron method invocations.
+ */
private final transient Counter cronMethodCounter;
+ /**
+ * Metric Counter for fixed rate method invocations.
+ */
private final transient Counter fixedRateMethodCounter;
+ /**
+ * Metric Counter for fixed rate initial delay method invocations.
+ */
private final transient Counter fixedRateInitialDelayMethodCounter;
+ /**
+ * Metric Counter for fixed delay method invocations.
+ */
private final transient Counter fixedDelayMethodCounter;
+ /**
+ * Metric Counter for fixed delay initial delay method invocations.
+ */
private final transient Counter fixedDelayInitialDelayMethodCounter;
/**
* The GreetingService business service.
*/
- private transient GreetingService greetingService;
+ private final transient GreetingService greetingService;
/**
* Construct a GreetingBatchBean with supplied dependencies.
diff --git a/src/main/java/com/leanstacks/ws/model/Account.java b/src/main/java/com/leanstacks/ws/model/Account.java
index f988612..7aeb2bd 100644
--- a/src/main/java/com/leanstacks/ws/model/Account.java
+++ b/src/main/java/com/leanstacks/ws/model/Account.java
@@ -21,24 +21,45 @@ public class Account extends TransactionalEntity {
private static final long serialVersionUID = 1L;
+ /**
+ * Login username.
+ */
@NotNull
private String username;
+ /**
+ * Login password.
+ */
@NotNull
private String password;
+ /**
+ * Account enabled status indicator.
+ */
@NotNull
private boolean enabled = true;
+ /**
+ * Credential status indicator.
+ */
@NotNull
private boolean credentialsexpired;
+ /**
+ * Account expired status indicator.
+ */
@NotNull
private boolean expired;
+ /**
+ * Account locked indicator.
+ */
@NotNull
private boolean locked;
+ /**
+ * Authorization information.
+ */
@ManyToMany(fetch = FetchType.EAGER,
cascade = CascadeType.ALL)
@JoinTable(name = "AccountRole",
@@ -48,6 +69,9 @@ public class Account extends TransactionalEntity {
referencedColumnName = "id"))
private Set roles;
+ /**
+ * Create a new Account object.
+ */
public Account() {
super();
}
diff --git a/src/main/java/com/leanstacks/ws/model/Greeting.java b/src/main/java/com/leanstacks/ws/model/Greeting.java
index 4670409..663c965 100644
--- a/src/main/java/com/leanstacks/ws/model/Greeting.java
+++ b/src/main/java/com/leanstacks/ws/model/Greeting.java
@@ -3,8 +3,6 @@
import javax.persistence.Entity;
import javax.validation.constraints.NotNull;
-import io.swagger.annotations.ApiModelProperty;
-
/**
* The Greeting class is an entity model object.
*
@@ -15,17 +13,24 @@ public class Greeting extends TransactionalEntity {
private static final long serialVersionUID = 1L;
- @ApiModelProperty(value = "The actual text of the Greeting.",
- required = true,
- position = 100,
- example = "Hello World!")
+ /**
+ * The text value.
+ */
@NotNull
private String text;
+ /**
+ * Create a new Greeting object.
+ */
public Greeting() {
super();
}
+ /**
+ * Create a new Greeting object with the supplied text value.
+ *
+ * @param text A String text value.
+ */
public Greeting(final String text) {
super();
this.text = text;
diff --git a/src/main/java/com/leanstacks/ws/model/TransactionalEntity.java b/src/main/java/com/leanstacks/ws/model/TransactionalEntity.java
index cef678f..6230c78 100644
--- a/src/main/java/com/leanstacks/ws/model/TransactionalEntity.java
+++ b/src/main/java/com/leanstacks/ws/model/TransactionalEntity.java
@@ -15,8 +15,6 @@
import com.leanstacks.ws.util.RequestContext;
-import io.swagger.annotations.ApiModelProperty;
-
/**
* The parent class for all transactional persistent entities.
*
@@ -33,11 +31,6 @@ public class TransactionalEntity implements Serializable {
/**
* The primary key identifier.
*/
- @ApiModelProperty(value = "Primary key identifier",
- required = false,
- position = 1,
- readOnly = true,
- example = "1")
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@@ -45,65 +38,35 @@ public class TransactionalEntity implements Serializable {
/**
* A secondary unique identifier which may be used as a reference to this entity by external systems.
*/
- @ApiModelProperty(value = "External reference identifier",
- required = false,
- position = 2,
- readOnly = true,
- example = "a1b2c3d4")
@NotNull
private String referenceId = UUID.randomUUID().toString();
/**
* The entity instance version used for optimistic locking.
*/
- @ApiModelProperty(value = "Version of this model object since creation",
- required = false,
- position = Integer.MAX_VALUE - 500,
- readOnly = true,
- example = "1")
@Version
private Integer version;
/**
* A reference to the entity or process which created this entity instance.
*/
- @ApiModelProperty(value = "Identifies the object creator",
- required = false,
- position = Integer.MAX_VALUE - 400,
- readOnly = true,
- example = "user")
@NotNull
private String createdBy;
/**
* The timestamp when this entity instance was created.
*/
- @ApiModelProperty(value = "The object creation timestamp",
- required = false,
- position = Integer.MAX_VALUE - 300,
- readOnly = true,
- example = "1499418339522")
@NotNull
private Instant createdAt;
/**
* A reference to the entity or process which most recently updated this entity instance.
*/
- @ApiModelProperty(value = "Identifies the object updater",
- required = false,
- position = Integer.MAX_VALUE - 200,
- readOnly = true,
- example = "usertoo")
private String updatedBy;
/**
* The timestamp when this entity instance was most recently updated.
*/
- @ApiModelProperty(value = "The object update timestamp",
- required = false,
- position = Integer.MAX_VALUE - 100,
- readOnly = true,
- example = "1499418343681")
private Instant updatedAt;
public Long getId() {
diff --git a/src/main/java/com/leanstacks/ws/repository/AccountRepository.java b/src/main/java/com/leanstacks/ws/repository/AccountRepository.java
index f0df7fd..6c29937 100644
--- a/src/main/java/com/leanstacks/ws/repository/AccountRepository.java
+++ b/src/main/java/com/leanstacks/ws/repository/AccountRepository.java
@@ -1,15 +1,16 @@
package com.leanstacks.ws.repository;
+import java.util.Optional;
+
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.leanstacks.ws.model.Account;
/**
- * The AccountRepository interface is a Spring Data JPA data repository for
- * Account entities. The AccountRepository provides all the data access
- * behaviors exposed by JpaRepository and additional custom
- * behaviors may be defined in this interface.
+ * The AccountRepository interface is a Spring Data JPA data repository for Account entities. The AccountRepository
+ * provides all the data access behaviors exposed by JpaRepository and additional custom behaviors may be
+ * defined in this interface.
*
* @author Matt Warman
*/
@@ -17,11 +18,11 @@
public interface AccountRepository extends JpaRepository {
/**
- * Query for a single Account entities by username.
+ * Query for a single Account entity by username.
*
* @param username The username value to query the repository.
- * @return An Account or null if none found.
+ * @return An Optional Account.
*/
- Account findByUsername(String username);
+ Optional findByUsername(String username);
}
diff --git a/src/main/java/com/leanstacks/ws/security/AccountUserDetailsService.java b/src/main/java/com/leanstacks/ws/security/AccountUserDetailsService.java
index 9e95090..c74fbc3 100644
--- a/src/main/java/com/leanstacks/ws/security/AccountUserDetailsService.java
+++ b/src/main/java/com/leanstacks/ws/security/AccountUserDetailsService.java
@@ -1,8 +1,8 @@
package com.leanstacks.ws.security;
-import java.util.ArrayList;
-import java.util.Collection;
+import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,11 +43,9 @@ public class AccountUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
logger.info("> loadUserByUsername {}", username);
- final Account account = accountService.findByUsername(username);
- if (account == null) {
- // Not found...
- throw new UsernameNotFoundException("Invalid credentials.");
- }
+ final Optional accountOptional = accountService.findByUsername(username);
+ final Account account = accountOptional
+ .orElseThrow(() -> new UsernameNotFoundException("Invalid credentials."));
final Set roles = account.getRoles();
if (roles == null || roles.isEmpty()) {
@@ -55,13 +53,11 @@ public UserDetails loadUserByUsername(final String username) throws UsernameNotF
throw new UsernameNotFoundException("Invalid credentials.");
}
- final Collection grantedAuthorities = new ArrayList();
- for (final Role role : roles) {
- grantedAuthorities.add(new SimpleGrantedAuthority(role.getCode()));
- }
+ final Set authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role.getCode()))
+ .collect(Collectors.toSet());
final User userDetails = new User(account.getUsername(), account.getPassword(), account.isEnabled(),
- !account.isExpired(), !account.isCredentialsexpired(), !account.isLocked(), grantedAuthorities);
+ !account.isExpired(), !account.isCredentialsexpired(), !account.isLocked(), authorities);
logger.info("< loadUserByUsername {}", username);
return userDetails;
diff --git a/src/main/java/com/leanstacks/ws/security/CorsProperties.java b/src/main/java/com/leanstacks/ws/security/CorsProperties.java
new file mode 100644
index 0000000..9881951
--- /dev/null
+++ b/src/main/java/com/leanstacks/ws/security/CorsProperties.java
@@ -0,0 +1,178 @@
+package com.leanstacks.ws.security;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * A container for CORS configuration values.
+ *
+ * @author Matt Warman
+ *
+ */
+@ConfigurationProperties("leanstacks.cors")
+public class CorsProperties {
+
+ /**
+ * The path at which the CorsFilter is registered. The CorsFilter will handle all requests matching this path.
+ */
+ private String filterRegistrationPath = "/**";
+
+ /**
+ * The value of the Access-Control-Allow-Credentials header.
+ */
+ private Boolean allowCredentials = false;
+
+ /**
+ * The value of the Access-Control-Allow-Headers header.
+ */
+ private List allowedHeaders = Arrays.asList("accept", "content-type");
+
+ /**
+ * The value of the Access-Control-Allow-Methods header.
+ */
+ private List allowedMethods = Arrays.asList("GET");
+
+ /**
+ * The value of the Access-Control-Allow-Origin header.
+ */
+ private List allowedOrigins = Arrays.asList("*");
+
+ /**
+ * The value of the Access-Control-Expose-Headers header.
+ */
+ private List exposedHeaders;
+
+ /**
+ * The value of the Access-Control-Max-Age header.
+ */
+ private Long maxAgeSeconds = 1800L;
+
+ /**
+ * Returns the filter registration path.
+ *
+ * @return A String.
+ */
+ public String getFilterRegistrationPath() {
+ return filterRegistrationPath;
+ }
+
+ /**
+ * Sets the filter registration path.
+ *
+ * @param filterRegistrationPath A String.
+ */
+ public void setFilterRegistrationPath(final String filterRegistrationPath) {
+ this.filterRegistrationPath = filterRegistrationPath;
+ }
+
+ /**
+ * Returns the value of the Access-Control-Allow-Credentials header.
+ *
+ * @return A Boolean.
+ */
+ public Boolean getAllowCredentials() {
+ return allowCredentials;
+ }
+
+ /**
+ * Sets the value of the Access-Control-Allow-Credentials header.
+ *
+ * @param allowCredentials A Boolean.
+ */
+ public void setAllowCredentials(final Boolean allowCredentials) {
+ this.allowCredentials = allowCredentials;
+ }
+
+ /**
+ * Returns the value of the Access-Control-Allow-Headers header.
+ *
+ * @return A List of Strings.
+ */
+ public List getAllowedHeaders() {
+ return allowedHeaders;
+ }
+
+ /**
+ * Sets the value of the Access-Control-Allow-Headers header.
+ *
+ * @param allowedHeaders A List of Strings.
+ */
+ public void setAllowedHeaders(final List allowedHeaders) {
+ this.allowedHeaders = allowedHeaders;
+ }
+
+ /**
+ * Returns the value of the Access-Control-Allow-Methods header.
+ *
+ * @return A List of Strings.
+ */
+ public List getAllowedMethods() {
+ return allowedMethods;
+ }
+
+ /**
+ * Sets the value of the Access-Control-Allow-Methods header.
+ *
+ * @param allowedMethods A List of Strings.
+ */
+ public void setAllowedMethods(final List allowedMethods) {
+ this.allowedMethods = allowedMethods;
+ }
+
+ /**
+ * Returns the value of the Access-Control-Allow-Origin header.
+ *
+ * @return A List of Strings.
+ */
+ public List getAllowedOrigins() {
+ return allowedOrigins;
+ }
+
+ /**
+ * Sets the value of the Access-Control-Allow-Origin header.
+ *
+ * @param allowedOrigins A List of Strings.
+ */
+ public void setAllowedOrigins(final List allowedOrigins) {
+ this.allowedOrigins = allowedOrigins;
+ }
+
+ /**
+ * Returns the value of the Access-Control-Expose-Headers header.
+ *
+ * @return A List of Strings.
+ */
+ public List getExposedHeaders() {
+ return exposedHeaders;
+ }
+
+ /**
+ * Sets the value of the Access-Control-Expose-Headers header.
+ *
+ * @param exposedHeaders A List of Strings.
+ */
+ public void setExposedHeaders(final List exposedHeaders) {
+ this.exposedHeaders = exposedHeaders;
+ }
+
+ /**
+ * Returns the value of the Access-Control-Max-Age header in seconds.
+ *
+ * @return A Long.
+ */
+ public Long getMaxAgeSeconds() {
+ return maxAgeSeconds;
+ }
+
+ /**
+ * Sets the value of the Access-Control-Max-Age header in seconds.
+ *
+ * @param maxAgeSeconds A Long.
+ */
+ public void setMaxAgeSeconds(final Long maxAgeSeconds) {
+ this.maxAgeSeconds = maxAgeSeconds;
+ }
+
+}
diff --git a/src/main/java/com/leanstacks/ws/SecurityConfiguration.java b/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java
similarity index 62%
rename from src/main/java/com/leanstacks/ws/SecurityConfiguration.java
rename to src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java
index c336328..b45fe69 100644
--- a/src/main/java/com/leanstacks/ws/SecurityConfiguration.java
+++ b/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java
@@ -1,11 +1,12 @@
-package com.leanstacks.ws;
+package com.leanstacks.ws.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -14,9 +15,9 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
-
-import com.leanstacks.ws.security.AccountAuthenticationProvider;
-import com.leanstacks.ws.security.RestBasicAuthenticationEntryPoint;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* The SecurityConfiguration class provides a centralized location for application security configuration. This class
@@ -26,6 +27,7 @@
*/
@Configuration
@EnableWebSecurity
+@EnableConfigurationProperties(CorsProperties.class)
public class SecurityConfiguration {
/**
@@ -67,21 +69,51 @@ public void configureGlobal(final AuthenticationManagerBuilder auth) throws Exce
@Order(1)
public static class ApiWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
+ /**
+ * The CORS configuration.
+ */
+ @Autowired
+ private transient CorsProperties corsProperties;
+
+ /**
+ * Defines a ConfigurationSource for CORS attributes.
+ *
+ * @return A CorsConfigurationSource.
+ */
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ final CorsConfiguration configuration = new CorsConfiguration();
+ configuration.setAllowedOrigins(corsProperties.getAllowedOrigins());
+ configuration.setAllowedMethods(corsProperties.getAllowedMethods());
+ configuration.setAllowedHeaders(corsProperties.getAllowedHeaders());
+ configuration.setAllowCredentials(corsProperties.getAllowCredentials());
+ configuration.setExposedHeaders(corsProperties.getExposedHeaders());
+ configuration.setMaxAge(corsProperties.getMaxAgeSeconds());
+
+ final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration(corsProperties.getFilterRegistrationPath(), configuration);
+ return source;
+ }
+
@Override
protected void configure(final HttpSecurity http) throws Exception {
// @formatter:off
http
- .csrf().disable()
- .antMatcher("/api/**")
- .authorizeRequests()
- .anyRequest().hasRole("USER")
- .and()
- .httpBasic().authenticationEntryPoint(apiAuthenticationEntryPoint())
- .and()
- .sessionManagement()
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+ .cors()
+ .and()
+ .csrf().disable()
+ .requestMatchers().antMatchers("/api/**")
+ .and()
+ .authorizeRequests()
+ .antMatchers(HttpMethod.OPTIONS).permitAll()
+ .anyRequest().hasRole("USER")
+ .and()
+ .httpBasic().authenticationEntryPoint(apiAuthenticationEntryPoint())
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// @formatter:on
@@ -118,18 +150,18 @@ protected void configure(final HttpSecurity http) throws Exception {
// @formatter:off
http
- .csrf().disable()
- .requestMatcher(EndpointRequest.toAnyEndpoint())
- .authorizeRequests()
- // Permit access to health check
- .requestMatchers(EndpointRequest.to("health")).permitAll()
- // Require authorization for everthing else
- .anyRequest().hasRole("SYSADMIN")
- .and()
- .httpBasic().authenticationEntryPoint(actuatorAuthenticationEntryPoint())
- .and()
- .sessionManagement()
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+ .csrf().disable()
+ .requestMatcher(EndpointRequest.toAnyEndpoint())
+ .authorizeRequests()
+ // Permit access to health check
+ .requestMatchers(EndpointRequest.to("health")).permitAll()
+ // Require authorization for everthing else
+ .anyRequest().hasRole("SYSADMIN")
+ .and()
+ .httpBasic().authenticationEntryPoint(actuatorAuthenticationEntryPoint())
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// @formatter:on
@@ -150,33 +182,4 @@ public AuthenticationEntryPoint actuatorAuthenticationEntryPoint() {
}
- /**
- * This inner class configures the WebSecurityConfigurerAdapter instance for any remaining context paths not handled
- * by other adapters.
- *
- * @author Matt Warman
- */
- @Profile("docs")
- @Configuration
- @Order(3)
- public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
-
- @Override
- protected void configure(final HttpSecurity http) throws Exception {
-
- // @formatter:off
-
- http
- .csrf().disable()
- .authorizeRequests()
- .anyRequest().authenticated()
- .and()
- .formLogin();
-
- // @formatter:on
-
- }
-
- }
-
}
diff --git a/src/main/java/com/leanstacks/ws/service/AccountService.java b/src/main/java/com/leanstacks/ws/service/AccountService.java
index b325afb..2569edb 100644
--- a/src/main/java/com/leanstacks/ws/service/AccountService.java
+++ b/src/main/java/com/leanstacks/ws/service/AccountService.java
@@ -1,5 +1,7 @@
package com.leanstacks.ws.service;
+import java.util.Optional;
+
import com.leanstacks.ws.model.Account;
/**
@@ -19,8 +21,8 @@ public interface AccountService {
* Find an Account by the username attribute value.
*
* @param username A String username to query the repository.
- * @return An Account instance or null if none found.
+ * @return An Optional wrapped Account.
*/
- Account findByUsername(String username);
+ Optional findByUsername(String username);
}
diff --git a/src/main/java/com/leanstacks/ws/service/AccountServiceBean.java b/src/main/java/com/leanstacks/ws/service/AccountServiceBean.java
index 4367b5b..62ce6de 100644
--- a/src/main/java/com/leanstacks/ws/service/AccountServiceBean.java
+++ b/src/main/java/com/leanstacks/ws/service/AccountServiceBean.java
@@ -1,5 +1,7 @@
package com.leanstacks.ws.service;
+import java.util.Optional;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,13 +31,13 @@ public class AccountServiceBean implements AccountService {
private transient AccountRepository accountRepository;
@Override
- public Account findByUsername(final String username) {
+ public Optional findByUsername(final String username) {
logger.info("> findByUsername");
- final Account account = accountRepository.findByUsername(username);
+ final Optional accountOptional = accountRepository.findByUsername(username);
logger.info("< findByUsername");
- return account;
+ return accountOptional;
}
}
diff --git a/src/main/java/com/leanstacks/ws/service/GreetingService.java b/src/main/java/com/leanstacks/ws/service/GreetingService.java
index 21c337c..a0c3f16 100644
--- a/src/main/java/com/leanstacks/ws/service/GreetingService.java
+++ b/src/main/java/com/leanstacks/ws/service/GreetingService.java
@@ -1,6 +1,7 @@
package com.leanstacks.ws.service;
-import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
import com.leanstacks.ws.model.Greeting;
@@ -19,17 +20,17 @@ public interface GreetingService {
/**
* Find all Greeting entities.
*
- * @return A Collection of Greeting objects.
+ * @return A List of Greeting objects.
*/
- Collection findAll();
+ List findAll();
/**
- * Find a single Greeting entity by primary key identifier.
+ * Find a single Greeting entity by primary key identifier. Returns an Optional wrapped Greeting.
*
- * @param id A BigInteger primary key identifier.
- * @return A Greeting or null if none found.
+ * @param id A Long primary key identifier.
+ * @return A Optional Greeting
*/
- Greeting findOne(Long id);
+ Optional findOne(Long id);
/**
* Persists a Greeting entity in the data store.
@@ -50,7 +51,7 @@ public interface GreetingService {
/**
* Removes a previously persisted Greeting entity from the data store.
*
- * @param id A BigInteger primary key identifier.
+ * @param id A Long primary key identifier.
*/
void delete(Long id);
diff --git a/src/main/java/com/leanstacks/ws/service/GreetingServiceBean.java b/src/main/java/com/leanstacks/ws/service/GreetingServiceBean.java
index f240ea6..64032e7 100644
--- a/src/main/java/com/leanstacks/ws/service/GreetingServiceBean.java
+++ b/src/main/java/com/leanstacks/ws/service/GreetingServiceBean.java
@@ -1,11 +1,8 @@
package com.leanstacks.ws.service;
-import java.util.Collection;
+import java.util.List;
import java.util.Optional;
-import javax.persistence.EntityExistsException;
-import javax.persistence.NoResultException;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -35,12 +32,29 @@ public class GreetingServiceBean implements GreetingService {
*/
private static final Logger logger = LoggerFactory.getLogger(GreetingServiceBean.class);
- // Metric Counters
+ /**
+ * Metric Counter for findAll method invocations.
+ */
private final transient Counter findAllMethodInvocationCounter;
+ /**
+ * Metric Counter for findOne method invocations.
+ */
private final transient Counter findOneMethodInvocationCounter;
+ /**
+ * Metric Counter for create method invocations.
+ */
private final transient Counter createMethodInvocationCounter;
+ /**
+ * Metric Counter for update method invocations.
+ */
private final transient Counter updateMethodInvocationCounter;
+ /**
+ * Metric Counter for delete method invocations.
+ */
private final transient Counter deleteMethodInvocationCounter;
+ /**
+ * Metric Counter for evictCache method invocations.
+ */
private final transient Counter evictCacheMethodInvocationCounter;
/**
@@ -66,12 +80,12 @@ public GreetingServiceBean(final GreetingRepository greetingRepository, final Me
}
@Override
- public Collection findAll() {
+ public List findAll() {
logger.info("> findAll");
findAllMethodInvocationCounter.increment();
- final Collection greetings = greetingRepository.findAll();
+ final List greetings = greetingRepository.findAll();
logger.info("< findAll");
return greetings;
@@ -80,19 +94,19 @@ public Collection findAll() {
@Cacheable(value = Application.CACHE_GREETINGS,
key = "#id")
@Override
- public Greeting findOne(final Long id) {
+ public Optional findOne(final Long id) {
logger.info("> findOne {}", id);
findOneMethodInvocationCounter.increment();
- final Optional result = greetingRepository.findById(id);
+ final Optional greetingOptional = greetingRepository.findById(id);
logger.info("< findOne {}", id);
- return result.isPresent() ? result.get() : null;
+ return greetingOptional;
}
@CachePut(value = Application.CACHE_GREETINGS,
- key = "#result.id")
+ key = "#result?.id")
@Transactional
@Override
public Greeting create(final Greeting greeting) {
@@ -106,7 +120,7 @@ public Greeting create(final Greeting greeting) {
if (greeting.getId() != null) {
logger.error("Attempted to create a Greeting, but id attribute was not null.");
logger.info("< create");
- throw new EntityExistsException(
+ throw new IllegalArgumentException(
"Cannot create new Greeting with supplied id. The id attribute must be null to create an entity.");
}
@@ -125,15 +139,10 @@ public Greeting update(final Greeting greeting) {
updateMethodInvocationCounter.increment();
- // Ensure the entity object to be updated exists in the repository to
- // prevent the default behavior of save() which will persist a new
+ // findOne returns an Optional which will throw NoSuchElementException when null.
+ // This will prevent the default behavior of save() which will persist a new
// entity if the entity matching the id does not exist
- final Greeting greetingToUpdate = findOne(greeting.getId());
- if (greetingToUpdate == null) {
- logger.error("Attempted to update a Greeting, but the entity does not exist.");
- logger.info("< update {}", greeting.getId());
- throw new NoResultException("Requested Greeting not found.");
- }
+ final Greeting greetingToUpdate = findOne(greeting.getId()).get();
greetingToUpdate.setText(greeting.getText());
final Greeting updatedGreeting = greetingRepository.save(greetingToUpdate);
diff --git a/src/main/java/com/leanstacks/ws/web/api/ExceptionDetail.java b/src/main/java/com/leanstacks/ws/web/api/ExceptionDetail.java
index 0d9c577..de636ca 100644
--- a/src/main/java/com/leanstacks/ws/web/api/ExceptionDetail.java
+++ b/src/main/java/com/leanstacks/ws/web/api/ExceptionDetail.java
@@ -1,5 +1,7 @@
package com.leanstacks.ws.web.api;
+import java.time.Instant;
+
/**
* The ExceptionDetail class models information about a web service request which results in an Exception. This
* information may be returned to the client.
@@ -10,15 +12,15 @@
public class ExceptionDetail {
/**
- * A timestamp expressed in milliseconds.
+ * The time the exception occurred.
*/
- private long timestamp;
+ private Instant timestamp;
/**
* The HTTP method (e.g. GET, POST, etc.)
*/
private String method = "";
/**
- * The web service servlet path.
+ * The web service context path.
*/
private String path = "";
/**
@@ -42,24 +44,24 @@ public class ExceptionDetail {
* Construct an ExceptionDetail.
*/
public ExceptionDetail() {
- this.timestamp = System.currentTimeMillis();
+ this.timestamp = Instant.now();
}
/**
* Returns the timestamp attribute value.
*
- * @return A long.
+ * @return An Instant.
*/
- public long getTimestamp() {
+ public Instant getTimestamp() {
return timestamp;
}
/**
* Sets the timestamp attribute value.
*
- * @param timestamp A long.
+ * @param timestamp An Instant.
*/
- public void setTimestamp(final long timestamp) {
+ public void setTimestamp(final Instant timestamp) {
this.timestamp = timestamp;
}
diff --git a/src/main/java/com/leanstacks/ws/web/api/GreetingController.java b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java
index fdd5ada..b54d2a5 100644
--- a/src/main/java/com/leanstacks/ws/web/api/GreetingController.java
+++ b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java
@@ -1,6 +1,7 @@
package com.leanstacks.ws.web.api;
-import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -8,24 +9,21 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.leanstacks.ws.model.Greeting;
import com.leanstacks.ws.service.EmailService;
import com.leanstacks.ws.service.GreetingService;
-import io.swagger.annotations.ApiImplicitParam;
-import io.swagger.annotations.ApiImplicitParams;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-
/**
* The GreetingController class is a RESTful web service controller. The @RestController annotation informs
* Spring that each @RequestMapping method returns a @ResponseBody.
@@ -33,6 +31,7 @@
* @author Matt Warman
*/
@RestController
+@RequestMapping("/api/greetings")
public class GreetingController {
/**
@@ -56,27 +55,16 @@ public class GreetingController {
* Web service endpoint to fetch all Greeting entities. The service returns the collection of Greeting entities as
* JSON.
*
- * @return A ResponseEntity containing a Collection of Greeting objects.
+ * @return A List of Greeting objects.
*/
- @ApiOperation(value = "${GreetingController.getGreetings.title}",
- notes = "${GreetingController.getGreetings.notes}",
- response = Greeting.class,
- responseContainer = "List")
- @ApiImplicitParams(@ApiImplicitParam(name = "Authorization",
- value = "Basic auth_creds",
- required = true,
- dataType = "string",
- paramType = "header"))
- @RequestMapping(value = "/api/greetings",
- method = RequestMethod.GET,
- produces = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity> getGreetings() {
+ @GetMapping
+ public List getGreetings() {
logger.info("> getGreetings");
- final Collection greetings = greetingService.findAll();
+ final List greetings = greetingService.findAll();
logger.info("< getGreetings");
- return new ResponseEntity>(greetings, HttpStatus.OK);
+ return greetings;
}
/**
@@ -89,31 +77,16 @@ public ResponseEntity> getGreetings() {
*
*
* @param id A Long URL path variable containing the Greeting primary key identifier.
- * @return A ResponseEntity containing a single Greeting object, if found, and a HTTP status code as described in
- * the method comment.
+ * @return A Greeting object, if found, and a HTTP status code as described in the method comment.
*/
- @ApiOperation(value = "${GreetingController.getGreeting.title}",
- notes = "${GreetingController.getGreeting.notes}",
- response = Greeting.class)
- @ApiImplicitParams(@ApiImplicitParam(name = "Authorization",
- value = "Basic auth_creds",
- required = true,
- dataType = "string",
- paramType = "header"))
- @RequestMapping(value = "/api/greetings/{id}",
- method = RequestMethod.GET,
- produces = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity getGreeting(@ApiParam("Greeting ID") @PathVariable final Long id) {
+ @GetMapping("/{id}")
+ public Greeting getGreeting(@PathVariable final Long id) {
logger.info("> getGreeting");
- final Greeting greeting = greetingService.findOne(id);
- if (greeting == null) {
- logger.info("< getGreeting");
- return new ResponseEntity(HttpStatus.NOT_FOUND);
- }
+ final Optional greetingOptional = greetingService.findOne(id);
logger.info("< getGreeting");
- return new ResponseEntity(greeting, HttpStatus.OK);
+ return greetingOptional.get();
}
/**
@@ -123,33 +96,21 @@ public ResponseEntity getGreeting(@ApiParam("Greeting ID") @PathVariab
*
*
* If created successfully, the persisted Greeting is returned as JSON with HTTP status 201. If not created
- * successfully, the service returns an empty response body with HTTP status 500.
+ * successfully, the service returns an ExceptionDetail response body with HTTP status 400 or 500.
*
*
* @param greeting The Greeting object to be created.
- * @return A ResponseEntity containing a single Greeting object, if created successfully, and a HTTP status code as
- * described in the method comment.
+ * @return A Greeting object, if created successfully, and a HTTP status code as described in the method comment.
*/
- @ApiOperation(value = "${GreetingController.createGreeting.title}",
- notes = "${GreetingController.createGreeting.notes}",
- response = Greeting.class,
- code = 201)
- @ApiImplicitParams(@ApiImplicitParam(name = "Authorization",
- value = "Basic auth_creds",
- required = true,
- dataType = "string",
- paramType = "header"))
- @RequestMapping(value = "/api/greetings",
- method = RequestMethod.POST,
- consumes = MediaType.APPLICATION_JSON_VALUE,
- produces = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity createGreeting(@RequestBody final Greeting greeting) {
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public Greeting createGreeting(@RequestBody final Greeting greeting) {
logger.info("> createGreeting");
final Greeting savedGreeting = greetingService.create(greeting);
logger.info("< createGreeting");
- return new ResponseEntity(savedGreeting, HttpStatus.CREATED);
+ return savedGreeting;
}
/**
@@ -159,28 +120,15 @@ public ResponseEntity createGreeting(@RequestBody final Greeting greet
*
*
* If updated successfully, the persisted Greeting is returned as JSON with HTTP status 200. If not found, the
- * service returns an empty response body and HTTP status 404. If not updated successfully, the service returns an
- * empty response body with HTTP status 500.
+ * service returns an ExceptionDetail response body and HTTP status 404. If not updated successfully, the service
+ * returns an empty response body with HTTP status 400 or 500.
*
*
* @param greeting The Greeting object to be updated.
- * @return A ResponseEntity containing a single Greeting object, if updated successfully, and a HTTP status code as
- * described in the method comment.
+ * @return A Greeting object, if updated successfully, and a HTTP status code as described in the method comment.
*/
- @ApiOperation(value = "${GreetingController.updateGreeting.title}",
- notes = "${GreetingController.updateGreeting.notes}",
- response = Greeting.class)
- @ApiImplicitParams(@ApiImplicitParam(name = "Authorization",
- value = "Basic auth_creds",
- required = true,
- dataType = "string",
- paramType = "header"))
- @RequestMapping(value = "/api/greetings/{id}",
- method = RequestMethod.PUT,
- consumes = MediaType.APPLICATION_JSON_VALUE,
- produces = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity updateGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id,
- @RequestBody final Greeting greeting) {
+ @PutMapping("/{id}")
+ public Greeting updateGreeting(@PathVariable("id") final Long id, @RequestBody final Greeting greeting) {
logger.info("> updateGreeting");
greeting.setId(id);
@@ -188,7 +136,7 @@ public ResponseEntity updateGreeting(@ApiParam("Greeting ID") @PathVar
final Greeting updatedGreeting = greetingService.update(greeting);
logger.info("< updateGreeting");
- return new ResponseEntity(updatedGreeting, HttpStatus.OK);
+ return updatedGreeting;
}
/**
@@ -198,29 +146,19 @@ public ResponseEntity updateGreeting(@ApiParam("Greeting ID") @PathVar
*
*
* If deleted successfully, the service returns an empty response body with HTTP status 204. If not deleted
- * successfully, the service returns an empty response body with HTTP status 500.
+ * successfully, the service returns an ExceptionDetail response body with HTTP status 500.
*
*
* @param id A Long URL path variable containing the Greeting primary key identifier.
- * @return A ResponseEntity with an empty response body and a HTTP status code as described in the method comment.
*/
- @ApiOperation(value = "${GreetingController.deleteGreeting.title}",
- notes = "${GreetingController.deleteGreeting.notes}",
- code = 204)
- @ApiImplicitParams(@ApiImplicitParam(name = "Authorization",
- value = "Basic auth_creds",
- required = true,
- dataType = "string",
- paramType = "header"))
- @RequestMapping(value = "/api/greetings/{id}",
- method = RequestMethod.DELETE)
- public ResponseEntity deleteGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id) {
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void deleteGreeting(@PathVariable("id") final Long id) {
logger.info("> deleteGreeting");
greetingService.delete(id);
logger.info("< deleteGreeting");
- return new ResponseEntity(HttpStatus.NO_CONTENT);
}
/**
@@ -229,40 +167,24 @@ public ResponseEntity deleteGreeting(@ApiParam("Greeting ID") @PathVariabl
*
*
* If found, the Greeting is returned as JSON with HTTP status 200 and sent via Email. If not found, the service
- * returns an empty response body with HTTP status 404.
+ * returns an Exception response body with HTTP status 404.
*
*
* @param id A Long URL path variable containing the Greeting primary key identifier.
* @param waitForAsyncResult A boolean indicating if the web service should wait for the asynchronous email
* transmission.
- * @return A ResponseEntity containing a single Greeting object, if found, and a HTTP status code as described in
- * the method comment.
+ * @return A Greeting object, if found, and a HTTP status code as described in the method comment.
*/
- @ApiOperation(value = "${GreetingController.sendGreeting.title}",
- notes = "${GreetingController.sendGreeting.notes}",
- response = Greeting.class)
- @ApiImplicitParams(@ApiImplicitParam(name = "Authorization",
- value = "Basic auth_creds",
- required = true,
- dataType = "string",
- paramType = "header"))
- @RequestMapping(value = "/api/greetings/{id}/send",
- method = RequestMethod.POST,
- produces = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity sendGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id,
- @ApiParam("Wait for Response") @RequestParam(value = "wait",
- defaultValue = "false") final boolean waitForAsyncResult) {
+ @PostMapping("/{id}/send")
+ public Greeting sendGreeting(@PathVariable("id") final Long id, @RequestParam(value = "wait",
+ defaultValue = "false") final boolean waitForAsyncResult) {
logger.info("> sendGreeting");
Greeting greeting;
try {
- greeting = greetingService.findOne(id);
- if (greeting == null) {
- logger.info("< sendGreeting");
- return new ResponseEntity(HttpStatus.NOT_FOUND);
- }
+ greeting = greetingService.findOne(id).get();
if (waitForAsyncResult) {
final Future asyncResponse = emailService.sendAsyncWithResult(greeting);
@@ -273,11 +195,11 @@ public ResponseEntity sendGreeting(@ApiParam("Greeting ID") @PathVaria
}
} catch (ExecutionException | InterruptedException ex) {
logger.error("A problem occurred sending the Greeting.", ex);
- return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
+ throw new IllegalStateException(ex);
}
logger.info("< sendGreeting");
- return new ResponseEntity(greeting, HttpStatus.OK);
+ return greeting;
}
diff --git a/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java b/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java
index 600ddc4..f39a691 100644
--- a/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java
+++ b/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java
@@ -23,14 +23,17 @@
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
+ /**
+ * The Logger for this Class.
+ */
private static final Logger logger = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class);
/**
- * Handles JPA NoResultExceptions thrown from web service controller methods. Creates a response with an empty body
- * and HTTP status code 404, not found.
+ * Handles JPA NoResultExceptions thrown from web service controller methods. Creates a response with an
+ * ExceptionDetail body and HTTP status code 404, not found.
*
* @param ex A NoResultException instance.
- * @return A ResponseEntity with an empty response body and HTTP status code 404.
+ * @return A ResponseEntity with an ExceptionDetail response body and HTTP status code 404.
*/
@ExceptionHandler(NoResultException.class)
public ResponseEntity