diff --git a/.gitignore b/.gitignore index 5b3874d..83d1a68 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ /nbdist/ /.nb-gradle/ /bin/ +/.idea/ diff --git a/README.md b/README.md index c413aad..58d699b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # springboot-course-data-api -Simple POC using Spring Boot to create a Course API. +A simple POC using Spring Boot to create a Course API. ---------------------------------------------------- Spring Boot Dependencies Used: diff --git a/derby.log b/derby.log deleted file mode 100644 index 8958f11..0000000 --- a/derby.log +++ /dev/null @@ -1,13 +0,0 @@ ----------------------------------------------------------------- -Mon Apr 13 16:49:18 IST 2020: -Booting Derby version The Apache Software Foundation - Apache Derby - 10.14.2.0 - (1828579): instance a816c00e-0171-7343-d5dd-00000a619e30 -on database directory memory:C:\Users\Haripr\git\springboot-course-data-api\testdb with class loader sun.misc.Launcher$AppClassLoader@42a57993 -Loaded from file:/C:/Users/Haripr/.m2/repository/org/apache/derby/derby/10.14.2.0/derby-10.14.2.0.jar -java.vendor=Oracle Corporation -java.runtime.version=1.8.0_221-b11 -user.dir=C:\Users\Haripr\git\springboot-course-data-api -os.name=Windows 10 -os.arch=amd64 -os.version=10.0 -derby.system.home=null -Database Class Loader started - derby.database.classpath='' diff --git a/pom.xml b/pom.xml index 9be6a8b..61edf7e 100644 --- a/pom.xml +++ b/pom.xml @@ -28,14 +28,35 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-devtools + runtime + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + + io.springfox + springfox-swagger-ui + 2.9.2 + - org.apache.derby - derby + com.h2database + h2 runtime diff --git a/src/main/java/in/hp/java/CourseDataApiApplication.java b/src/main/java/in/hp/java/CourseDataApiApplication.java index 65fdf7c..6f56190 100644 --- a/src/main/java/in/hp/java/CourseDataApiApplication.java +++ b/src/main/java/in/hp/java/CourseDataApiApplication.java @@ -2,6 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; + +import java.util.Locale; @SpringBootApplication public class CourseDataApiApplication { @@ -9,4 +14,21 @@ public class CourseDataApiApplication { public static void main(String[] args) { SpringApplication.run(CourseDataApiApplication.class, args); } + + /** + * In this bean we are configuring default locale and returning + * If a locale is not being sent in request this default will be used + * + * Since we are fetching from header here + * replacing SessionLocaleResolver to AcceptHeaderLocaleResolver + * + * @return AcceptHeaderLocaleResolver + */ + @Bean + public LocaleResolver buildLocale() { + AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver(); + acceptHeaderLocaleResolver.setDefaultLocale(Locale.US); + return acceptHeaderLocaleResolver; + } + } diff --git a/src/main/java/in/hp/java/SwaggerConfig.java b/src/main/java/in/hp/java/SwaggerConfig.java new file mode 100644 index 0000000..56ef9e1 --- /dev/null +++ b/src/main/java/in/hp/java/SwaggerConfig.java @@ -0,0 +1,60 @@ +package in.hp.java; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.service.VendorExtension; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + private static final Contact DEFAULT_CONTACT = new Contact( + "Hariprasath", + "https://www.github.com/hariprasath-r", + ""); + + private static final ApiInfo DEFAULT_API_INFO = new ApiInfo( + "Course Data API", + "Provides Creation and Maintaining Courses", + "1.0", + "urn:tos", + DEFAULT_CONTACT, + "Apache 2.0", + "http://www.apache.org/licenses/LICENSE-2.0", + new ArrayList()); + + private static final Set DEFAULT_PRODUCES_CONSUMES = + new HashSet<>(Arrays.asList("application/json", "application.xml")); + + /** + * Configuring and building Docket Object + * select() - allows to build the Docket with ApiSelectBuilder + * apis() - allows to configure the basePackage from which the models will be read + * paths() - allows to configure URI/resource path + * build() - returns a Docket object + * + * @return + */ + @Bean + public Docket configureDocket() { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.basePackage("in.hp.java")) +// .paths(PathSelectors.ant("/*")) + .build() // building Docker Object + .apiInfo(DEFAULT_API_INFO) + .produces(DEFAULT_PRODUCES_CONSUMES) + .consumes(DEFAULT_PRODUCES_CONSUMES); + } +} diff --git a/src/main/java/in/hp/java/boot/InternationalizationDemo.java b/src/main/java/in/hp/java/boot/InternationalizationDemo.java new file mode 100644 index 0000000..df21bdc --- /dev/null +++ b/src/main/java/in/hp/java/boot/InternationalizationDemo.java @@ -0,0 +1,36 @@ +package in.hp.java.boot; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController() +public class InternationalizationDemo { + + /** + * Autowiring message source to read the messages against passed input + */ + @Autowired + ResourceBundleMessageSource messageSource; + + @GetMapping("/greeting") + public String greet() { + return "Good Morning"; + } + + /** + * Injecting the locale from request from the header + * Accept-Language - where locale is passed + * required - false, since we already have default locale configured + * + * Instead of accepting from header everywhere, we can use LocaleContextHolder to retrieve the value + * + * @return + */ + @GetMapping("/i18n") + public String internationalGreeting() { + return messageSource.getMessage("good.morning.message", null, LocaleContextHolder.getLocale()); + } +} diff --git a/src/main/java/in/hp/java/boot/controller/course/CourseController.java b/src/main/java/in/hp/java/boot/controller/course/CourseController.java deleted file mode 100644 index 956ca7a..0000000 --- a/src/main/java/in/hp/java/boot/controller/course/CourseController.java +++ /dev/null @@ -1,56 +0,0 @@ -package in.hp.java.boot.controller.course; - -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -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.RestController; - -import in.hp.java.boot.controller.topic.Topic; - -/** - * Added Root RequestMapping topics URI - * @author haripr - * - */ -@RestController -@RequestMapping("/topics") -public class CourseController { - - @Autowired - private CourseService courseService; - - @GetMapping(value = "/{topicId}/courses") - public List getCourses(@PathVariable String topicId) { - return courseService.getAllCourses(topicId); - } - - @GetMapping(value = "/{topicId}/courses/{id}") - public Course getCourse(@PathVariable String id) { - return courseService.getCourse(id); - } - - @PostMapping(value = "/{topicId}/courses") - public void addCourse(@PathVariable String topicId, @RequestBody Course course) { - course.setTopic(new Topic(topicId, "", "")); - courseService.addCourse(course); - } - - @PutMapping(value = "/{topicId}/courses/{id}") - public void updateCourse(@RequestBody Course course, @PathVariable String topicId, @PathVariable String id) { - course.setTopic(new Topic(topicId, "", "")); - courseService.updateCourse(course); - } - - @DeleteMapping(value = "/{topicId}/courses/{id}") - public void deleteCourse(@PathVariable String id) { - courseService.deleteCourse(id); - } - -} \ No newline at end of file diff --git a/src/main/java/in/hp/java/boot/controller/topic/TopicController.java b/src/main/java/in/hp/java/boot/controller/topic/TopicController.java deleted file mode 100644 index 94b55cd..0000000 --- a/src/main/java/in/hp/java/boot/controller/topic/TopicController.java +++ /dev/null @@ -1,52 +0,0 @@ -package in.hp.java.boot.controller.topic; - -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -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.RestController; - -/** - * Added Root RequestMapping topics URI - * @author haripr - * - */ -@RestController -@RequestMapping("/topics") -public class TopicController { - - @Autowired - private TopicService topicService; - - @GetMapping - public List getTopics() { - return topicService.getAllTopics(); - } - - @GetMapping(value = "/{id}") - public Topic getTopic(@PathVariable String id) { - return topicService.getTopic(id); - } - - @PostMapping - public void addTopic(@RequestBody Topic topic) { - topicService.addTopic(topic); - } - - @PutMapping(value = "/{id}") - public void updateTopic(@RequestBody Topic topic, @PathVariable String id) { - topicService.updateTopic(topic); - } - - @DeleteMapping(value = "/{id}") - public void deleteTopic(@PathVariable String id) { - topicService.deleteTopic(id); - } - -} \ No newline at end of file diff --git a/src/main/java/in/hp/java/boot/controller/topic/TopicRepository.java b/src/main/java/in/hp/java/boot/controller/topic/TopicRepository.java deleted file mode 100644 index faae213..0000000 --- a/src/main/java/in/hp/java/boot/controller/topic/TopicRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package in.hp.java.boot.controller.topic; - -import org.springframework.data.repository.CrudRepository; - -public interface TopicRepository extends CrudRepository {} diff --git a/src/main/java/in/hp/java/boot/controller/course/Course.java b/src/main/java/in/hp/java/boot/course/Course.java similarity index 60% rename from src/main/java/in/hp/java/boot/controller/course/Course.java rename to src/main/java/in/hp/java/boot/course/Course.java index faef2fc..40326d8 100644 --- a/src/main/java/in/hp/java/boot/controller/course/Course.java +++ b/src/main/java/in/hp/java/boot/course/Course.java @@ -1,27 +1,46 @@ -package in.hp.java.boot.controller.course; +package in.hp.java.boot.course; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import in.hp.java.boot.topic.Topic; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.ManyToOne; - -import in.hp.java.boot.controller.topic.Topic; - +import javax.validation.constraints.Size; + +/** + * @JsonIgnoreProperties - used to ignore certain properties during Json conversion + * @JsonIgnore - recommended to use + * @JsonFilter - Indicated that the bean can be filtered + */ +//@JsonIgnoreProperties(value = {"topic"}) +//@JsonFilter("SomeBeanFilter") +@ApiModel("Course Details") @Entity public class Course { @Id + @Size(min = 2, message = "Id should be of 2 chars minimum") + @ApiModelProperty(notes = "Id should be of 2 chars minimum") private String id; + + @Size(min = 4, message = "Name should be of 4 chars minimum") + @ApiModelProperty(notes = "Name should be of 4 chars minimum") private String name; private String description; /* * Adding JPA @ManyToOne annotation to let Spring JPA know it needs to establish * a Foreign Key relationship - * * FetchType can be made LAZY + * + * @JsonIgnore is used to ignore this field in response */ @ManyToOne(fetch = FetchType.EAGER) + @JsonIgnore private Topic topic; public Course() { @@ -76,3 +95,4 @@ public void setTopic(Topic topic) { } } + diff --git a/src/main/java/in/hp/java/boot/course/CourseController.java b/src/main/java/in/hp/java/boot/course/CourseController.java new file mode 100644 index 0000000..f31f6a6 --- /dev/null +++ b/src/main/java/in/hp/java/boot/course/CourseController.java @@ -0,0 +1,72 @@ +package in.hp.java.boot.course; + +import in.hp.java.boot.topic.Topic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.validation.Valid; +import java.net.URI; +import java.util.List; + +/** + * Added Root RequestMapping topics URI + * + * @author haripr + */ +@RestController +@RequestMapping("/topics") +public class CourseController { + + @Autowired + private CourseService courseService; + + private static final String SLASHPATH = "/"; + + @GetMapping(value = "/{topicId}/courses") + public List getCourses(@PathVariable String topicId) { + /* + * Code to add Dynamic Filter + SimpleBeanPropertyFilter simpleBeanPropertyFilter = + SimpleBeanPropertyFilter.filterOutAllExcept("topic"); + FilterProvider filterProvider = + new SimpleFilterProvider().addFilter("SomeBeanFilter", simpleBeanPropertyFilter); + MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(courseService.getAllCourses(topicId)); + mappingJacksonValue.setFilters(filterProvider); + */ + return courseService.getAllCourses(topicId); + } + + @GetMapping(value = "/{topicId}/courses/{id}") + public Course getCourse(@PathVariable String id) { + return courseService.getCourse(id); + } + + @PostMapping(value = "/{topicId}/courses") + public ResponseEntity addCourse(@PathVariable String topicId, @Valid @RequestBody Course course) { + course.setTopic(new Topic(topicId, "", "")); + courseService.addCourse(course); + + URI uri = ServletUriComponentsBuilder + .fromCurrentRequest() + .path(SLASHPATH + course.getId()) + .buildAndExpand() + .toUri(); + + return ResponseEntity.created(uri).build(); + } + + @PutMapping(value = "/{topicId}/courses/{id}") + public ResponseEntity updateCourse(@RequestBody Course course, @PathVariable String topicId, @PathVariable String id) { + course.setTopic(new Topic(topicId, "", "")); + courseService.updateCourse(course, id); + return ResponseEntity.accepted().body(course); + } + + @DeleteMapping(value = "/{topicId}/courses/{id}") + public void deleteCourse(@PathVariable String id) { + courseService.deleteCourse(id); + } + +} \ No newline at end of file diff --git a/src/main/java/in/hp/java/boot/controller/course/CourseRepository.java b/src/main/java/in/hp/java/boot/course/CourseRepository.java similarity index 73% rename from src/main/java/in/hp/java/boot/controller/course/CourseRepository.java rename to src/main/java/in/hp/java/boot/course/CourseRepository.java index aa8e4a1..f51727c 100644 --- a/src/main/java/in/hp/java/boot/controller/course/CourseRepository.java +++ b/src/main/java/in/hp/java/boot/course/CourseRepository.java @@ -1,10 +1,10 @@ -package in.hp.java.boot.controller.course; +package in.hp.java.boot.course; -import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.CrudRepository; +import java.util.List; -public interface CourseRepository extends CrudRepository { +public interface CourseRepository extends JpaRepository { /* * Custom method to retrieve list of courses by passing "name" as property @@ -18,9 +18,9 @@ public interface CourseRepository extends CrudRepository { * Note, no implementation is required, * as Spring Data JPA takes care of it if created a proper method name following the conventions. */ - public List findByName(String name); + List findByName(String name); - public List findByDescription(String description); + List findByDescription(String description); /* * To Retrieve the courses based on Topic because it has foreign key dependencies, @@ -31,5 +31,5 @@ public interface CourseRepository extends CrudRepository { * Model -> in this case it is "Topic" * Property -> primary key name of that model "Id" */ - public List findByTopicId(String topicId); + List findByTopicId(String topicId); } diff --git a/src/main/java/in/hp/java/boot/controller/course/CourseService.java b/src/main/java/in/hp/java/boot/course/CourseService.java similarity index 66% rename from src/main/java/in/hp/java/boot/controller/course/CourseService.java rename to src/main/java/in/hp/java/boot/course/CourseService.java index fa74a13..0715beb 100644 --- a/src/main/java/in/hp/java/boot/controller/course/CourseService.java +++ b/src/main/java/in/hp/java/boot/course/CourseService.java @@ -1,12 +1,13 @@ -package in.hp.java.boot.controller.course; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +package in.hp.java.boot.course; +import in.hp.java.boot.exceptions.ResourceNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + @Service public class CourseService { @@ -17,8 +18,6 @@ public class CourseService { private CourseRepository courseRepository; public List getAllCourses(String topicId) { - List courses = new ArrayList<>(); - /* * The findAll() method of Spring JPA returns an Iterable of Topic objects after * retrieving it from the DB, all the connections and other stuffs are taken @@ -29,20 +28,14 @@ public List getAllCourses(String topicId) { * Spring Data JPA accepts a topic id and returns back all the courses * associated with the topic */ - courseRepository.findByTopicId(topicId).forEach(courses::add); - - return courses; + return new ArrayList<>(courseRepository.findByTopicId(topicId)); } public Course getCourse(String id) { - /** - * Ideally Optional.isPresent should be used - * Optional course = courseRepository.findById(id); - * return course.ifPresent() ? course.get() : null; - * or - * return course.orElse(null); + /* + * Using orElseThrow to throw unchecked exception */ - return courseRepository.findById(id).orElse(null); + return courseRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id)); } public void addCourse(Course course) { @@ -52,12 +45,14 @@ public void addCourse(Course course) { courseRepository.save(course); } - public void updateCourse(Course course) { - courseRepository.save(course); + public void updateCourse(Course course, String id) { + if (Objects.nonNull(getCourse(id))) + courseRepository.save(course); } public void deleteCourse(String id) { - courseRepository.deleteById(id); + if (Objects.nonNull(getCourse(id))) + courseRepository.deleteById(id); } } diff --git a/src/main/java/in/hp/java/boot/exceptions/CustomExceptionHandler.java b/src/main/java/in/hp/java/boot/exceptions/CustomExceptionHandler.java new file mode 100644 index 0000000..3a9edea --- /dev/null +++ b/src/main/java/in/hp/java/boot/exceptions/CustomExceptionHandler.java @@ -0,0 +1,80 @@ +package in.hp.java.boot.exceptions; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.Date; + +/** + * Custom Exception Handler + * Used to impose org level exception standard + * + * @RestController - this acts as a controller as it directly sends response + * @ControllerAdvice + * - the methods in the class should be common to all controllers, + * - hence aop kind of advice is used + * + * Any exceptions returned from any controller comes to this handler + * The method is executed based on @ExceptionHandler advice + */ +@RestController +@ControllerAdvice +public class CustomExceptionHandler extends ResponseEntityExceptionHandler { + + /** + * The below method is used to handle all the exceptions reported by controller + * It returns back custom exception with status code 500 + *

