JpaRepository and additional custom
12 | * behaviors may be defined in this interface.
13 | *
14 | * @author Matt Warman
15 | */
16 | @Repository
17 | public interface GreetingRepository extends JpaRepository9 | * The AccountService interface defines all public business behaviors for operations on the Account entity model and 10 | * some related entities such as Role. 11 | *
12 | *13 | * This interface should be injected into AccountService clients, not the implementation bean. 14 | *
15 | * 16 | * @author Matt Warman 17 | */ 18 | public interface AccountService { 19 | 20 | /** 21 | * Find an Account by the username attribute value. 22 | * 23 | * @param username A String username to query the repository. 24 | * @return An Optional wrapped Account. 25 | */ 26 | OptionalJpaRepository and additional custom behaviors may be
13 | * defined in this interface.
14 | *
15 | * @author Matt Warman
16 | */
17 | @Repository
18 | public interface AccountRepository extends JpaRepository9 | * The EmailService interface defines all public business behaviors for composing and transmitting email messages. 10 | *
11 | *12 | * This interface should be injected into EmailService clients, not the implementation bean. 13 | *
14 | * 15 | * @author Matt Warman 16 | */ 17 | public interface EmailService { 18 | 19 | /** 20 | * Send a Greeting via email synchronously. 21 | * 22 | * @param greeting A Greeting to send. 23 | * @return A Boolean whose value is TRUE if sent successfully; otherwise FALSE. 24 | */ 25 | Boolean send(Greeting greeting); 26 | 27 | /** 28 | * Send a Greeting via email asynchronously. 29 | * 30 | * @param greeting A Greeting to send. 31 | */ 32 | void sendAsync(Greeting greeting); 33 | 34 | /** 35 | * Send a Greeting via email asynchronously. Returns a Future<Boolean> response allowing the client to obtain 36 | * the status of the operation once it is completed. 37 | * 38 | * @param greeting A Greeting to send. 39 | * @return A Future<Boolean> whose value is TRUE if sent successfully; otherwise, FALSE. 40 | */ 41 | Future8 | * The RequestContext facilitates the storage of information for the duration of a single request (or web service 9 | * transaction). 10 | *
11 | *12 | * RequestContext attributes are stored in ThreadLocal objects. 13 | *
14 | * 15 | * @author Matt Warman 16 | * 17 | */ 18 | public final class RequestContext { 19 | 20 | /** 21 | * The Logger for this Class. 22 | */ 23 | private static final Logger logger = LoggerFactory.getLogger(RequestContext.class); 24 | 25 | /** 26 | * ThreadLocal storage of username Strings. 27 | */ 28 | private static ThreadLocal10 | * The GreetingService interface defines all public business behaviors for operations on the Greeting entity model. 11 | *
12 | *13 | * This interface should be injected into GreetingService clients, not the implementation bean. 14 | *
15 | * 16 | * @author Matt Warman 17 | */ 18 | public interface GreetingService { 19 | 20 | /** 21 | * Find all Greeting entities. 22 | * 23 | * @return A List of Greeting objects. 24 | */ 25 | Listnull if a problem occurred.
40 | */
41 | Greeting create(Greeting greeting);
42 |
43 | /**
44 | * Updates a previously persisted Greeting entity in the data store.
45 | *
46 | * @param greeting A Greeting object to be updated.
47 | * @return An updated Greeting object or null if a problem occurred.
48 | */
49 | Greeting update(Greeting greeting);
50 |
51 | /**
52 | * Removes a previously persisted Greeting entity from the data store.
53 | *
54 | * @param id A Long primary key identifier.
55 | */
56 | void delete(Long id);
57 |
58 | /**
59 | * Evicts all members of the "greetings" cache.
60 | */
61 | void evictCache();
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/com/leanstacks/ws/service/EmailServiceBean.java:
--------------------------------------------------------------------------------
1 | package com.leanstacks.ws.service;
2 |
3 | import java.util.concurrent.Future;
4 |
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.scheduling.annotation.Async;
8 | import org.springframework.scheduling.annotation.AsyncResult;
9 | import org.springframework.stereotype.Service;
10 |
11 | import com.leanstacks.ws.model.Greeting;
12 |
13 | /**
14 | * The EmailServiceBean encapsulates all business behaviors defined by the EmailService interface.
15 | *
16 | * @author Matt Warman
17 | */
18 | @Service
19 | public class EmailServiceBean implements EmailService {
20 |
21 | /**
22 | * The Logger for this Class.
23 | */
24 | private static final Logger logger = LoggerFactory.getLogger(EmailServiceBean.class);
25 |
26 | /**
27 | * Default Thread sleep time in milliseconds.
28 | */
29 | private static final long SLEEP_MILLIS = 5000;
30 |
31 | @Override
32 | public Boolean send(final Greeting greeting) {
33 | logger.info("> send");
34 |
35 | Boolean success;
36 |
37 | // Simulate method execution time
38 | try {
39 | Thread.sleep(SLEEP_MILLIS);
40 | } catch (InterruptedException ie) {
41 | logger.info("- Thread interrupted.", ie);
42 | // Do nothing.
43 | }
44 | logger.info("Processing time was {} seconds.", SLEEP_MILLIS / 1000);
45 |
46 | success = Boolean.TRUE;
47 |
48 | logger.info("< send");
49 | return success;
50 | }
51 |
52 | @Async
53 | @Override
54 | public void sendAsync(final Greeting greeting) {
55 | logger.info("> sendAsync");
56 |
57 | send(greeting);
58 |
59 | logger.info("< sendAsync");
60 | }
61 |
62 | @Async
63 | @Override
64 | public Future17 | * Generate REST API documentation for the GreetingController. 18 | *
19 | *20 | * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class 21 | * responsible for unit testing functionality. 22 | *
23 | * 24 | * @author Matt Warman 25 | */ 26 | @RunWith(SpringRunner.class) 27 | @RestControllerTest 28 | public class DeleteGreetingDocTest extends AbstractDocTest { 29 | 30 | @Override 31 | public void doBeforeEachTest() { 32 | // perform test initialization 33 | } 34 | 35 | @Override 36 | public void doAfterEachTest() { 37 | // perform test cleanup 38 | } 39 | 40 | /** 41 | * Generate API documentation for DELETE /api/greetings/{id}. 42 | * 43 | * @throws Exception Thrown if documentation generation failure occurs. 44 | */ 45 | @Test 46 | public void documentDeleteGreeting() throws Exception { 47 | 48 | // Generate API Documentation 49 | final MvcResult result = this.mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/greetings/{id}", 1)) 50 | .andExpect(MockMvcResultMatchers.status().isNoContent()) 51 | .andDo(MockMvcRestDocumentation.document("delete-greeting")).andReturn(); 52 | 53 | // Perform a simple, standard JUnit assertion to satisfy PMD rule 54 | Assert.assertEquals("failure - expected HTTP status 204", 204, result.getResponse().getStatus()); 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/resources/config/application.properties: -------------------------------------------------------------------------------- 1 | ## 2 | # The Base Application Configuration File 3 | ## 4 | 5 | ## 6 | # Profile Configuration 7 | # profiles: hsqldb, mysql, batch 8 | ## 9 | spring.profiles.active=hsqldb,batch 10 | 11 | ## 12 | # Web Server Configuration 13 | ## 14 | #server.port= 15 | 16 | ## 17 | # Cache Configuration 18 | ## 19 | spring.cache.cache-names=greetings 20 | spring.cache.caffeine.spec=maximumSize=250,expireAfterAccess=600s 21 | 22 | ## 23 | # Task Execution (Async) Configuration 24 | ## 25 | spring.task.execution.pool.core-size=8 26 | 27 | ## 28 | # Task Scheduling Configuration 29 | ## 30 | spring.task.scheduling.pool.size=2 31 | 32 | ## 33 | # Data Source Configuration 34 | ## 35 | 36 | # Hibernate 37 | spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 38 | spring.jpa.properties.jadira.usertype.autoRegisterUserTypes=true 39 | 40 | # Liquibase 41 | spring.liquibase.change-log=classpath:/data/changelog/db.changelog-master.xml 42 | 43 | ## 44 | # Actuator Configuration 45 | ## 46 | management.endpoints.web.base-path=/actuators 47 | management.endpoints.web.exposure.include=* 48 | management.endpoints.web.exposure.exclude=shutdown 49 | 50 | management.endpoint.health.show-details=when-authorized 51 | management.endpoint.health.roles=SYSADMIN 52 | 53 | ## 54 | # Logging Configuration 55 | ## 56 | # Use a logging pattern easily parsed by aggregation tools. Comment to use standard Spring Boot logging pattern. 57 | logging.pattern.console=[%date{ISO8601}] [%clr(%-5level)] [${PID:-}] [%-15.15thread] [%-40.40logger{39}] [%m]%n 58 | logging.level.com.leanstacks.ws=DEBUG 59 | logging.level.org.springboot=INFO 60 | logging.level.org.springframework=INFO 61 | logging.level.org.springframework.security=INFO 62 | logging.level.org.springframework.restdocs=DEBUG 63 | # Uncomment the 2 hibernate appenders below to show SQL and params in logs 64 | logging.level.org.hibernate.SQL=DEBUG 65 | #logging.level.org.hibernate.type.descriptor.sql=TRACE 66 | 67 | ## 68 | # CORS Configuration 69 | ## 70 | leanstacks.cors.filter-registration-path=/** 71 | leanstacks.cors.allow-credentials=false 72 | leanstacks.cors.allowed-headers=accept,authorization,content-type 73 | leanstacks.cors.allowed-methods=GET,OPTIONS,POST,PUT,PATCH,DELETE 74 | leanstacks.cors.allowed-origins=* 75 | leanstacks.cors.exposed-headers= 76 | leanstacks.cors.max-age-seconds=3600 77 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /etc/pmd/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 |11 | * The BCryptPasswordEncoderUtil class assists engineers during application construction. It is not intended for use in 12 | * a 'live' application. 13 | *
14 | *15 | * The class uses a BCryptPasswordEncoder to encrypt clear text values using it's native hashing algorithm. This utility 16 | * may be used to create encrypted password values in a database initialization script used for unit testing or local 17 | * machine development. 18 | *
19 | * 20 | * @author Matt Warman 21 | * 22 | */ 23 | public class BCryptPasswordEncoderUtil { 24 | 25 | /** 26 | * The format for encoder messages. 27 | */ 28 | private static final String ENCODED_FORMAT = "Argument: %s \tEncoded: %s \n"; 29 | 30 | /** 31 | * A Writer for printing messages to the console. 32 | */ 33 | private transient Writer writer; 34 | 35 | /** 36 | * Uses a BCryptPasswordEncoder to hash the clear text value. 37 | * 38 | * @param clearText A String of clear text to be encrypted. 39 | * @return The encrypted (hashed) value. 40 | */ 41 | public String encode(final String clearText) { 42 | final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 43 | return encoder.encode(clearText); 44 | } 45 | 46 | /** 47 | * Facilitates gathering user input and invoking the class behavior. 48 | * 49 | * @param args An array of command line input values. (not used) 50 | * @throws IOException Thrown if performing IO operations fails. 51 | */ 52 | public static void main(final String... args) throws IOException { 53 | 54 | final BCryptPasswordEncoderUtil encoderUtil = new BCryptPasswordEncoderUtil(); 55 | 56 | for (final String arg : args) { 57 | final String encodedText = encoderUtil.encode(arg); 58 | final String message = String.format(ENCODED_FORMAT, arg, encodedText); 59 | encoderUtil.write(message); 60 | } 61 | 62 | encoderUtil.close(); 63 | 64 | } 65 | 66 | /** 67 | * Writes a message to the console. 68 | * 69 | * @param str A String message value. 70 | * @throws IOException Thrown if writing output fails. 71 | */ 72 | private void write(final String str) throws IOException { 73 | 74 | if (writer == null) { 75 | writer = new OutputStreamWriter(System.out); 76 | } 77 | writer.write(str); 78 | 79 | } 80 | 81 | /** 82 | * Closes all system resources and prepares for application termination. 83 | * 84 | * @throws IOException Thrown if closing the output stream fails. 85 | */ 86 | private void close() throws IOException { 87 | 88 | if (writer != null) { 89 | writer.close(); 90 | } 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/com/leanstacks/ws/AbstractDocTest.java: -------------------------------------------------------------------------------- 1 | package com.leanstacks.ws; 2 | 3 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; 4 | import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; 5 | 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.restdocs.JUnitRestDocumentation; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 13 | import org.springframework.web.context.WebApplicationContext; 14 | 15 | import com.leanstacks.ws.util.RequestContext; 16 | 17 | /** 18 | * The AbstractDocTest class is the parent of all JUnit test classes which create Spring REST Docs. This class 19 | * configures the test ApplicationContext and test runner environment to facilitate the creation of API documentation 20 | * via Spring REST Docs. 21 | * 22 | * @author Matt Warman 23 | */ 24 | public abstract class AbstractDocTest { 25 | 26 | /** 27 | * A MockMvc instance configured with Spring REST Docs configuration. 28 | */ 29 | protected transient MockMvc mockMvc; 30 | 31 | /** 32 | * A WebApplicationContext instance. 33 | */ 34 | @Autowired 35 | private transient WebApplicationContext context; 36 | 37 | /** 38 | * A JUnit 4.x Rule for Spring REST Documentation generation. Note that the snippet output directory is only 39 | * provided because this project contains both 'build.gradle' and 'pom.xml' files. Spring REST Docs uses those files 40 | * to auto-detect the build system and automatically sets certain configuration values which cannot be overridden. 41 | */ 42 | @Rule 43 | public transient JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); 44 | 45 | /** 46 | * Perform set up activities before each unit test. Invoked by the JUnit framework. 47 | */ 48 | @Before 49 | public void before() { 50 | RequestContext.setUsername(AbstractTest.USERNAME); 51 | this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) 52 | .apply(documentationConfiguration(this.restDocumentation).uris().withScheme("https") 53 | .withHost("api.leanstacks.net").withPort(443).and().operationPreprocessors() 54 | .withRequestDefaults(prettyPrint()).withResponseDefaults(prettyPrint())) 55 | .build(); 56 | doBeforeEachTest(); 57 | } 58 | 59 | /** 60 | * Perform initialization tasks before the execution of each test method. 61 | */ 62 | public abstract void doBeforeEachTest(); 63 | 64 | /** 65 | * Perform clean up activities after each unit test. Invoked by the JUnit framework. 66 | */ 67 | @After 68 | public void after() { 69 | doAfterEachTest(); 70 | } 71 | 72 | /** 73 | * Perform clean up tasks after the execution of each test method. 74 | */ 75 | public abstract void doAfterEachTest(); 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/leanstacks/ws/model/ReferenceEntity.java: -------------------------------------------------------------------------------- 1 | package com.leanstacks.ws.model; 2 | 3 | import java.io.Serializable; 4 | import java.time.Instant; 5 | 6 | import javax.persistence.Id; 7 | import javax.persistence.MappedSuperclass; 8 | import javax.validation.constraints.NotNull; 9 | 10 | /** 11 | * The parent class for all reference entities (i.e. reference data as opposed to transactional data). 12 | * 13 | * @see com.leanstacks.ws.model.TransactionalEntity 14 | * 15 | * @author Matt Warman 16 | */ 17 | @MappedSuperclass 18 | public class ReferenceEntity implements Serializable { 19 | 20 | /** 21 | * The default serial version UID. 22 | */ 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The primary key identifier. 27 | */ 28 | @Id 29 | private Long id; 30 | 31 | /** 32 | * The unique code value, sometimes used for external reference. 33 | */ 34 | @NotNull 35 | private String code; 36 | 37 | /** 38 | * A brief description of the entity. 39 | */ 40 | @NotNull 41 | private String label; 42 | 43 | /** 44 | * The ordinal value facilitates sorting the entities. 45 | */ 46 | @NotNull 47 | private Integer ordinal; 48 | 49 | /** 50 | * The timestamp at which the entity's values may be applied or used by the system. 51 | */ 52 | @NotNull 53 | private Instant effectiveAt; 54 | 55 | /** 56 | * The timestamp at which the entity's values cease to be used by the system. Ifnull the entity is not
57 | * expired.
58 | */
59 | private Instant expiresAt;
60 |
61 | /**
62 | * The timestamp when this entity instance was created.
63 | */
64 | @NotNull
65 | private Instant createdAt;
66 |
67 | public Long getId() {
68 | return id;
69 | }
70 |
71 | public void setId(final Long id) {
72 | this.id = id;
73 | }
74 |
75 | public String getCode() {
76 | return code;
77 | }
78 |
79 | public void setCode(final String code) {
80 | this.code = code;
81 | }
82 |
83 | public String getLabel() {
84 | return label;
85 | }
86 |
87 | public void setLabel(final String label) {
88 | this.label = label;
89 | }
90 |
91 | public Integer getOrdinal() {
92 | return ordinal;
93 | }
94 |
95 | public void setOrdinal(final Integer ordinal) {
96 | this.ordinal = ordinal;
97 | }
98 |
99 | public Instant getEffectiveAt() {
100 | return effectiveAt;
101 | }
102 |
103 | public void setEffectiveAt(final Instant effectiveAt) {
104 | this.effectiveAt = effectiveAt;
105 | }
106 |
107 | public Instant getExpiresAt() {
108 | return expiresAt;
109 | }
110 |
111 | public void setExpiresAt(final Instant expiresAt) {
112 | this.expiresAt = expiresAt;
113 | }
114 |
115 | public Instant getCreatedAt() {
116 | return createdAt;
117 | }
118 |
119 | public void setCreatedAt(final Instant createdAt) {
120 | this.createdAt = createdAt;
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/main/java/com/leanstacks/ws/security/AccountAuthenticationProvider.java:
--------------------------------------------------------------------------------
1 | package com.leanstacks.ws.security;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.security.authentication.BadCredentialsException;
7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
8 | import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
9 | import org.springframework.security.core.AuthenticationException;
10 | import org.springframework.security.core.userdetails.UserDetails;
11 | import org.springframework.security.crypto.password.PasswordEncoder;
12 | import org.springframework.stereotype.Component;
13 |
14 | import com.leanstacks.ws.util.RequestContext;
15 |
16 | /**
17 | *
18 | * A Spring Security AuthenticationProvider which extends AbstractUserDetailsAuthenticationProvider. This
19 | * class uses the AccountUserDetailsService to retrieve a UserDetails instance.
20 | *
22 | * A PasswordEncoder compares the supplied authentication credentials to those in the UserDetails. 23 | *
24 | * 25 | * @author Matt Warman 26 | */ 27 | @Component 28 | public class AccountAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { 29 | 30 | /** 31 | * The Logger for this Class. 32 | */ 33 | private static final Logger logger = LoggerFactory.getLogger(AccountAuthenticationProvider.class); 34 | 35 | /** 36 | * A Spring Security UserDetailsService implementation based upon the Account entity model. 37 | */ 38 | @Autowired 39 | private transient AccountUserDetailsService userDetailsService; 40 | 41 | /** 42 | * A PasswordEncoder instance to hash clear test password values. 43 | */ 44 | @Autowired 45 | private transient PasswordEncoder passwordEncoder; 46 | 47 | @Override 48 | protected void additionalAuthenticationChecks(final UserDetails userDetails, 49 | final UsernamePasswordAuthenticationToken token) throws AuthenticationException { 50 | logger.info("> additionalAuthenticationChecks"); 51 | 52 | if (token.getCredentials() == null || userDetails.getPassword() == null) { 53 | logger.info("< additionalAuthenticationChecks"); 54 | throw new BadCredentialsException("Credentials may not be null."); 55 | } 56 | 57 | if (!passwordEncoder.matches((String) token.getCredentials(), userDetails.getPassword())) { 58 | logger.info("< additionalAuthenticationChecks"); 59 | throw new BadCredentialsException("Invalid credentials."); 60 | } 61 | 62 | RequestContext.setUsername(userDetails.getUsername()); 63 | 64 | logger.info("< additionalAuthenticationChecks"); 65 | } 66 | 67 | @Override 68 | protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken token) 69 | throws AuthenticationException { 70 | logger.info("> retrieveUser"); 71 | 72 | final UserDetails userDetails = userDetailsService.loadUserByUsername(username); 73 | 74 | logger.info("< retrieveUser"); 75 | return userDetails; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/leanstacks/ws/web/api/GetGreetingsDocTest.java: -------------------------------------------------------------------------------- 1 | package com.leanstacks.ws.web.api; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; 8 | import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; 9 | import org.springframework.restdocs.payload.PayloadDocumentation; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | import org.springframework.test.web.servlet.MvcResult; 12 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 13 | 14 | import com.leanstacks.ws.AbstractDocTest; 15 | import com.leanstacks.ws.RestControllerTest; 16 | 17 | /** 18 | *19 | * Generate REST API documentation for the GreetingController. 20 | *
21 | *22 | * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class 23 | * responsible for unit testing functionality. 24 | *
25 | * 26 | * @author Matt Warman 27 | */ 28 | @RunWith(SpringRunner.class) 29 | @RestControllerTest 30 | public class GetGreetingsDocTest extends AbstractDocTest { 31 | 32 | @Override 33 | public void doBeforeEachTest() { 34 | // perform test initialization 35 | } 36 | 37 | @Override 38 | public void doAfterEachTest() { 39 | // perform test cleanup 40 | } 41 | 42 | /** 43 | * Generate API documentation for GET /api/greetings. 44 | * 45 | * @throws Exception Thrown if documentation generation failure occurs. 46 | */ 47 | @Test 48 | public void documentGetGreetings() throws Exception { 49 | 50 | // Generate API Documentation 51 | final MvcResult result = this.mockMvc 52 | .perform(RestDocumentationRequestBuilders.get("/api/greetings").accept(MediaType.APPLICATION_JSON)) 53 | .andExpect(MockMvcResultMatchers.status().isOk()) 54 | .andDo(MockMvcRestDocumentation.document("get-greetings", 55 | PayloadDocumentation.relaxedResponseFields( 56 | PayloadDocumentation.fieldWithPath("[].id").description( 57 | "The identifier. Used to reference specific greetings in API requests."), 58 | PayloadDocumentation.fieldWithPath("[].referenceId") 59 | .description("The supplementary identifier."), 60 | PayloadDocumentation.fieldWithPath("[].text").description("The text."), 61 | PayloadDocumentation.fieldWithPath("[].version").description("The entity version."), 62 | PayloadDocumentation.fieldWithPath("[].createdBy").description("The entity creator."), 63 | PayloadDocumentation.fieldWithPath("[].createdAt") 64 | .description("The creation timestamp."), 65 | PayloadDocumentation.fieldWithPath("[].updatedBy").description("The last modifier."), 66 | PayloadDocumentation.fieldWithPath("[].updatedAt") 67 | .description("The last modification timestamp.")))) 68 | .andReturn(); 69 | 70 | // Perform a simple, standard JUnit assertion to satisfy PMD rule 71 | Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); 72 | 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 |21 | * Generate REST API documentation for the GreetingController. 22 | *
23 | *24 | * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class 25 | * responsible for unit testing functionality. 26 | *
27 | * 28 | * @author Matt Warman 29 | */ 30 | @RunWith(SpringRunner.class) 31 | @RestControllerTest 32 | public class GetGreetingDocTest extends AbstractDocTest { 33 | 34 | @Override 35 | public void doBeforeEachTest() { 36 | // perform test initialization 37 | } 38 | 39 | @Override 40 | public void doAfterEachTest() { 41 | // perform test cleanup 42 | } 43 | 44 | /** 45 | * Generate API documentation for GET /api/greetings/{id}. 46 | * 47 | * @throws Exception Thrown if documentation generation failure occurs. 48 | */ 49 | @Test 50 | public void documentGetGreeting() throws Exception { 51 | 52 | // Generate API Documentation 53 | final MvcResult result = this.mockMvc 54 | .perform(RestDocumentationRequestBuilders 55 | .get("/api/greetings/{id}", "0").accept(MediaType.APPLICATION_JSON)) 56 | .andExpect(MockMvcResultMatchers.status().isOk()) 57 | .andDo(MockMvcRestDocumentation.document("get-greeting", 58 | RequestDocumentation.pathParameters( 59 | RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), 60 | PayloadDocumentation.relaxedResponseFields( 61 | PayloadDocumentation.fieldWithPath("id") 62 | .description( 63 | "The identifier. Used to reference specific greetings in API requests.") 64 | .type(JsonFieldType.NUMBER), 65 | PayloadDocumentation.fieldWithPath("referenceId") 66 | .description("The supplementary identifier.").type(JsonFieldType.STRING), 67 | PayloadDocumentation.fieldWithPath("text").description("The text.") 68 | .type(JsonFieldType.STRING), 69 | PayloadDocumentation.fieldWithPath("version").description("The entity version.") 70 | .type(JsonFieldType.NUMBER), 71 | PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") 72 | .type(JsonFieldType.STRING), 73 | PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") 74 | .type(JsonFieldType.STRING), 75 | PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") 76 | .type(JsonFieldType.STRING).optional(), 77 | PayloadDocumentation.fieldWithPath("updatedAt") 78 | .description("The last modification timestamp.").type(JsonFieldType.STRING) 79 | .optional()))) 80 | .andReturn(); 81 | 82 | // Perform a simple, standard JUnit assertion to satisfy PMD rule 83 | Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); 84 | 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/leanstacks/ws/model/Account.java: -------------------------------------------------------------------------------- 1 | package com.leanstacks.ws.model; 2 | 3 | import java.util.Set; 4 | 5 | import javax.persistence.CascadeType; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.JoinColumn; 9 | import javax.persistence.JoinTable; 10 | import javax.persistence.ManyToMany; 11 | import javax.validation.constraints.NotNull; 12 | 13 | /** 14 | * The Account class is an entity model object. An Account describes the security credentials and authentication flags 15 | * that permit access to application functionality. 16 | * 17 | * @author Matt Warman 18 | */ 19 | @Entity 20 | public class Account extends TransactionalEntity { 21 | 22 | private static final long serialVersionUID = 1L; 23 | 24 | /** 25 | * Login username. 26 | */ 27 | @NotNull 28 | private String username; 29 | 30 | /** 31 | * Login password. 32 | */ 33 | @NotNull 34 | private String password; 35 | 36 | /** 37 | * Account enabled status indicator. 38 | */ 39 | @NotNull 40 | private boolean enabled = true; 41 | 42 | /** 43 | * Credential status indicator. 44 | */ 45 | @NotNull 46 | private boolean credentialsexpired; 47 | 48 | /** 49 | * Account expired status indicator. 50 | */ 51 | @NotNull 52 | private boolean expired; 53 | 54 | /** 55 | * Account locked indicator. 56 | */ 57 | @NotNull 58 | private boolean locked; 59 | 60 | /** 61 | * Authorization information. 62 | */ 63 | @ManyToMany(fetch = FetchType.EAGER, 64 | cascade = CascadeType.ALL) 65 | @JoinTable(name = "AccountRole", 66 | joinColumns = @JoinColumn(name = "accountId", 67 | referencedColumnName = "id"), 68 | inverseJoinColumns = @JoinColumn(name = "roleId", 69 | referencedColumnName = "id")) 70 | private Set20 | * Generate REST API documentation for the GreetingController. 21 | *
22 | *23 | * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class 24 | * responsible for unit testing functionality. 25 | *
26 | * 27 | * @author Matt Warman 28 | */ 29 | @RunWith(SpringRunner.class) 30 | @RestControllerTest 31 | public class CreateGreetingDocTest extends AbstractDocTest { 32 | 33 | /** 34 | * The HTTP request body content. 35 | */ 36 | private static final String REQUEST_BODY = "{ \"text\": \"Bonjour le monde!\" }"; 37 | 38 | @Override 39 | public void doBeforeEachTest() { 40 | // perform test initialization 41 | } 42 | 43 | @Override 44 | public void doAfterEachTest() { 45 | // perform test cleanup 46 | } 47 | 48 | /** 49 | * Generate API documentation for POST /api/greetings. 50 | * 51 | * @throws Exception Thrown if documentation generation failure occurs. 52 | */ 53 | @Test 54 | public void documentCreateGreeting() throws Exception { 55 | 56 | // Generate API Documentation 57 | final MvcResult result = this.mockMvc 58 | .perform(RestDocumentationRequestBuilders.post("/api/greetings").contentType(MediaType.APPLICATION_JSON) 59 | .accept(MediaType.APPLICATION_JSON).content(REQUEST_BODY)) 60 | .andExpect(MockMvcResultMatchers.status().isCreated()) 61 | .andDo(MockMvcRestDocumentation.document("create-greeting", 62 | PayloadDocumentation.relaxedRequestFields(PayloadDocumentation.fieldWithPath("text") 63 | .description("The text.").type(JsonFieldType.STRING)), 64 | PayloadDocumentation.relaxedResponseFields( 65 | PayloadDocumentation 66 | .fieldWithPath("id") 67 | .description( 68 | "The identifier. Used to reference specific greetings in API requests.") 69 | .type(JsonFieldType.NUMBER), 70 | PayloadDocumentation.fieldWithPath("referenceId") 71 | .description("The supplementary identifier.").type(JsonFieldType.STRING), 72 | PayloadDocumentation.fieldWithPath("text").description("The text.") 73 | .type(JsonFieldType.STRING), 74 | PayloadDocumentation.fieldWithPath("version").description("The entity version.") 75 | .type(JsonFieldType.NUMBER), 76 | PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") 77 | .type(JsonFieldType.STRING), 78 | PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") 79 | .type(JsonFieldType.STRING), 80 | PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") 81 | .type(JsonFieldType.STRING).optional(), 82 | PayloadDocumentation.fieldWithPath("updatedAt") 83 | .description("The last modification timestamp.").type(JsonFieldType.STRING) 84 | .optional()))) 85 | .andReturn(); 86 | 87 | // Perform a simple, standard JUnit assertion to satisfy PMD rule 88 | Assert.assertEquals("failure - expected HTTP status 201", 201, result.getResponse().getStatus()); 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/com/leanstacks/ws/web/api/SendGreetingDocTest.java: -------------------------------------------------------------------------------- 1 | package com.leanstacks.ws.web.api; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; 8 | import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; 9 | import org.springframework.restdocs.payload.JsonFieldType; 10 | import org.springframework.restdocs.payload.PayloadDocumentation; 11 | import org.springframework.restdocs.request.RequestDocumentation; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.test.web.servlet.MvcResult; 14 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 15 | 16 | import com.leanstacks.ws.AbstractDocTest; 17 | import com.leanstacks.ws.RestControllerTest; 18 | 19 | /** 20 | *21 | * Generate REST API documentation for the GreetingController. 22 | *
23 | *24 | * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class 25 | * responsible for unit testing functionality. 26 | *
27 | * 28 | * @author Matt Warman 29 | */ 30 | @RunWith(SpringRunner.class) 31 | @RestControllerTest 32 | public class SendGreetingDocTest extends AbstractDocTest { 33 | 34 | @Override 35 | public void doBeforeEachTest() { 36 | // perform test initialization 37 | } 38 | 39 | @Override 40 | public void doAfterEachTest() { 41 | // perform test cleanup 42 | } 43 | 44 | /** 45 | * Generate API documentation for POST /api/greetings/{id}/send. 46 | * 47 | * @throws Exception Thrown if documentation generation failure occurs. 48 | */ 49 | @Test 50 | public void documentSendGreeting() throws Exception { 51 | 52 | // Generate API Documentation 53 | final MvcResult result = this.mockMvc 54 | .perform(RestDocumentationRequestBuilders 55 | .post("/api/greetings/{id}/send?wait=true", 1).accept(MediaType.APPLICATION_JSON)) 56 | .andExpect(MockMvcResultMatchers.status().isOk()) 57 | .andDo(MockMvcRestDocumentation.document("send-greeting", 58 | RequestDocumentation.pathParameters( 59 | RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), 60 | RequestDocumentation.requestParameters(RequestDocumentation.parameterWithName("wait") 61 | .description("Optional. Boolean. Wait for email to be sent.").optional()), 62 | PayloadDocumentation.relaxedResponseFields( 63 | PayloadDocumentation 64 | .fieldWithPath("id") 65 | .description( 66 | "The identifier. Used to reference specific greetings in API requests.") 67 | .type(JsonFieldType.NUMBER), 68 | PayloadDocumentation.fieldWithPath("referenceId") 69 | .description("The supplementary identifier.").type(JsonFieldType.STRING), 70 | PayloadDocumentation.fieldWithPath("text").description("The text.") 71 | .type(JsonFieldType.STRING), 72 | PayloadDocumentation.fieldWithPath("version").description("The entity version.") 73 | .type(JsonFieldType.NUMBER), 74 | PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") 75 | .type(JsonFieldType.STRING), 76 | PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") 77 | .type(JsonFieldType.STRING), 78 | PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") 79 | .type(JsonFieldType.STRING).optional(), 80 | PayloadDocumentation.fieldWithPath("updatedAt") 81 | .description("The last modification timestamp.").type(JsonFieldType.STRING) 82 | .optional()))) 83 | .andReturn(); 84 | 85 | // Perform a simple, standard JUnit assertion to satisfy PMD rule 86 | Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); 87 | 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/com/leanstacks/ws/web/api/UpdateGreetingDocTest.java: -------------------------------------------------------------------------------- 1 | package com.leanstacks.ws.web.api; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; 8 | import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; 9 | import org.springframework.restdocs.payload.JsonFieldType; 10 | import org.springframework.restdocs.payload.PayloadDocumentation; 11 | import org.springframework.restdocs.request.RequestDocumentation; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.test.web.servlet.MvcResult; 14 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 15 | 16 | import com.leanstacks.ws.AbstractDocTest; 17 | import com.leanstacks.ws.RestControllerTest; 18 | 19 | /** 20 | *21 | * Generate REST API documentation for the GreetingController. 22 | *
23 | *24 | * These tests utilize Spring's REST Docs Framework to generate API documentation. There is a separate test class 25 | * responsible for unit testing functionality. 26 | *
27 | * 28 | * @author Matt Warman 29 | */ 30 | @RunWith(SpringRunner.class) 31 | @RestControllerTest 32 | public class UpdateGreetingDocTest extends AbstractDocTest { 33 | 34 | /** 35 | * The HTTP request body content. 36 | */ 37 | private static final String REQUEST_BODY = "{ \"text\": \"Bonjour le monde!\" }"; 38 | 39 | @Override 40 | public void doBeforeEachTest() { 41 | // perform test initialization 42 | } 43 | 44 | @Override 45 | public void doAfterEachTest() { 46 | // perform test cleanup 47 | } 48 | 49 | /** 50 | * Generate API documentation for PUT /api/greetings/{id}. 51 | * 52 | * @throws Exception Thrown if documentation generation failure occurs. 53 | */ 54 | @Test 55 | public void documentUpdateGreeting() throws Exception { 56 | 57 | // Generate API Documentation 58 | final MvcResult result = this.mockMvc.perform(RestDocumentationRequestBuilders.put("/api/greetings/{id}", 1) 59 | .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).content(REQUEST_BODY)) 60 | .andExpect(MockMvcResultMatchers.status().isOk()) 61 | .andDo(MockMvcRestDocumentation.document("update-greeting", 62 | RequestDocumentation.pathParameters( 63 | RequestDocumentation.parameterWithName("id").description("The greeting identifier.")), 64 | PayloadDocumentation.relaxedRequestFields(PayloadDocumentation.fieldWithPath("text") 65 | .description("The text.").type(JsonFieldType.STRING)), 66 | PayloadDocumentation.relaxedResponseFields( 67 | PayloadDocumentation 68 | .fieldWithPath("id") 69 | .description( 70 | "The identifier. Used to reference specific greetings in API requests.") 71 | .type(JsonFieldType.NUMBER), 72 | PayloadDocumentation.fieldWithPath("referenceId") 73 | .description("The supplementary identifier.").type(JsonFieldType.STRING), 74 | PayloadDocumentation.fieldWithPath("text").description("The text.") 75 | .type(JsonFieldType.STRING), 76 | PayloadDocumentation.fieldWithPath("version").description("The entity version.") 77 | .type(JsonFieldType.NUMBER), 78 | PayloadDocumentation.fieldWithPath("createdBy").description("The entity creator.") 79 | .type(JsonFieldType.STRING), 80 | PayloadDocumentation.fieldWithPath("createdAt").description("The creation timestamp.") 81 | .type(JsonFieldType.STRING), 82 | PayloadDocumentation.fieldWithPath("updatedBy").description("The last modifier.") 83 | .type(JsonFieldType.STRING).optional(), 84 | PayloadDocumentation.fieldWithPath("updatedAt") 85 | .description("The last modification timestamp.").type(JsonFieldType.STRING) 86 | .optional()))) 87 | .andReturn(); 88 | 89 | // Perform a simple, standard JUnit assertion to satisfy PMD rule 90 | Assert.assertEquals("failure - expected HTTP status 200", 200, result.getResponse().getStatus()); 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/leanstacks/ws/security/CorsProperties.java: -------------------------------------------------------------------------------- 1 | package com.leanstacks.ws.security; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | 8 | /** 9 | * A container for CORS configuration values. 10 | * 11 | * @author Matt Warman 12 | * 13 | */ 14 | @ConfigurationProperties("leanstacks.cors") 15 | public class CorsProperties { 16 | 17 | /** 18 | * The path at which the CorsFilter is registered. The CorsFilter will handle all requests matching this path. 19 | */ 20 | private String filterRegistrationPath = "/**"; 21 | 22 | /** 23 | * The value of the Access-Control-Allow-Credentials header. 24 | */ 25 | private Boolean allowCredentials = false; 26 | 27 | /** 28 | * The value of the Access-Control-Allow-Headers header. 29 | */ 30 | private List@ControllerAdvice class which provides exception handling to all REST controllers.
20 | *
21 | * @author Matt Warman
22 | */
23 | @ControllerAdvice
24 | public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
25 |
26 | /**
27 | * The Logger for this Class.
28 | */
29 | private static final Logger logger = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class);
30 |
31 | /**
32 | * Handles JPA NoResultExceptions thrown from web service controller methods. Creates a response with an
33 | * ExceptionDetail body and HTTP status code 404, not found.
34 | *
35 | * @param ex A NoResultException instance.
36 | * @return A ResponseEntity with an ExceptionDetail response body and HTTP status code 404.
37 | */
38 | @ExceptionHandler(NoResultException.class)
39 | public ResponseEntity