diff --git a/schools/pom.xml b/schools/pom.xml index 189d2175..24925535 100644 --- a/schools/pom.xml +++ b/schools/pom.xml @@ -36,13 +36,28 @@ com.h2database h2 - + org.springframework.boot spring-boot-starter-test test + + io.springfox + springfox-bean-validators + 2.9.2 + + + io.springfox + springfox-spring-web + 2.9.2 + + + io.springfox + springfox-swagger2 + 2.9.2 + @@ -54,4 +69,26 @@ + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + io.springfox + springfox-bean-validators + 2.9.2 + + + diff --git a/schools/src/main/java/com/lambdaschool/schools/config/Swagger2Config.java b/schools/src/main/java/com/lambdaschool/schools/config/Swagger2Config.java new file mode 100644 index 00000000..c826dc4f --- /dev/null +++ b/schools/src/main/java/com/lambdaschool/schools/config/Swagger2Config.java @@ -0,0 +1,45 @@ +package com.lambdaschool.schools.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 +@Import(BeanValidatorPluginsConfiguration.class) +public class Swagger2Config +{ + @Bean + public Docket api() + { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors + .basePackage("com.lambdaschool.sampleemps")) + .paths(PathSelectors.regex("/.*")) + .build() + .apiInfo(apiEndPointsInfo()); + } + + private ApiInfo apiEndPointsInfo() + { + return new ApiInfoBuilder().title("Custom Swagger Documentation Example") + .description("Custom Swagger Documentation Example") + .contact(new Contact("John Mitchell", + "http://www.lambdaschool.com", + "john@lambdaschool.com")) + .license("MIT") + .licenseUrl("https://github.com/LambdaSchool/java-sampleswagger/blob/master/LICENSE") + .version("1.0.0") + .build(); + } +} diff --git a/schools/src/main/java/com/lambdaschool/schools/controllers/CourseController.java b/schools/src/main/java/com/lambdaschool/schools/controllers/CourseController.java index d9afc553..2d5a966c 100644 --- a/schools/src/main/java/com/lambdaschool/schools/controllers/CourseController.java +++ b/schools/src/main/java/com/lambdaschool/schools/controllers/CourseController.java @@ -1,7 +1,12 @@ package com.lambdaschool.schools.controllers; import com.lambdaschool.schools.models.Course; +import com.lambdaschool.schools.models.ErrorDetails; import com.lambdaschool.schools.services.CoursesService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -21,26 +26,16 @@ import java.net.URISyntaxException; import java.util.List; -/** - * The entry point for clients to work primary with courses - */ @RestController @RequestMapping(value = "/courses") public class CourseController { - /** - * Using the Courses service to process Course data - */ @Autowired private CoursesService coursesService; - /** - * Returns a list of all courses - *
Example: http://localhost:2019/courses/courses - * - * @return JSON list of all courses with a status of OK - * @see CoursesService#findAll() CoursesService.findAll() - */ + @ApiOperation(value = "returns all Courses", + response = Course.class, + responseContainer = "List") @GetMapping(value = "/courses", produces = {"application/json"}) public ResponseEntity listAllCourses() @@ -50,14 +45,13 @@ public ResponseEntity listAllCourses() HttpStatus.OK); } - /** - * Returns a single course based off a course id number - *
Example: http://localhost:2019/courses/course/7 - * - * @param courseId The primary key of the course you seek - * @return JSON object of the course you seek - * @see CoursesService#findCourseById(long) CoursesService.findCourseById(long) - */ + @ApiOperation(value = "Retrieve a course based off of the id", + response = Course.class) + @ApiResponses(value = {@ApiResponse(code = 200, + message = "Course Found", + response = Course.class), @ApiResponse(code = 404, + message = "Course Not Found", + response = ErrorDetails.class)}) @GetMapping(value = "/course/{courseId}", produces = {"application/json"}) public ResponseEntity getCourseById( @@ -69,17 +63,6 @@ public ResponseEntity getCourseById( HttpStatus.OK); } - /** - * Given a complete Course Object, create a new Course record and student course records. - *
Example: http://localhost:2019/courses/course - * - * @param newcourse A complete new course to add including instructor and students. - * instructor must already exist. - * students must already exist. - * @return A location header with the URI to the newly created course and a status of CREATED - * @throws URISyntaxException Exception if something does not work in creating the location header - * @see CoursesService#save(Course) CoursesService.save(Course) - */ @PostMapping(value = "/course", consumes = {"application/json"}) public ResponseEntity addCourse( @@ -104,17 +87,13 @@ public ResponseEntity addCourse( HttpStatus.CREATED); } - /** - * Given a complete Course Object - * Given the course id, primary key, is in the Course table, - * replace the Course record, student course combinations. - *
Example: http://localhost:2019/courses/course/15 - * - * @param updateCourse A complete Course including all students. Students and Instructor must already exist. - * @param courseid The primary key of the course you wish to replace. - * @return status of OK - * @see CoursesService#save(Course) CoursesService.save(Course) - */ + @ApiOperation(value = "updates a course given in the request body", + response = Void.class) + @ApiResponses(value = {@ApiResponse(code = 200, + message = "Course Found", + response = Void.class), @ApiResponse(code = 404, + message = "Course Not Found", + response = ErrorDetails.class)}) @PutMapping(value = "/course/{courseid}", consumes = {"application/json"}) public ResponseEntity updateFullCourse( @@ -130,14 +109,6 @@ public ResponseEntity updateFullCourse( return new ResponseEntity<>(HttpStatus.OK); } - /** - * Deletes a given course along with associated student course enrollments - *
Example: http://localhost:2019/courses/courses/14 - * - * @param id the primary key of the course you wish to delete - * @return Status of OK - * @see CoursesService#delete(long) CoursesService.delete(long) - */ @DeleteMapping(value = "/course/{id}") public ResponseEntity deleteCourseById( @PathVariable diff --git a/schools/src/main/java/com/lambdaschool/schools/controllers/StudentController.java b/schools/src/main/java/com/lambdaschool/schools/controllers/StudentController.java index 3257a667..bb8decb4 100644 --- a/schools/src/main/java/com/lambdaschool/schools/controllers/StudentController.java +++ b/schools/src/main/java/com/lambdaschool/schools/controllers/StudentController.java @@ -21,26 +21,13 @@ import java.net.URISyntaxException; import java.util.List; -/** - * The entry point for clients to work primary with student data - */ @RestController @RequestMapping(value = "/students") public class StudentController { - /** - * Using the Student service to process Student data - */ @Autowired private StudentService studentService; - /** - * Returns a list of all students - *
Example: http://localhost:2019/students/students - * - * @return JSON list of all students with a status of OK - * @see StudentService#findAll() StudentService.findAll() - */ @GetMapping(value = "/students", produces = {"application/json"}) public ResponseEntity listAllStudents() @@ -50,14 +37,6 @@ public ResponseEntity listAllStudents() HttpStatus.OK); } - /** - * Returns a single user based off a student id number - *
Example: http://localhost:2019/students/student/7 - * - * @param studentId The primary key of the student you seek - * @return JSON object of the student you seek - * @see StudentService#findStudentById(long) StudentService.findStudentById(long) - */ @GetMapping(value = "/student/{studentId}", produces = {"application/json"}) public ResponseEntity getStudentById( @@ -69,16 +48,6 @@ public ResponseEntity getStudentById( HttpStatus.OK); } - /** - * Given a complete student Object, create a new student record and student course records. - *
Example: http://localhost:2019/students/student - * - * @param newStudent A complete new student to add including enrolled courses. - * Courses must already exist. - * @return A location header with the URI to the newly created student and a status of CREATED - * @throws URISyntaxException Exception if something does not work in creating the location header - * @see StudentService#save(Student) StudentService.save(User) - */ @PostMapping(value = "/student", consumes = {"application/json"}) public ResponseEntity addStudentUser( @@ -103,17 +72,6 @@ public ResponseEntity addStudentUser( HttpStatus.CREATED); } - /** - * Given a complete student Object - * Given the student id, primary key, is in the student table, - * replace the student record, student course combinations. - *
Example: http://localhost:2019/students/student/15 - * - * @param updateStudent A complete student including all students. Students and Instructor must already exist. - * @param studentid The primary key of the student you wish to replace. - * @return status of OK - * @see StudentService#save(Student) StudentService.save(Student) - */ @PutMapping(value = "/student/{studentid}", consumes = {"application/json"}) public ResponseEntity updateFullstudent( @@ -129,14 +87,6 @@ public ResponseEntity updateFullstudent( return new ResponseEntity<>(HttpStatus.OK); } - /** - * Deletes a given student along with associated student course enrollments - *
Example: http://localhost:2019/students/students/14 - * - * @param id the primary key of the student you wish to delete - * @return Status of OK - * @see StudentService#delete(long) StudentService.delete(long) - */ @DeleteMapping(value = "/student/{id}") public ResponseEntity deleteStudentById( @PathVariable diff --git a/schools/src/main/java/com/lambdaschool/schools/exceptions/CustomErrorDetails.java b/schools/src/main/java/com/lambdaschool/schools/exceptions/CustomErrorDetails.java new file mode 100644 index 00000000..25a52971 --- /dev/null +++ b/schools/src/main/java/com/lambdaschool/schools/exceptions/CustomErrorDetails.java @@ -0,0 +1,41 @@ +//Structure for all messages: +// title +// status (Http status) +// details (human readable message) +// timestamp +// developerMessage +// errors - validation errors +// fieldname +// message + +package com.lambdaschool.schools.exceptions; + +import com.lambdaschool.schools.services.HelperFunctions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.WebRequest; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Component +public class CustomErrorDetails extends DefaultErrorAttributes { + @Autowired + HelperFunctions helperFunctions; + + @Override + public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { + Map errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); + + Map errorDetails = new LinkedHashMap<>(); + errorDetails.put("title", errorAttributes.get("error")); + errorDetails.put("status", errorAttributes.get("status")); + errorDetails.put("details", errorAttributes.get("message")); + errorDetails.put("timestamp", errorAttributes.get("timestamp")); + errorDetails.put("developerMessage", "path: " + errorAttributes.get("path")); + errorDetails.put("errors", helperFunctions.getConstraintViolations(this.getError(webRequest))); + + return errorDetails; + } +} diff --git a/schools/src/main/java/com/lambdaschool/schools/exceptions/MissingException.java b/schools/src/main/java/com/lambdaschool/schools/exceptions/MissingException.java new file mode 100644 index 00000000..b5a69609 --- /dev/null +++ b/schools/src/main/java/com/lambdaschool/schools/exceptions/MissingException.java @@ -0,0 +1,7 @@ +package com.lambdaschool.schools.exceptions; + +public class MissingException extends RuntimeException { + public MissingException(String message) { + super("Exception from in School:" + message); + } +} \ No newline at end of file diff --git a/schools/src/main/java/com/lambdaschool/schools/handlers/RestException.java b/schools/src/main/java/com/lambdaschool/schools/handlers/RestException.java new file mode 100644 index 00000000..7de5b33f --- /dev/null +++ b/schools/src/main/java/com/lambdaschool/schools/handlers/RestException.java @@ -0,0 +1,55 @@ +package com.lambdaschool.schools.handlers; + +import com.lambdaschool.schools.exceptions.MissingException; +import com.lambdaschool.schools.models.ErrorDetails; +import com.lambdaschool.schools.services.HelperFunctions; +import org.apache.coyote.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.Date; + +@Order(Ordered.HIGHEST_PRECEDENCE) +@RestControllerAdvice +public class RestException extends ResponseEntityExceptionHandler { + @Autowired + HelperFunctions helperFunctions; + + public RestException() { + super(); + } + + @ExceptionHandler(MissingException.class) + public ResponseEntity handleMissingException(MissingException rnfe) { + ErrorDetails errorDetails = new ErrorDetails(); + errorDetails.setTimestamp(new Date()); + errorDetails.setStatus(HttpStatus.NOT_FOUND.value()); + errorDetails.setTitle("Resource Not Found"); + errorDetails.setDetails(rnfe.getMessage()); + errorDetails.setDeveloperMessage(rnfe.getClass().getName()); + errorDetails.setErrors(helperFunctions.getConstraintViolations(rnfe)); + + return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND); + } + + @Override + protected ResponseEntity handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { + ErrorDetails errorDetails = new ErrorDetails(); + errorDetails.setTimestamp(new Date()); + errorDetails.setStatus(status.value()); + errorDetails.setTitle("Rest Internal Exception"); + errorDetails.setDetails(ex.getMessage()); + errorDetails.setDeveloperMessage(ex.getClass().getName()); + errorDetails.setErrors(helperFunctions.getConstraintViolations(ex)); + + return new ResponseEntity<>(errorDetails, status); + } +} diff --git a/schools/src/main/java/com/lambdaschool/schools/models/Course.java b/schools/src/main/java/com/lambdaschool/schools/models/Course.java index 1cc82edb..56cbae49 100644 --- a/schools/src/main/java/com/lambdaschool/schools/models/Course.java +++ b/schools/src/main/java/com/lambdaschool/schools/models/Course.java @@ -1,49 +1,46 @@ package com.lambdaschool.schools.models; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.OneToMany; -import javax.persistence.Table; +import javax.persistence.*; import java.util.ArrayList; import java.util.List; -/** - * The entity allowing interaction with the courses table - */ +@ApiModel(value = "Course", + description = "This is the course record") @Entity @Table(name = "courses") -public class Course - extends Auditable +@JsonIgnoreProperties(value = {"hasinstructorforcourse"}) +public class Course extends Auditable { - /** - * Primary key (long) for this course - */ + @ApiModelProperty(name = "course id", + value = "primary key for course", + required = true, + example = "1") @Id @GeneratedValue(strategy = GenerationType.AUTO) private long courseid; - /** - * Name (String) of this Course. Cannot be null and must be unique - */ + @ApiModelProperty(name = "course name", + value = "full course name", + required = true, + example = "Physics 101") @Column(nullable = true, unique = true) private String coursename; - /** - * The instructor object (Instructor) of this course - *
- * Forms a Many to one relationship between course and instructor. - * An instructor has many courses! - */ + @Transient + public boolean hasinstructorforcourse = false; + + @ApiModelProperty(name = "course name", + value = "instructor of course", + require = false, + example = "") + private String instructor; + @ManyToOne @JoinColumn(name = "instructorid", nullable = false) diff --git a/schools/src/main/java/com/lambdaschool/schools/models/ErrorDetails.java b/schools/src/main/java/com/lambdaschool/schools/models/ErrorDetails.java new file mode 100644 index 00000000..ef1e1042 --- /dev/null +++ b/schools/src/main/java/com/lambdaschool/schools/models/ErrorDetails.java @@ -0,0 +1,65 @@ +package com.lambdaschool.schools.models; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class ErrorDetails { + private String title; + private int status; + private String details; + private Date timestamp; + private String developerMessage; + private List errors = new ArrayList<>(); + + public ErrorDetails() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getDetails() { + return details; + } + + public void setDetails(String details) { + this.details = details; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public String getDeveloperMessage() { + return developerMessage; + } + + public void setDeveloperMessage(String developerMessage) { + this.developerMessage = developerMessage; + } + + public List getErrors() { + return errors; + } + + public void setErrors(List errors) { + this.errors = errors; + } +} diff --git a/schools/src/main/java/com/lambdaschool/schools/models/ValidationError.java b/schools/src/main/java/com/lambdaschool/schools/models/ValidationError.java new file mode 100644 index 00000000..2781280e --- /dev/null +++ b/schools/src/main/java/com/lambdaschool/schools/models/ValidationError.java @@ -0,0 +1,26 @@ +package com.lambdaschool.schools.models; + +public class ValidationError { + private String fieldname; + private String message; + + public ValidationError() { + } + + public String getFieldname() { + return fieldname; + } + + public void setFieldname(String fieldname) { + this.fieldname = fieldname; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} + diff --git a/schools/src/main/java/com/lambdaschool/schools/services/HelperFunctions.java b/schools/src/main/java/com/lambdaschool/schools/services/HelperFunctions.java new file mode 100644 index 00000000..064f46aa --- /dev/null +++ b/schools/src/main/java/com/lambdaschool/schools/services/HelperFunctions.java @@ -0,0 +1,9 @@ +package com.lambdaschool.schools.services; + +import com.lambdaschool.schools.models.ValidationError; + +import java.util.List; + +public interface HelperFunctions { + List getConstraintViolations(Throwable cause); +} diff --git a/schools/src/main/java/com/lambdaschool/schools/services/HelperFunctionsImpl.java b/schools/src/main/java/com/lambdaschool/schools/services/HelperFunctionsImpl.java new file mode 100644 index 00000000..2ae74429 --- /dev/null +++ b/schools/src/main/java/com/lambdaschool/schools/services/HelperFunctionsImpl.java @@ -0,0 +1,49 @@ +package com.lambdaschool.schools.services; + +import com.lambdaschool.schools.models.ValidationError; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.stereotype.Service; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.util.ArrayList; +import java.util.List; + +@Service(value = "helperFunctions") +public class HelperFunctionsImpl implements HelperFunctions { + @Override + public List getConstraintViolations(Throwable cause) { + List listVE = new ArrayList<>(); + + while((cause != null) && + !(cause instanceof ConstraintViolationException) && + !(cause instanceof MethodArgumentNotValidException)) { + cause = cause.getCause(); + } + + if (cause != null) { + if (cause instanceof ConstraintViolationException) { + ConstraintViolationException ex = (ConstraintViolationException) cause; + + ValidationError newVE = new ValidationError(); + newVE.setFieldname(ex.getConstraintName()); + newVE.setMessage(ex.getMessage()); + + listVE.add(newVE); + } else { + MethodArgumentNotValidException ex = (MethodArgumentNotValidException) cause; + + List fieldErrors = ex.getBindingResult().getFieldErrors(); + for (FieldError fe : fieldErrors) { + ValidationError newVE = new ValidationError(); + newVE.setFieldname(fe.getField()); + newVE.setMessage(fe.getDefaultMessage()); + + listVE.add(newVE); + } + } + } + + return listVE; + } +} diff --git a/schools/src/main/java/com/lambdaschool/schools/services/StudentService.java b/schools/src/main/java/com/lambdaschool/schools/services/StudentService.java index e34a9e26..4565005c 100644 --- a/schools/src/main/java/com/lambdaschool/schools/services/StudentService.java +++ b/schools/src/main/java/com/lambdaschool/schools/services/StudentService.java @@ -4,40 +4,13 @@ import java.util.List; -/** - * The service that works with the Student Model - */ public interface StudentService { - /** - * Returns a list of all the Students - * - * @return List of Students. If no students, empty list. - */ List findAll(); - /** - * Returns the student with the given primary key. - * - * @param id The primary key (long) of the student you seek. - * @return The given student or throws an exception if not found. - */ Student findStudentById(long id); - /** - * Deletes the student record and its student and course combinations from the database based off of the provided primary key - * - * @param id The primary key (long) of the student you seek. - */ void delete(long id); - /** - * Given a complete student object, saves that student object in the database. - * If a primary key is provided, the record is completely replaced - * If no primary key is provided, one is automatically generated and the record is added to the database. - * - * @param student the student object to be saved - * @return the saved student object including any automatically generated fields - */ Student save(Student student); } diff --git a/schools/src/main/resources/application.properties b/schools/src/main/resources/application.properties index 9758fe0c..80de698c 100644 --- a/schools/src/main/resources/application.properties +++ b/schools/src/main/resources/application.properties @@ -23,3 +23,6 @@ spring.datasource.initialization-mode=always # spring.jpa.hibernate.ddl-auto=update # since we have our data in SeedData, do not also load it from data.sql # spring.datasource.initialization-mode=never +server.error.whitelabel.enabled=false +spring.mvc.throw-exception-if-no-handler-found=true +spring.resources.add-mappings=false \ No newline at end of file