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..49241a9 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/controller/RootController.java @@ -0,0 +1,97 @@ +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; + + +@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", "email already exist"); + 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", "email already exist"); + 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..378a2bc --- /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("Application error"), + DATA_NOT_FOUND("Data not found"), + DATA_ERROR("Data error"), + VALIDATION_ERROR("Validation error"), + WRONG_REQUEST("Wrong request"); + + 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/ModificationRestrictionException.java b/src/main/java/org/gvp/boookmanager/exception/ModificationRestrictionException.java new file mode 100644 index 0000000..61b01e7 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/ModificationRestrictionException.java @@ -0,0 +1,11 @@ +package org.gvp.boookmanager.exception; + +import org.springframework.http.HttpStatus; + +public class ModificationRestrictionException extends ApplicationException { + public static final String EXCEPTION_MODIFICATION_RESTRICTION = "Admin/User modification is forbidden"; + + public ModificationRestrictionException() { + super(EXCEPTION_MODIFICATION_RESTRICTION, HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS); + } +} \ 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..4c74ecf --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/exception/NotFoundException.java @@ -0,0 +1,12 @@ +package org.gvp.boookmanager.exception; + +import org.springframework.http.HttpStatus; + +public class NotFoundException extends ApplicationException { + public static final String NOT_FOUND_EXCEPTION = "Not found entity with {0}"; + + // http://stackoverflow.com/a/22358422/548473 + 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/model/AbstractBaseEntity.java b/src/main/java/org/gvp/boookmanager/model/AbstractBaseEntity.java new file mode 100644 index 0000000..cbc47cc --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/model/AbstractBaseEntity.java @@ -0,0 +1,54 @@ +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); + } + + +} 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..5c5d1de 100644 --- a/src/main/java/org/gvp/boookmanager/model/Book.java +++ b/src/main/java/org/gvp/boookmanager/model/Book.java @@ -1,12 +1,9 @@ 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 com.fasterxml.jackson.annotation.JsonIgnore; 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.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.validator.constraints.NotBlank; import javax.persistence.*; @@ -14,31 +11,16 @@ @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") + @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") -@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 class Book extends AbstractBaseEntity{ 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; @@ -48,7 +30,6 @@ public class Book { 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; @@ -64,6 +45,16 @@ public class Book { @Column(name = "readAlready") private boolean readAlready; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + //@NotNull(groups = View.Persist.class) + @JsonIgnore + private User user; + + public Book() { + } + public Long getId() { return id; } @@ -120,6 +111,14 @@ 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{" + 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..3c136ca 100644 --- a/src/main/java/org/gvp/boookmanager/service/BookServiceImpl.java +++ b/src/main/java/org/gvp/boookmanager/service/BookServiceImpl.java @@ -9,11 +9,11 @@ import java.util.List; @Service -@Transactional +@Transactional(readOnly = true) public class BookServiceImpl implements BookService { - private BookDao bookDao; + private final BookDao bookDao; @Autowired public BookServiceImpl(BookDao bookDao) { @@ -21,41 +21,41 @@ public BookServiceImpl(BookDao bookDao) { } @Override - public void makeRead(Book book) { - book.setReadAlready(true); - bookDao.save(book); + public Book get(long id, long userId) { + return bookDao.get(id, userId); } + @Override @Transactional - public Book save(Book book) { - book.setReadAlready(false); - return bookDao.save(book); + public void delete(long id, long userId) { + bookDao.delete(id, userId); } - @Override @Transactional - public boolean delete(Long id) { - return bookDao.delete(id); + public Book create(Book book, long userId) { + return bookDao.save(book, userId); } - @Override @Transactional - public Book get(Long id) { - return bookDao.get(id); + public Book update(Book book, long userId) { + book.setReadAlready(false); + return bookDao.save(book, userId); } @Override - @Transactional - public List getALL() { - return bookDao.getAll(); + public List getAll(long userId) { + return bookDao.getAll(userId); } - + @Override @Transactional - public List search(String searchText) { - return bookDao.search(searchText); + 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..6462c97 --- /dev/null +++ b/src/main/java/org/gvp/boookmanager/service/UserServiceImpl.java @@ -0,0 +1,86 @@ +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.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; + +@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) { + Assert.notNull(user, "user must not be null"); + return userDao.save(UserUtil.prepareToSave(user, passwordEncoder)); + } + + @Transactional + @Override + public void delete(long id) { + userDao.delete(id); + } + + @Override + public User get(long id) { + return userDao.get(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/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/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/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..87328e4 --- /dev/null +++ b/src/main/resources/messages/messages_en.properties @@ -0,0 +1,60 @@ +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 +exception.user.modificationRestriction=Admin/User modification is forbidden + + +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..13037be --- /dev/null +++ b/src/main/resources/messages/messages_ru.properties @@ -0,0 +1,60 @@ +app.title = Добро пожаловать! +app.login=Логин +app.password=Пароль +app.welcome = Пожалуйста войдите +app.enter = Войти +app.profile=Мой профиль +app.register=Регистрация +app.registered=Вы зарегистрированы. Введите ваш логин/пароль. +app.logout = Выйти +app.main = На главную + +user.title=Пользователи +user.edit=Редактирование пользователя +user.add=Добавление пользователя +user.name=Имя +user.email=Почта +user.roles=Роли +user.active=Активный +user.registered=Зарегистрирован +user.password=Пароль + +books.title=Книги +book.title=Название +book.edit=Редактировать книгу +book.add=Добавить книгу +book.description=Описание +book.author = Автор +book.isbn = ISBN +book.printYear = Издано +book.isRead = Прочитано + +common.add=Добавить +common.save=Сохранить +common.cancel=Отменить +common.deleted=Запись удалена +common.saved=Запись сохранена +common.enabled=Запись активирована +common.disabled=Запись деактивирована +common.search=Искать +commom.language = Язык + +error.appError=Ошибка приложения +error.dataNotFound=Данные не найдены +error.dataError=Ошибка в данных +error.validationError=Ошибка проверки данных +error.wrongRequest=Неверный запрос + +exception.common.notFound=Не найдена запись с {0} +exception.user.duplicateEmail=Пользователь с такой почтой уже есть в приложении +exception.user.modificationRestriction=Модификация Admin/User запрещена +exception.meal.duplicateDateTime=У вас уже есть еда с такой датой/временем + +NotEmpty=Поле "{0}" не должно быть пустым +NotBlank=Поле "{0}" не должно быть пустым +NotNull=Поле "{0}" не должно быть пустым +Email=Неверный формат поля "{0}" +Range=Поле "{0}" должно быть между {2} и {1} +Length=Длинна поля "{0}" должена быть между {2} и {1} +Size=Размер поля "{0}" должен быть между {2} и {1} +SafeHtml=Поле "{0}" может содержать небезопасный html контент 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/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