diff --git a/README.md b/README.md index 87fb5e2..d67da98 100644 --- a/README.md +++ b/README.md @@ -1 +1,87 @@ -# skeleton-ws-spring-boot +# Project Skeleton for Spring Boot Web Services + +## Acknowledgements + +This is a [LEAN**STACKS**](http://www.leanstacks.com) solution. + +## Getting Started + +This is a project skeleton for a [Spring Boot](http://projects.spring.io/spring-boot/) RESTful web services application. + +## Languages + +This project is authored in Java. + +## Installation + +### Fork the Repository + +Fork the [Spring Boot web services skeleton project](https://github.com/mwarman/skeleton-ws-spring-boot) on GitHub. Clone the project to the host machine. + +### Dependencies + +The project requires the following dependencies be installed on the host machine: + +* Java Development Kit 7 or later +* Apache Maven 3 or later + +## Running + +The project uses [Maven](http://maven.apache.org/) for build, package, and test workflow automation. The following Maven goals are the most commonly used. + +### spring-boot:run + +The `spring-boot:run` Maven goal performs the following workflow steps: + +* compiles Java classes to the /target directory +* copies all resources to the /target directory +* starts an embedded Apache Tomcat server + +To execute the `spring-boot:run` Maven goal, type the following command at a terminal prompt in the project base directory. + +``` +mvn spring-boot:run +``` + +Type `ctrl-C` to halt the web server. + +This goal is used for local machine development and functional testing. Use the `package` goal for server deployment. + +### package + +The `package` Maven goal performs the following workflow steps: + +* compiles Java classes to the /target directory +* copies all resources to the /target directory +* prepares an executable JAR file in the /target directory + +The `package` Maven goal is designed to prepare the application for distribution to server environments. The application and all dependencies are packaged into a single, executable JAR file. + +To execute the `package` goal, type the following command at a terminal prompt in the project base directory. + +``` +mvn clean package +``` + +The application distribution artifact is placed in the /target directory and is named using the `artifactId` and `version` from the pom.xml file. To run the JAR file use the following command: + +``` +java -jar example-1.0.0.jar +``` + +### test + +The `test` Maven goal performs the following workflow steps: + +* compiles Java classes to the /target directory +* copies all resources to the /target directory +* executes the unit test suites +* produces unit test reports + +The `test` Maven goal is designed to allow engineers the means to run the unit test suites against the main source code. This goal may also be used on continuous integration servers such as Jenkins, etc. + +To execute the `test` Maven goal, type the following command at a terminal prompt in the project base directory. + +``` +mvn clean test +``` diff --git a/pom.xml b/pom.xml index 0ce72dd..cae194f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.leanstacks skeleton-ws-spring-boot - 0.1.0 + 0.2.0 org.springframework.boot @@ -18,6 +18,27 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.hsqldb + hsqldb + runtime + + + + + org.springframework + spring-context-support + + + com.google.guava + guava + 18.0 + diff --git a/src/main/java/com/leanstacks/ws/Application.java b/src/main/java/com/leanstacks/ws/Application.java index 0869428..526766b 100644 --- a/src/main/java/com/leanstacks/ws/Application.java +++ b/src/main/java/com/leanstacks/ws/Application.java @@ -2,15 +2,39 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.guava.GuavaCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.transaction.annotation.EnableTransactionManagement; /** - * Spring Boot Main Application Class. Entry point for the application. + * Spring Boot main application class. + * + * @author Matt Warman */ @SpringBootApplication -public class Application -{ - public static void main( String[] args ) throws Exception - { +@EnableTransactionManagement +@EnableCaching +@EnableScheduling +public class Application { + + /** + * Entry point for the application. + * @param args Command line arguments. + * @throws Exception Thrown when an unexpected exception is thrown from the + * application. + */ + public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } + + @Bean + public CacheManager cacheManager() { + GuavaCacheManager cacheManager = new GuavaCacheManager("greetings"); + + return cacheManager; + } + } diff --git a/src/main/java/com/leanstacks/ws/batch/GreetingBatchBean.java b/src/main/java/com/leanstacks/ws/batch/GreetingBatchBean.java new file mode 100644 index 0000000..0171543 --- /dev/null +++ b/src/main/java/com/leanstacks/ws/batch/GreetingBatchBean.java @@ -0,0 +1,132 @@ +package com.leanstacks.ws.batch; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.leanstacks.ws.model.Greeting; +import com.leanstacks.ws.service.GreetingService; + +/** + * The GreetingBatchBean contains @Scheduled methods operating on + * Greeting entities to perform batch operations. + * + * @author Matt Warman + */ +@Component +public class GreetingBatchBean { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private GreetingService greetingService; + + /** + * Use a cron expression to execute logic on a schedule. Expression: second + * minute hour day-of-month month weekday + * + * @see http + * ://docs.spring.io/spring/docs/current/javadoc-api/org/springframework + * /scheduling/support/CronSequenceGenerator.html + */ + @Scheduled( + cron = "${batch.greeting.cron}") + public void cronJob() { + logger.info("> cronJob"); + + // Add scheduled logic here + + Collection greetings = greetingService.findAll(); + logger.info("There are {} greetings in the data store.", + greetings.size()); + + logger.info("< cronJob"); + } + + /** + * Execute logic beginning at fixed intervals. Use the + * fixedRate element to indicate how frequently the method is + * to be invoked. + */ + @Scheduled( + fixedRateString = "${batch.greeting.fixedrate}") + public void fixedRateJob() { + logger.info("> fixedRateJob"); + + // Add scheduled logic here + + Collection greetings = greetingService.findAll(); + logger.info("There are {} greetings in the data store.", + greetings.size()); + + logger.info("< fixedRateJob"); + } + + /** + * Execute logic beginning at fixed intervals with a delay after the + * application starts. Use the fixedRate element to indicate + * how frequently the method is to be invoked. Use the + * initialDelay element to indicate how long to wait after + * application startup to schedule the first execution. + */ + @Scheduled( + initialDelayString = "${batch.greeting.initialdelay}", + fixedRateString = "${batch.greeting.fixedrate}") + public void fixedRateJobWithInitialDelay() { + logger.info("> fixedRateJobWithInitialDelay"); + + // Add scheduled logic here + + Collection greetings = greetingService.findAll(); + logger.info("There are {} greetings in the data store.", + greetings.size()); + + logger.info("< fixedRateJobWithInitialDelay"); + } + + /** + * Execute logic with a delay between the end of the last execution and the + * beginning of the next. Use the fixedDelay element to + * indicate the time to wait between executions. + */ + @Scheduled( + fixedDelayString = "${batch.greeting.fixeddelay}") + public void fixedDelayJob() { + logger.info("> fixedDelayJob"); + + // Add scheduled logic here + + Collection greetings = greetingService.findAll(); + logger.info("There are {} greetings in the data store.", + greetings.size()); + + logger.info("< fixedDelayJob"); + } + + /** + * Execute logic with a delay between the end of the last execution and the + * beginning of the next. Use the fixedDelay element to + * indicate the time to wait between executions. Use the + * initialDelay element to indicate how long to wait after + * application startup to schedule the first execution. + */ + @Scheduled( + initialDelayString = "${batch.greeting.initialdelay}", + fixedDelayString = "${batch.greeting.fixeddelay}") + public void fixedDelayJobWithInitialDelay() { + logger.info("> fixedDelayJobWithInitialDelay"); + + // Add scheduled logic here + + Collection greetings = greetingService.findAll(); + logger.info("There are {} greetings in the data store.", + greetings.size()); + + logger.info("< fixedDelayJobWithInitialDelay"); + } + +} diff --git a/src/main/java/com/leanstacks/ws/model/Greeting.java b/src/main/java/com/leanstacks/ws/model/Greeting.java new file mode 100644 index 0000000..5259f23 --- /dev/null +++ b/src/main/java/com/leanstacks/ws/model/Greeting.java @@ -0,0 +1,43 @@ +package com.leanstacks.ws.model; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.validation.constraints.NotNull; + +/** + * The Greeting class is an entity model object. + * + * @author Matt Warman + */ +@Entity +public class Greeting { + + @Id + @GeneratedValue + private Long id; + + @NotNull + private String text; + + public Greeting() { + + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + +} diff --git a/src/main/java/com/leanstacks/ws/repository/GreetingRepository.java b/src/main/java/com/leanstacks/ws/repository/GreetingRepository.java new file mode 100644 index 0000000..46fda5b --- /dev/null +++ b/src/main/java/com/leanstacks/ws/repository/GreetingRepository.java @@ -0,0 +1,11 @@ +package com.leanstacks.ws.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.leanstacks.ws.model.Greeting; + +@Repository +public interface GreetingRepository extends JpaRepository { + +} diff --git a/src/main/java/com/leanstacks/ws/service/GreetingService.java b/src/main/java/com/leanstacks/ws/service/GreetingService.java new file mode 100644 index 0000000..e13bab4 --- /dev/null +++ b/src/main/java/com/leanstacks/ws/service/GreetingService.java @@ -0,0 +1,53 @@ +package com.leanstacks.ws.service; + +import java.util.Collection; + +import com.leanstacks.ws.model.Greeting; + +/** + * The GreetingService interface defines all public business behaviors for + * operations on the Greeting entity model. + * + * This interface should be injected into GreetingService clients, not the + * implementation bean. + * + * @author Matt Warman + */ +public interface GreetingService { + + /** + * Find all Greeting entities. + * @return A Collection of Greeting objects. + */ + Collection findAll(); + + /** + * Find a single Greeting entity by primary key identifier. + * @param id A BigInteger primary key identifier. + * @return A Greeting or null if none found. + */ + Greeting findOne(Long id); + + /** + * Persists a Greeting entity in the data store. + * @param greeting A Greeting object to be persisted. + * @return A persisted Greeting object or null if a problem + * occurred. + */ + Greeting create(Greeting greeting); + + /** + * Updates a previously persisted Greeting entity in the data store. + * @param greeting A Greeting object to be updated. + * @return An updated Greeting object or null if a problem + * occurred. + */ + Greeting update(Greeting greeting); + + /** + * Removes a previously persisted Greeting entity from the data store. + * @param id A BigInteger 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 new file mode 100644 index 0000000..c1dda9f --- /dev/null +++ b/src/main/java/com/leanstacks/ws/service/GreetingServiceBean.java @@ -0,0 +1,114 @@ +package com.leanstacks.ws.service; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.leanstacks.ws.model.Greeting; +import com.leanstacks.ws.repository.GreetingRepository; + +/** + * The GreetingServiceBean encapsulates all business behaviors operating on the + * Greeting entity model. + * + * @author Matt Warman + */ +@Service +public class GreetingServiceBean implements GreetingService { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private GreetingRepository greetingRepository; + + @Override + public Collection findAll() { + logger.info("> findAll"); + + Collection greetings = greetingRepository.findAll(); + + logger.info("< findAll"); + return greetings; + } + + @Cacheable( + value = "greetings", + key = "#id") + @Override + public Greeting findOne(Long id) { + logger.info("> findOne {}", id); + + Greeting greeting = greetingRepository.findOne(id); + + logger.info("< findOne {}", id); + return greeting; + } + + @CachePut( + value = "greetings", + key = "#result.id") + @Transactional + @Override + public Greeting create(Greeting greeting) { + logger.info("> create"); + + // Ensure the entity object to be created does NOT exist in the + // repository. Prevent the default behavior of save() which will update + // an existing entity if the entity matching the supplied id exists. + if (greeting.getId() != null) { + logger.error("Attempted to create a Greeting, but id attribute was not null."); + logger.info("< create"); + return null; + } + + Greeting savedGreeting = greetingRepository.save(greeting); + + logger.info("< create"); + return savedGreeting; + } + + @CachePut( + value = "greetings", + key = "#greeting.id") + @Transactional + @Override + public Greeting update(Greeting greeting) { + logger.info("> update {}", greeting.getId()); + + // Ensure the entity object to be updated exists in the repository to + // prevent the default behavior of save() which will persist a new + // entity if the entity matching the id does not exist + 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()); + return null; + } + + Greeting updatedGreeting = greetingRepository.save(greeting); + + logger.info("< update {}", greeting.getId()); + return updatedGreeting; + } + + @CacheEvict( + value = "greetings", + key = "#id") + @Transactional + @Override + public void delete(Long id) { + logger.info("> delete {}", id); + + greetingRepository.delete(id); + + logger.info("< delete {}", id); + } + +} diff --git a/src/main/java/com/leanstacks/ws/web/api/GreetingController.java b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java new file mode 100644 index 0000000..988a4a4 --- /dev/null +++ b/src/main/java/com/leanstacks/ws/web/api/GreetingController.java @@ -0,0 +1,131 @@ +package com.leanstacks.ws.web.api; + +import java.util.Collection; + +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.ExceptionHandler; +import org.springframework.web.bind.annotation.PathVariable; +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.RestController; + +import com.leanstacks.ws.model.Greeting; +import com.leanstacks.ws.service.GreetingService; + +/** + * The GreetingController class is a RESTful web service controller. The + * @RestController annotation informs Spring that each + * @RequestMapping method returns a @ResponseBody. + * + * @author Matt Warman + */ +@RestController +public class GreetingController { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private GreetingService greetingService; + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + logger.error("> handleException"); + logger.error("- Exception: ", e); + logger.error("< handleException"); + return new ResponseEntity(e, + HttpStatus.INTERNAL_SERVER_ERROR); + } + + @RequestMapping( + value = "/api/greetings", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getGreetings() throws Exception { + logger.info("> getGreetings"); + + Collection greetings = greetingService.findAll(); + + logger.info("< getGreetings"); + return new ResponseEntity>(greetings, + HttpStatus.OK); + } + + @RequestMapping( + value = "/api/greetings/{id}", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getGreeting(@PathVariable Long id) + throws Exception { + logger.info("> getGreeting"); + + Greeting greeting = greetingService.findOne(id); + if (greeting == null) { + logger.info("< getGreeting"); + return new ResponseEntity(HttpStatus.NOT_FOUND); + } + + logger.info("< getGreeting"); + return new ResponseEntity(greeting, HttpStatus.OK); + } + + @RequestMapping( + value = "/api/greetings", + method = RequestMethod.POST, + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createGreeting( + @RequestBody Greeting greeting) throws Exception { + logger.info("> createGreeting"); + + Greeting savedGreeting = greetingService.create(greeting); + if (savedGreeting == null) { + logger.info("< createGreeting"); + return new ResponseEntity( + HttpStatus.INTERNAL_SERVER_ERROR); + } + + logger.info("< createGreeting"); + return new ResponseEntity(savedGreeting, HttpStatus.CREATED); + } + + @RequestMapping( + value = "/api/greetings/{id}", + method = RequestMethod.PUT, + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity updateGreeting( + @RequestBody Greeting greeting) throws Exception { + logger.info("> updateGreeting"); + + Greeting updatedGreeting = greetingService.update(greeting); + if (updatedGreeting == null) { + logger.info("< updateGreeting"); + return new ResponseEntity( + HttpStatus.INTERNAL_SERVER_ERROR); + } + + logger.info("< updateGreeting"); + return new ResponseEntity(updatedGreeting, HttpStatus.OK); + } + + @RequestMapping( + value = "/api/greetings/{id}", + method = RequestMethod.DELETE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity deleteGreeting(@PathVariable("id") Long id, + @RequestBody Greeting greeting) throws Exception { + logger.info("> deleteGreeting"); + + greetingService.delete(id); + + logger.info("< deleteGreeting"); + return new ResponseEntity(HttpStatus.NO_CONTENT); + } + +} diff --git a/src/main/java/com/leanstacks/ws/web/filter/SimpleCORSFilter.java b/src/main/java/com/leanstacks/ws/web/filter/SimpleCORSFilter.java new file mode 100644 index 0000000..0dd2d0f --- /dev/null +++ b/src/main/java/com/leanstacks/ws/web/filter/SimpleCORSFilter.java @@ -0,0 +1,56 @@ +package com.leanstacks.ws.web.filter; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +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; + +/** + * 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 implements Filter { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public void destroy() { + + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, + FilterChain chain) throws IOException, ServletException { + logger.info("> doFilter"); + + HttpServletResponse response = (HttpServletResponse) res; + 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"); + + logger.info("< doFilter"); + chain.doFilter(req, res); + } + + @Override + public void init(FilterConfig config) throws ServletException { + + } + +} diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties new file mode 100644 index 0000000..e244104 --- /dev/null +++ b/src/main/resources/config/application.properties @@ -0,0 +1,23 @@ +## +# The Base Application Configuration File +## + +## +# Web Server Configuration +## +server.port= + +## +# Data Source Configuration +## + +# Initialization +spring.datasource.data=classpath:/data/hsqldb/data.sql + +## +# Batch Configuration +## +batch.greeting.fixedrate=360000 +batch.greeting.fixeddelay=360000 +batch.greeting.initialdelay=15000 +batch.greeting.cron=0 0 * * * * \ No newline at end of file diff --git a/src/main/resources/data/hsqldb/data.sql b/src/main/resources/data/hsqldb/data.sql new file mode 100644 index 0000000..55190bc --- /dev/null +++ b/src/main/resources/data/hsqldb/data.sql @@ -0,0 +1,2 @@ +INSERT INTO Greeting (text) VALUES ('Hello World!'); +INSERT INTO Greeting (text) VALUES ('Hola Mundo!'); \ No newline at end of file