Browse Source

Common clean-up; update string formats & Book class annotations

Signed-off-by: Pekka Helenius <fincer89@hotmail.com>
v0.0.1-alpha
Pekka Helenius 4 years ago
parent
commit
64b26e407f
8 changed files with 341 additions and 325 deletions
  1. +6
    -6
      bookstore/src/main/java/com/fjordtek/bookstore/BookstoreApplication.java
  2. +11
    -2
      bookstore/src/main/java/com/fjordtek/bookstore/annotation/CurrentYear.java
  3. +5
    -5
      bookstore/src/main/java/com/fjordtek/bookstore/annotation/CurrentYearValidator.java
  4. +61
    -50
      bookstore/src/main/java/com/fjordtek/bookstore/model/Book.java
  5. +1
    -2
      bookstore/src/main/java/com/fjordtek/bookstore/model/BookRepository.java
  6. +186
    -189
      bookstore/src/main/java/com/fjordtek/bookstore/web/BookController.java
  7. +20
    -20
      bookstore/src/main/java/com/fjordtek/bookstore/web/HttpExceptionHandler.java
  8. +51
    -51
      bookstore/src/main/java/com/fjordtek/bookstore/web/HttpServerLogger.java

+ 6
- 6
bookstore/src/main/java/com/fjordtek/bookstore/BookstoreApplication.java View File

