From 26a7576c1cdd036be1ea5008d0193be3e66b0523 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Wed, 5 Dec 2018 14:22:31 -0500 Subject: [PATCH 01/10] See #59. Updated project metadata to version 2.3.0 --- build.gradle | 2 +- pom.xml | 2 +- src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 081a996..df30363 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ ext { } group = 'com.leanstacks' -version = '2.2.0' +version = '2.3.0' sourceCompatibility = 11 targetCompatibility = 11 diff --git a/pom.xml b/pom.xml index 0eab6b9..b617fcb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.leanstacks skeleton-ws-spring-boot - 2.2.0 + 2.3.0 Spring Boot Starter Project Starter application stack for RESTful web services using Spring Boot. diff --git a/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java b/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java index 096241f..1d54a54 100644 --- a/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java +++ b/src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java @@ -27,7 +27,7 @@ public class ApiDocsConfiguration { /** * The project version. */ - public static final String PROJECT_VERSION = "2.2.0"; + public static final String PROJECT_VERSION = "2.3.0"; /** * The project contact name. */ From bece3c8558450d729064ea9ed2bb7c3d7e504a06 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Sat, 8 Dec 2018 06:31:07 -0500 Subject: [PATCH 02/10] See #57. Update PMD ruleset to comply with v7 PMD standards. --- build.gradle | 3 +- etc/pmd/ruleset.xml | 119 +++++++++--------- .../ws/batch/GreetingBatchBean.java | 21 +++- .../java/com/leanstacks/ws/model/Account.java | 24 ++++ .../com/leanstacks/ws/model/Greeting.java | 11 ++ .../ws/service/GreetingServiceBean.java | 19 ++- .../RestResponseEntityExceptionHandler.java | 3 + .../java/com/leanstacks/ws/AbstractTest.java | 12 +- .../ws/service/GreetingServiceTest.java | 46 +++++-- .../ws/util/BCryptPasswordEncoderUtil.java | 42 +++---- .../ws/web/api/GreetingControllerTest.java | 35 ++++++ 11 files changed, 234 insertions(+), 101 deletions(-) diff --git a/build.gradle b/build.gradle index df30363..65df97e 100644 --- a/build.gradle +++ b/build.gradle @@ -90,7 +90,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..01b1a71 100644 --- a/etc/pmd/ruleset.xml +++ b/etc/pmd/ruleset.xml @@ -1,78 +1,83 @@ - + 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/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..f63b01d 100644 --- a/src/main/java/com/leanstacks/ws/model/Greeting.java +++ b/src/main/java/com/leanstacks/ws/model/Greeting.java @@ -15,6 +15,9 @@ public class Greeting extends TransactionalEntity { private static final long serialVersionUID = 1L; + /** + * The text value. + */ @ApiModelProperty(value = "The actual text of the Greeting.", required = true, position = 100, @@ -22,10 +25,18 @@ public class Greeting extends TransactionalEntity { @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/service/GreetingServiceBean.java b/src/main/java/com/leanstacks/ws/service/GreetingServiceBean.java index f240ea6..12646a7 100644 --- a/src/main/java/com/leanstacks/ws/service/GreetingServiceBean.java +++ b/src/main/java/com/leanstacks/ws/service/GreetingServiceBean.java @@ -35,12 +35,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; /** 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..f7403db 100644 --- a/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java +++ b/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java @@ -23,6 +23,9 @@ @ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { + /** + * The Logger for this Class. + */ private static final Logger logger = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class); /** diff --git a/src/test/java/com/leanstacks/ws/AbstractTest.java b/src/test/java/com/leanstacks/ws/AbstractTest.java index 77c0fa2..edcb804 100644 --- a/src/test/java/com/leanstacks/ws/AbstractTest.java +++ b/src/test/java/com/leanstacks/ws/AbstractTest.java @@ -18,6 +18,9 @@ public abstract class AbstractTest { */ public static final String USERNAME = "unittest"; + /** + * Tasks performed before each test method. + */ @Before public void before() { RequestContext.setUsername(AbstractTest.USERNAME); @@ -25,17 +28,22 @@ public void before() { } /** - * Perform initialization tasks before the execution of each test method. + * Perform initialization tasks before the execution of each test method. Concrete test classes may override this + * method to implement class-specific tasks. */ public abstract void doBeforeEachTest(); + /** + * Tasks performed after each test method. + */ @After public void after() { doAfterEachTest(); } /** - * Perform clean up tasks after the execution of each test method. + * Perform clean up tasks after the execution of each test method. Concrete test classes may override this method to + * implement class-specific tasks. */ public abstract void doAfterEachTest(); diff --git a/src/test/java/com/leanstacks/ws/service/GreetingServiceTest.java b/src/test/java/com/leanstacks/ws/service/GreetingServiceTest.java index 793e506..2e0a119 100644 --- a/src/test/java/com/leanstacks/ws/service/GreetingServiceTest.java +++ b/src/test/java/com/leanstacks/ws/service/GreetingServiceTest.java @@ -24,8 +24,14 @@ @BasicTransactionalTest public class GreetingServiceTest extends AbstractTest { + /** + * Constant 'test'. + */ private static final String VALUE_TEXT = "test"; + /** + * The GreetingService business service. + */ @Autowired private transient GreetingService greetingService; @@ -39,6 +45,9 @@ public void doAfterEachTest() { // perform test clean up } + /** + * Test fetch a collection of Greetings. + */ @Test public void testGetGreetings() { @@ -49,6 +58,9 @@ public void testGetGreetings() { } + /** + * Test fetch a single Greeting. + */ @Test public void testGetGreeting() { @@ -61,6 +73,9 @@ public void testGetGreeting() { } + /** + * Test fetch a single greeting with invalid identifier. + */ @Test public void testGetGreetingNotFound() { @@ -72,6 +87,9 @@ public void testGetGreetingNotFound() { } + /** + * Test create a Greeting. + */ @Test public void testCreateGreeting() { @@ -90,26 +108,28 @@ public void testCreateGreeting() { } + /** + * Test create a Greeting with invalid data. + */ @Test public void testCreateGreetingWithId() { - Exception exception = null; - final Greeting greeting = new Greeting(); greeting.setId(Long.MAX_VALUE); greeting.setText(VALUE_TEXT); try { greetingService.create(greeting); + Assert.fail("failure - expected exception"); } catch (EntityExistsException eee) { - exception = eee; + Assert.assertNotNull("failure - expected exception not null", eee); } - Assert.assertNotNull("failure - expected exception", exception); - Assert.assertTrue("failure - expected EntityExistsException", exception instanceof EntityExistsException); - } + /** + * Test update a Greeting. + */ @Test public void testUpdateGreeting() { @@ -129,26 +149,28 @@ public void testUpdateGreeting() { } + /** + * Test update a Greeting which does not exist. + */ @Test public void testUpdateGreetingNotFound() { - Exception exception = null; - final Greeting greeting = new Greeting(); greeting.setId(Long.MAX_VALUE); greeting.setText("test"); try { greetingService.update(greeting); + Assert.fail("failure - expected exception"); } catch (NoResultException nre) { - exception = nre; + Assert.assertNotNull("failure - expected exception not null", nre); } - Assert.assertNotNull("failure - expected exception", exception); - Assert.assertTrue("failure - expected NoResultException", exception instanceof NoResultException); - } + /** + * Test delete a Greeting. + */ @Test public void testDeleteGreeting() { diff --git a/src/test/java/com/leanstacks/ws/util/BCryptPasswordEncoderUtil.java b/src/test/java/com/leanstacks/ws/util/BCryptPasswordEncoderUtil.java index f934556..65f9dd7 100644 --- a/src/test/java/com/leanstacks/ws/util/BCryptPasswordEncoderUtil.java +++ b/src/test/java/com/leanstacks/ws/util/BCryptPasswordEncoderUtil.java @@ -4,8 +4,6 @@ import java.io.OutputStreamWriter; import java.io.Writer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** @@ -24,11 +22,6 @@ */ public class BCryptPasswordEncoderUtil { - /** - * The Logger for this Class. - */ - private static final Logger logger = LoggerFactory.getLogger(BCryptPasswordEncoderUtil.class); - /** * The format for encoder messages. */ @@ -54,8 +47,9 @@ public String encode(final String clearText) { * Facilitates gathering user input and invoking the class behavior. * * @param args An array of command line input values. (not used) + * @throws IOException Thrown if performing IO operations fails. */ - public static void main(final String... args) { + public static void main(final String... args) throws IOException { final BCryptPasswordEncoderUtil encoderUtil = new BCryptPasswordEncoderUtil(); @@ -73,32 +67,28 @@ public static void main(final String... args) { * Writes a message to the console. * * @param str A String message value. + * @throws IOException Thrown if writing output fails. */ - private void write(final String str) { - - try { - if (writer == null) { - writer = new OutputStreamWriter(System.out); - } - writer.write(str); - } catch (IOException ioe) { - logger.error("Writer cannot write.", ioe); - System.exit(1); + private void write(final String str) throws IOException { + + if (writer == null) { + writer = new OutputStreamWriter(System.out); } + writer.write(str); + } /** * Closes all system resources and prepares for application termination. + * + * @throws IOException Thrown if closing the output stream fails. */ - private void close() { - try { - if (writer != null) { - writer.close(); - } - } catch (IOException ioe) { - logger.error("Problem closing resources.", ioe); - System.exit(1); + private void close() throws IOException { + + if (writer != null) { + writer.close(); } + } } diff --git a/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java b/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java index 9923d72..f240c3f 100644 --- a/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java +++ b/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java @@ -92,6 +92,11 @@ public void doAfterEachTest() { // perform test clean up } + /** + * Test fetch collection of Greetings. + * + * @throws Exception Thrown if mocking failure occurs. + */ @Test public void testGetGreetings() throws Exception { @@ -118,6 +123,11 @@ public void testGetGreetings() throws Exception { } + /** + * Test fetch a Greeting by identifier. + * + * @throws Exception Thrown if mocking failure occurs. + */ @Test public void testGetGreeting() throws Exception { @@ -145,6 +155,11 @@ public void testGetGreeting() throws Exception { Assert.assertTrue("failure - expected HTTP response body to have a value", !Strings.isNullOrEmpty(content)); } + /** + * Test fetch a Greeting with unknown identifier. + * + * @throws Exception Thrown if mocking failure occurs. + */ @Test public void testGetGreetingNotFound() throws Exception { @@ -172,6 +187,11 @@ public void testGetGreetingNotFound() throws Exception { } + /** + * Test create a Greeting. + * + * @throws Exception Thrown if mocking failure occurs. + */ @Test public void testCreateGreeting() throws Exception { @@ -208,6 +228,11 @@ public void testCreateGreeting() throws Exception { Assert.assertEquals("failure - expected text attribute match", entity.getText(), createdEntity.getText()); } + /** + * Test update a Greeting. + * + * @throws Exception Thrown if mocking failure occurs. + */ @Test public void testUpdateGreeting() throws Exception { @@ -245,6 +270,11 @@ public void testUpdateGreeting() throws Exception { } + /** + * Test delete a Greeting. + * + * @throws Exception Thrown if mocking failure occurs. + */ @Test public void testDeleteGreeting() throws Exception { @@ -267,6 +297,11 @@ public void testDeleteGreeting() throws Exception { } + /** + * Test sending email asynchronously. + * + * @throws Exception Thrown if mocking failure occurs. + */ @Test public void testSendGreetingAsync() throws Exception { From b9274e56eacc12aef1846b42c99e100239754e8f Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Sun, 9 Dec 2018 12:24:52 -0500 Subject: [PATCH 03/10] See #60. Using Spring method-specific shortcuts for RequestMapping. --- .../ws/web/api/GreetingController.java | 68 ++++++++----------- .../ws/web/api/GreetingControllerTest.java | 2 +- 2 files changed, 31 insertions(+), 39 deletions(-) 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..7e779d9 100644 --- a/src/main/java/com/leanstacks/ws/web/api/GreetingController.java +++ b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java @@ -4,17 +4,21 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import javax.persistence.NoResultException; + import org.slf4j.Logger; 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; @@ -33,6 +37,7 @@ * @author Matt Warman */ @RestController +@RequestMapping("/api/greetings") public class GreetingController { /** @@ -67,16 +72,14 @@ public class GreetingController { required = true, dataType = "string", paramType = "header")) - @RequestMapping(value = "/api/greetings", - method = RequestMethod.GET, - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getGreetings() { + @GetMapping + public Collection getGreetings() { logger.info("> getGreetings"); final Collection greetings = greetingService.findAll(); logger.info("< getGreetings"); - return new ResponseEntity>(greetings, HttpStatus.OK); + return greetings; } /** @@ -100,20 +103,18 @@ public ResponseEntity> getGreetings() { 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(@ApiParam("Greeting ID") @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); + throw new NoResultException("Greeting not found."); } logger.info("< getGreeting"); - return new ResponseEntity(greeting, HttpStatus.OK); + return greeting; } /** @@ -139,17 +140,15 @@ public ResponseEntity getGreeting(@ApiParam("Greeting ID") @PathVariab 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; } /** @@ -175,11 +174,8 @@ public ResponseEntity createGreeting(@RequestBody final Greeting greet 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, + @PutMapping("/{id}") + public Greeting updateGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id, @RequestBody final Greeting greeting) { logger.info("> updateGreeting"); @@ -188,7 +184,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; } /** @@ -202,7 +198,6 @@ public ResponseEntity updateGreeting(@ApiParam("Greeting ID") @PathVar *

