diff --git a/pom.xml b/pom.xml index 9421357..03a2e74 100644 --- a/pom.xml +++ b/pom.xml @@ -13,9 +13,10 @@ 1.8 - 4.3.12.RELEASE + 5.0.5.RELEASE 1.7.4 - 1.7.5 + 1.2.3 + 1.7.5 5.3.0.Final 2.1.1 2.5 @@ -23,12 +24,14 @@ 1.2 5.1.38 4.1.0 - 3.3.1 + 3.3.1-1 3.1.0 + 2.22.2 1.10.16 5.4.1.Final 5.10.0.Final 5.0.5.RELEASE + 2.9.5 @@ -72,19 +75,17 @@ org.springframework.security - spring-security-core + spring-security-web ${spring-security.version} - org.springframework.security - spring-security-web + spring-security-config ${spring-security.version} - org.springframework.security - spring-security-config + spring-security-taglibs ${spring-security.version} @@ -108,60 +109,25 @@ ${hibernate-validator.version} - - - org.hibernate - hibernate-search-orm - ${hibernate-search.version} - - - org.hibernate - hibernate-commons-annotations - - - - - org.slf4j slf4j-api - ${org.slf4j-version} - - - org.slf4j - jcl-over-slf4j - ${org.slf4j-version} - runtime + ${slf4j.version} + compile + org.slf4j - slf4j-log4j12 - ${org.slf4j-version} + jul-to-slf4j + ${slf4j.version} runtime + - log4j - log4j - 1.2.15 - - - javax.mail - mail - - - javax.jms - jms - - - com.sun.jdmk - jmxtools - - - com.sun.jmx - jmxri - - + ch.qos.logback + logback-classic + ${logback.version} runtime @@ -189,6 +155,7 @@ jquery ${webjars-jquery.version} + org.webjars bootstrap @@ -217,6 +184,25 @@ ${webjars-noty.version} + + org.webjars.bower + moment + ${webjars-moment.version} + + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-json.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate5 + ${jackson-json.version} + + diff --git a/src/main/java/org/gvp/boookmanager/controller/BookController.java b/src/main/java/org/gvp/boookmanager/controller/BookController.java index 0116404..8e4bcec 100644 --- a/src/main/java/org/gvp/boookmanager/controller/BookController.java +++ b/src/main/java/org/gvp/boookmanager/controller/BookController.java @@ -2,97 +2,52 @@ import org.gvp.boookmanager.model.Book; import org.gvp.boookmanager.service.BookService; +import org.gvp.boookmanager.support.security.AuthorizedUser; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.support.PagedListHolder; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; -@Controller() +@RestController +@RequestMapping(value = "/bookmanager/books") public class BookController { private BookService bookService; - private int currentPage = 0; - private String sort = "books"; @Autowired public BookController(BookService bookService) { this.bookService = bookService; } - @GetMapping(value = "/books") - public String listBooks(@RequestParam(required = false) Integer page, Model model) { - sort = "books"; - model.addAttribute("book", new Book()); - List books = bookService.getALL(); - setPaging(page, model, books); - return "books"; + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll(@AuthenticationPrincipal AuthorizedUser authorizedUser) { + return bookService.getAll(authorizedUser.getId()); } - @GetMapping(value = "/addForm") - public String bookAddForm(Model model) { - model.addAttribute("book", new Book()); - return "bookForm"; + @GetMapping( value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public Book get(@PathVariable("id") long id) { + return bookService.get(id, AuthorizedUser.id()); } - @PostMapping(value = "/save") - public String saveBook(@Valid @ModelAttribute("book") Book book, BindingResult bindingResult){ - - if(bindingResult.hasErrors()){ - return "bookForm"; - } - bookService.save(book); - return "redirect:/books"; - } - - @RequestMapping(value = "/delete/{id}") - public String removeBook(@PathVariable("id") Long id){ - bookService.delete(id); - return "redirect:/books"; - } - - @GetMapping(value = "editForm") - public String bookEditForm(@RequestParam(name = "id") Long id, Model model){ - model.addAttribute("book", bookService.get(id)); - return "bookForm"; - - } - - @RequestMapping(value = "makeRead/{id}") - public String makeRead(@PathVariable("id") Long id){ - Book book = bookService.get(id); - bookService.makeRead(book); - return "redirect:/books"; + @DeleteMapping(value = "/{id}") + public void delete(@PathVariable("id") long id){ + bookService.delete(id, AuthorizedUser.id()); } - - @GetMapping(value = "books/search") - public String booksSearch(@RequestParam("searchText")String searchText, - Model model) { - List books = bookService.search(searchText); - model.addAttribute("listBooks", books); - return "search"; + @PostMapping() + public void update(@Valid @ModelAttribute Book book){ + if(book.getId() == null) { + bookService.create(book, AuthorizedUser.id()); + } else { + bookService.update(book, AuthorizedUser.id()); + } } - private void setPaging(Integer page, Model model, List books) { - PagedListHolder pagedListHolder = new PagedListHolder<>(books); - pagedListHolder.setPageSize(10); - - model.addAttribute("maxPages", pagedListHolder.getPageCount()); - - model.addAttribute("currentSort", sort); - - if (page == null || page < 1 || page > pagedListHolder.getPageCount()) - page = 1; - - model.addAttribute("page", page); - currentPage = page; - - pagedListHolder.setPage(page - 1); - model.addAttribute("listBooks", pagedListHolder.getPageList()); + @PostMapping(value = "/{id}") + public void makeRead(@PathVariable("id") int id, @RequestParam("readAlready") boolean readAlready) { + bookService.makeRead(id, readAlready, AuthorizedUser.id()); } } diff --git a/src/main/java/org/gvp/boookmanager/controller/RootController.java b/src/main/java/org/gvp/boookmanager/controller/RootController.java new file mode 100644 index 0000000..0862b3e --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/controller/RootController.java @@ -0,0 +1,99 @@ +package org.gvp.boookmanager.controller; + +import org.gvp.boookmanager.service.UserService; +import org.gvp.boookmanager.support.security.AuthorizedUser; +import org.gvp.boookmanager.to.UserTo; +import org.gvp.boookmanager.util.UserUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.support.SessionStatus; + +import javax.validation.Valid; + +import static org.gvp.boookmanager.exception.handlers.ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL; + + +@Controller +public class RootController { + + private UserService userService; + + @Autowired + public RootController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/users") + public String users() { + return "users"; + } + + @GetMapping("/books") + public String books() { + return "books"; + } + + @GetMapping(value = "/login") + public String login() { + return "login"; + } + + @GetMapping("/profile") + public String profile(Model model, @AuthenticationPrincipal AuthorizedUser authorizedUser) { + model.addAttribute("userTo", authorizedUser.getUserTo()); + return "profile"; + } + + @PostMapping("/profile") + public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status, @AuthenticationPrincipal AuthorizedUser authorizedUser) { + if (result.hasErrors()) { + return "profile"; + } + try { + userService.update(userTo); + authorizedUser.setUserTo(userTo); + status.setComplete(); + return "redirect:books"; + } catch (DataIntegrityViolationException ex) { + result.rejectValue("email", EXCEPTION_DUPLICATE_EMAIL); + return "profile"; + } + + } + + @GetMapping("/register") + public String register(ModelMap model) { + model.addAttribute("userTo", new UserTo()); + model.addAttribute("register", true); + return "profile"; + } + + @PostMapping("/register") + public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) { + if (result.hasErrors()) { + model.addAttribute("register", true); + return "profile"; + } + try { + userService.create(UserUtil.createNewFromTo(userTo)); + status.setComplete(); + return "redirect:login?success=true"; + } catch (DataIntegrityViolationException ex) { + result.rejectValue("email", EXCEPTION_DUPLICATE_EMAIL); + model.addAttribute("register", true); + return "profile"; + } + } + + +} diff --git a/src/main/java/org/gvp/boookmanager/controller/UserController.java b/src/main/java/org/gvp/boookmanager/controller/UserController.java new file mode 100644 index 0000000..6e6f507 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/controller/UserController.java @@ -0,0 +1,57 @@ +package org.gvp.boookmanager.controller; + +import org.gvp.boookmanager.model.User; +import org.gvp.boookmanager.service.UserService; +import org.gvp.boookmanager.to.UserTo; +import org.gvp.boookmanager.util.UserUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping(value = "/bookmanager/users") +public class UserController { + + private UserService userService; + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + return userService.getAll(); + } + + + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public User get(@PathVariable("id") int id) { + return userService.get(id); + } + + + @DeleteMapping("/{id}") + public void delete(@PathVariable("id") int id) { + userService.delete(id); + } + + @PostMapping + public void createOrUpdate(@Valid @ModelAttribute UserTo userTo) { + if (userTo.getId() == null) { + userService.create(UserUtil.createNewFromTo(userTo)); + } else { + userService.update(userTo); + } + } + + @PostMapping(value = "/{id}") + public void enable(@PathVariable("id") int id, @RequestParam("enabled") boolean enabled) { + userService.enable(id, enabled); + } +} diff --git a/src/main/java/org/gvp/boookmanager/dao/BookDao.java b/src/main/java/org/gvp/boookmanager/dao/BookDao.java index 7b7fa4c..ebc2d65 100644 --- a/src/main/java/org/gvp/boookmanager/dao/BookDao.java +++ b/src/main/java/org/gvp/boookmanager/dao/BookDao.java @@ -5,13 +5,13 @@ import java.util.List; public interface BookDao { - Book save(Book book); - boolean delete(Long id); + Book save(Book book, long userId); - Book get(Long id); + boolean delete(long id, long userId); - List getAll(); + Book get(long id, long userId); + + List getAll(long userId); - List search(String searchText); } diff --git a/src/main/java/org/gvp/boookmanager/dao/BookDaoImpl.java b/src/main/java/org/gvp/boookmanager/dao/BookDaoImpl.java index ceb1a54..8598be2 100644 --- a/src/main/java/org/gvp/boookmanager/dao/BookDaoImpl.java +++ b/src/main/java/org/gvp/boookmanager/dao/BookDaoImpl.java @@ -1,21 +1,14 @@ package org.gvp.boookmanager.dao; - -import org.apache.lucene.search.Query; import org.gvp.boookmanager.model.Book; -import org.gvp.boookmanager.support.search.SearchField; -import org.hibernate.search.jpa.FullTextEntityManager; -import org.hibernate.search.jpa.Search; -import org.hibernate.search.query.dsl.QueryBuilder; +import org.gvp.boookmanager.model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import java.util.Collections; import java.util.List; @Repository @@ -28,8 +21,12 @@ public class BookDaoImpl implements BookDao { @Override - public Book save(Book book) { + public Book save(Book book, long userId) { logger.info("Save" + book); + if(book.getId() != null && get(book.getId(), userId) == null) { + return null; + } + book.setUser(em.getReference(User.class, userId)); if (book.getId() == null) { em.persist(book); return book; @@ -40,48 +37,28 @@ public Book save(Book book) { @Override - public boolean delete(Long id) { + public boolean delete(long id, long userId) { /*Book book = em.getReference(Book.class, id); em.remove(book); */ return em.createNamedQuery(Book.DELETE) .setParameter("id", id) + .setParameter("userId", userId) .executeUpdate() != 0; } @Override - public Book get(Long id) { - return em.find(Book.class, id); + public Book get(long id, long userId) { + Book book = em.find(Book.class, id); + return book != null && book.getUser().getId() == userId ? book : null; } @Override - public List getAll() { + public List getAll(long userId) { logger.info("Get all"); - return em.createNamedQuery(Book.GET_ALL, Book.class).getResultList(); - } - - - @Override - public List search(String searchText) { - if(searchText == null || searchText.isEmpty()) return Collections.emptyList(); - FullTextEntityManager fem = Search.getFullTextEntityManager(em); - try { - fem.createIndexer().startAndWait(); - } catch (InterruptedException e) { - throw new RuntimeException("Indexing db error"); - } - QueryBuilder qb = fem.getSearchFactory() - .buildQueryBuilder().forEntity(Book.class).get(); - Query query = qb.keyword() - .onFields(SearchField.TITLE.toString(), SearchField.AUTHOR.toString()) - .matching(searchText) - .createQuery(); - javax.persistence.Query pq = fem.createFullTextQuery(query, Book.class); - return pq.getResultList(); - - + return em.createNamedQuery(Book.GET_ALL, Book.class).setParameter("userId", userId).getResultList(); } } diff --git a/src/main/java/org/gvp/boookmanager/dao/UserDao.java b/src/main/java/org/gvp/boookmanager/dao/UserDao.java new file mode 100644 index 0000000..f99bc7a --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/dao/UserDao.java @@ -0,0 +1,18 @@ +package org.gvp.boookmanager.dao; + +import org.gvp.boookmanager.model.User; + +import java.util.List; + +public interface UserDao { + + User save(User user); + + boolean delete(long id); + + User get(long id); + + User getByEmail(String email); + + List getAll(); +} diff --git a/src/main/java/org/gvp/boookmanager/dao/UserDaoImpl.java b/src/main/java/org/gvp/boookmanager/dao/UserDaoImpl.java new file mode 100644 index 0000000..a825d33 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/dao/UserDaoImpl.java @@ -0,0 +1,54 @@ +package org.gvp.boookmanager.dao; + +import org.gvp.boookmanager.model.User; +import org.hibernate.jpa.QueryHints; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; + +@Repository +public class UserDaoImpl implements UserDao { + + @PersistenceContext + private EntityManager em; + + @Override + public User save(User user) { + if(user.getId() == null) { + em.persist(user); + return user; + } else { + return em.merge(user); + } + } + + @Override + public User get(long id) { + return em.find(User.class, id); + } + + @Override + public boolean delete(long id) { + return em.createNamedQuery(User.DELETE) + .setParameter("id", id) + .executeUpdate() != 0; + } + + @Override + public User getByEmail(String email) { + List users = em.createNamedQuery(User.BY_EMAIL, User.class) + .setParameter("email", email) + .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false) + .getResultList(); + return DataAccessUtils.singleResult(users); + } + + @Override + public List getAll() { + return em.createNamedQuery(User.ALL_SORTED, User.class).getResultList(); + } +} diff --git a/src/main/java/org/gvp/boookmanager/exception/ApplicationException.java b/src/main/java/org/gvp/boookmanager/exception/ApplicationException.java new file mode 100644 index 0000000..aa6eeae --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/ApplicationException.java @@ -0,0 +1,42 @@ +package org.gvp.boookmanager.exception; + +import org.springframework.http.HttpStatus; + +import java.util.Arrays; + +public class ApplicationException extends RuntimeException { + + private final ErrorType type; + private final String msgCode; + private final HttpStatus httpStatus; + private final String[] args; + + public ApplicationException(String msgCode, HttpStatus httpStatus) { + this(ErrorType.APP_ERROR, msgCode, httpStatus); + } + + public ApplicationException(ErrorType type, String msgCode, HttpStatus httpStatus, String... args) { + super(String.format("type=%s, msgCode=%s, args=%s", type, msgCode, Arrays.toString(args))); + this.type = type; + this.msgCode = msgCode; + this.httpStatus = httpStatus; + this.args = args; + } + + public ErrorType getType() { + return type; + } + + public String getMsgCode() { + return msgCode; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String[] getArgs() { + return args; + } + +} diff --git a/src/main/java/org/gvp/boookmanager/exception/ErrorInfo.java b/src/main/java/org/gvp/boookmanager/exception/ErrorInfo.java new file mode 100644 index 0000000..cf835aa --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/ErrorInfo.java @@ -0,0 +1,15 @@ +package org.gvp.boookmanager.exception; + +public class ErrorInfo { + private final String url; + private final ErrorType type; + private final String typeMessage; + private final String[] details; + + public ErrorInfo(CharSequence url, ErrorType type, String typeMessage, String... details) { + this.url = url.toString(); + this.type = type; + this.typeMessage = typeMessage; + this.details = details; + } +} \ No newline at end of file diff --git a/src/main/java/org/gvp/boookmanager/exception/ErrorType.java b/src/main/java/org/gvp/boookmanager/exception/ErrorType.java new file mode 100644 index 0000000..794b763 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/ErrorType.java @@ -0,0 +1,19 @@ +package org.gvp.boookmanager.exception; + +public enum ErrorType { + APP_ERROR("error.appError"), + DATA_NOT_FOUND("error.dataNotFound"), + DATA_ERROR("error.dataError"), + VALIDATION_ERROR("error.validationError"), + WRONG_REQUEST("error.wrongRequest"); + + private final String errorCode; + + ErrorType(String errorCode) { + this.errorCode = errorCode; + } + + public String getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/org/gvp/boookmanager/exception/IllegalRequestDataException.java b/src/main/java/org/gvp/boookmanager/exception/IllegalRequestDataException.java new file mode 100644 index 0000000..27b8b02 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/IllegalRequestDataException.java @@ -0,0 +1,15 @@ +package org.gvp.boookmanager.exception; + +import org.springframework.lang.NonNull; + +public class IllegalRequestDataException extends RuntimeException { + public IllegalRequestDataException(@NonNull String msg) { + super(msg); + } + + @Override + public String toString() { + return getMessage(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/gvp/boookmanager/exception/NotFoundException.java b/src/main/java/org/gvp/boookmanager/exception/NotFoundException.java new file mode 100644 index 0000000..191b3ab --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package org.gvp.boookmanager.exception; + +import org.springframework.http.HttpStatus; + +public class NotFoundException extends ApplicationException { + public static final String NOT_FOUND_EXCEPTION = "exception.common.notFound"; + + public NotFoundException(String arg) { + super(ErrorType.DATA_NOT_FOUND, NOT_FOUND_EXCEPTION, HttpStatus.UNPROCESSABLE_ENTITY, arg); + } +} \ No newline at end of file diff --git a/src/main/java/org/gvp/boookmanager/exception/handlers/ExceptionInfoHandler.java b/src/main/java/org/gvp/boookmanager/exception/handlers/ExceptionInfoHandler.java new file mode 100644 index 0000000..6ff5bf0 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/handlers/ExceptionInfoHandler.java @@ -0,0 +1,113 @@ +package org.gvp.boookmanager.exception.handlers; + +import org.gvp.boookmanager.exception.ApplicationException; +import org.gvp.boookmanager.exception.ErrorInfo; +import org.gvp.boookmanager.exception.ErrorType; +import org.gvp.boookmanager.exception.IllegalRequestDataException; +import org.gvp.boookmanager.util.MessageUtil; +import org.gvp.boookmanager.util.ValidationUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.gvp.boookmanager.exception.ErrorType.APP_ERROR; +import static org.gvp.boookmanager.exception.ErrorType.DATA_ERROR; +import static org.gvp.boookmanager.exception.ErrorType.VALIDATION_ERROR; + + +@RestControllerAdvice(annotations = RestController.class) +@Order(Ordered.HIGHEST_PRECEDENCE + 5) +public class ExceptionInfoHandler { + private static Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); + + public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; + + private static final Map CONSTRAINS_I18N_MAP = Collections.unmodifiableMap( + new HashMap() { + { + put("users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL); + } + }); + + @Autowired + private MessageUtil messageUtil; + + @ExceptionHandler(ApplicationException.class) + public ResponseEntity applicationError(HttpServletRequest req, ApplicationException appEx) { + ErrorInfo errorInfo = logAndGetErrorInfo(req, appEx, false, appEx.getType(), messageUtil.getMessage(appEx)); + return new ResponseEntity<>(errorInfo, appEx.getHttpStatus()); + } + + @ResponseStatus(value = HttpStatus.CONFLICT) // 409 + @ExceptionHandler(DataIntegrityViolationException.class) + public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { + String rootMsg = ValidationUtil.getRootCause(e).getMessage(); + if (rootMsg != null) { + String lowerCaseMsg = rootMsg.toLowerCase(); + Optional> entry = CONSTRAINS_I18N_MAP.entrySet().stream() + .filter(it -> lowerCaseMsg.contains(it.getKey())) + .findAny(); + if (entry.isPresent()) { + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, messageUtil.getMessage(entry.get().getValue())); + } + } + return logAndGetErrorInfo(req, e, true, DATA_ERROR); + } + + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class}) + public ErrorInfo bindValidationError(HttpServletRequest req, Exception e) { + BindingResult result = e instanceof BindException ? + ((BindException) e).getBindingResult() : ((MethodArgumentNotValidException) e).getBindingResult(); + + String[] details = result.getFieldErrors().stream() + .map(fe -> messageUtil.getMessage(fe)) + .toArray(String[]::new); + + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, details); + } + + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler({IllegalRequestDataException.class, MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class}) + public ErrorInfo illegalRequestDataError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ErrorInfo handleError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, true, APP_ERROR); + } + + private ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType, String... details) { + Throwable rootCause = ValidationUtil.getRootCause(e); + if (logException) { + log.error(errorType + " at request " + req.getRequestURL(), rootCause); + } else { + log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); + } + return new ErrorInfo(req.getRequestURL(), errorType, + messageUtil.getMessage(errorType.getErrorCode()), + details.length != 0 ? details : new String[]{ValidationUtil.getMessage(rootCause)}); + } +} \ No newline at end of file diff --git a/src/main/java/org/gvp/boookmanager/exception/handlers/GlobalControllerExceptionHandler.java b/src/main/java/org/gvp/boookmanager/exception/handlers/GlobalControllerExceptionHandler.java new file mode 100644 index 0000000..d3ec7e7 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/handlers/GlobalControllerExceptionHandler.java @@ -0,0 +1,52 @@ +package org.gvp.boookmanager.exception.handlers; + +import org.gvp.boookmanager.exception.ApplicationException; +import org.gvp.boookmanager.exception.ErrorType; +import org.gvp.boookmanager.util.MessageUtil; +import org.gvp.boookmanager.util.ValidationUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.servlet.http.HttpServletRequest; + +@ControllerAdvice +public class GlobalControllerExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalControllerExceptionHandler.class); + + @Autowired + private MessageUtil messageUtil; + + @ExceptionHandler(NoHandlerFoundException.class) + public ModelAndView wrongRequest(HttpServletRequest req, NoHandlerFoundException e) throws Exception { + return logAndGetExceptionView(req, e, false, ErrorType.WRONG_REQUEST, null); + } + + @ExceptionHandler(ApplicationException.class) + public ModelAndView applicationErrorHandler(HttpServletRequest req, ApplicationException appEx) throws Exception { + return logAndGetExceptionView(req, appEx, true, appEx.getType(), messageUtil.getMessage(appEx)); + } + + @ExceptionHandler(Exception.class) + public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { + return logAndGetExceptionView(req, e, true, ErrorType.APP_ERROR, null); + } + + private ModelAndView logAndGetExceptionView(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType, String msg) { + Throwable rootCause = ValidationUtil.getRootCause(e); + if (logException) { + log.error(errorType + " at request " + req.getRequestURL(), rootCause); + } else { + log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); + } + ModelAndView mav = new ModelAndView("exception/exception"); + mav.addObject("typeMessage", messageUtil.getMessage(errorType.getErrorCode())); + mav.addObject("exception", rootCause); + mav.addObject("message", msg != null ? msg : ValidationUtil.getMessage(rootCause)); + return mav; + } +} diff --git a/src/main/java/org/gvp/boookmanager/model/AbstractBaseEntity.java b/src/main/java/org/gvp/boookmanager/model/AbstractBaseEntity.java new file mode 100644 index 0000000..4ea1975 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/model/AbstractBaseEntity.java @@ -0,0 +1,58 @@ +package org.gvp.boookmanager.model; + +import org.hibernate.Hibernate; + +import javax.persistence.*; + +@MappedSuperclass +public abstract class AbstractBaseEntity { + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected Long id; + + protected AbstractBaseEntity() { + } + + protected AbstractBaseEntity(Long id) { + this.id = id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + @Override + public String toString() { + return String.format("Entity %s (%s)", getClass().getName(), id); + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(Hibernate.getClass(o))) { + return false; + } + AbstractBaseEntity that = (AbstractBaseEntity) o; + return id != null && id.equals(that.id); + } + + @Override + public int hashCode() { + return id == null ? 0 : Long.hashCode(id); + } + + public boolean isNew() { + return id != null; + } + + +} diff --git a/src/main/java/org/gvp/boookmanager/model/AbstractNamedEntity.java b/src/main/java/org/gvp/boookmanager/model/AbstractNamedEntity.java new file mode 100644 index 0000000..fcdf7ad --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/model/AbstractNamedEntity.java @@ -0,0 +1,37 @@ +package org.gvp.boookmanager.model; + +import org.hibernate.validator.constraints.NotBlank; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.Size; + +@MappedSuperclass +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + @NotBlank + @Size(min = 2, max = 100) + @Column(name = "name", nullable = false) + protected String name; + + protected AbstractNamedEntity() { + } + + protected AbstractNamedEntity(Long id, String name) { + super(id); + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return String.format("Entity %s (%s, '%s')", getClass().getName(), id, name); + } +} diff --git a/src/main/java/org/gvp/boookmanager/model/Book.java b/src/main/java/org/gvp/boookmanager/model/Book.java index 8c1c40e..2f63ea2 100644 --- a/src/main/java/org/gvp/boookmanager/model/Book.java +++ b/src/main/java/org/gvp/boookmanager/model/Book.java @@ -1,135 +1,133 @@ -package org.gvp.boookmanager.model; - -import org.apache.lucene.analysis.core.LowerCaseFilterFactory; -import org.apache.lucene.analysis.snowball.SnowballPorterFilterFactory; -import org.apache.lucene.analysis.standard.StandardTokenizerFactory; -import org.gvp.boookmanager.support.validation.Year; -import org.hibernate.search.annotations.*; -import org.hibernate.search.annotations.Index; -import org.hibernate.search.annotations.Parameter; -import org.hibernate.validator.constraints.NotBlank; - -import javax.persistence.*; -import javax.validation.constraints.*; - -@Entity -@NamedQueries({ - @NamedQuery(name = Book.DELETE, query = "DELETE FROM Book b WHERE b.id=:id"), - @NamedQuery(name = Book.GET_ALL, query = "SELECT b FROM Book b") -}) -@Table(name = "books") -@Indexed -@AnalyzerDef(name = "customAnalyzer", - tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class), - filters = { - @TokenFilterDef(factory = LowerCaseFilterFactory.class), - @TokenFilterDef(factory = SnowballPorterFilterFactory.class, params = { - @Parameter(name = "language", value = "English") - }) - }) -public class Book { - - public static final String DELETE = "Book.delete"; - public static final String GET_ALL = "Book.getAll"; - - @Id - @Column(name = "id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "title") - @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO, analyzer = @Analyzer(definition = "customAnalyzer")) - @NotBlank(message = "Enter a title") - private String title; - - @Column(name = "description") - @NotBlank(message = "Enter a title") - @Size(min = 1, max = 255, message = "Description must contain from 1 to 255 characters") - private String description; - - @Column(name = "author") - @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO) - @NotBlank(message = "Enter a name of author") - private String author; - - @Column(name = "isbn") - @NotNull(message = "Enter ISBN") - @Pattern(regexp = "^978[0-9]{10}$", message = "Enter correct ISBN. 13 digit starts with 978") - private String isbn; - - @Column(name = "printYear") - @Year() - private Integer printYear; - - @Column(name = "readAlready") - private boolean readAlready; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - } - - public String getIsbn() { - return isbn; - } - - public void setIsbn(String isbn) { - this.isbn = isbn; - } - - public Integer getPrintYear() { - return printYear; - } - - public void setPrintYear(Integer printYear) { - this.printYear = printYear; - } - - public boolean isReadAlready() { - return readAlready; - } - - public void setReadAlready(boolean readAlready) { - this.readAlready = readAlready; - } - - @Override - public String toString() { - return "Book{" + - "id=" + id + - ", title='" + title + '\'' + - ", description='" + description + '\'' + - ", author='" + author + '\'' + - ", isbn='" + isbn + '\'' + - ", printYear=" + printYear + - ", readAlready=" + readAlready + - '}'; - } -} +package org.gvp.boookmanager.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.gvp.boookmanager.support.validation.Year; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.validator.constraints.NotBlank; + +import javax.persistence.*; +import javax.validation.constraints.*; + +@Entity +@NamedQueries({ + @NamedQuery(name = Book.DELETE, query = "DELETE FROM Book b WHERE b.id=:id AND b.user.id=:userId"), + @NamedQuery(name = Book.GET_ALL, query = "SELECT b FROM Book b WHERE b.user.id=:userId ORDER BY b.author ASC") +}) +@Table(name = "books") +public class Book extends AbstractBaseEntity{ + + public static final String DELETE = "Book.delete"; + public static final String GET_ALL = "Book.getAll"; + + @Column(name = "title") + @NotBlank(message = "Enter a title") + private String title; + + @Column(name = "description") + @NotBlank(message = "Enter a title") + @Size(min = 1, max = 255, message = "Description must contain from 1 to 255 characters") + private String description; + + @Column(name = "author") + @NotBlank(message = "Enter a name of author") + private String author; + + @Column(name = "isbn") + @NotNull(message = "Enter ISBN") + @Pattern(regexp = "^978[0-9]{10}$", message = "Enter correct ISBN. 13 digit starts with 978") + private String isbn; + + @Column(name = "printYear") + @Year() + private Integer printYear; + + @Column(name = "readAlready") + private boolean readAlready; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + private User user; + + public Book() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public Integer getPrintYear() { + return printYear; + } + + public void setPrintYear(Integer printYear) { + this.printYear = printYear; + } + + public boolean isReadAlready() { + return readAlready; + } + + public void setReadAlready(boolean readAlready) { + this.readAlready = readAlready; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public String toString() { + return "Book{" + + "id=" + id + + ", title='" + title + '\'' + + ", description='" + description + '\'' + + ", author='" + author + '\'' + + ", isbn='" + isbn + '\'' + + ", printYear=" + printYear + + ", readAlready=" + readAlready + + '}'; + } +} diff --git a/src/main/java/org/gvp/boookmanager/model/Role.java b/src/main/java/org/gvp/boookmanager/model/Role.java new file mode 100644 index 0000000..6223164 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/model/Role.java @@ -0,0 +1,13 @@ +package org.gvp.boookmanager.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum Role implements GrantedAuthority { + ROLE_USER, + ROLE_ADMIN; + + @Override + public String getAuthority() { + return name(); + } +} diff --git a/src/main/java/org/gvp/boookmanager/model/User.java b/src/main/java/org/gvp/boookmanager/model/User.java new file mode 100644 index 0000000..847ab0a --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/model/User.java @@ -0,0 +1,135 @@ +package org.gvp.boookmanager.model; + + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.annotations.BatchSize; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.util.CollectionUtils; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.*; + +@NamedQueries({ + @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), + @NamedQuery(name = User.BY_EMAIL, query = "SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=:email"), + @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u ORDER BY u.name"), +}) + +@Entity +@Table(name = "users") +public class User extends AbstractNamedEntity { + + public static final String DELETE = "User.delete"; + public static final String BY_EMAIL = "User.getByEmail"; + public static final String ALL_SORTED = "User.getAllSorted"; + + @Column(name = "email") + @Email + @NotBlank + @Size(max = 100) + private String email; + + @Column(name = "password") + @NotBlank + @Size(min = 5, max = 100) + // https://stackoverflow.com/a/12505165/548473 + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String password; + + @Column(name = "enabled") + private boolean enabled = true; + + @Column(name = "registered") + @NotNull + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Date registered = new Date(); + + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) + @BatchSize(size = 200) + private Set roles; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true) + @JsonIgnore + private List books; + + public User() { + } + + public User(User u) { + this(u.getId(), u.getName(), u.getEmail(), u.getPassword(), u.isEnabled(), u.getRegistered(), u.getRoles()); + } + + public User(Long id, String name, String email, String password, Role role, Role... roles) { + this(id, name, email, password, true, new Date(), EnumSet.of(role, roles)); + } + + public User(Long id, String name, String email, String password, boolean enabled, Date registered, Collection roles) { + super(id, name); + this.email = email; + this.password = password; + this.enabled = enabled; + this.registered = registered; + setRoles(roles); + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getRegistered() { + return registered; + } + + public void setRegistered(Date registered) { + this.registered = registered; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public Set getRoles() { + return roles; + } + + public String getPassword() { + return password; + } + + public void setRoles(Collection roles) { + this.roles = CollectionUtils.isEmpty(roles) ? Collections.emptySet() : EnumSet.copyOf(roles); + } + + public List getBooks() { + return books; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", email=" + email + + ", name=" + name + + ", enabled=" + enabled + + ", roles=" + roles + '}'; + } +} diff --git a/src/main/java/org/gvp/boookmanager/service/BookService.java b/src/main/java/org/gvp/boookmanager/service/BookService.java index 8b609b5..1675ec5 100644 --- a/src/main/java/org/gvp/boookmanager/service/BookService.java +++ b/src/main/java/org/gvp/boookmanager/service/BookService.java @@ -1,19 +1,18 @@ package org.gvp.boookmanager.service; - import org.gvp.boookmanager.model.Book; import java.util.List; public interface BookService { - Book save(Book book); + Book get(long id, long userId); - boolean delete(Long id); + void delete(long id, long userId); - Book get(Long id); + Book create(Book book, long userId); - List getALL(); + Book update(Book book, long userId); - List search(String searchText); + List getAll(long userId); - void makeRead(Book book); + void makeRead(long id, boolean readAlready, long userId); } diff --git a/src/main/java/org/gvp/boookmanager/service/BookServiceImpl.java b/src/main/java/org/gvp/boookmanager/service/BookServiceImpl.java index b8e8469..95d9f11 100644 --- a/src/main/java/org/gvp/boookmanager/service/BookServiceImpl.java +++ b/src/main/java/org/gvp/boookmanager/service/BookServiceImpl.java @@ -1,61 +1,65 @@ -package org.gvp.boookmanager.service; - -import org.gvp.boookmanager.dao.BookDao; -import org.gvp.boookmanager.model.Book; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service -@Transactional -public class BookServiceImpl implements BookService { - - - private BookDao bookDao; - - @Autowired - public BookServiceImpl(BookDao bookDao) { - this.bookDao = bookDao; - } - - @Override - public void makeRead(Book book) { - book.setReadAlready(true); - bookDao.save(book); - } - - @Override - @Transactional - public Book save(Book book) { - book.setReadAlready(false); - return bookDao.save(book); - } - - - @Override - @Transactional - public boolean delete(Long id) { - return bookDao.delete(id); - } - - - @Override - @Transactional - public Book get(Long id) { - return bookDao.get(id); - } - - @Override - @Transactional - public List getALL() { - return bookDao.getAll(); - } - - - @Transactional - public List search(String searchText) { - return bookDao.search(searchText); - } -} +package org.gvp.boookmanager.service; + +import org.gvp.boookmanager.dao.BookDao; +import org.gvp.boookmanager.model.Book; +import org.gvp.boookmanager.util.ValidationUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.util.List; + +import static org.gvp.boookmanager.util.ValidationUtil.checkNotFoundWithId; + +@Service +@Transactional(readOnly = true) +public class BookServiceImpl implements BookService { + + + private final BookDao bookDao; + + @Autowired + public BookServiceImpl(BookDao bookDao) { + this.bookDao = bookDao; + } + + @Override + public Book get(long id, long userId) { + return checkNotFoundWithId(bookDao.get(id, userId), id); + } + + + @Override + @Transactional + public void delete(long id, long userId) { + checkNotFoundWithId(bookDao.delete(id, userId), id); + } + + @Override + @Transactional + public Book create(Book book, long userId) { + return bookDao.save(book, userId); + } + + @Override + @Transactional + public Book update(Book book, long userId) { + book.setReadAlready(false); + return bookDao.save(book, userId); + } + + @Override + public List getAll(long userId) { + return bookDao.getAll(userId); + } + + @Override + @Transactional + public void makeRead(long id, boolean readAlready, long userId) { + Book book = get(id, userId); + book.setReadAlready(readAlready); + bookDao.save(book, userId); + } + +} diff --git a/src/main/java/org/gvp/boookmanager/service/UserService.java b/src/main/java/org/gvp/boookmanager/service/UserService.java new file mode 100644 index 0000000..2f100bb --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/service/UserService.java @@ -0,0 +1,24 @@ +package org.gvp.boookmanager.service; + +import org.gvp.boookmanager.model.User; +import org.gvp.boookmanager.to.UserTo; + +import java.util.List; + +public interface UserService { + + User create(User user); + + void delete(long id); + + User get(long id); + + void update(User user); + + void update(UserTo userTo); + + List getAll(); + + void enable(long id, boolean enabled); + +} diff --git a/src/main/java/org/gvp/boookmanager/service/UserServiceImpl.java b/src/main/java/org/gvp/boookmanager/service/UserServiceImpl.java new file mode 100644 index 0000000..04d3aa7 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/service/UserServiceImpl.java @@ -0,0 +1,88 @@ +package org.gvp.boookmanager.service; + +import org.gvp.boookmanager.dao.UserDao; +import org.gvp.boookmanager.model.User; +import org.gvp.boookmanager.support.security.AuthorizedUser; +import org.gvp.boookmanager.to.UserTo; +import org.gvp.boookmanager.util.UserUtil; +import org.gvp.boookmanager.util.ValidationUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.util.List; + +import static org.gvp.boookmanager.util.ValidationUtil.*; + +@Service +@Transactional(readOnly = true) +public class UserServiceImpl implements UserService, UserDetailsService { + + private final UserDao userDao; + private final PasswordEncoder passwordEncoder; + + @Autowired + public UserServiceImpl(UserDao userDao, PasswordEncoder passwordEncoder) { + this.userDao = userDao; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + @Override + public User create(User user) { + return userDao.save(UserUtil.prepareToSave(user, passwordEncoder)); + } + + @Transactional + @Override + public void delete(long id) { + checkNotFoundWithId(userDao.delete(id), id); + } + + @Override + public User get(long id) { + return checkNotFoundWithId(userDao.get(id), id); + } + + + @Transactional + @Override + public void update(User user) { + userDao.save(user); + } + + + @Transactional + @Override + public void update(UserTo userTo) { + User user = UserUtil.updateFromTo(get(userTo.getId()), userTo); + userDao.save(UserUtil.prepareToSave(user, passwordEncoder)); + } + + @Override + public List getAll() { + return userDao.getAll(); + } + + @Transactional + @Override + public void enable(long id, boolean enabled) { + User user = get(id); + user.setEnabled(enabled); + userDao.save(user); + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userDao.getByEmail(email.toLowerCase()); + if (user == null) { + throw new UsernameNotFoundException("User " + email + " is not found"); + } + return new AuthorizedUser(user); + } +} diff --git a/src/main/java/org/gvp/boookmanager/support/search/SearchField.java b/src/main/java/org/gvp/boookmanager/support/search/SearchField.java deleted file mode 100644 index df3ff7c..0000000 --- a/src/main/java/org/gvp/boookmanager/support/search/SearchField.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.gvp.boookmanager.support.search; - -public enum SearchField { - TITLE("title"), AUTHOR("author"); - - private final String value; - - SearchField(String value) { - this.value = value; - } - - @Override - public String toString() { - return value; - } -} diff --git a/src/main/java/org/gvp/boookmanager/support/security/AuthorizedUser.java b/src/main/java/org/gvp/boookmanager/support/security/AuthorizedUser.java new file mode 100644 index 0000000..71d2bcb --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/support/security/AuthorizedUser.java @@ -0,0 +1,51 @@ +package org.gvp.boookmanager.support.security; + + +import org.gvp.boookmanager.model.User; +import org.gvp.boookmanager.to.UserTo; +import org.gvp.boookmanager.util.UserUtil; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import static java.util.Objects.requireNonNull; + +public class AuthorizedUser extends org.springframework.security.core.userdetails.User { + + private UserTo userTo; + + public AuthorizedUser(User user) { + super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles()); + this.userTo = UserUtil.asTo(user); + } + + public static AuthorizedUser safeGet() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null) { + return null; + } + Object principal = auth.getPrincipal(); + return (principal instanceof AuthorizedUser) ? (AuthorizedUser) principal : null; + } + + public static AuthorizedUser get() { + AuthorizedUser user = safeGet(); + requireNonNull(user, "No authorized user found"); + return user; + } + + public static long id() { + return get().userTo.getId(); + } + + public long getId() { + return userTo.getId(); + } + + public UserTo getUserTo() { + return userTo; + } + + public void setUserTo(UserTo userTo) { + this.userTo = userTo; + } +} diff --git a/src/main/java/org/gvp/boookmanager/to/BaseTo.java b/src/main/java/org/gvp/boookmanager/to/BaseTo.java new file mode 100644 index 0000000..c70035f --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/to/BaseTo.java @@ -0,0 +1,24 @@ +package org.gvp.boookmanager.to; + +public abstract class BaseTo { + + protected Long id; + + public BaseTo() { + } + + public BaseTo(Long id) { + this.id = id; + } + + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + +} diff --git a/src/main/java/org/gvp/boookmanager/to/UserTo.java b/src/main/java/org/gvp/boookmanager/to/UserTo.java new file mode 100644 index 0000000..55942b5 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/to/UserTo.java @@ -0,0 +1,57 @@ +package org.gvp.boookmanager.to; + +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.SafeHtml; + +import javax.validation.constraints.Size; +import java.io.Serializable; + +public class UserTo extends BaseTo { + + @NotBlank + @Size(min = 2, max = 100) + private String name; + + @Email + @NotBlank + @Size(max = 100) + private String email; + + @Size(min = 5, max = 32, message = "length must between 5 and 32 characters") + private String password; + + public UserTo() { + } + + public UserTo(Long id, String name, String email, String password) { + super(id); + this.name = name; + this.email = email; + this.password = password; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/org/gvp/boookmanager/util/MessageUtil.java b/src/main/java/org/gvp/boookmanager/util/MessageUtil.java new file mode 100644 index 0000000..3d3f9ed --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/util/MessageUtil.java @@ -0,0 +1,36 @@ +package org.gvp.boookmanager.util; + +import org.gvp.boookmanager.exception.ApplicationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Locale; + +@Component +public class MessageUtil { + private final MessageSource messageSource; + + @Autowired + public MessageUtil(MessageSource messageSource) { + this.messageSource = messageSource; + } + + public String getMessage(String code, Locale locale, String... args) { + return messageSource.getMessage(code, args, locale); + } + + public String getMessage(String code, String... args) { + return getMessage(code, LocaleContextHolder.getLocale(), args); + } + + public String getMessage(ApplicationException appEx) { + return getMessage(appEx.getMsgCode(), appEx.getArgs()); + } + + public String getMessage(MessageSourceResolvable resolvable) { + return messageSource.getMessage(resolvable, LocaleContextHolder.getLocale()); + } +} diff --git a/src/main/java/org/gvp/boookmanager/util/UserUtil.java b/src/main/java/org/gvp/boookmanager/util/UserUtil.java new file mode 100644 index 0000000..649bb6e --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/util/UserUtil.java @@ -0,0 +1,33 @@ +package org.gvp.boookmanager.util; + +import org.gvp.boookmanager.model.Role; +import org.gvp.boookmanager.model.User; +import org.gvp.boookmanager.to.UserTo; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.StringUtils; + +public class UserUtil { + + + public static User createNewFromTo(UserTo newUser) { + return new User(null, newUser.getName(), newUser.getEmail().toLowerCase(), newUser.getPassword(), Role.ROLE_USER); + } + + public static UserTo asTo(User user) { + return new UserTo(user.getId(), user.getName(), user.getEmail(), user.getPassword()); + } + + public static User updateFromTo(User user, UserTo userTo) { + user.setName(userTo.getName()); + user.setEmail(userTo.getEmail().toLowerCase()); + user.setPassword(userTo.getPassword()); + return user; + } + + public static User prepareToSave(User user, PasswordEncoder passwordEncoder) { + String password = user.getPassword(); + user.setPassword(StringUtils.isEmpty(password) ? password : passwordEncoder.encode(password)); + user.setEmail(user.getEmail().toLowerCase()); + return user; + } +} diff --git a/src/main/java/org/gvp/boookmanager/util/ValidationUtil.java b/src/main/java/org/gvp/boookmanager/util/ValidationUtil.java new file mode 100644 index 0000000..29e5b80 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/util/ValidationUtil.java @@ -0,0 +1,39 @@ +package org.gvp.boookmanager.util; + +import org.gvp.boookmanager.exception.NotFoundException; + +public class ValidationUtil { + + private ValidationUtil() { + } + + public static void checkNotFoundWithId(boolean found, long id) { + checkNotFound(found, "id=" + id); + } + + public static T checkNotFoundWithId(T object, long id) { + checkNotFound(object != null, "id=" + id); + return object; + } + + + public static void checkNotFound(boolean found, String arg) { + if (!found) { + throw new NotFoundException(arg); + } + } + + public static Throwable getRootCause(Throwable t) { + Throwable result = t; + Throwable cause; + + while (null != (cause = result.getCause()) && (result != cause)) { + result = cause; + } + return result; + } + + public static String getMessage(Throwable e) { + return e.getLocalizedMessage() != null ? e.getLocalizedMessage() : e.getClass().getName(); + } +} \ No newline at end of file diff --git a/src/main/resources/db/dataBaseScrpt.sql b/src/main/resources/db/dataBaseScrpt.sql deleted file mode 100644 index 004518c..0000000 --- a/src/main/resources/db/dataBaseScrpt.sql +++ /dev/null @@ -1,47 +0,0 @@ -USE test; - -DROP TABLE IF EXISTS books; - -CREATE TABLE books ( - id INT(10) NOT NULL AUTO_INCREMENT DEFAULT 1, - title VARCHAR(100) NOT NULL, - description VARCHAR(255) NOT NULL, - author VARCHAR(100) NOT NULL, - isbn VARCHAR(20) NOT NULL, - printYear INT(4) NOT NULL, - readAlready BIT(1) NOT NULL DEFAULT 0, - PRIMARY KEY(id)) - -ENGINE = InnoDB DEFAULT CHARSET = utf8; - -INSERT INTO books (title, description, author, isbn, printYear) VALUES - ('A Brief History Of Time', 'description', 'Stephen Hawking', '9780857501004', 2015), - ('The Picture of Dorian Gray', 'description', 'Oscar Wilde', '9781853260155', 1997), - ('Don Quixote', 'description', 'Miguel de Cervantes', '9780099469698', 2005), - ('Frankenstein', 'description', 'Mary Shelley', '9780141198965', 2016), - ('Fahrenheit 451', 'description', 'Ray Bradbury', '9780006546061', 2008), - ('The Little Prince', 'description', 'Antoine de Saint-Exupery', '9780749707231', 1991), - ('The Idiot', 'description', 'Fyodor Dostoyevsky', '9780140447927', 2015), - ('The Call of the Wild', 'description', 'Jack London', '9780141321059', 2008), - ('Robinson Crusoe', 'description', 'Daniel Defoe', '9780199553976', 2009), - ('A Game of Thrones', 'description', 'George Martin', '9780006479888', 2009), - ('The Eye Of The World', 'description', 'Robert Jordan', '9780356503820', 2014), - ('Eragon', 'description', 'Christopher Paolini', '9780552553209', 2008), - ('It', 'description', 'Stephen King', '9781444707861', 2011), - ('Sleeping Beauties', 'description', 'Stephen King', '9781473665194', 2011), - ('The Shining', 'description', 'Stephen King', '9780307743657', 2012), - ('The Stand', 'description', 'Stephen King', '9780307743688', 2011), - ('11.22.63', 'description', 'Stephen King', '9781444727333', 2016), - ('Mr Mercedes', 'description', 'Stephen King', '9781444788648', 2015), - ('Finders Keepers', 'description', 'Stephen King', '9781473698956', 2016), - ('The Dead Zone', 'description', 'Stephen King', '9781444708097', 2011), - ('Revival', 'description', 'Stephen King', '9781444789218', '2015'), - ('The Eyes of the Dragon', 'description', 'Stephen King', '9781444723229', 2015), - ('Carrie', 'description', 'Stephen King', '9780307743664', '2011'), - ('The Great Book of Amber', 'description', 'Roger Zelazny', '9780380809066', 2011), - ('Misery', 'description', 'Stephen King', '9781444720716', 2011), - ('The Dry', 'description', 'Jane Harper', '9780349142111', 2017), - ('The Big Sleep and Other Novels', 'description', 'Raymond Chandler', '9780141182612', 2000), - ('The Maltese Falcon', 'description', 'Dashiell Hammett', '9780752865331', 2008), - ('The Black Dahlia', 'description', 'James Ellroy', '9780099537861', 2011), - ('The Long Goodbye', 'description', 'Raymond Chandler', '9780394757681', 1999); \ No newline at end of file diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql new file mode 100644 index 0000000..bb48583 --- /dev/null +++ b/src/main/resources/db/initDB.sql @@ -0,0 +1,40 @@ +DROP DATABASE IF EXISTS test; +CREATE DATABASE test + DEFAULT CHARSET = utf8; + + USE test; + +CREATE TABLE users ( + id BIGINT(10) NOT NULL AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + registered TIMESTAMP DEFAULT now() NOT NULL, + enabled BOOLEAN DEFAULT TRUE NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB; + +CREATE TABLE user_roles +( + user_id BIGINT(10) NOT NULL, + role VARCHAR(255), + FOREIGN KEY (user_id) REFERENCES users (id) + ON DELETE CASCADE +) + ENGINE = InnoDB; + +CREATE TABLE books ( + id BIGINT(10) NOT NULL AUTO_INCREMENT, + title VARCHAR(100) NOT NULL, + description VARCHAR(255) NOT NULL, + author VARCHAR(100) NOT NULL, + isbn VARCHAR(20) NOT NULL, + printYear INT(4) NOT NULL, + readAlready BOOLEAN NOT NULL DEFAULT FALSE, + user_id BIGINT(10) NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (user_id) REFERENCES users (id) + ON DELETE CASCADE +) + ENGINE = InnoDB; diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql new file mode 100644 index 0000000..e5828be --- /dev/null +++ b/src/main/resources/db/populateDB.sql @@ -0,0 +1,41 @@ +INSERT INTO users (name, email, password) VALUES + ('User', 'user@yandex.ru', '{noop}password'), + ('Admin', 'admin@gmail.com', '{noop}admin'); + +INSERT INTO user_roles (role, user_id) VALUES + ('ROLE_USER', 1), + ('ROLE_ADMIN', 2), + ('ROLE_USER', 2); + + +INSERT INTO books (title, description, author, isbn, printYear, user_id) VALUES + ('A Brief History Of Time', 'description', 'Stephen Hawking', '9780857501004', 2015, 1), + ('The Picture of Dorian Gray', 'description', 'Oscar Wilde', '9781853260155', 1997, 1), + ('Don Quixote', 'description', 'Miguel de Cervantes', '9780099469698', 2005, 1), + ('Frankenstein', 'description', 'Mary Shelley', '9780141198965', 2016, 1), + ('Fahrenheit 451', 'description', 'Ray Bradbury', '9780006546061', 2008, 1), + ('The Little Prince', 'description', 'Antoine de Saint-Exupery', '9780749707231', 1991, 1), + ('The Idiot', 'description', 'Fyodor Dostoyevsky', '9780140447927', 2015, 1), + ('The Call of the Wild', 'description', 'Jack London', '9780141321059', 2008, 1), + ('Robinson Crusoe', 'description', 'Daniel Defoe', '9780199553976', 2009, 1), + ('A Game of Thrones', 'description', 'George Martin', '9780006479888', 2009, 1), + ('The Eye Of The World', 'description', 'Robert Jordan', '9780356503820', 2014, 1), + ('Eragon', 'description', 'Christopher Paolini', '9780552553209', 2008, 1), + ('It', 'description', 'Stephen King', '9781444707861', 2011, 1), + ('Sleeping Beauties', 'description', 'Stephen King', '9781473665194', 2011, 1), + ('The Shining', 'description', 'Stephen King', '9780307743657', 2012, 1), + ('The Stand', 'description', 'Stephen King', '9780307743688', 2011, 1), + ('11.22.63', 'description', 'Stephen King', '9781444727333', 2016, 1), + ('Mr Mercedes', 'description', 'Stephen King', '9781444788648', 2015, 1), + ('Finders Keepers', 'description', 'Stephen King', '9781473698956', 2016, 1), + ('The Dead Zone', 'description', 'Stephen King', '9781444708097', 2011, 1), + ('Revival', 'description', 'Stephen King', '9781444789218', 2015, 1), + ('The Eyes of the Dragon', 'description', 'Stephen King', '9781444723229', 2015, 1), + ('Carrie', 'description', 'Stephen King', '9780307743664', 2011, 1), + ('The Great Book of Amber', 'description', 'Roger Zelazny', '9780380809066', 2011, 1), + ('Misery', 'description', 'Stephen King', '9781444720716', 2011, 1), + ('The Dry', 'description', 'Jane Harper', '9780349142111', 2017, 1), + ('The Big Sleep and Other Novels', 'description', 'Raymond Chandler', '9780141182612', 2000, 1), + ('The Maltese Falcon', 'description', 'Dashiell Hammett', '9780752865331', 2008, 1), + ('The Black Dahlia', 'description', 'James Ellroy', '9780099537861', 2011, 1), + ('The Long Goodbye', 'description', 'Raymond Chandler', '9780394757681', 1999, 1); \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..d118509 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + + UTF-8 + %date %-5level %logger{50}.%M:%L - %msg%n + + + + + + UTF-8 + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n + + + + + + + + + + + + + + diff --git a/src/main/resources/messages/messages_en.properties b/src/main/resources/messages/messages_en.properties new file mode 100644 index 0000000..d453e28 --- /dev/null +++ b/src/main/resources/messages/messages_en.properties @@ -0,0 +1,58 @@ +app.title = Welcome! +app.login=Login as +app.password = Password +app.welcome = Please log in +app.enter = Log in +app.profile=My profile +app.register=Registration +app.registered=You are registered. Please Sign in. +app.logout = Log out +app.main = Main + +user.title=Users +user.edit=Edit user +user.add=Add user +user.name=Name +user.email=Email +user.roles=Roles +user.active=Active +user.registered=Registered +user.password=Password + +books.title=Books +book.title=Title +book.edit=Edit book +book.add=Add book +book.description=Description +book.author = Author +book.isbn = ISBN +book.printYear = PrintYear +book.isRead = Read + +common.add=Add +common.save=Save +common.cancel=Cancel +common.deleted=Record deleted +common.saved=Record saved +common.enabled=Record enabled +common.disabled=Record disabled +common.search=Search +commom.language = Language + +error.appError=Application error +error.dataNotFound=Data not found +error.dataError=Data error +error.validationError=Validation error +error.wrongRequest=Wrong request + +exception.common.notFound=Not found entity with {0} +exception.user.duplicateEmail=User with this email already exists + +NotEmpty="{0}" must not be empty +NotBlank="{0}" must not be empty +NotNull="{0}" must not be empty +Email= Invalid format of "{0}" +Range="{0}" must be between {2} and {1} +Length="{0}" length must be between {2} and {1} +Size="{0}" size must be between {2} and {1} +SafeHtml="{0}" may has unsafe html content diff --git a/src/main/resources/messages/messages_ru.properties b/src/main/resources/messages/messages_ru.properties new file mode 100644 index 0000000..ef9b85c --- /dev/null +++ b/src/main/resources/messages/messages_ru.properties @@ -0,0 +1,59 @@ +app.title = \u0414\u043E\u0431\u0440\u043E \u043F\u043E\u0436\u0430\u043B\u043E\u0432\u0430\u0442\u044C! +app.login=\u041B\u043E\u0433\u0438\u043D +app.password=\u041F\u0430\u0440\u043E\u043B\u044C +app.welcome = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430 \u0432\u043E\u0439\u0434\u0438\u0442\u0435 +app.enter = \u0412\u043E\u0439\u0442\u0438 +app.profile=\u041C\u043E\u0439 \u043F\u0440\u043E\u0444\u0438\u043B\u044C +app.register=\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F +app.registered=\u0412\u044B \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u044B. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0432\u0430\u0448 \u043B\u043E\u0433\u0438\u043D/\u043F\u0430\u0440\u043E\u043B\u044C. +app.logout = \u0412\u044B\u0439\u0442\u0438 +app.main = \u041D\u0430 \u0433\u043B\u0430\u0432\u043D\u0443\u044E + +user.title=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 +user.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F +user.add=\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F +user.name=\u0418\u043C\u044F +user.email=\u041F\u043E\u0447\u0442\u0430 +user.roles=\u0420\u043E\u043B\u0438 +user.active=\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0439 +user.registered=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D +user.password=\u041F\u0430\u0440\u043E\u043B\u044C + +books.title=\u041A\u043D\u0438\u0433\u0438 +book.title=\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 +book.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u043D\u0438\u0433\u0443 +book.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u043D\u0438\u0433\u0443 +book.description=\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 +book.author = \u0410\u0432\u0442\u043E\u0440 +book.isbn = ISBN +book.printYear = \u0418\u0437\u0434\u0430\u043D\u043E +book.isRead = \u041F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E +book.notNull = book + +common.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C +common.save=\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C +common.cancel=\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C +common.deleted=\u0417\u0430\u043F\u0438\u0441\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u0430 +common.saved=\u0417\u0430\u043F\u0438\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 +common.enabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430 +common.disabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0434\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430 +common.search=\u0418\u0441\u043A\u0430\u0442\u044C +commom.language = \u042F\u0437\u044B\u043A + +error.appError=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F +error.dataNotFound=\u0414\u0430\u043D\u043D\u044B\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B +error.dataError=\u041E\u0448\u0438\u0431\u043A\u0430 \u0432 \u0434\u0430\u043D\u043D\u044B\u0445 +error.validationError=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0434\u0430\u043D\u043D\u044B\u0445 +error.wrongRequest=\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0437\u0430\u043F\u0440\u043E\u0441 + +exception.common.notFound=\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0437\u0430\u043F\u0438\u0441\u044C \u0441 {0} +exception.user.duplicateEmail=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0441 \u0442\u0430\u043A\u043E\u0439 \u043F\u043E\u0447\u0442\u043E\u0439 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0432 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 + +NotEmpty=\u041F\u043E\u043B\u0435 "{0}" \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C +NotBlank=\u041F\u043E\u043B\u0435 "{0}" \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C +NotNull=\u041F\u043E\u043B\u0435 "{0}" \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C +Email=\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u043F\u043E\u043B\u044F "{0}" +Range=\u041F\u043E\u043B\u0435 "{0}" \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} +Length=\u0414\u043B\u0438\u043D\u043D\u0430 \u043F\u043E\u043B\u044F "{0}" \u0434\u043E\u043B\u0436\u0435\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} +Size=\u0420\u0430\u0437\u043C\u0435\u0440 \u043F\u043E\u043B\u044F "{0}" \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} +SafeHtml=\u041F\u043E\u043B\u0435 "{0}" \u043C\u043E\u0436\u0435\u0442 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 html \u043A\u043E\u043D\u0442\u0435\u043D\u0442 diff --git a/src/main/webapp/WEB-INF/appconfig-data.xml b/src/main/resources/spring/appconfig-data.xml similarity index 100% rename from src/main/webapp/WEB-INF/appconfig-data.xml rename to src/main/resources/spring/appconfig-data.xml diff --git a/src/main/resources/spring/appconfig-mvc.xml b/src/main/resources/spring/appconfig-mvc.xml new file mode 100644 index 0000000..30ac3a3 --- /dev/null +++ b/src/main/resources/spring/appconfig-mvc.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/appconfig-root.xml b/src/main/resources/spring/appconfig-root.xml similarity index 93% rename from src/main/webapp/WEB-INF/appconfig-root.xml rename to src/main/resources/spring/appconfig-root.xml index 70a88ce..ab94093 100644 --- a/src/main/webapp/WEB-INF/appconfig-root.xml +++ b/src/main/resources/spring/appconfig-root.xml @@ -10,6 +10,8 @@ + + diff --git a/src/main/resources/spring/appconfig-security.xml b/src/main/resources/spring/appconfig-security.xml new file mode 100644 index 0000000..ca2f9f0 --- /dev/null +++ b/src/main/resources/spring/appconfig-security.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/appconfig-mvc.xml b/src/main/webapp/WEB-INF/appconfig-mvc.xml deleted file mode 100644 index 3471410..0000000 --- a/src/main/webapp/WEB-INF/appconfig-mvc.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/bookForm.jsp b/src/main/webapp/WEB-INF/jsp/bookForm.jsp deleted file mode 100644 index b612b8d..0000000 --- a/src/main/webapp/WEB-INF/jsp/bookForm.jsp +++ /dev/null @@ -1,73 +0,0 @@ -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> -<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> -<%@ taglib prefix="from" uri="http://www.springframework.org/tags/form" %> -<%@ page session="false" %> -<%@ page contentType="text/html;charset=UTF-8" %> - - - - - Add/Edit book - <%@include file="parts/header.jsp"%> - - - - -
- <%@include file="parts/navigation.jsp"%> -
-
- -
-
-

Add/Edit book

-
- - - - - -
- Title - - -
-
- Description - - -
-
- Author - - - - - - - - - -
-
- ISBN - - -
-
- PrintYear - - -
- -
-
-
- -
-
-
- - diff --git a/src/main/webapp/WEB-INF/jsp/books.jsp b/src/main/webapp/WEB-INF/jsp/books.jsp index 24652e3..ceb8c1a 100644 --- a/src/main/webapp/WEB-INF/jsp/books.jsp +++ b/src/main/webapp/WEB-INF/jsp/books.jsp @@ -7,66 +7,101 @@ - Book Manager + <spring:message code="books.title"/> + <%@include file="parts/header.jsp" %> + + -
-
- <%@include file="parts/navigation.jsp" %> -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
TitleDescriptionAuthorISBNPrintYearisReadEditDeleteRead
${book.title}${book.description}${book.author}${book.isbn}${book.printYear}${book.readAlready} - - - - - - - - - - - - - -
-
- -

Your bookList is empty. Add a book.

-
-
+
+ <%@include file="parts/navigation.jsp" %> +
+
+ + + + + + + + + + + + + + +
+
- <%@include file="parts/pagination.jsp" %>
+ + + diff --git a/src/main/webapp/WEB-INF/jsp/exception/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp new file mode 100644 index 0000000..4e789a7 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp @@ -0,0 +1,16 @@ +<%-- + Created by IntelliJ IDEA. + User: GVP + Date: 31.07.2018 + Time: 14:23 + To change this template use File | Settings | File Templates. +--%> +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Title + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/login.jsp b/src/main/webapp/WEB-INF/jsp/login.jsp new file mode 100644 index 0000000..a3ec40b --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/login.jsp @@ -0,0 +1,46 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + + <spring:message code="app.title"/> + + + +
+ <%@include file="parts/navigation.jsp" %> +
+
+
+ + + +

+ +
+ + +
+
+ + + + + +
+ +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/parts/header.jsp b/src/main/webapp/WEB-INF/jsp/parts/header.jsp index efd4cc7..babf07f 100644 --- a/src/main/webapp/WEB-INF/jsp/parts/header.jsp +++ b/src/main/webapp/WEB-INF/jsp/parts/header.jsp @@ -1,11 +1,15 @@ <%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + - - - + + + - - - - - + + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/parts/i18n.jsp b/src/main/webapp/WEB-INF/jsp/parts/i18n.jsp new file mode 100644 index 0000000..c5df986 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/parts/i18n.jsp @@ -0,0 +1,13 @@ +<%@ page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/parts/navigation.jsp b/src/main/webapp/WEB-INF/jsp/parts/navigation.jsp index 619b9a5..b35f45e 100644 --- a/src/main/webapp/WEB-INF/jsp/parts/navigation.jsp +++ b/src/main/webapp/WEB-INF/jsp/parts/navigation.jsp @@ -1,24 +1,55 @@ +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
-
\ No newline at end of file +
+ \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/parts/pagination.jsp b/src/main/webapp/WEB-INF/jsp/parts/pagination.jsp deleted file mode 100644 index 3abb1cd..0000000 --- a/src/main/webapp/WEB-INF/jsp/parts/pagination.jsp +++ /dev/null @@ -1,39 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -
-
- -
-
diff --git a/src/main/webapp/WEB-INF/jsp/profile.jsp b/src/main/webapp/WEB-INF/jsp/profile.jsp new file mode 100644 index 0000000..6488e85 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/profile.jsp @@ -0,0 +1,44 @@ +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + <spring:message code="${register ? 'app.register' : 'app.profile'}"/> + <%@include file="parts/header.jsp" %> + + +
+ <%@include file="parts/navigation.jsp" %> +
+
+
+ + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+
+
+ + diff --git a/src/main/webapp/WEB-INF/jsp/search.jsp b/src/main/webapp/WEB-INF/jsp/search.jsp deleted file mode 100644 index c9ea256..0000000 --- a/src/main/webapp/WEB-INF/jsp/search.jsp +++ /dev/null @@ -1,67 +0,0 @@ -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> -<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> -<%@ page session="false" %> -<%@ page contentType="text/html;charset=UTF-8" %> - - - - Search Result - <%@include file="parts/header.jsp" %> - - - -
- <%@include file="parts/navigation.jsp" %> -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
TitleDescriptionAuthorISBNPrintYearisReadEditDeleteRead
${book.title}${book.description}${book.author}${book.isbn}${book.printYear}${book.readAlready} - - - - - - - - - - - - - -
-
-
-
-
- <%@include file="parts/pagination.jsp"%> -
- - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp new file mode 100644 index 0000000..891c137 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -0,0 +1,88 @@ +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> +<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> +<%@ taglib prefix="from" uri="http://www.springframework.org/tags/form" %> +<%@ page session="false" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + + <spring:message code="user.title"/> + + <%@include file="parts/header.jsp" %> + + + + +
+ <%@include file="parts/navigation.jsp" %> +
+
+ + + + + + + + + + + + + +
+ +
+
+
+ + + + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 72ea58f..70b3a79 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -4,6 +4,7 @@ http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> Book Manager + org.springframework.web.context.ContextLoaderListener @@ -13,7 +14,7 @@ contextConfigLocation - /WEB-INF/appconfig-root.xml + classpath:/spring/appconfig-root.xml @@ -37,4 +38,32 @@ *.css + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + springSecurityFilterChain + /* + + + + \ No newline at end of file diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp deleted file mode 100644 index 52e7a99..0000000 --- a/src/main/webapp/index.jsp +++ /dev/null @@ -1,3 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ page contentType="text/html;charset=UTF-8" language="java" %> - \ No newline at end of file diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css new file mode 100644 index 0000000..bf1fa0a --- /dev/null +++ b/src/main/webapp/resources/css/style.css @@ -0,0 +1,6 @@ +tr[book-readAlready="true"] { + color: green; +} +tr[data-userEnabled="false"] { + opacity: 0.3; +} diff --git a/src/main/webapp/resources/js/booksDatatables.js b/src/main/webapp/resources/js/booksDatatables.js new file mode 100644 index 0000000..9173530 --- /dev/null +++ b/src/main/webapp/resources/js/booksDatatables.js @@ -0,0 +1,81 @@ +var ajaxUrl = "/bookmanager/books/"; +var datatableApi; + +function updateTable() { + $.get(ajaxUrl, updateTableByData); +} + +function makeRead(chkbox, id) { + var readAlready = chkbox.is(":checked"); + $.ajax({ + url: ajaxUrl + id, + type: "POST", + data: "readAlready=" + readAlready + }).done(function () { + chkbox.closest("tr").attr("book-readAlready", readAlready); + if(readAlready) { + successNoty(i18n["read"]); + } + }).fail(function () { + $(chkbox).prop("checked", !readAlready); + }); +} + +$(function () { + datatableApi = $("#bookTable").DataTable({ + "ajax": { + "url": ajaxUrl, + "dataSrc": "", + }, + "info": true, + "language": { + "search": i18n["common.search"] + }, + "columns": [ + { + "data": "title", + }, + { + "data": "description" + }, + { + "data": "author" + }, + { + "data": "isbn" + }, + { + "data": "printYear" + }, + { + "data": "readAlready", + "render": function (data, type, row) { + if (type === "display") { + return ""; + } + return data; + } + }, + { + "render": renderEditBtn, + "defaultContent": "", + "orderable": false + }, + { + "render": renderDeleteBtn, + "defaultContent": "", + "orderable": false + } + ], + "order": [ + [ + 0, + "asc" + ] + ], + "createdRow": function (row, data, dataIndex) { + $(row).attr("book-readAlready", data.readAlready); + }, + "initComplete": makeEditable + }); +}); \ No newline at end of file diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js new file mode 100644 index 0000000..c210f71 --- /dev/null +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -0,0 +1,103 @@ +var form; + +function makeEditable() { + form = $('#detailsForm'); + $(document).ajaxError(function (event, jqXHR, options, jsExc) { + failNoty(jqXHR); + }); + $.ajaxSetup({cache: false}); + var token = $("meta[name='_csrf']").attr("content"); + var header = $("meta[name='_csrf_header']").attr("content"); + $(document).ajaxSend(function (e, xhr, options) { + xhr.setRequestHeader(header, token); + }); +} + +function add() { + $("#modalTitle").html(i18n["addTitle"]); + form.find(":input").val(""); + form.find("input[name='readAlready']").val('false'); + $("#editRow").modal(); +} + + +function updateRow(id) { + $("#modalTitle").html(i18n["editTitle"]); + $.get(ajaxUrl + id, function (data) { + $.each(data, function (key, value) { + if (key === "description") { + form.find("textarea[name='" + key + "']").val(value); + } + form.find("input[name='" + key + "']").val(value); + }); + $('#editRow').modal(); + }); +} + +function deleteRow(id) { + $.ajax({ + url: ajaxUrl + id, + type: "DELETE" + }).done(function () { + updateTable(); + successNoty(i18n["common.deleted"]); + + }); +} + +function updateTableByData(data) { + datatableApi.clear().rows.add(data).draw(); +} + +function save() { + $.ajax({ + type: "POST", + url: ajaxUrl, + data: form.serialize() + }).done(function () { + $("#editRow").modal("hide"); + updateTable(); + successNoty(i18n["common.saved"]); + }); +} + +var failedNote; + +function closeNoty() { + if (failedNote) { + failedNote.close(); + failedNote = undefined; + } +} + +function successNoty(key) { + closeNoty(); + new Noty({ + text: "  " + [key], + type: 'success', + layout: "bottomRight", + timeout: 1000 + }).show(); +} + +function failNoty(jqXHR) { + closeNoty(); + failedNote = new Noty({ + text: "  " + errorInfo.typeMessage + "
" + errorInfo.details.join("
"), + type: "error", + layout: "bottomRight", + timeout: 1000 + }).show(); +} + +function renderEditBtn(data, type, row) { + if (type === "display") { + return ""; + } +} + +function renderDeleteBtn(data, type, row) { + if (type === "display") { + return ""; + } +} \ No newline at end of file diff --git a/src/main/webapp/resources/js/userDatatables.js b/src/main/webapp/resources/js/userDatatables.js new file mode 100644 index 0000000..daccdc4 --- /dev/null +++ b/src/main/webapp/resources/js/userDatatables.js @@ -0,0 +1,92 @@ +var ajaxUrl = "/bookmanager/users/"; +var datatableApi; + +function updateTable() { + $.get(ajaxUrl, updateTableByData); +} + +function enable(chkbox, id) { + var enabled = chkbox.is(":checked"); + $.ajax({ + url: ajaxUrl + id, + type: "POST", + data: "enabled=" + enabled + }).done(function () { + chkbox.closest("tr").attr("data-userEnabled", enabled); + successNoty(enabled ? i18n["common.enabled"] : i18n["common.disabled"]); + }).fail(function () { + $(chkbox).prop("checked", !enabled); + }); +} + +// $(document).ready(function () { +$(function () { + datatableApi = $("#userTable").DataTable({ + "ajax": { + "url": ajaxUrl, + "dataSrc": "" + }, + "info": true, + "language": { + "search": i18n["common.search"] + }, + "columns": [ + { + "data": "name" + }, + { + "data": "email", + "render": function (data, type, row) { + if (type === "display") { + return "" + data + ""; + } + return data; + } + }, + { + "data": "roles" + }, + { + "data": "enabled", + "render": function (data, type, row) { + if (type === "display") { + return ""; + } + return data; + } + }, + { + "data": "registered", + "render": function (date, type, row) { + if (type === "display") { + var momentDate = moment(date); + return momentDate.format("DD-MM-YYYY"); + } + return date; + } + }, + { + "orderable": false, + "defaultContent": "", + "render": renderEditBtn + }, + { + "orderable": false, + "defaultContent": "", + "render": renderDeleteBtn + } + ], + "order": [ + [ + 0, + "asc" + ] + ], + "createdRow": function (row, data, dataIndex) { + if (!data.enabled) { + $(row).attr("data-userEnabled", false); + } + }, + "initComplete": makeEditable + }); +}); \ No newline at end of file