+ * request.getDescription(false) - here false is passed to not reveal client info + * + * @param ex + * @param request + * @return + */ + @ExceptionHandler(Exception.class) + public final ResponseEntity handleAllException(Exception ex, WebRequest request) { + GenericException genericException = new GenericException( + new Date().toString(), ex.getMessage(), request.getDescription(false)); + return new ResponseEntity<>(genericException, HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * This method is executed for only ResourceNotFoundException + * + * @param ex + * @param request + * @return + */ + @ExceptionHandler(ResourceNotFoundException.class) + public final ResponseEntity handleAllException(ResourceNotFoundException ex, WebRequest request) { + GenericException genericException = new GenericException( + new Date().toString(), ex.getMessage(), request.getDescription(false)); + return new ResponseEntity<>(genericException, HttpStatus.NOT_FOUND); + } + + /** + * Overriding method to handle MethodArgumentNotValidException + * We can fetch the result of validation from "BindingResult" + * + * @param ex + * @param headers + * @param status + * @param request + * @return + */ + @Override + public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + GenericException genericException = new GenericException( + new Date().toString(), "Validation failed for input.", ex.getBindingResult().toString() + ); + return new ResponseEntity<>(genericException, status); + } +} \ No newline at end of file diff --git a/src/main/java/in/hp/java/boot/exceptions/GenericException.java b/src/main/java/in/hp/java/boot/exceptions/GenericException.java new file mode 100644 index 0000000..0c3eebf --- /dev/null +++ b/src/main/java/in/hp/java/boot/exceptions/GenericException.java @@ -0,0 +1,25 @@ +package in.hp.java.boot.exceptions; + +public class GenericException { + private final String timestamp; + private final String message; + private final String details; + + public GenericException(String timestamp, String message, String details) { + this.timestamp = timestamp; + this.message = message; + this.details = details; + } + + public String getTimestamp() { + return timestamp; + } + + public String getMessage() { + return message; + } + + public String getDetails() { + return details; + } +} diff --git a/src/main/java/in/hp/java/boot/exceptions/ResourceNotFoundException.java b/src/main/java/in/hp/java/boot/exceptions/ResourceNotFoundException.java new file mode 100644 index 0000000..e7c6833 --- /dev/null +++ b/src/main/java/in/hp/java/boot/exceptions/ResourceNotFoundException.java @@ -0,0 +1,16 @@ +package in.hp.java.boot.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Custom exception + * @ResponseStatus(HttpStatus.NOT_FOUND) + * - throws spring format exception but with the given error code + */ +@ResponseStatus(HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException{ + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/in/hp/java/boot/controller/topic/Topic.java b/src/main/java/in/hp/java/boot/topic/Topic.java similarity index 61% rename from src/main/java/in/hp/java/boot/controller/topic/Topic.java rename to src/main/java/in/hp/java/boot/topic/Topic.java index f898e25..18a8c17 100644 --- a/src/main/java/in/hp/java/boot/controller/topic/Topic.java +++ b/src/main/java/in/hp/java/boot/topic/Topic.java @@ -1,13 +1,23 @@ -package in.hp.java.boot.controller.topic; +package in.hp.java.boot.topic; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import javax.persistence.Entity; import javax.persistence.Id; +import javax.validation.constraints.Size; @Entity +@ApiModel("Topic Details") public class Topic { @Id + @Size(min = 2, message = "Id should be of 2 chars minimum") + @ApiModelProperty(notes = "Id should be of 2 chars minimum") private String id; + + @Size(min = 4, message = "Name should be of 4 chars minimum") + @ApiModelProperty(notes = "Name should be of 4 chars minimum") private String name; private String description; diff --git a/src/main/java/in/hp/java/boot/topic/TopicController.java b/src/main/java/in/hp/java/boot/topic/TopicController.java new file mode 100644 index 0000000..bcac9d1 --- /dev/null +++ b/src/main/java/in/hp/java/boot/topic/TopicController.java @@ -0,0 +1,72 @@ +package in.hp.java.boot.topic; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.validation.Valid; +import java.net.URI; +import java.util.List; + +/** + * Added Root RequestMapping topics URI + * + * @author haripr + */ +@RestController +@RequestMapping("/topics") +public class TopicController { + + @Autowired + private TopicService topicService; + + private static final String SLASHPATH = "/"; + + @GetMapping + @ApiOperation(value = "Return list of all Topics", response = Topic.class, responseContainer = "List") + public List getTopics() { + return topicService.getAllTopics(); + } + + @GetMapping(value = "/{id}") + @ApiOperation(value = "Gets a Topic with specified Id", response = Topic.class) + public Topic getTopic(@PathVariable String id) { + return topicService.getTopic(id); + } + + /** + * @param topic + * @return + * @Valid - used to validated the bean with the specified validators in bean class + */ + @PostMapping + @ApiOperation(value = "Adds a Topic") + public ResponseEntity addTopic(@Valid @RequestBody Topic topic) { + topicService.addTopic(topic); + + URI uri = ServletUriComponentsBuilder + .fromCurrentRequest() + .path(SLASHPATH + topic.getId()) + .buildAndExpand() + .toUri(); + + return ResponseEntity.created(uri).build(); + } + + @PutMapping(value = "/{id}") + @ApiOperation(value = "Updates the Topic") + public ResponseEntity updateTopic(@RequestBody Topic topic, @PathVariable String id) { + topicService.updateTopic(topic, id); + return ResponseEntity.accepted().body(topic); + } + + @DeleteMapping(value = "/{id}") + @ApiOperation(value = "Deletes a Topic") + public void deleteTopic(@ApiParam(required = true) @PathVariable String id) { + topicService.deleteTopic(id); + } + +} \ No newline at end of file diff --git a/src/main/java/in/hp/java/boot/topic/TopicRepository.java b/src/main/java/in/hp/java/boot/topic/TopicRepository.java new file mode 100644 index 0000000..db66152 --- /dev/null +++ b/src/main/java/in/hp/java/boot/topic/TopicRepository.java @@ -0,0 +1,5 @@ +package in.hp.java.boot.topic; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TopicRepository extends JpaRepository {} diff --git a/src/main/java/in/hp/java/boot/controller/topic/TopicService.java b/src/main/java/in/hp/java/boot/topic/TopicService.java similarity index 56% rename from src/main/java/in/hp/java/boot/controller/topic/TopicService.java rename to src/main/java/in/hp/java/boot/topic/TopicService.java index cd38888..928d615 100644 --- a/src/main/java/in/hp/java/boot/controller/topic/TopicService.java +++ b/src/main/java/in/hp/java/boot/topic/TopicService.java @@ -1,12 +1,13 @@ -package in.hp.java.boot.controller.topic; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +package in.hp.java.boot.topic; +import in.hp.java.boot.exceptions.ResourceNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + /** * @Service - Indicates that this is a singleton * @author haripr @@ -22,24 +23,15 @@ public class TopicService { private TopicRepository topicRepository; public List getAllTopics() { - List topics = new ArrayList<>(); - - /* - * The findAll() method of Spring JPA returns an Iterable of Topic objects after - * retrieving it from the DB, all the connections and other stuffs are taken - * care by Spring We need to iterate through the Iterable and create a list and - * send it back, for that we are using MethodReferences in Java 8. - */ - topicRepository.findAll().forEach(topics::add); - - return topics; + return new ArrayList<>(topicRepository.findAll()); } public Topic getTopic(String id) { - /** + /* * orElse of Optional checks and return if the Optional has value or null + * orElseThrow will throw the supplied exception if Optional is null or empty */ - return topicRepository.findById(id).orElse(null); + return topicRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id)); } public void addTopic(Topic topic) { @@ -49,16 +41,18 @@ public void addTopic(Topic topic) { topicRepository.save(topic); } - public void updateTopic(Topic topic) { + public void updateTopic(Topic topic, String id) { /* * Same method save() checks the primary key existence, and creates or updates * accordingly */ - topicRepository.save(topic); + if (Objects.nonNull(getTopic(id))) + topicRepository.save(topic); } public void deleteTopic(String id) { - topicRepository.deleteById(id); + if (Objects.nonNull(getTopic(id))) + topicRepository.deleteById(id); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 801a36d..0f681c4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,7 +1,25 @@ #tomcat configurations server: port: 8082 - + error: + # to exclude stack trace in custom exception messages + include-stacktrace: never + +spring: + h2: + console: + enabled: true + jpa: + show-sql: true + # configuring basename here instead of @Bean config for ResourceBundleMessageSource + messages: + basename: messages + # by default the username will be "user" and password will be generated and printed in console + security: + user: + name: username + password: password + #spring boot actuator configurations management: endpoint: diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..82b33d6 --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1 @@ +good.morning.message = Good Morning \ No newline at end of file diff --git a/src/main/resources/messages_fr.properties b/src/main/resources/messages_fr.properties new file mode 100644 index 0000000..151f193 --- /dev/null +++ b/src/main/resources/messages_fr.properties @@ -0,0 +1 @@ +good.morning.message = Bonjour \ No newline at end of file