* * @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}", @@ -212,15 +207,14 @@ public ResponseEntity updateGreeting(@ApiParam("Greeting ID") @PathVar 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(@ApiParam("Greeting ID") @PathVariable("id") final Long id) { logger.info("> deleteGreeting"); greetingService.delete(id); logger.info("< deleteGreeting"); - return new ResponseEntity(HttpStatus.NO_CONTENT); } /** @@ -246,10 +240,8 @@ public ResponseEntity deleteGreeting(@ApiParam("Greeting ID") @PathVariabl 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, + @PostMapping("/{id}/send") + public Greeting sendGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id, @ApiParam("Wait for Response") @RequestParam(value = "wait", defaultValue = "false") final boolean waitForAsyncResult) { @@ -261,7 +253,7 @@ public ResponseEntity sendGreeting(@ApiParam("Greeting ID") @PathVaria greeting = greetingService.findOne(id); if (greeting == null) { logger.info("< sendGreeting"); - return new ResponseEntity(HttpStatus.NOT_FOUND); + throw new NoResultException("Greeting not found."); } if (waitForAsyncResult) { @@ -273,11 +265,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/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java b/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java index f240c3f..2c79659 100644 --- a/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java +++ b/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java @@ -183,7 +183,7 @@ public void testGetGreetingNotFound() throws Exception { // Perform standard JUnit assertions on the test results Assert.assertEquals("failure - expected HTTP status 404", 404, status); - Assert.assertTrue("failure - expected HTTP response body to be empty", Strings.isNullOrEmpty(content)); + Assert.assertTrue("failure - expected HTTP response body to have a value", !Strings.isNullOrEmpty(content)); } From b38aaa67c59c758389e475d23c57e4e587f07155 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Mon, 10 Dec 2018 09:48:18 -0500 Subject: [PATCH 04/10] See #62. Use Optional wrappers for GreetingService --- .../ws/service/GreetingService.java | 17 +++---- .../ws/service/GreetingServiceBean.java | 30 +++++------- .../ws/web/api/GreetingController.java | 47 +++++++------------ .../RestResponseEntityExceptionHandler.java | 38 +++++++++++---- .../ws/service/GreetingServiceTest.java | 32 ++++++------- .../ws/web/api/GreetingControllerTest.java | 19 ++++---- 6 files changed, 91 insertions(+), 92 deletions(-) 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 12646a7..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; @@ -83,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; @@ -97,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) { @@ -123,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."); } @@ -142,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/GreetingController.java b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java index 7e779d9..a06b3ef 100644 --- a/src/main/java/com/leanstacks/ws/web/api/GreetingController.java +++ b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java @@ -1,11 +1,10 @@ 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; -import javax.persistence.NoResultException; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -61,7 +60,7 @@ 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}", @@ -73,10 +72,10 @@ public class GreetingController { dataType = "string", paramType = "header")) @GetMapping - public Collection getGreetings() { + public List getGreetings() { logger.info("> getGreetings"); - final Collection greetings = greetingService.findAll(); + final List greetings = greetingService.findAll(); logger.info("< getGreetings"); return greetings; @@ -92,8 +91,7 @@ public Collection 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}", @@ -107,14 +105,10 @@ public Collection getGreetings() { public Greeting getGreeting(@ApiParam("Greeting ID") @PathVariable final Long id) { logger.info("> getGreeting"); - final Greeting greeting = greetingService.findOne(id); - if (greeting == null) { - logger.info("< getGreeting"); - throw new NoResultException("Greeting not found."); - } + final Optional greetingOptional = greetingService.findOne(id); logger.info("< getGreeting"); - return greeting; + return greetingOptional.get(); } /** @@ -124,12 +118,11 @@ public Greeting getGreeting(@ApiParam("Greeting ID") @PathVariable final Long id *

*

* 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}", @@ -158,13 +151,12 @@ public Greeting createGreeting(@RequestBody final Greeting greeting) { *

*

* 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}", @@ -194,7 +186,7 @@ public Greeting updateGreeting(@ApiParam("Greeting ID") @PathVariable("id") fina *

*

* 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. @@ -223,14 +215,13 @@ public void deleteGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Lo *

*

* 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}", @@ -250,11 +241,7 @@ public Greeting sendGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Greeting greeting; try { - greeting = greetingService.findOne(id); - if (greeting == null) { - logger.info("< sendGreeting"); - throw new NoResultException("Greeting not found."); - } + greeting = greetingService.findOne(id).get(); if (waitForAsyncResult) { final Future asyncResponse = emailService.sendAsyncWithResult(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 f7403db..281d77d 100644 --- a/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java +++ b/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java @@ -29,11 +29,11 @@ public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionH 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 handleNoResultException(final NoResultException ex, final WebRequest request) { @@ -46,11 +46,11 @@ public ResponseEntity handleNoResultException(final NoResultException ex } /** - * Handles JPA NoSuchElementException thrown when an empty Optional is accessed. Creates a response with an empty - * body and HTTP status code 404, not found. + * Handles JPA NoSuchElementException thrown when an empty Optional is accessed. Creates a response with an + * ExceptionDetail body and HTTP status code 404, not found. * * @param ex A NoSuchElementException 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(NoSuchElementException.class) public ResponseEntity handleNoSuchElementException(final NoSuchElementException ex, @@ -65,10 +65,10 @@ public ResponseEntity handleNoSuchElementException(final NoSuchElementEx /** * Handles EmptyResultDataAccessException thrown from web service controller methods. Creates a response with an - * empty body and HTTP status code 404, not found. + * ExceptionDetail body and HTTP status code 404, not found. * * @param ex An EmptyResultDataAccessException 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(EmptyResultDataAccessException.class) public ResponseEntity handleEmptyResultDataAccessException(final EmptyResultDataAccessException ex, @@ -81,12 +81,30 @@ public ResponseEntity handleEmptyResultDataAccessException(final EmptyRe return handleExceptionInternal(ex, detail, new HttpHeaders(), HttpStatus.NOT_FOUND, request); } + /** + * Handles IllegalArgumentException thrown from web service controller methods. Creates a response with an + * ExceptionDetail body and HTTP status code 400, not found. + * + * @param ex An IllegalArgumentException instance. + * @return A ResponseEntity with an ExceptionDetail response body and HTTP status code 400. + */ + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(final IllegalArgumentException ex, + final WebRequest request) { + logger.info("> handleIllegalArgumentException"); + logger.warn("- IllegalArgumentException: ", ex); + final ExceptionDetail detail = new ExceptionDetailBuilder().exception(ex).httpStatus(HttpStatus.BAD_REQUEST) + .webRequest(request).build(); + logger.info("< handleIllegalArgumentException"); + return handleExceptionInternal(ex, detail, new HttpHeaders(), HttpStatus.BAD_REQUEST, request); + } + /** * Handles all Exceptions not addressed by more specific @ExceptionHandler methods. Creates a response - * with the Exception detail in the response body as JSON and a HTTP status code of 500, internal server error. + * with the ExceptionDetail in the response body as JSON and a HTTP status code of 500, internal server error. * * @param ex An Exception instance. - * @return A ResponseEntity containing a the Exception attributes in the response body and a HTTP status code 500. + * @return A ResponseEntity containing a the ExceptionDetail in the response body and a HTTP status code 500. */ @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException(final Exception ex, final WebRequest request) { diff --git a/src/test/java/com/leanstacks/ws/service/GreetingServiceTest.java b/src/test/java/com/leanstacks/ws/service/GreetingServiceTest.java index 2e0a119..59f3767 100644 --- a/src/test/java/com/leanstacks/ws/service/GreetingServiceTest.java +++ b/src/test/java/com/leanstacks/ws/service/GreetingServiceTest.java @@ -1,9 +1,9 @@ package com.leanstacks.ws.service; import java.util.Collection; - -import javax.persistence.EntityExistsException; -import javax.persistence.NoResultException; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; import org.junit.Assert; import org.junit.Test; @@ -66,7 +66,7 @@ public void testGetGreeting() { final Long id = Long.valueOf(1); - final Greeting greeting = greetingService.findOne(id); + final Greeting greeting = greetingService.findOne(id).get(); Assert.assertNotNull("failure - expected not null", greeting); Assert.assertEquals("failure - expected greeting.id match", id, greeting.getId()); @@ -81,9 +81,9 @@ public void testGetGreetingNotFound() { final Long id = Long.MAX_VALUE; - final Greeting greeting = greetingService.findOne(id); + final Optional greetingOptional = greetingService.findOne(id); - Assert.assertNull("failure - expected null", greeting); + Assert.assertTrue("failure - expected null", greetingOptional.isEmpty()); } @@ -102,7 +102,7 @@ public void testCreateGreeting() { Assert.assertNotNull("failure - expected greeting.id not null", createdGreeting.getId()); Assert.assertEquals("failure - expected greeting.text match", VALUE_TEXT, createdGreeting.getText()); - final Collection greetings = greetingService.findAll(); + final List greetings = greetingService.findAll(); Assert.assertEquals("failure - expected 3 greetings", 3, greetings.size()); @@ -121,8 +121,8 @@ public void testCreateGreetingWithId() { try { greetingService.create(greeting); Assert.fail("failure - expected exception"); - } catch (EntityExistsException eee) { - Assert.assertNotNull("failure - expected exception not null", eee); + } catch (IllegalArgumentException ex) { + Assert.assertNotNull("failure - expected exception not null", ex); } } @@ -135,7 +135,7 @@ public void testUpdateGreeting() { final Long id = Long.valueOf(1); - final Greeting greeting = greetingService.findOne(id); + final Greeting greeting = greetingService.findOne(id).get(); Assert.assertNotNull("failure - expected greeting not null", greeting); @@ -162,8 +162,8 @@ public void testUpdateGreetingNotFound() { try { greetingService.update(greeting); Assert.fail("failure - expected exception"); - } catch (NoResultException nre) { - Assert.assertNotNull("failure - expected exception not null", nre); + } catch (NoSuchElementException ex) { + Assert.assertNotNull("failure - expected exception not null", ex); } } @@ -176,19 +176,19 @@ public void testDeleteGreeting() { final Long id = Long.valueOf(1); - final Greeting greeting = greetingService.findOne(id); + final Greeting greeting = greetingService.findOne(id).get(); Assert.assertNotNull("failure - expected greeting not null", greeting); greetingService.delete(id); - final Collection greetings = greetingService.findAll(); + final List greetings = greetingService.findAll(); Assert.assertEquals("failure - expected 1 greeting", 1, greetings.size()); - final Greeting deletedGreeting = greetingService.findOne(id); + final Optional deletedGreetingOptional = greetingService.findOne(id); - Assert.assertNull("failure - expected greeting to be deleted", deletedGreeting); + Assert.assertTrue("failure - expected greeting to be deleted", deletedGreetingOptional.isEmpty()); } diff --git a/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java b/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java index 2c79659..2fbde3e 100644 --- a/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java +++ b/src/test/java/com/leanstacks/ws/web/api/GreetingControllerTest.java @@ -6,7 +6,8 @@ import static org.mockito.Mockito.when; import java.util.ArrayList; -import java.util.Collection; +import java.util.List; +import java.util.Optional; import org.junit.Assert; import org.junit.Test; @@ -101,7 +102,7 @@ public void doAfterEachTest() { public void testGetGreetings() throws Exception { // Create some test data - final Collection list = getEntityListStubData(); + final List list = getEntityListStubData(); // Stub the GreetingService.findAll method return value when(greetingService.findAll()).thenReturn(list); @@ -133,10 +134,10 @@ public void testGetGreeting() throws Exception { // Create some test data final Long id = Long.valueOf(1); - final Greeting entity = getEntityStubData(); + final Optional greetingOptional = Optional.of(getEntityStubData()); // Stub the GreetingService.findOne method return value - when(greetingService.findOne(id)).thenReturn(entity); + when(greetingService.findOne(id)).thenReturn(greetingOptional); // Perform the behavior being tested final MvcResult result = mvc @@ -167,7 +168,7 @@ public void testGetGreetingNotFound() throws Exception { final Long id = Long.MAX_VALUE; // Stub the GreetingService.findOne method return value - when(greetingService.findOne(id)).thenReturn(null); + when(greetingService.findOne(id)).thenReturn(Optional.empty()); // Perform the behavior being tested final MvcResult result = mvc @@ -307,10 +308,10 @@ public void testSendGreetingAsync() throws Exception { // Create some test data final Long id = Long.valueOf(1); - final Greeting entity = getEntityStubData(); + final Optional greetingOptional = Optional.of(getEntityStubData()); // Stub the GreetingService.findOne method return value - when(greetingService.findOne(id)).thenReturn(entity); + when(greetingService.findOne(id)).thenReturn(greetingOptional); // Perform the behavior being tested final MvcResult result = mvc.perform( @@ -332,8 +333,8 @@ public void testSendGreetingAsync() throws Exception { Assert.assertTrue("failure - expected HTTP response body to have a value", !Strings.isNullOrEmpty(content)); } - private Collection getEntityListStubData() { - final Collection list = new ArrayList(); + private List getEntityListStubData() { + final List list = new ArrayList(); list.add(getEntityStubData()); return list; } From 4302831c91630d3467d8b1022f16dff0ff363988 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Mon, 10 Dec 2018 14:44:47 -0500 Subject: [PATCH 05/10] See #62. Use Optional wrappers for AccountService --- .../ws/repository/AccountRepository.java | 15 +++++++------- .../security/AccountUserDetailsService.java | 20 ++++++++----------- .../leanstacks/ws/service/AccountService.java | 6 ++++-- .../ws/service/AccountServiceBean.java | 8 +++++--- 4 files changed, 25 insertions(+), 24 deletions(-) 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/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; } } From 366ded94f1bf38f5ac21c05408f40172fbe1ccef Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Tue, 11 Dec 2018 05:18:25 -0500 Subject: [PATCH 06/10] See #61. Use Spring Security built-in CORS support. --- .../ws/security/CorsProperties.java | 178 ++++++++++++++++++ .../{ => security}/SecurityConfiguration.java | 93 ++++++--- .../ws/web/filter/SimpleCorsFilter.java | 45 ----- .../resources/config/application.properties | 11 ++ 4 files changed, 252 insertions(+), 75 deletions(-) create mode 100644 src/main/java/com/leanstacks/ws/security/CorsProperties.java rename src/main/java/com/leanstacks/ws/{ => security}/SecurityConfiguration.java (63%) delete mode 100644 src/main/java/com/leanstacks/ws/web/filter/SimpleCorsFilter.java 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 63% rename from src/main/java/com/leanstacks/ws/SecurityConfiguration.java rename to src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java index c336328..4586d86 100644 --- a/src/main/java/com/leanstacks/ws/SecurityConfiguration.java +++ b/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java @@ -1,11 +1,13 @@ -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 +16,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 +28,7 @@ */ @Configuration @EnableWebSecurity +@EnableConfigurationProperties(CorsProperties.class) public class SecurityConfiguration { /** @@ -67,21 +70,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 +151,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 @@ -167,11 +200,11 @@ protected void configure(final HttpSecurity http) throws Exception { // @formatter:off http - .csrf().disable() - .authorizeRequests() - .anyRequest().authenticated() - .and() - .formLogin(); + .csrf().disable() + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin(); // @formatter:on diff --git a/src/main/java/com/leanstacks/ws/web/filter/SimpleCorsFilter.java b/src/main/java/com/leanstacks/ws/web/filter/SimpleCorsFilter.java deleted file mode 100644 index 60de027..0000000 --- a/src/main/java/com/leanstacks/ws/web/filter/SimpleCorsFilter.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.leanstacks.ws.web.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.GenericFilterBean; - -/** - * The SimpleCORSFilter class is a standard web Filter which intercepts all inbound HTTP requests. The filter sets - * several Headers on the HTTP response which inform a browser that the web services handle Cross-Origin requests. - * - * @author Matt Warman - */ -@Component -public class SimpleCorsFilter extends GenericFilterBean { - - /** - * The Logger for this class. - */ - private static final Logger logger = LoggerFactory.getLogger(SimpleCorsFilter.class); - - @Override - public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain chain) - throws IOException, ServletException { - logger.info("> doFilter"); - - final HttpServletResponse response = (HttpServletResponse) resp; - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "DELETE, GET, OPTIONS, PATCH, POST, PUT"); - response.setHeader("Access-Control-Max-Age", "3600"); - response.setHeader("Access-Control-Allow-Headers", "x-requested-with, content-type"); - - chain.doFilter(req, resp); - logger.info("< doFilter"); - } - -} diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index 9dfa0bb..c46bbcf 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -62,3 +62,14 @@ logging.level.org.springframework.security=INFO # Uncomment the 2 hibernate appenders below to show SQL and params in logs logging.level.org.hibernate.SQL=DEBUG #logging.level.org.hibernate.type.descriptor.sql=TRACE + +## +# CORS Configuration +## +leanstacks.cors.filter-registration-path=/** +leanstacks.cors.allow-credentials=false +leanstacks.cors.allowed-headers=accept,authorization,content-type +leanstacks.cors.allowed-methods=GET,OPTIONS,POST,PUT,PATCH,DELETE +leanstacks.cors.allowed-origins=* +leanstacks.cors.exposed-headers= +leanstacks.cors.max-age-seconds=3600 From ed7b895f9d34d152fc4c55d5448b9d5779988e66 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Fri, 14 Dec 2018 06:26:15 -0500 Subject: [PATCH 07/10] See #48. Spring REST Docs test-driven documentation. --- build.gradle | 16 +++- etc/pmd/ruleset.xml | 6 ++ .../_includes/http-response-error.adoc | 16 ++++ .../_includes/response-fields-error.adoc | 32 +++++++ src/docs/asciidoc/create-greeting.adoc | 40 ++++++++ src/docs/asciidoc/delete-greeting.adoc | 32 +++++++ src/docs/asciidoc/get-greeting.adoc | 40 ++++++++ src/docs/asciidoc/get-greetings.adoc | 36 +++++++ src/docs/asciidoc/index.adoc | 27 ++++++ src/docs/asciidoc/send-greeting.adoc | 44 +++++++++ src/docs/asciidoc/update-greeting.adoc | 44 +++++++++ .../ws/web/api/ExceptionDetail.java | 18 ++-- .../resources/config/application.properties | 5 +- .../com/leanstacks/ws/AbstractDocTest.java | 75 +++++++++++++++ .../ws/web/api/CreateGreetingDocTest.java | 92 ++++++++++++++++++ .../ws/web/api/DeleteGreetingDocTest.java | 58 ++++++++++++ .../ws/web/api/GetGreetingDocTest.java | 87 +++++++++++++++++ .../ws/web/api/GetGreetingsDocTest.java | 75 +++++++++++++++ .../ws/web/api/SendGreetingDocTest.java | 90 ++++++++++++++++++ .../ws/web/api/UpdateGreetingDocTest.java | 94 +++++++++++++++++++ 20 files changed, 916 insertions(+), 11 deletions(-) create mode 100644 src/docs/asciidoc/_includes/http-response-error.adoc create mode 100644 src/docs/asciidoc/_includes/response-fields-error.adoc create mode 100644 src/docs/asciidoc/create-greeting.adoc create mode 100644 src/docs/asciidoc/delete-greeting.adoc create mode 100644 src/docs/asciidoc/get-greeting.adoc create mode 100644 src/docs/asciidoc/get-greetings.adoc create mode 100644 src/docs/asciidoc/index.adoc create mode 100644 src/docs/asciidoc/send-greeting.adoc create mode 100644 src/docs/asciidoc/update-greeting.adoc create mode 100644 src/test/java/com/leanstacks/ws/AbstractDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/CreateGreetingDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/DeleteGreetingDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/GetGreetingDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java create mode 100644 src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java diff --git a/build.gradle b/build.gradle index 65df97e..322faec 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,6 +13,8 @@ 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' @@ -45,6 +48,9 @@ dependencies { 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' + + asciidoctor group: 'org.springframework.restdocs', name: 'spring-restdocs-asciidoctor' } defaultTasks 'clean', 'build' @@ -81,7 +87,15 @@ jacocoTestReport { } } -test.finalizedBy jacocoTestReport +test { + outputs.dir snippetsDir + finalizedBy jacocoTestReport +} + +asciidoctor { + inputs.dir snippetsDir + dependsOn test +} checkstyle { toolVersion = checkstyleVersion diff --git a/etc/pmd/ruleset.xml b/etc/pmd/ruleset.xml index 01b1a71..0b5750e 100644 --- a/etc/pmd/ruleset.xml +++ b/etc/pmd/ruleset.xml @@ -51,6 +51,12 @@ + + + + + + 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/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/resources/config/application.properties b/src/main/resources/config/application.properties index c46bbcf..e5281f9 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -4,9 +4,9 @@ ## # Profile Configuration -# profiles: hsqldb, mysql, batch, docs +# profiles: hsqldb, mysql, batch ## -spring.profiles.active=hsqldb,batch,docs +spring.profiles.active=hsqldb,batch ## # Web Server Configuration @@ -59,6 +59,7 @@ logging.level.com.leanstacks.ws=DEBUG logging.level.org.springboot=INFO logging.level.org.springframework=INFO logging.level.org.springframework.security=INFO +logging.level.org.springframework.restdocs=DEBUG # Uncomment the 2 hibernate appenders below to show SQL and params in logs logging.level.org.hibernate.SQL=DEBUG #logging.level.org.hibernate.type.descriptor.sql=TRACE diff --git a/src/test/java/com/leanstacks/ws/AbstractDocTest.java b/src/test/java/com/leanstacks/ws/AbstractDocTest.java new file mode 100644 index 0000000..ab4a84e --- /dev/null +++ b/src/test/java/com/leanstacks/ws/AbstractDocTest.java @@ -0,0 +1,75 @@ +package com.leanstacks.ws; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.leanstacks.ws.util.RequestContext; + +/** + * The AbstractDocTest class is the parent of all JUnit test classes which create Spring REST Docs. This class + * configures the test ApplicationContext and test runner environment to facilitate the creation of API documentation + * via Spring REST Docs. + * + * @author Matt Warman + */ +public abstract class AbstractDocTest { + + /** + * A MockMvc instance configured with Spring REST Docs configuration. + */ + protected transient MockMvc mockMvc; + + /** + * A WebApplicationContext instance. + */ + @Autowired + private transient WebApplicationContext context; + + /** + * A JUnit 4.x Rule for Spring REST Documentation generation. + */ + @Rule + public transient JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + + /** + * Perform set up activities before each unit test. Invoked by the JUnit framework. + */ + @Before + public void before() { + RequestContext.setUsername(AbstractTest.USERNAME); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).uris().withScheme("https") + .withHost("api.leanstacks.net").withPort(443).and().operationPreprocessors() + .withRequestDefaults(prettyPrint()).withResponseDefaults(prettyPrint())) + .build(); + doBeforeEachTest(); + } + + /** + * Perform initialization tasks before the execution of each test method. + */ + public abstract void doBeforeEachTest(); + + /** + * Perform clean up activities after each unit test. Invoked by the JUnit framework. + */ + @After + public void after() { + doAfterEachTest(); + } + + /** + * Perform clean up tasks after the execution of each test method. + */ + public abstract void doAfterEachTest(); + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/CreateGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/CreateGreetingDocTest.java new file mode 100644 index 0000000..c90d2ea --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/CreateGreetingDocTest.java @@ -0,0 +1,92 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class CreateGreetingDocTest extends AbstractDocTest { + + /** + * The HTTP request body content. + */ + private static final String REQUEST_BODY = "{ \"text\": \"Bonjour le monde!\" }"; + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for POST /api/greetings. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentCreateGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc + .perform(RestDocumentationRequestBuilders.post("/api/greetings").contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON).content(REQUEST_BODY)) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(MockMvcRestDocumentation.document("create-greeting", + PayloadDocumentation.relaxedRequestFields(PayloadDocumentation.fieldWithPath("text") + .description("The text.").type(JsonFieldType.STRING)), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation + .fieldWithPath("id") + .description( + "The identifier. Used to reference specific greetings in API requests.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("referenceId") + .description("The supplementary identifier.").type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("text").description("The text.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("version").description("The entity version.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") + .type(JsonFieldType.STRING).optional(), + PayloadDocumentation.fieldWithPath("updatedAt") + .description("The last modification timestamp.").type(JsonFieldType.STRING) + .optional()))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 201", 201, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/DeleteGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/DeleteGreetingDocTest.java new file mode 100644 index 0000000..478024d --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/DeleteGreetingDocTest.java @@ -0,0 +1,58 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class DeleteGreetingDocTest extends AbstractDocTest { + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for DELETE /api/greetings/{id}. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentDeleteGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/greetings/{id}", 1)) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(MockMvcRestDocumentation.document("delete-greeting")).andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 204", 204, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/GetGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/GetGreetingDocTest.java new file mode 100644 index 0000000..306a658 --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/GetGreetingDocTest.java @@ -0,0 +1,87 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.restdocs.request.RequestDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class GetGreetingDocTest extends AbstractDocTest { + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for GET /api/greetings/{id}. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentGetGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc + .perform(RestDocumentationRequestBuilders + .get("/api/greetings/{id}", "0").accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("get-greeting", + RequestDocumentation.pathParameters( + RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id") + .description( + "The identifier. Used to reference specific greetings in API requests.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("referenceId") + .description("The supplementary identifier.").type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("text").description("The text.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("version").description("The entity version.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") + .type(JsonFieldType.STRING).optional(), + PayloadDocumentation.fieldWithPath("updatedAt") + .description("The last modification timestamp.").type(JsonFieldType.STRING) + .optional()))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java b/src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java new file mode 100644 index 0000000..7b95b0c --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java @@ -0,0 +1,75 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class GetGreetingsDocTest extends AbstractDocTest { + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for GET /api/greetings. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentGetGreetings() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc + .perform(RestDocumentationRequestBuilders.get("/api/greetings").accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("get-greetings", + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("[].id").description( + "The identifier. Used to reference specific greetings in API requests."), + PayloadDocumentation.fieldWithPath("[].referenceId") + .description("The supplementary identifier."), + PayloadDocumentation.fieldWithPath("[].text").description("The text."), + PayloadDocumentation.fieldWithPath("[].version").description("The entity version."), + PayloadDocumentation.fieldWithPath("[].createdBy").description("The entity creator."), + PayloadDocumentation.fieldWithPath("[].createdAt") + .description("The creation timestamp."), + PayloadDocumentation.fieldWithPath("[].updatedBy").description("The last modifier."), + PayloadDocumentation.fieldWithPath("[].updatedAt") + .description("The last modification timestamp.")))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java new file mode 100644 index 0000000..f139b0f --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java @@ -0,0 +1,90 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.restdocs.request.RequestDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class SendGreetingDocTest extends AbstractDocTest { + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for POST /api/greetings/{id}/send. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentSendGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc + .perform(RestDocumentationRequestBuilders + .post("/api/greetings/{id}/send?wait=true", 1).accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("send-greeting", + RequestDocumentation.pathParameters( + RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), + RequestDocumentation.requestParameters(RequestDocumentation.parameterWithName("wait") + .description("Optional. Boolean. Wait for email to be sent.").optional()), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation + .fieldWithPath("id") + .description( + "The identifier. Used to reference specific greetings in API requests.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("referenceId") + .description("The supplementary identifier.").type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("text").description("The text.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("version").description("The entity version.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") + .type(JsonFieldType.STRING).optional(), + PayloadDocumentation.fieldWithPath("updatedAt") + .description("The last modification timestamp.").type(JsonFieldType.STRING) + .optional()))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); + + } + +} diff --git a/src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java b/src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java new file mode 100644 index 0000000..6b31a86 --- /dev/null +++ b/src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java @@ -0,0 +1,94 @@ +package com.leanstacks.ws.web.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.restdocs.request.RequestDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.leanstacks.ws.AbstractDocTest; +import com.leanstacks.ws.RestControllerTest; + +/** + *

+ * Generate REST API documentation for the GreetingController. + *

+ *

+ * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class + * responsible for unit testing functionality. + *

+ * + * @author Matt Warman + */ +@RunWith(SpringRunner.class) +@RestControllerTest +public class UpdateGreetingDocTest extends AbstractDocTest { + + /** + * The HTTP request body content. + */ + private static final String REQUEST_BODY = "{ \"text\": \"Bonjour le monde!\" }"; + + @Override + public void doBeforeEachTest() { + // perform test initialization + } + + @Override + public void doAfterEachTest() { + // perform test cleanup + } + + /** + * Generate API documentation for PUT /api/greetings/{id}. + * + * @throws Exception Thrown if documentation generation failure occurs. + */ + @Test + public void documentUpdateGreeting() throws Exception { + + // Generate API Documentation + final MvcResult result = this.mockMvc.perform(RestDocumentationRequestBuilders.put("/api/greetings/{id}", 1) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).content(REQUEST_BODY)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("update-greeting", + RequestDocumentation.pathParameters( + RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), + PayloadDocumentation.relaxedRequestFields(PayloadDocumentation.fieldWithPath("text") + .description("The text.").type(JsonFieldType.STRING)), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation + .fieldWithPath("id") + .description( + "The identifier. Used to reference specific greetings in API requests.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("referenceId") + .description("The supplementary identifier.").type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("text").description("The text.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("version").description("The entity version.") + .type(JsonFieldType.NUMBER), + PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") + .type(JsonFieldType.STRING), + PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") + .type(JsonFieldType.STRING).optional(), + PayloadDocumentation.fieldWithPath("updatedAt") + .description("The last modification timestamp.").type(JsonFieldType.STRING) + .optional()))) + .andReturn(); + + // Perform a simple, standard JUnit assertion to satisfy PMD rule + Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); + + } + +} From 336fbc2ace6dd9d31e2a2a81a842306d179066a3 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Sat, 15 Dec 2018 07:18:30 -0500 Subject: [PATCH 08/10] See #48. README --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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. From be899c8bb4f5f966252850b9690b75915987ec71 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Sat, 15 Dec 2018 07:24:23 -0500 Subject: [PATCH 09/10] See #48. Remove Swagger --- build.gradle | 4 +- pom.xml | 13 --- .../leanstacks/ws/ApiDocsConfiguration.java | 88 ------------------- .../com/leanstacks/ws/model/Greeting.java | 6 -- .../ws/model/TransactionalEntity.java | 37 -------- .../ws/security/SecurityConfiguration.java | 30 ------- .../ws/web/api/GreetingController.java | 67 ++------------ .../RestResponseEntityExceptionHandler.java | 2 +- .../ws/web/docs/ApiDocsController.java | 28 ------ .../resources/config/application-docs.yaml | 19 ---- 10 files changed, 7 insertions(+), 287 deletions(-) delete mode 100644 src/main/java/com/leanstacks/ws/ApiDocsConfiguration.java delete mode 100644 src/main/java/com/leanstacks/ws/web/docs/ApiDocsController.java delete mode 100644 src/main/resources/config/application-docs.yaml diff --git a/build.gradle b/build.gradle index 322faec..55fe8fe 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,6 @@ ext { jacocoVersion = '0.8.2' checkstyleVersion = '8.14' pmdVersion = '6.9.0' - swaggerVersion = '2.7.0' } group = 'com.leanstacks' @@ -40,8 +39,6 @@ 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' @@ -49,6 +46,7 @@ dependencies { 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' } diff --git a/pom.xml b/pom.xml index b617fcb..d6d8add 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,6 @@ UTF-8 11 - 2.7.0 @@ -66,18 +65,6 @@ - - - io.springfox - springfox-swagger2 - ${swagger.version} - - - io.springfox - springfox-swagger-ui - ${swagger.version} - - org.springframework.boot 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 1d54a54..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.3.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/model/Greeting.java b/src/main/java/com/leanstacks/ws/model/Greeting.java index f63b01d..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. * @@ -18,10 +16,6 @@ public class Greeting extends TransactionalEntity { /** * The text value. */ - @ApiModelProperty(value = "The actual text of the Greeting.", - required = true, - position = 100, - example = "Hello World!") @NotNull private String 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/security/SecurityConfiguration.java b/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java index 4586d86..b45fe69 100644 --- a/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java +++ b/src/main/java/com/leanstacks/ws/security/SecurityConfiguration.java @@ -5,7 +5,6 @@ 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; @@ -183,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/web/api/GreetingController.java b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java index a06b3ef..b54d2a5 100644 --- a/src/main/java/com/leanstacks/ws/web/api/GreetingController.java +++ b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java @@ -24,11 +24,6 @@ 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. @@ -62,15 +57,6 @@ public class GreetingController { * * @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")) @GetMapping public List getGreetings() { logger.info("> getGreetings"); @@ -93,16 +79,8 @@ public List getGreetings() { * @param id A Long URL path variable containing the Greeting primary key identifier. * @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")) @GetMapping("/{id}") - public Greeting getGreeting(@ApiParam("Greeting ID") @PathVariable final Long id) { + public Greeting getGreeting(@PathVariable final Long id) { logger.info("> getGreeting"); final Optional greetingOptional = greetingService.findOne(id); @@ -124,15 +102,6 @@ public Greeting getGreeting(@ApiParam("Greeting ID") @PathVariable final Long id * @param greeting The Greeting object to be created. * @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")) @PostMapping @ResponseStatus(HttpStatus.CREATED) public Greeting createGreeting(@RequestBody final Greeting greeting) { @@ -158,17 +127,8 @@ public Greeting createGreeting(@RequestBody final Greeting greeting) { * @param greeting The Greeting object to be updated. * @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")) @PutMapping("/{id}") - public Greeting updateGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id, - @RequestBody final Greeting greeting) { + public Greeting updateGreeting(@PathVariable("id") final Long id, @RequestBody final Greeting greeting) { logger.info("> updateGreeting"); greeting.setId(id); @@ -191,17 +151,9 @@ public Greeting updateGreeting(@ApiParam("Greeting ID") @PathVariable("id") fina * * @param id A Long URL path variable containing the Greeting primary key identifier. */ - @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")) @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id) { + public void deleteGreeting(@PathVariable("id") final Long id) { logger.info("> deleteGreeting"); greetingService.delete(id); @@ -223,18 +175,9 @@ public void deleteGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Lo * transmission. * @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")) @PostMapping("/{id}/send") - public Greeting sendGreeting(@ApiParam("Greeting ID") @PathVariable("id") final Long id, - @ApiParam("Wait for Response") @RequestParam(value = "wait", - defaultValue = "false") final boolean waitForAsyncResult) { + public Greeting sendGreeting(@PathVariable("id") final Long id, @RequestParam(value = "wait", + defaultValue = "false") final boolean waitForAsyncResult) { logger.info("> sendGreeting"); 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 281d77d..f39a691 100644 --- a/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java +++ b/src/main/java/com/leanstacks/ws/web/api/RestResponseEntityExceptionHandler.java @@ -74,7 +74,7 @@ public ResponseEntity handleNoSuchElementException(final NoSuchElementEx public ResponseEntity handleEmptyResultDataAccessException(final EmptyResultDataAccessException ex, final WebRequest request) { logger.info("> handleEmptyResultDataAccessException"); - logger.warn("- EmptyResultDataAccessException: ", ex); + logger.info("- EmptyResultDataAccessException: ", ex); final ExceptionDetail detail = new ExceptionDetailBuilder().exception(ex).httpStatus(HttpStatus.NOT_FOUND) .webRequest(request).build(); logger.info("< handleEmptyResultDataAccessException"); diff --git a/src/main/java/com/leanstacks/ws/web/docs/ApiDocsController.java b/src/main/java/com/leanstacks/ws/web/docs/ApiDocsController.java deleted file mode 100644 index 21594d1..0000000 --- a/src/main/java/com/leanstacks/ws/web/docs/ApiDocsController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.leanstacks.ws.web.docs; - -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -/** - * The ApiDocsController is a Spring MVC web controller class which serves the - * Swagger user interface HTML page. - * - * @author Matt Warman - */ -@Profile("docs") -@Controller -public class ApiDocsController { - - /** - * Request handler to serve the Swagger user interface HTML page configured - * to the mapped context path. - * - * @return A String name of the Swagger user interface HTML page name. - */ - @RequestMapping("/docs") - public String getSwaggerApiDocsPage() { - return "redirect:swagger-ui.html"; - } - -} diff --git a/src/main/resources/config/application-docs.yaml b/src/main/resources/config/application-docs.yaml deleted file mode 100644 index 0666f6e..0000000 --- a/src/main/resources/config/application-docs.yaml +++ /dev/null @@ -1,19 +0,0 @@ -GreetingController: - getGreetings: - title: 'Find all Greetings' - notes: 'Retrieves a Collection of Greeting objects from the data store and returns them as JSON.' - getGreeting: - title: 'Find Greeting by ID' - notes: 'Retrieves a Greeting object from the data store by its Identifier and returns it as JSON.' - createGreeting: - title: 'Create a Greeting' - notes: 'Creates a Greeting object in the data store and returns it as JSON.' - updateGreeting: - title: 'Update a Greeting' - notes: 'Updates a Greeting object in the data store and returns it as JSON.' - deleteGreeting: - title: 'Delete a Greeting' - notes: 'Deletes a Greeting object from the data store.' - sendGreeting: - title: 'Send a Greeting' - notes: 'Send a Greeting object via Email and returns it as JSON.' From 117b43c4de2ca7d5c3455fa325de90fcffab63d9 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Sat, 15 Dec 2018 08:31:13 -0500 Subject: [PATCH 10/10] See #48. Maven dependencies for Spring REST Docs --- pom.xml | 12 ++++++++++++ src/test/java/com/leanstacks/ws/AbstractDocTest.java | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d6d8add..abc307e 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,18 @@ spring-security-test test + + com.google.guava + guava + 27.0.1-jre + test + + + + + org.springframework.restdocs + spring-restdocs-mockmvc + diff --git a/src/test/java/com/leanstacks/ws/AbstractDocTest.java b/src/test/java/com/leanstacks/ws/AbstractDocTest.java index ab4a84e..e452d40 100644 --- a/src/test/java/com/leanstacks/ws/AbstractDocTest.java +++ b/src/test/java/com/leanstacks/ws/AbstractDocTest.java @@ -35,7 +35,9 @@ public abstract class AbstractDocTest { private transient WebApplicationContext context; /** - * A JUnit 4.x Rule for Spring REST Documentation generation. + * A JUnit 4.x Rule for Spring REST Documentation generation. Note that the snippet output directory is only + * provided because this project contains both 'build.gradle' and 'pom.xml' files. Spring REST Docs uses those files + * to auto-detect the build system and automatically sets certain configuration values which cannot be overridden. */ @Rule public transient JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets");