Add search ability
This commit is contained in:
parent
1791fd909d
commit
00f277797e
13 changed files with 193 additions and 41 deletions
|
@ -6,21 +6,21 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
import ru.redrise.marinesco.data.InpEntryRepository;
|
import ru.redrise.marinesco.data.BookRepository;
|
||||||
import ru.redrise.marinesco.library.InpEntry;
|
import ru.redrise.marinesco.library.Book;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/book")
|
@RequestMapping("/book")
|
||||||
public class BookController {
|
public class BookController {
|
||||||
InpEntryRepository inpEntryRepository;
|
BookRepository bookRepository;
|
||||||
|
|
||||||
public BookController(InpEntryRepository inpEntryRepository){
|
public BookController(BookRepository bookRepository){
|
||||||
this.inpEntryRepository = inpEntryRepository;
|
this.bookRepository = bookRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{bookId}")
|
@GetMapping("/{bookId}")
|
||||||
public String getPage(@PathVariable("bookId") Long bookId, Model model) {
|
public String getPage(@PathVariable("bookId") Integer bookId, Model model) {
|
||||||
InpEntry book = inpEntryRepository.findById(bookId).orElse(null);
|
Book book = bookRepository.findById(bookId).orElse(null);
|
||||||
if (book == null){
|
if (book == null){
|
||||||
model.addAttribute("Error", "Not found");
|
model.addAttribute("Error", "Not found");
|
||||||
return "book";
|
return "book";
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ru.redrise.marinesco;
|
package ru.redrise.marinesco;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
47
src/main/java/ru/redrise/marinesco/SearchController.java
Normal file
47
src/main/java/ru/redrise/marinesco/SearchController.java
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package ru.redrise.marinesco;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import ru.redrise.marinesco.data.AuthorRepository;
|
||||||
|
import ru.redrise.marinesco.data.BookRepository;
|
||||||
|
import ru.redrise.marinesco.library.Author;
|
||||||
|
import ru.redrise.marinesco.library.Book;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/search")
|
||||||
|
public class SearchController {
|
||||||
|
|
||||||
|
private BookRepository inpEntryRepository;
|
||||||
|
private AuthorRepository authorRepository;
|
||||||
|
|
||||||
|
public SearchController(BookRepository bookRepository, AuthorRepository authorRepository){
|
||||||
|
this.inpEntryRepository = bookRepository;
|
||||||
|
this.authorRepository = authorRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String requestMethodName(@RequestParam String search, Model model) {
|
||||||
|
|
||||||
|
if (search.trim().equals(""))
|
||||||
|
return "search";
|
||||||
|
|
||||||
|
List<Book> books = inpEntryRepository.findByTitleContainingIgnoreCase(search);
|
||||||
|
model.addAttribute("books", books);
|
||||||
|
|
||||||
|
List<Book> bookSeries = inpEntryRepository.findByTitleContainingIgnoreCase(search);
|
||||||
|
model.addAttribute("series", bookSeries);
|
||||||
|
|
||||||
|
List<Author> authors = authorRepository.findByAuthorNameContainingIgnoreCase(search);
|
||||||
|
model.addAttribute("authors", authors);
|
||||||
|
|
||||||
|
return "search";
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,14 @@ import org.springframework.data.repository.CrudRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import ru.redrise.marinesco.library.Author;
|
import ru.redrise.marinesco.library.Author;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface AuthorRepository extends CrudRepository<Author, Long>{
|
public interface AuthorRepository extends CrudRepository<Author, Long>{
|
||||||
Optional<Author> findByAuthorName(String authorName);
|
Optional<Author> findByAuthorName(String authorName);
|
||||||
|
List<Author> findByAuthorNameContainingIgnoreCase(String authorName);
|
||||||
}
|
}
|
||||||
|
|
16
src/main/java/ru/redrise/marinesco/data/BookRepository.java
Normal file
16
src/main/java/ru/redrise/marinesco/data/BookRepository.java
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package ru.redrise.marinesco.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import ru.redrise.marinesco.library.Book;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface BookRepository extends CrudRepository<Book, Integer>{
|
||||||
|
List<Book> findBySeriesContainingIgnoreCase(String title);
|
||||||
|
List<Book> findByTitleContainingIgnoreCase(String title);
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
package ru.redrise.marinesco.data;
|
|
||||||
|
|
||||||
import org.springframework.data.repository.CrudRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import ru.redrise.marinesco.library.InpEntry;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface InpEntryRepository extends CrudRepository<InpEntry, Long>{
|
|
||||||
}
|
|
|
@ -21,7 +21,7 @@ import ru.redrise.marinesco.data.GenreRepository;
|
||||||
@Entity
|
@Entity
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
|
||||||
public class InpEntry {
|
public class Book {
|
||||||
@Id
|
@Id
|
||||||
private Integer id;
|
private Integer id;
|
||||||
private Long libraryId;
|
private Long libraryId;
|
||||||
|
@ -36,19 +36,19 @@ public class InpEntry {
|
||||||
private String serNo;
|
private String serNo;
|
||||||
private String fsFileName; // inside zip
|
private String fsFileName; // inside zip
|
||||||
private Long fileSize; // extracted, in bytes
|
private Long fileSize; // extracted, in bytes
|
||||||
|
private String fileSizeForHumans;
|
||||||
private String libId; // same to filename
|
private String libId; // same to filename
|
||||||
private String deleted; // is deleted flag
|
private String deleted; // is deleted flag
|
||||||
private String fileExtension; // - concatenate to fsFileName
|
private String fileExtension; // - concatenate to fsFileName
|
||||||
private LocalDate addedDate;
|
private LocalDate addedDate;
|
||||||
private String container;
|
private String container;
|
||||||
|
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private int position = 0;
|
private int position = 0;
|
||||||
@Transient
|
@Transient
|
||||||
private byte[] line;
|
private byte[] line;
|
||||||
|
|
||||||
public InpEntry(byte[] line,
|
public Book(byte[] line,
|
||||||
String container,
|
String container,
|
||||||
AuthorRepository authorRepository,
|
AuthorRepository authorRepository,
|
||||||
GenreRepository genreRepository,
|
GenreRepository genreRepository,
|
||||||
|
@ -69,6 +69,7 @@ public class InpEntry {
|
||||||
this.serNo = parseNextString();
|
this.serNo = parseNextString();
|
||||||
this.fsFileName = parseNextString();
|
this.fsFileName = parseNextString();
|
||||||
this.fileSize = Long.valueOf(parseNextString());
|
this.fileSize = Long.valueOf(parseNextString());
|
||||||
|
this.fileSizeForHumans = formatByteSize(fileSize);
|
||||||
this.libId = parseNextString();
|
this.libId = parseNextString();
|
||||||
this.deleted = parseNextString();
|
this.deleted = parseNextString();
|
||||||
this.fileExtension = parseNextString();
|
this.fileExtension = parseNextString();
|
||||||
|
@ -101,9 +102,11 @@ public class InpEntry {
|
||||||
String allAuthors = new String(line, 0, position, StandardCharsets.UTF_8);
|
String allAuthors = new String(line, 0, position, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
for (String authorName : allAuthors.split(":")) {
|
for (String authorName : allAuthors.split(":")) {
|
||||||
Author author = authorRepository.findByAuthorName(authorName).orElse(new Author(authorName));
|
authorName = authorName.replaceAll(",", " ").trim();
|
||||||
|
if (!authorName.equals("")) {
|
||||||
authors.add(authorRepository.save(author));
|
Author author = authorRepository.findByAuthorName(authorName).orElse(new Author(authorName));
|
||||||
|
authors.add(authorRepository.save(author));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
++position;
|
++position;
|
||||||
|
@ -143,4 +146,12 @@ public class InpEntry {
|
||||||
RainbowDump.hexDumpUTF8(line);
|
RainbowDump.hexDumpUTF8(line);
|
||||||
throw new Exception("Invalid 'inp' file format (parse Title)");
|
throw new Exception("Invalid 'inp' file format (parse Title)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String formatByteSize(double length) {
|
||||||
|
final String[] measureName = { "bytes", "KB", "MB", "GB", "TB" };
|
||||||
|
int i;
|
||||||
|
for (i = 0; length > 1024 && i < measureName.length - 1; i++)
|
||||||
|
length = length / 1024;
|
||||||
|
return String.format("%,.2f %s", length, measureName[i]);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -8,14 +8,13 @@ import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import ru.redrise.marinesco.data.AuthorRepository;
|
import ru.redrise.marinesco.data.AuthorRepository;
|
||||||
|
import ru.redrise.marinesco.data.BookRepository;
|
||||||
import ru.redrise.marinesco.data.GenreRepository;
|
import ru.redrise.marinesco.data.GenreRepository;
|
||||||
import ru.redrise.marinesco.data.InpEntryRepository;
|
|
||||||
import ru.redrise.marinesco.data.LibraryMetadataRepository;
|
import ru.redrise.marinesco.data.LibraryMetadataRepository;
|
||||||
import ru.redrise.marinesco.settings.ApplicationSettings;
|
import ru.redrise.marinesco.settings.ApplicationSettings;
|
||||||
|
|
||||||
|
@ -30,19 +29,19 @@ public class InpxScanner implements Runnable {
|
||||||
private LibraryMetadataRepository libraryMetadataRepository;
|
private LibraryMetadataRepository libraryMetadataRepository;
|
||||||
private AuthorRepository authorRepository;
|
private AuthorRepository authorRepository;
|
||||||
private GenreRepository genreRepository;
|
private GenreRepository genreRepository;
|
||||||
private InpEntryRepository inpEntryRepository;
|
private BookRepository bookRepository;
|
||||||
|
|
||||||
private String filesLocation;
|
private String filesLocation;
|
||||||
|
|
||||||
public InpxScanner(ApplicationSettings applicationSettings,
|
public InpxScanner(ApplicationSettings applicationSettings,
|
||||||
AuthorRepository authorRepository,
|
AuthorRepository authorRepository,
|
||||||
GenreRepository genreRepository,
|
GenreRepository genreRepository,
|
||||||
InpEntryRepository inpEntryRepository,
|
BookRepository bookRepository,
|
||||||
LibraryMetadataRepository libraryMetadataRepository) {
|
LibraryMetadataRepository libraryMetadataRepository) {
|
||||||
this.filesLocation = applicationSettings.getFilesLocation();
|
this.filesLocation = applicationSettings.getFilesLocation();
|
||||||
this.authorRepository = authorRepository;
|
this.authorRepository = authorRepository;
|
||||||
this.genreRepository = genreRepository;
|
this.genreRepository = genreRepository;
|
||||||
this.inpEntryRepository = inpEntryRepository;
|
this.bookRepository = bookRepository;
|
||||||
this.libraryMetadataRepository = libraryMetadataRepository;
|
this.libraryMetadataRepository = libraryMetadataRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,14 +169,14 @@ public class InpxScanner implements Runnable {
|
||||||
byte[] line = new byte[i - lastIndex];
|
byte[] line = new byte[i - lastIndex];
|
||||||
System.arraycopy(content, lastIndex, line, 0, i - lastIndex - 1);
|
System.arraycopy(content, lastIndex, line, 0, i - lastIndex - 1);
|
||||||
|
|
||||||
InpEntry book = new InpEntry(line,
|
Book book = new Book(line,
|
||||||
name,
|
name,
|
||||||
authorRepository,
|
authorRepository,
|
||||||
genreRepository,
|
genreRepository,
|
||||||
libraryMetadata.getId(),
|
libraryMetadata.getId(),
|
||||||
libraryMetadata.getVersion());
|
libraryMetadata.getVersion());
|
||||||
|
|
||||||
inpEntryRepository.save(book);
|
bookRepository.save(book);
|
||||||
|
|
||||||
if (isNextCarriageReturn(i, content)) {
|
if (isNextCarriageReturn(i, content)) {
|
||||||
i += 2;
|
i += 2;
|
||||||
|
|
|
@ -5,7 +5,8 @@ spring:
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: org.h2.Driver
|
||||||
generate-unique-name: false
|
generate-unique-name: false
|
||||||
name: marinesco
|
name: marinesco
|
||||||
url: jdbc:h2:mem:marinesco
|
# url: jdbc:h2:mem:marinesco
|
||||||
|
url: jdbc:h2:file:/tmp/h2
|
||||||
username: sa
|
username: sa
|
||||||
password:
|
password:
|
||||||
jpa:
|
jpa:
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<br /><span th:if="${book.serNo} != null" th:text="${'Series # : ' + book.serNo}"></span>
|
<br /><span th:if="${book.serNo} != null" th:text="${'Series # : ' + book.serNo}"></span>
|
||||||
<br /><span th:text="${'Format: ' + book.fileExtension}"></span>
|
<br /><span th:text="${'Format: ' + book.fileExtension}"></span>
|
||||||
<br /><span th:if="${book.addedDate} != null" th:text="${'Added : ' + book.addedDate}"></span>
|
<br /><span th:if="${book.addedDate} != null" th:text="${'Added : ' + book.addedDate}"></span>
|
||||||
<br /><span th:text="${'Size: ' + book.fileSize + ' bytes'}"></span>
|
<br /><span th:text="${'Size: ' + book.fileSizeForHumans}"></span>
|
||||||
<p>
|
<p>
|
||||||
<a th:href="${'/download/?container=' + book.container + '&file=' + book.fsFileName}" th:text="Download"></a>
|
<a th:href="${'/download/?container=' + book.container + '&file=' + book.fsFileName}" th:text="Download"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -31,13 +31,15 @@
|
||||||
<div class="header_entry header_title">
|
<div class="header_entry header_title">
|
||||||
Marinesco
|
Marinesco
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
<div th:replace="~{fragments/header :: ${#authentication.principal.isAdmin()} ? 'header-admin' : 'header'}"></div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!--
|
||||||
|
<div th:replace="~{fragments/header :: ${#authentication.principal.isAdmin()} ? 'header-admin' : 'header'}"></div>
|
||||||
|
-->
|
||||||
|
<div th:fragment="header-content-admin" >
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -20,7 +20,7 @@
|
||||||
<br /><a href="/h2">H2</a>
|
<br /><a href="/h2">H2</a>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<form action='' method='get'>
|
<form action='/search' method='get'>
|
||||||
<input type='text' name='search'>
|
<input type='text' name='search'>
|
||||||
<button class="sign" type='submit'>Search</button>
|
<button class="sign" type='submit'>Search</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
83
src/main/resources/templates/search.html
Normal file
83
src/main/resources/templates/search.html
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Marinesco</title>
|
||||||
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
|
<link rel="alternate icon" href="/favicon.png" type="image/png">
|
||||||
|
<link rel="stylesheet" th:href="@{/styles/styles.css}" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div th:replace="~{fragments/header :: 'header'}"></div>
|
||||||
|
<div class="container base">
|
||||||
|
<form action='' method='get'>
|
||||||
|
<input type='text' name='search'>
|
||||||
|
<button class="sign" type='submit'>Search</button>
|
||||||
|
</form>
|
||||||
|
<div th:if="${books} != null">
|
||||||
|
<hr>
|
||||||
|
<h3>Titles</h3>
|
||||||
|
<div th:each="book : ${books}">
|
||||||
|
<a th:href="${'/book/' + book.id}">
|
||||||
|
<span th:text="${book.title}"></span>
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<div th:if="${book.series} != ''" th:text="${'Series: ' + book.series}">
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<span th:if="${#lists.size(book.authors)} == 1" th:text="${book.authors[0].authorName}"></span>
|
||||||
|
<span th:if="${#lists.size(book.authors)} > 1" th:each="author : ${book.authors}" >
|
||||||
|
<span th:text="${author.authorName} + ', '"></span>
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<a th:href="${'/download/?container=' + book.container + '&file=' + book.fsFileName}"
|
||||||
|
th:text="Download">
|
||||||
|
</a>
|
||||||
|
<span th:text="${' (' + book.fileExtension + ' ' + book.fileSizeForHumans + ')'}"></span>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div th:if="${series} != null">
|
||||||
|
<hr>
|
||||||
|
<h3>Series</h3>
|
||||||
|
<div th:each="book : ${series}">
|
||||||
|
<a th:href="${'/book/' + book.id}">
|
||||||
|
<span th:text="${book.title}"></span>
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<div th:if="${book.series} != ''" th:text="${'Series: ' + book.series}">
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<span th:if="${#lists.size(book.authors)} == 1" th:text="${book.authors[0].authorName}"></span>
|
||||||
|
<span th:if="${#lists.size(book.authors)} > 1" th:each="author : ${book.authors}" >
|
||||||
|
<span th:text="${author.authorName} + ', '"></span>
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<a th:href="${'/download/?container=' + book.container + '&file=' + book.fsFileName}"
|
||||||
|
th:text="Download">
|
||||||
|
</a>
|
||||||
|
<span th:text="${' (' + book.fileExtension + ' ' + book.fileSizeForHumans + ')'}"></span>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div th:if="${authors} != null">
|
||||||
|
<hr>
|
||||||
|
<h3>Authors</h3>
|
||||||
|
<div th:each="author : ${authors}">
|
||||||
|
<a th:href="${'/author/' + author.id}">
|
||||||
|
<span th:text="${author.authorName}"></span>
|
||||||
|
</a>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:replace="~{fragments/footer :: 'footer'}"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in a new issue