Browse Source

Implement random hashes as replacement for direct book IDs for front-end

Signed-off-by: Pekka Helenius <fincer89@hotmail.com>
v0.0.2-alpha
Pekka Helenius 4 years ago
parent
commit
e6a6325d5a
9 changed files with 307 additions and 31 deletions
  1. +32
    -7
      bookstore/src/main/java/com/fjordtek/bookstore/BookstoreApplication.java
  2. +32
    -7
      bookstore/src/main/java/com/fjordtek/bookstore/model/Book.java
  3. +141
    -0
      bookstore/src/main/java/com/fjordtek/bookstore/model/BookHash.java
  4. +29
    -0
      bookstore/src/main/java/com/fjordtek/bookstore/model/BookHashRepository.java
  5. +17
    -0
      bookstore/src/main/java/com/fjordtek/bookstore/model/BookRepository.java
  6. +44
    -11
      bookstore/src/main/java/com/fjordtek/bookstore/web/BookController.java
  7. +8
    -2
      bookstore/src/main/java/com/fjordtek/bookstore/web/BookRestController.java
  8. +1
    -1
      bookstore/src/main/resources/templates/bookedit.html
  9. +3
    -3
      bookstore/src/main/resources/templates/booklist.html

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

@ -15,6 +15,8 @@ import org.springframework.context.annotation.Bean;
import com.fjordtek.bookstore.model.Author; import com.fjordtek.bookstore.model.Author;
import com.fjordtek.bookstore.model.AuthorRepository; import com.fjordtek.bookstore.model.AuthorRepository;
import com.fjordtek.bookstore.model.Book; import com.fjordtek.bookstore.model.Book;
import com.fjordtek.bookstore.model.BookHash;
import com.fjordtek.bookstore.model.BookHashRepository;
import com.fjordtek.bookstore.model.BookRepository; import com.fjordtek.bookstore.model.BookRepository;
import com.fjordtek.bookstore.model.Category; import com.fjordtek.bookstore.model.Category;
import com.fjordtek.bookstore.model.CategoryRepository; import com.fjordtek.bookstore.model.CategoryRepository;
@ -30,6 +32,7 @@ public class BookstoreApplication extends SpringBootServletInitializer {
@Bean @Bean
public CommandLineRunner bookDatabaseRunner( public CommandLineRunner bookDatabaseRunner(
BookRepository bookRepository, BookRepository bookRepository,
BookHashRepository bookHashRepository,
CategoryRepository categoryRepository, CategoryRepository categoryRepository,
AuthorRepository authorRepository AuthorRepository authorRepository
) { ) {
@ -37,17 +40,15 @@ public class BookstoreApplication extends SpringBootServletInitializer {
return (args) -> { return (args) -> {
commonLogger.info("Add new categories to database"); commonLogger.info("Add new categories to database");
categoryRepository.save(new Category("Horror")); categoryRepository.save(new Category("Horror"));
categoryRepository.save(new Category("Fantasy")); categoryRepository.save(new Category("Fantasy"));
categoryRepository.save(new Category("Sci-Fi")); categoryRepository.save(new Category("Sci-Fi"));
commonLogger.info("Add new authors to database");
authorRepository.save(new Author("Angela","Carter")); authorRepository.save(new Author("Angela","Carter"));
authorRepository.save(new Author("Andrzej","Sapkowski")); authorRepository.save(new Author("Andrzej","Sapkowski"));
commonLogger.info("Add new sample books to database");
bookRepository.save(new Book(
Book bookA = new Book(
"Bloody Chamber", "Bloody Chamber",
authorRepository.findByFirstNameIgnoreCaseContainingAndLastNameIgnoreCaseContaining( authorRepository.findByFirstNameIgnoreCaseContainingAndLastNameIgnoreCaseContaining(
"Angela","Carter" "Angela","Carter"
@ -56,8 +57,9 @@ public class BookstoreApplication extends SpringBootServletInitializer {
"1231231-12", "1231231-12",
new BigDecimal("18.00"), new BigDecimal("18.00"),
categoryRepository.findByName("Horror").get(0) categoryRepository.findByName("Horror").get(0)
));
bookRepository.save(new Book(
);
Book bookB = new Book(
"The Witcher: The Lady of the Lake", "The Witcher: The Lady of the Lake",
authorRepository.findByFirstNameIgnoreCaseContainingAndLastNameIgnoreCaseContaining( authorRepository.findByFirstNameIgnoreCaseContainingAndLastNameIgnoreCaseContaining(
"Andrzej","Sapkowski" "Andrzej","Sapkowski"
@ -66,7 +68,25 @@ public class BookstoreApplication extends SpringBootServletInitializer {
"3213221-3", "3213221-3",
new BigDecimal("19.99"), new BigDecimal("19.99"),
categoryRepository.findByName("Fantasy").get(0) categoryRepository.findByName("Fantasy").get(0)
));
);
commonLogger.info("Add new sample books to database");
bookRepository.save(bookA);
bookRepository.save(bookB);
BookHash bookHashA = new BookHash();
BookHash bookHashB = new BookHash();
// One-to-one unidirectional relationship
// Both directions for table operations must be considered here.
bookA.setBookHash(bookHashA);
bookB.setBookHash(bookHashB);
bookHashA.setBook(bookA);
bookHashB.setBook(bookB);
bookHashRepository.save(bookHashA);
bookHashRepository.save(bookHashB);
commonLogger.info("------------------------------"); commonLogger.info("------------------------------");
commonLogger.info("Sample categories in the database"); commonLogger.info("Sample categories in the database");
@ -81,6 +101,11 @@ public class BookstoreApplication extends SpringBootServletInitializer {
for (Book book : bookRepository.findAll()) { for (Book book : bookRepository.findAll()) {
commonLogger.info(book.toString()); commonLogger.info(book.toString());
} }
commonLogger.info("Sample book hashes in the database");
commonLogger.info("**THIS IS ADDED FOR SECURITY PURPOSES**");
for (BookHash hash : bookHashRepository.findAll()) {
commonLogger.info(hash.toString());
}
}; };
} }


+ 32
- 7
bookstore/src/main/java/com/fjordtek/bookstore/model/Book.java View File

@ -4,6 +4,7 @@ package com.fjordtek.bookstore.model;
import java.math.BigDecimal; import java.math.BigDecimal;
import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
//import java.sql.Timestamp; //import java.sql.Timestamp;
@ -16,6 +17,8 @@ import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.SequenceGenerator; import javax.persistence.SequenceGenerator;
import javax.validation.constraints.DecimalMax; import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin; import javax.validation.constraints.DecimalMin;
@ -79,6 +82,19 @@ public class Book {
@JsonIgnore @JsonIgnore
private Long id; private Long id;
////////////////////
// Random hash id purely for front-end purposes
// Difficult to enumerate
@JsonIgnore
@OneToOne(
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
targetEntity = BookHash.class
)
@PrimaryKeyJoinColumn
private BookHash bookHash;
//////////////////// ////////////////////
// Attributes with hard-coded constraints // Attributes with hard-coded constraints
@ -200,6 +216,10 @@ public class Book {
this.id = id; this.id = id;
} }
public void setBookHash(BookHash bookHash) {
this.bookHash = bookHash;
}
public void setTitle(String title) { public void setTitle(String title) {
this.title = title; this.title = title;
} }
@ -233,6 +253,10 @@ public class Book {
return id; return id;
} }
public BookHash getBookHash() {
return bookHash;
}
public String getTitle() { public String getTitle() {
return title; return title;
} }
@ -277,13 +301,14 @@ public class Book {
@Override @Override
public String toString() { public String toString() {
return "[" + "id: " + this.id + ", " +
"title: " + this.title + ", " +
"author: " + this.author + ", " +
"year: " + this.year + ", " +
"isbn: " + this.isbn + ", " +
"price: " + this.price + ", " +
"category: " + this.category + "]";
return "[" + "id: " + this.id + ", " +
"bookhash_id: " + this.bookHash + ", " +
"title: " + this.title + ", " +
"author: " + this.author + ", " +
"year: " + this.year + ", " +
"isbn: " + this.isbn + ", " +
"price: " + this.price + ", " +
"category: " + this.category + "]";
} }
} }

+ 141
- 0
bookstore/src/main/java/com/fjordtek/bookstore/model/BookHash.java View File

@ -0,0 +1,141 @@
package com.fjordtek.bookstore.model;
import java.security.SecureRandom;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import com.fasterxml.jackson.annotation.JsonIgnore;
/*
* This entity shares same primary key with the Book entity
* which is simultaneously a foreign key (unidirectional mapping)
* for this one.
* For implementation reference, see also:
* https://www.programmersought.com/article/1610322983/
*
* This entity generates a table which holds auto-generated
* random string values associated to each book.
*
* These random string values represent difficult-to-enumerate
* unique book IDs used for front-end purposes. Main purpose
* is not to expose real Book entity id value.
*/
@Entity
public class BookHash {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "bookHashIdGenerator"
)
@GenericGenerator(
name = "bookHashIdGenerator",
strategy = "foreign",
parameters = { @Parameter(name = "property", value = "book") }
)
@Column(
name = "book_id",
unique = true,
nullable = false
)
@JsonIgnore
private Long bookId;
//@MapsId
@OneToOne(
cascade = { CascadeType.MERGE, CascadeType.REMOVE },
fetch = FetchType.LAZY,
mappedBy = "bookHash",
targetEntity = Book.class
)
@PrimaryKeyJoinColumn(
referencedColumnName = "id"
)
private Book book;
////////////////////
// Attribute setters
@Column(
name = "hash_id",
unique = true,
columnDefinition = "CHAR(32)",
updatable = false
)
private String hashId;
public void setBookId(Long bookId) {
this.bookId = bookId;
}
public void setBook(Book book) {
this.book = book;
}
/*
* This setter sets an auto-generated value
* No manual intervention.
*/
public void setHashId() {
byte[] byteInit = new byte[16];
new SecureRandom().nextBytes(byteInit);
StringBuilder shaStringBuilder = new StringBuilder();
for (byte b : byteInit) {
shaStringBuilder.append(String.format("%02x", b));
}
this.hashId = shaStringBuilder.toString();
}
////////////////////
// Attribute getters
public Long getBookId() {
return bookId;
}
public Book getBook() {
return book;
}
public String getHashId() {
return hashId;
}
////////////////////
// Class constructors
public BookHash() {
this.setHashId();
}
////////////////////
// Class overrides
@Override
public String toString() {
return "[" + "book_id: " + this.bookId + ", " +
"hash_id: " + this.hashId + "]";
}
}

+ 29
- 0
bookstore/src/main/java/com/fjordtek/bookstore/model/BookHashRepository.java View File

@ -0,0 +1,29 @@
package com.fjordtek.bookstore.model;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(
path = "bookhashes",
itemResourceRel = "bookhashes",
exported = true
)
public interface BookHashRepository extends CrudRepository<BookHash, String> {
public BookHash findByHashId(String bookHashId);
/*
* We need to override native delete method.
* This is a native query, do not unnecessarily validate it.
*/
@Modifying
@Query(
value = "DELETE FROM BOOK_HASH i WHERE i.book_id = :bookId",
nativeQuery = true
)
public void deleteByBookId(@Param("bookId") Long bookId);
}

+ 17
- 0
bookstore/src/main/java/com/fjordtek/bookstore/model/BookRepository.java View File

@ -5,6 +5,8 @@ package com.fjordtek.bookstore.model;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@ -36,4 +38,19 @@ public interface BookRepository extends CrudRepository<Book, Long> {
@RestResource(exported = false) @RestResource(exported = false)
public boolean existsByIsbn(String isbn); public boolean existsByIsbn(String isbn);
@Override
public List<Book> findAll();
/*
* We need to override native delete method due to book hash id usage.
* This is a native query, do not unnecessarily validate it.
*/
@Override
@Modifying
@Query(
value = "DELETE FROM BOOK i WHERE i.id = :id",
nativeQuery = true
)
public void deleteById(@Param("id") Long id);
} }

+ 44
- 11
bookstore/src/main/java/com/fjordtek/bookstore/web/BookController.java View File

@ -14,6 +14,7 @@ import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.WebDataBinder;
@ -28,6 +29,8 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import com.fjordtek.bookstore.model.Author; import com.fjordtek.bookstore.model.Author;
import com.fjordtek.bookstore.model.AuthorRepository; import com.fjordtek.bookstore.model.AuthorRepository;
import com.fjordtek.bookstore.model.Book; import com.fjordtek.bookstore.model.Book;
import com.fjordtek.bookstore.model.BookHash;
import com.fjordtek.bookstore.model.BookHashRepository;
import com.fjordtek.bookstore.model.BookRepository; import com.fjordtek.bookstore.model.BookRepository;
import com.fjordtek.bookstore.model.CategoryRepository; import com.fjordtek.bookstore.model.CategoryRepository;
@ -51,6 +54,9 @@ public class BookController {
@Autowired @Autowired
private BookRepository bookRepository; private BookRepository bookRepository;
@Autowired
private BookHashRepository bookHashRepository;
private static final String RestJSONPageView = "json"; private static final String RestJSONPageView = "json";
private static final String RestAPIRefPageView = "apiref"; private static final String RestAPIRefPageView = "apiref";
@ -182,8 +188,20 @@ public class BookController {
httpServerLogger.log(requestData, responseData); httpServerLogger.log(requestData, responseData);
detectAndSaveBookAuthor(book); detectAndSaveBookAuthor(book);
bookRepository.save(book);
/*
* Generate hash id for the book. One-to-one unidirectional tables.
* Associate generated book hash object information
* to the book (table).
* Associate new book object information
* to the book hash (table).
*/
BookHash bookHash = new BookHash();
book.setBookHash(bookHash);
bookHash.setBook(book);
bookRepository.save(book);
bookHashRepository.save(bookHash);
return "redirect:" + bookListPageView; return "redirect:" + bookListPageView;
} }
@ -191,16 +209,24 @@ public class BookController {
////////////////////////////// //////////////////////////////
// DELETE BOOK // DELETE BOOK
@Transactional
@RequestMapping( @RequestMapping(
value = bookDeletePageView + "/{id}",
value = bookDeletePageView + "/{hash_id}",
method = RequestMethod.GET method = RequestMethod.GET
) )
public String webFormDeleteBook( public String webFormDeleteBook(
@PathVariable("id") Long bookId,
@PathVariable("hash_id") String bookHashId,
HttpServletRequest requestData, HttpServletRequest requestData,
HttpServletResponse responseData HttpServletResponse responseData
) { ) {
Long bookId = new Long(bookHashRepository.findByHashId(bookHashId).getBookId());
/*
* Delete associated book id foreign key (+ other row data) from book hash table
* at first, after which delete the book.
*/
bookHashRepository.deleteByBookId(bookId);
bookRepository.deleteById(bookId); bookRepository.deleteById(bookId);
httpServerLogger.log(requestData, responseData); httpServerLogger.log(requestData, responseData);
@ -212,16 +238,18 @@ public class BookController {
// UPDATE BOOK // UPDATE BOOK
@RequestMapping( @RequestMapping(
value = bookEditPageView + "/{id}",
value = bookEditPageView + "/{hash_id}",
method = RequestMethod.GET method = RequestMethod.GET
) )
public String webFormEditBook( public String webFormEditBook(
@PathVariable("id") Long bookId,
@PathVariable("hash_id") String bookHashId,
Model dataModel, Model dataModel,
HttpServletRequest requestData, HttpServletRequest requestData,
HttpServletResponse responseData HttpServletResponse responseData
) { ) {
Long bookId = new Long(bookHashRepository.findByHashId(bookHashId).getBookId());
Book book = bookRepository.findById(bookId).get(); Book book = bookRepository.findById(bookId).get();
dataModel.addAttribute("book", book); dataModel.addAttribute("book", book);
@ -237,29 +265,29 @@ public class BookController {
* but just as an URL end point. * but just as an URL end point.
*/ */
@RequestMapping( @RequestMapping(
value = bookEditPageView + "/{id}",
value = bookEditPageView + "/{hash_id}",
method = RequestMethod.POST method = RequestMethod.POST
) )
public String webFormUpdateBook( public String webFormUpdateBook(
@Valid @ModelAttribute("book") Book book, @Valid @ModelAttribute("book") Book book,
BindingResult bindingResult, BindingResult bindingResult,
Model dataModel,
@PathVariable("id") Long bookId,
@PathVariable("hash_id") String bookHashId,
HttpServletRequest requestData, HttpServletRequest requestData,
HttpServletResponse responseData HttpServletResponse responseData
) { ) {
// NOTE: We have a unique and non-nullable ISBN value for each book.
if (bookId != book.getId()) {
BookHash bookHash = bookHashRepository.findByHashId(bookHashId);
if (bookHash == null) {
bindingResult.rejectValue("name", "error.user", "Wrong book"); bindingResult.rejectValue("name", "error.user", "Wrong book");
} }
Long bookId = bookHash.getBookId();
// TODO consider better solution. Add custom Hibernate annotation for Book class? // TODO consider better solution. Add custom Hibernate annotation for Book class?
Book bookI = bookRepository.findByIsbn(book.getIsbn()); Book bookI = bookRepository.findByIsbn(book.getIsbn());
// If existing ISBN value is not attached to the current book... // If existing ISBN value is not attached to the current book...
if (bookI != null) { if (bookI != null) {
if (bookI.getId() != book.getId()) {
if (bookI.getId() != bookId) {
bindingResult.rejectValue("isbn", "error.user", "ISBN code already exists"); bindingResult.rejectValue("isbn", "error.user", "ISBN code already exists");
} }
} }
@ -270,6 +298,11 @@ public class BookController {
return bookEditPageView; return bookEditPageView;
} }
/*
* This is necessary so that Hibernate does not attempt to INSERT data
* but UPDATEs current table data.
*/
book.setId(bookId);
detectAndSaveBookAuthor(book); detectAndSaveBookAuthor(book);
bookRepository.save(book); bookRepository.save(book);


+ 8
- 2
bookstore/src/main/java/com/fjordtek/bookstore/web/BookRestController.java View File

@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.fjordtek.bookstore.model.Book; import com.fjordtek.bookstore.model.Book;
import com.fjordtek.bookstore.model.BookHashRepository;
import com.fjordtek.bookstore.model.BookRepository; import com.fjordtek.bookstore.model.BookRepository;
@RestController @RestController
@ -23,6 +24,9 @@ public class BookRestController {
@Autowired @Autowired
private BookRepository bookRepository; private BookRepository bookRepository;
@Autowired
private BookHashRepository bookHashRepository;
/* /*
@Autowired @Autowired
private CategoryRepository categoryRepository; private CategoryRepository categoryRepository;
@ -48,15 +52,17 @@ public class BookRestController {
} }
@RequestMapping( @RequestMapping(
value = "book" + "/{id}",
value = "book" + "/{hash_id}",
method = RequestMethod.GET method = RequestMethod.GET
) )
public @ResponseBody Optional<Book> getBookRestData( public @ResponseBody Optional<Book> getBookRestData(
@PathVariable("id") Long bookId,
@PathVariable("hash_id") String bookHashId,
HttpServletRequest requestData, HttpServletRequest requestData,
HttpServletResponse responseData HttpServletResponse responseData
) { ) {
Long bookId = new Long(bookHashRepository.findByHashId(bookHashId).getBookId());
httpServerLogger.log(requestData, responseData); httpServerLogger.log(requestData, responseData);
return bookRepository.findById(bookId); return bookRepository.findById(bookId);


+ 1
- 1
bookstore/src/main/resources/templates/bookedit.html View File

@ -18,7 +18,7 @@
page.title.webform.edit page.title.webform.edit
</h1> </h1>
<form th:object="${book}" action="#" th:action="@{{id}(id=${book.id})}" method="post">
<form th:object="${book}" action="#" th:action="@{{hash_id}(hash_id=${book.bookHash.hashId})}" method="post">
<div class="bookform-section"> <div class="bookform-section">
<div> <div>


+ 3
- 3
bookstore/src/main/resources/templates/booklist.html View File

@ -119,7 +119,7 @@ Idea of the following syntax used in this and other HTML document:
<td> <td>
<a class="btn btn-danger" <a class="btn btn-danger"
th:attr="onclick='javascript:return confirm(\'' + 'Delete book: ' + ${book.title} + '?' + '\');'" th:attr="onclick='javascript:return confirm(\'' + 'Delete book: ' + ${book.title} + '?' + '\');'"
th:href="@{__${deletepage}__/{id}(id=${book.id})}"
th:href="@{__${deletepage}__/{hash_id}(hash_id=${book.bookHash.hashId})}"
th:text="${#messages.msgOrNull('page.text.list.delete')} ?: 'page.text.list.delete'"> th:text="${#messages.msgOrNull('page.text.list.delete')} ?: 'page.text.list.delete'">
page.text.list.delete page.text.list.delete
</a> </a>
@ -127,7 +127,7 @@ Idea of the following syntax used in this and other HTML document:
<td> <td>
<a class="btn btn-warning" <a class="btn btn-warning"
th:href="@{__${editpage}__/{id}(id=${book.id})}"
th:href="@{__${editpage}__/{hash_id}(hash_id=${book.bookHash.hashId})}"
th:text="${#messages.msgOrNull('page.text.list.edit')} ?: 'page.text.list.edit'"> th:text="${#messages.msgOrNull('page.text.list.edit')} ?: 'page.text.list.edit'">
page.text.list.edit page.text.list.edit
</a> </a>
@ -135,7 +135,7 @@ Idea of the following syntax used in this and other HTML document:
<td> <td>
<a class="btn btn-info" <a class="btn btn-info"
th:href="@{__${restpage}__/book/{id}(id=${book.id})}"
th:href="@{__${restpage}__/book/{hash_id}(hash_id=${book.bookHash.hashId})}"
th:text="${#messages.msgOrNull('page.text.list.json')} ?: 'page.text.list.json'"> th:text="${#messages.msgOrNull('page.text.list.json')} ?: 'page.text.list.json'">
page.text.list.json page.text.list.json
</a> </a>


Loading…
Cancel
Save