@ -1,18 +1,18 @@
// Pekka Helenius <fincer89@hotmail.com>, Fjordtek 2020
package com.fjordtek.bookstore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import com.fjordtek.bookstore.model.Book;
import com.fjordtek.bookstore.model.BookRepository;
import com.fjordtek.bookstore.model.*;
@SpringBootApplication
public class BookstoreApplication extends SpringBootServletInitializer {
private static final Logger commonLogger = LoggerFactory.getLogger(BookstoreApplication.class);
@ -23,10 +23,10 @@ public class BookstoreApplication extends SpringBootServletInitializer {
@Bean
public CommandLineRunner bookDatabaseRunner(BookRepository repository) {
return (args) -> {
commonLogger.info("Add new sample books to database");
repository.save(new Book("Book 1 title", "Book 1 author", 2020, "1231231-12", 40.00));
repository.save(new Book("Book 2 title", "Book 2 author", 2005, "3213221-3", 20.17));
@ -38,5 +38,5 @@ public class BookstoreApplication extends SpringBootServletInitializer {
};
}
}

+ 11
- 2
bookstore/src/main/java/com/fjordtek/bookstore/annotation/CurrentYear.java View File

@ -3,17 +3,26 @@
package com.fjordtek.bookstore.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import javax.validation.Constraint;
import javax.validation.Payload;
@Documented
@Constraint(validatedBy = CurrentYearValidator.class)
@Target(value={ElementType.TYPE_USE, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Target(
value={
ElementType.TYPE_USE,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.FIELD,
ElementType.METHOD,
ElementType.PARAMETER
}
)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentYear {


+ 5
- 5
bookstore/src/main/java/com/fjordtek/bookstore/annotation/CurrentYearValidator.java View File

@ -11,13 +11,13 @@ import org.springframework.stereotype.Component;
@Component
public class CurrentYearValidator implements ConstraintValidator<CurrentYear, Integer> {
private static final int yearNow = Year.now().getValue();
@Override
public void initialize(CurrentYear year) {
}
public void initialize(CurrentYear yearAnnotation) {
}
@Override
public boolean isValid(Integer year, ConstraintValidatorContext constraintValidatorContext) {
return year <= yearNow;


+ 61
- 50
bookstore/src/main/java/com/fjordtek/bookstore/model/Book.java View File

@ -2,27 +2,25 @@
package com.fjordtek.bookstore.model;
//import java.sql.Timestamp;
//import javax.validation.constraints.PastOrPresent;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.NotNull;
import com.fjordtek.bookstore.annotation.CurrentYear;
import javax.validation.constraints.Size;
import org.springframework.format.annotation.NumberFormat;
import org.springframework.format.annotation.NumberFormat.Style;
//import java.sql.Timestamp;
//import javax.validation.constraints.PastOrPresent;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import com.fjordtek.bookstore.annotation.CurrentYear;
@Entity
public class Book {
@ -31,23 +29,33 @@ public class Book {
private static final int strMax = 100;
// We format length check in Size annotation, not here
private static final String regexCommon = "^[a-zA-Z0-9\\-\\s]*$";
private static final int strIsbnFirstPartMin = 7;
private static final int strIsbnFirstPartMax = 7;
private static final int strIsbnLastPartMin = 1;
private static final int strIsbnLastPartMax = 3;
private static final int strIsbnMin = strIsbnFirstPartMin + strIsbnLastPartMin + 1;
private static final int strIsbnMax = strIsbnFirstPartMax + strIsbnLastPartMax + 1;
// We format length and syntax here for ISBN input string
// 1231234-1 <--> 1231234-123
private static final String regexIsbn = "^[0-9]{" + strIsbnFirstPartMin +
"," + strIsbnFirstPartMax + "}\\-[0-9]{" + strIsbnLastPartMin + "," + strIsbnLastPartMax + "}$";
private static final String regexIsbn = "^[0-9]{" + strIsbnFirstPartMin + "," + strIsbnFirstPartMax + "}" +
"\\-[0-9]{" + strIsbnLastPartMin + "," + strIsbnLastPartMax + "}$";
private static final int yearMin = 1800;
private static final String minPrice = "0.01";
private static final String maxPrice = "999.99";
/* For future reference
public static final enum BookDataTypes {
STRING,
BOOLEAN,
INTEGER,
DOUBLE,
LONG
}
*/
////////////////////
// Primary key value in database
@ -56,10 +64,11 @@ public class Book {
strategy = GenerationType.AUTO
)
private long id;
////////////////////
// Attributes with hard-coded constraints
//////////
@Size(
min = strMin, max = strMax,
message = "Title length must be " + strMin + "-" + strMax + " characters"
@ -72,7 +81,8 @@ public class Book {
message = "Invalid characters"
)
private String title;
//////////
@Size(
min = strMin, max = strMax,
message = "Author length must be " + strMin + "-" + strMax + " characters"
@ -85,13 +95,13 @@ public class Book {
message = "Invalid characters"
)
private String author;
//////////
// TODO: Prefer Timestamp data type
// @DateTimeFormat(pattern = "yyyy")
// private Timestamp year;
// ...
@NotNull
@Min(
value = yearMin,
message = "Minimum allowed year: " + yearMin
@ -99,12 +109,13 @@ public class Book {
@CurrentYear
private int year;
//////////
@NotBlank(
message = "Fill the ISBN code form"
)
@Pattern(
regexp = regexIsbn,
message = "Please use syntax: <" +
message = "Please use syntax: <" +
strIsbnFirstPartMin + " integers>-<" +
strIsbnLastPartMin + "-" +
strIsbnLastPartMax + " integers>"
@ -114,11 +125,12 @@ public class Book {
message = "Length must be " + strIsbnMin + "-" + strIsbnMax + " characters"
)
private String isbn;
//////////
@NumberFormat(style = Style.NUMBER, pattern = "#,###.###")
@Digits(
integer = 3, fraction = 2,
message = "Invalid price, possibly too many decimals"
message = "Invalid price, possibly too many decimals"
)
@DecimalMin(
value = minPrice, message = "Too low price value. Minimum allowed: " + minPrice
@ -131,66 +143,66 @@ public class Book {
////////////////////
// Attribute setters
// TODO consider accessibility restrictions?
// NOTE: in default scenario, this is automatically generated
public void setId(long id) {
this.id = id;
}
public void setTitle(String title) {
this.title = title;
}
public void setAuthor(String author) {
this.author = author;
}
public void setYear(int year) {
this.year = year;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public void setPrice(double price) {
this.price = price;
}
////////////////////
// Attribute getters
// TODO consider accessibility restrictions?
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public int getYear() {
return year;
}
public String getIsbn() {
return isbn;
}
public double getPrice() {
public double getPrice() {
return price;
}
////////////////////
// Class constructors
public Book() {}
public Book(String title, String author, int year, String isbn, double price) {
@ -201,19 +213,18 @@ public class Book {
this.isbn = isbn;
this.price = price;
}
////////////////////
// Class overrides
@Override
public String toString() {
return "id: " + this.id + ", " +
return "[" + "id: " + this.id + ", " +
"title: " + this.title + ", " +
"author: " + this.author + ", " +
"year: " + this.year + ", " +
"isbn: " + this.isbn + ", " +
"price: " + this.price;
"price: " + this.price + "]";
}
// ...
}

+ 1
- 2
bookstore/src/main/java/com/fjordtek/bookstore/model/BookRepository.java View File

@ -6,10 +6,9 @@ import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface BookRepository extends CrudRepository<Book, Long> {
// Handles both INSERT and UPDATE queries
List<Book> findById(String title);
List<Book> findById(String title);
}

+ 186
- 189
bookstore/src/main/java/com/fjordtek/bookstore/web/BookController.java View File

@ -2,6 +2,13 @@
package com.fjordtek.bookstore.web;
import java.time.Year;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@ -10,25 +17,14 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.time.Year;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import com.fjordtek.bookstore.model.*;
import com.fjordtek.bookstore.model.Book;
import com.fjordtek.bookstore.model.BookRepository;
@Controller
public class BookController {
/*
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}
*/
protected static final String landingPageURL = "index";
protected static final String bookListPageURL = "booklist";
protected static final String bookAddPageURL = "bookadd";
@ -41,179 +37,180 @@ public class BookController {
@Autowired
private BookRepository bookRepository;
@RequestMapping(
value = bookListPageURL,
method = { RequestMethod.GET, RequestMethod.POST }
)
public String defaultWebFormGet(HttpServletRequest requestData, Model dataModel) {
httpServerLogger.logMessageNormal(
requestData,
bookListPageURL + ": " + "HTTPOK"
);
dataModel.addAttribute("books", bookRepository.findAll());
return bookListPageURL;
}
//////////////////////////////
// ADD BOOK
@RequestMapping(
value = bookAddPageURL,
method = { RequestMethod.GET, RequestMethod.PUT }
)
public String webFormAddBook(
HttpServletRequest requestData,
Model dataModel
) {
httpServerLogger.logMessageNormal(
requestData,
bookAddPageURL + ": " + "HTTPOK"
);
Book newBook = new Book();
dataModel.addAttribute("book", newBook);
if (newBook.getYear() == 0) {
newBook.setYear(Year.now().getValue());
}
return bookAddPageURL;
}
@RequestMapping(
value = bookAddPageURL,
method = RequestMethod.POST
)
public String webFormSaveNewBook(
@Valid @ModelAttribute("book") Book book,
BindingResult bindingResult,
HttpServletRequest requestData
) {
if (bindingResult.hasErrors()) {
httpServerLogger.commonError("Book add: error [" + book.toString() + "]", requestData);
return bookAddPageURL;
}
httpServerLogger.logMessageNormal(
requestData,
bookAddPageURL + ": " + "HTTPOK"
);
bookRepository.save(book);
return "redirect:" + bookListPageURL;
}
//////////////////////////////
// DELETE BOOK
@RequestMapping(
value = bookDeletePageURL + "/{id}",
method = RequestMethod.GET
)
public String webFormDeleteBook(
@PathVariable("id") long bookId,
HttpServletRequest requestData
) {
httpServerLogger.logMessageNormal(
requestData,
bookDeletePageURL + ": " + "HTTPOK"
);
bookRepository.deleteById(bookId);
return "redirect:../" + bookListPageURL;
}
//////////////////////////////
// UPDATE BOOK
@RequestMapping(
value = bookEditPageURL + "/{id}",
method = RequestMethod.GET
)
public String webFormEditBook(
@PathVariable("id") long bookId,
Model dataModel,
HttpServletRequest requestData
) {
httpServerLogger.logMessageNormal(
requestData,
bookEditPageURL + ": " + "HTTPOK"
);
Book book = bookRepository.findById(bookId).get();
dataModel.addAttribute("book", book);
return bookEditPageURL;
}
@RequestMapping(
value = bookEditPageURL + "/{id}",
method = RequestMethod.POST
)
public String webFormUpdateBook(
@Valid @ModelAttribute("book") Book book,
BindingResult bindingResult,
HttpServletRequest requestData
) {
if (bindingResult.hasErrors()) {
httpServerLogger.commonError("Book edit: error [" + book.toString() + "]", requestData);
return bookAddPageURL;
}
httpServerLogger.logMessageNormal(
requestData,
bookEditPageURL + ": " + "HTTPOK"
);
bookRepository.save(book);
return "redirect:../" + bookListPageURL;
}
//////////////////////////////
// REDIRECTS
@RequestMapping(
value = { "/", landingPageURL },
method = RequestMethod.GET
)
@ResponseStatus(HttpStatus.FOUND)
public String redirectToDefaultWebForm() {
return "redirect:" + bookListPageURL;
}
// Other URL requests
@RequestMapping(
value = "*",
method = { RequestMethod.GET, RequestMethod.POST }
)
public String errorWebForm(HttpServletRequest requestData) {
return httpExceptionHandler.notFoundErrorHandler(requestData);
}
@RequestMapping(
value = "favicon.ico",
method = RequestMethod.GET
)
@ResponseBody
public void faviconRequest() {
/*
* We do not offer favicon for this website.
* Avoid HTTP status 404, and return nothing
* in server response when client requests the icon file.
*/
}
//////////////////////////////
// LIST PAGE
@RequestMapping(
value = bookListPageURL,
method = { RequestMethod.GET, RequestMethod.POST }
)
public String defaultWebFormGet(HttpServletRequest requestData, Model dataModel) {
httpServerLogger.logMessageNormal(
requestData,
bookListPageURL + ": " + "HTTPOK"
);
dataModel.addAttribute("books", bookRepository.findAll());
return bookListPageURL;
}
//////////////////////////////
// ADD BOOK
@RequestMapping(
value = bookAddPageURL,
method = { RequestMethod.GET, RequestMethod.PUT }
)
public String webFormAddBook(
HttpServletRequest requestData,
Model dataModel
) {
httpServerLogger.logMessageNormal(
requestData,
bookAddPageURL + ": " + "HTTPOK"
);
Book newBook = new Book();
dataModel.addAttribute("book", newBook);
if (newBook.getYear() == 0) {
newBook.setYear(Year.now().getValue());
}
return bookAddPageURL;
}
@RequestMapping(
value = bookAddPageURL,
method = RequestMethod.POST
)
public String webFormSaveNewBook(
@Valid @ModelAttribute("book") Book book,
BindingResult bindingResult,
HttpServletRequest requestData
) {
if (bindingResult.hasErrors()) {
httpServerLogger.commonError("Book add: error " + book.toString(), requestData);
return bookAddPageURL;
}
httpServerLogger.logMessageNormal(
requestData,
bookAddPageURL + ": " + "HTTPOK"
);
bookRepository.save(book);
return "redirect:" + bookListPageURL;
}
//////////////////////////////
// DELETE BOOK
@RequestMapping(
value = bookDeletePageURL + "/{id}",
method = RequestMethod.GET
)
public String webFormDeleteBook(
@PathVariable("id") long bookId,
HttpServletRequest requestData
) {
httpServerLogger.logMessageNormal(
requestData,
bookDeletePageURL + ": " + "HTTPOK"
);
bookRepository.deleteById(bookId);
return "redirect:../" + bookListPageURL;
}
//////////////////////////////
// UPDATE BOOK
@RequestMapping(
value = bookEditPageURL + "/{id}",
method = RequestMethod.GET
)
public String webFormEditBook(
@PathVariable("id") long bookId,
Model dataModel,
HttpServletRequest requestData
) {
httpServerLogger.logMessageNormal(
requestData,
bookEditPageURL + ": " + "HTTPOK"
);
Book book = bookRepository.findById(bookId).get();
dataModel.addAttribute("book", book);
return bookEditPageURL;
}
@RequestMapping(
value = bookEditPageURL + "/{id}",
method = RequestMethod.POST
)
public String webFormUpdateBook(
@Valid @ModelAttribute("book") Book book,
BindingResult bindingResult,
HttpServletRequest requestData
) {
if (bindingResult.hasErrors()) {
httpServerLogger.commonError("Book edit: error " + book.toString(), requestData);
return bookEditPageURL;
}
httpServerLogger.logMessageNormal(
requestData,
bookEditPageURL + ": " + "HTTPOK"
);
bookRepository.save(book);
return "redirect:../" + bookListPageURL;
}
//////////////////////////////
// REDIRECTS
@RequestMapping(
value = { "/", landingPageURL },
method = RequestMethod.GET
)
@ResponseStatus(HttpStatus.FOUND)
public String redirectToDefaultWebForm() {
return "redirect:" + bookListPageURL;
}
// Other URL requests
@RequestMapping(
value = "*",
method = { RequestMethod.GET, RequestMethod.POST }
)
public String errorWebForm(HttpServletRequest requestData) {
return httpExceptionHandler.notFoundErrorHandler(requestData);
}
@RequestMapping(
value = "favicon.ico",
method = RequestMethod.GET
)
@ResponseBody
public void faviconRequest() {
/*
* We do not offer favicon for this website.
* Avoid HTTP status 404, and return nothing
* in server response when client requests the icon file.
*/
}
}

+ 20
- 20
bookstore/src/main/java/com/fjordtek/bookstore/web/HttpExceptionHandler.java View File

@ -2,32 +2,32 @@
package com.fjordtek.bookstore.web;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public class HttpExceptionHandler {
private static final String HTTPNOTFOUND = "Invalid request";
private HttpServerLogger httpServerLogger = new HttpServerLogger();
@ResponseStatus(
value = HttpStatus.NOT_FOUND,
reason = HTTPNOTFOUND
)
// Very simple exception handler (not any sophistication)
@ExceptionHandler(Exception.class)
public String notFoundErrorHandler(HttpServletRequest requestData) {
httpServerLogger.logMessageError(
requestData,
"HTTPNOTFOUND"
);
return "error";
}
private static final String HTTPNOTFOUND = "Invalid request";
private HttpServerLogger httpServerLogger = new HttpServerLogger();
@ResponseStatus(
value = HttpStatus.NOT_FOUND,
reason = HTTPNOTFOUND
)
// Very simple exception handler (not any sophistication)
@ExceptionHandler(Exception.class)
public String notFoundErrorHandler(HttpServletRequest requestData) {
httpServerLogger.logMessageError(
requestData,
"HTTPNOTFOUND"
);
return "error";
}
}

+ 51
- 51
bookstore/src/main/java/com/fjordtek/bookstore/web/HttpServerLogger.java View File

@ -2,59 +2,59 @@
package com.fjordtek.bookstore.web;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import javax.servlet.http.HttpServletRequest;
public class HttpServerLogger {
private LocalDateTime logTimeStamp;
private void setLogTimeStamp() {
this.logTimeStamp = LocalDateTime.now();
}
private LocalDateTime getLogTimeStamp() {
return this.logTimeStamp;
}
public void logMessageNormal(
HttpServletRequest request,
String HttpRawStatusType
) {
setLogTimeStamp();
System.out.printf(
"%s: HTTP request to '%s' from client %s (%s)\n",
getLogTimeStamp(),
request.getRequestURL(),
request.getRemoteAddr(),
HttpRawStatusType
);
}
public void logMessageError(
HttpServletRequest request,
String HttpRawStatusType
) {
setLogTimeStamp();
System.err.printf(
"%s: Invalid HTTP request to '%s' from client %s (%s)\n",
getLogTimeStamp(),
request.getRequestURL(),
request.getRemoteAddr(),
HttpRawStatusType
);
}
public void commonError(String errorMsg, HttpServletRequest request, String ...strings) {
// Splitting log streams for proper server error logging
System.err.printf("%s: %s - %s\n", getLogTimeStamp(), request.getRemoteAddr(), errorMsg);
for (String dataString : strings) {
System.err.printf("%s\n", dataString);
}
}
private LocalDateTime logTimeStamp;
private void setLogTimeStamp() {
this.logTimeStamp = LocalDateTime.now();
}
private LocalDateTime getLogTimeStamp() {
return this.logTimeStamp;
}
public void logMessageNormal(
HttpServletRequest request,
String HttpRawStatusType
) {
setLogTimeStamp();
System.out.printf(
"%s: HTTP request to '%s' from client %s (%s)\n",
getLogTimeStamp(),
request.getRequestURL(),
request.getRemoteAddr(),
HttpRawStatusType
);
}
public void logMessageError(
HttpServletRequest request,
String HttpRawStatusType
) {
setLogTimeStamp();
System.err.printf(
"%s: Invalid HTTP request to '%s' from client %s (%s)\n",
getLogTimeStamp(),
request.getRequestURL(),
request.getRemoteAddr(),
HttpRawStatusType
);
}
public void commonError(String errorMsg, HttpServletRequest request, String ...strings) {
// Splitting log streams for proper server error logging
System.err.printf("%s: %s - %s\n", getLogTimeStamp(), request.getRemoteAddr(), errorMsg);
for (String dataString : strings) {
System.err.printf("%s\n", dataString);
}
}
}

Loading…
Cancel
Save