From a52dd1e8c3e3366dfd5fc30dfe8339a646227252 Mon Sep 17 00:00:00 2001
From: Dao <comnuoc@users.noreply.gitlab.utu.fi>
Date: Sun, 23 Jun 2024 03:19:01 +0300
Subject: [PATCH] feat(part2): Exercise 2.

---
 part_2/exercise_2/.gitignore              |  30 +++++++
 part_2/exercise_2/exercise_2.iml          |  11 +++
 part_2/exercise_2/src/Book.java           | 103 ++++++++++++++++++++++
 part_2/exercise_2/src/BookDto.java        |   2 +
 part_2/exercise_2/src/BookRepository.java |  22 +++++
 part_2/exercise_2/src/BookService.java    |  49 ++++++++++
 part_2/exercise_2/src/Id.java             |   7 ++
 part_2/exercise_2/src/NotEmptyString.java |  26 ++++++
 part_2/exercise_2/src/User.java           |  23 +++++
 part_2/exercise_2/src/UserProvider.java   |   8 ++
 part_2/exercise_2/src/Year.java           |  21 +++++
 11 files changed, 302 insertions(+)
 create mode 100644 part_2/exercise_2/.gitignore
 create mode 100644 part_2/exercise_2/exercise_2.iml
 create mode 100644 part_2/exercise_2/src/Book.java
 create mode 100644 part_2/exercise_2/src/BookDto.java
 create mode 100644 part_2/exercise_2/src/BookRepository.java
 create mode 100644 part_2/exercise_2/src/BookService.java
 create mode 100644 part_2/exercise_2/src/Id.java
 create mode 100644 part_2/exercise_2/src/NotEmptyString.java
 create mode 100644 part_2/exercise_2/src/User.java
 create mode 100644 part_2/exercise_2/src/UserProvider.java
 create mode 100644 part_2/exercise_2/src/Year.java

diff --git a/part_2/exercise_2/.gitignore b/part_2/exercise_2/.gitignore
new file mode 100644
index 0000000..d578053
--- /dev/null
+++ b/part_2/exercise_2/.gitignore
@@ -0,0 +1,30 @@
+### IntelliJ IDEA ###
+/.idea/
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/part_2/exercise_2/exercise_2.iml b/part_2/exercise_2/exercise_2.iml
new file mode 100644
index 0000000..c90834f
--- /dev/null
+++ b/part_2/exercise_2/exercise_2.iml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/part_2/exercise_2/src/Book.java b/part_2/exercise_2/src/Book.java
new file mode 100644
index 0000000..becf5e0
--- /dev/null
+++ b/part_2/exercise_2/src/Book.java
@@ -0,0 +1,103 @@
+import java.util.Objects;
+
+/*
+ * @.classInvariant:
+ *     getId() != null
+ *     && getTitle() != null
+ *     && getYear() != null
+ *     && getPublisher() != null
+ */
+public class Book {
+    private final Id id;
+    private NotEmptyString title;
+    private Year year;
+    private NotEmptyString publisher;
+
+    /*
+     * @.pre: true
+     * @.post: Book object is constructed, throw NullPointerException if one of id, title, year, or publisher is null.
+     */
+    public Book(Id id, NotEmptyString title, Year year, NotEmptyString publisher) {
+        this.id = Objects.requireNonNull(id, "Id should not be null");
+
+        setTitle(title);
+        setYear(year);
+        setPublisher(publisher);
+    }
+
+    /*
+     * @.pre: true
+     * @.post: RESULT != null
+     */
+    public Id getId() {
+        return id;
+    }
+
+    /*
+     * @.pre: true
+     * @.post: RESULT != null
+     */
+    public NotEmptyString getTitle() {
+        return title;
+    }
+
+    /*
+     * @.pre: true
+     * @.post: RESULT != null
+     */
+    public Year getYear() {
+        return year;
+    }
+
+    /*
+     * @.pre: true
+     * @.post: RESULT != null
+     */
+    public NotEmptyString getPublisher() {
+        return publisher;
+    }
+
+    /*
+     * @.pre: true
+     * @.post: data is updated.
+     */
+    protected void updateData(BookDto dto) {
+        Objects.requireNonNull(dto, "Id should not be null");
+
+        if (null != dto.title()) {
+            setTitle(dto.title());
+        }
+
+        if (null != dto.year()) {
+            setYear(dto.year());
+        }
+
+        if (null != dto.publisher()) {
+            setPublisher(dto.publisher());
+        }
+    }
+
+    /*
+     * @.pre: true
+     * @.post: data is assigned, throw NullPointerException if year is null.
+     */
+    private void setYear(Year year) {
+        this.year = Objects.requireNonNull(year, "Year should not be null");
+    }
+
+    /*
+     * @.pre: true
+     * @.post: data is assigned, throw NullPointerException if title is null.
+     */
+    private void setTitle(NotEmptyString title) {
+        this.title = Objects.requireNonNull(title, "Title should not be null");
+    }
+
+    /*
+     * @.pre: true
+     * @.post: data is assigned, throw NullPointerException if publisher is null.
+     */
+    private void setPublisher(NotEmptyString publisher) {
+        this.publisher = Objects.requireNonNull(publisher, "Publisher should not be null");
+    }
+}
diff --git a/part_2/exercise_2/src/BookDto.java b/part_2/exercise_2/src/BookDto.java
new file mode 100644
index 0000000..a439bc1
--- /dev/null
+++ b/part_2/exercise_2/src/BookDto.java
@@ -0,0 +1,2 @@
+public record BookDto(Id id, NotEmptyString title, Year year, NotEmptyString publisher) {
+}
diff --git a/part_2/exercise_2/src/BookRepository.java b/part_2/exercise_2/src/BookRepository.java
new file mode 100644
index 0000000..f592cb5
--- /dev/null
+++ b/part_2/exercise_2/src/BookRepository.java
@@ -0,0 +1,22 @@
+import java.util.List;
+
+public interface BookRepository {
+    /*
+     * @.pre: Id != null
+     * @.post: RESULT != null,
+     *      throw RuntimeException if book is not found
+     */
+    Book find(Id id);
+
+    /*
+     * @.pre: BookDto != null
+     * @.post: Books are returned based on the search criteria
+     */
+    List<Book> search(BookDto criteria);
+
+    /*
+     * @.pre: Book != null
+     * @.post: book is updated
+     */
+    void update(Book book);
+}
diff --git a/part_2/exercise_2/src/BookService.java b/part_2/exercise_2/src/BookService.java
new file mode 100644
index 0000000..0eca39f
--- /dev/null
+++ b/part_2/exercise_2/src/BookService.java
@@ -0,0 +1,49 @@
+import java.util.List;
+import java.util.Objects;
+
+public class BookService {
+    private final BookRepository bookRepository;
+    private final UserProvider userProvider;
+
+    public BookService(BookRepository bookRepository, UserProvider userProvider) {
+        this.bookRepository = bookRepository;
+        this.userProvider = userProvider;
+    }
+
+    /*
+     * @.pre: true
+     * @.post: Books are returned based on the search criteria, throw SecurityException if user is not allowed to search
+     */
+    public List<Book> search(BookDto criteria) {
+        Objects.requireNonNull(criteria, "Criteria should not be null");
+
+        User user = userProvider.getUser();
+
+        if (!user.canSearch()) {
+            throw new SecurityException("User is not allowed to search books.");
+        }
+
+        return bookRepository.search(criteria);
+    }
+
+    /*
+     * @.pre: true
+     * @.post: Book is updated,
+     *      throw SecurityException if user is not allowed to update
+     */
+    public void update(BookDto dto) {
+        Objects.requireNonNull(dto, "Book data should not be null");
+        Objects.requireNonNull(dto.id(), "Book id should not be null");
+
+        Book book = bookRepository.find(dto.id());
+        User user = userProvider.getUser();
+
+        if (!user.canUpdate(book)) {
+            throw new SecurityException("User is not allowed to update book.");
+        }
+
+        book.updateData(dto);
+
+        bookRepository.update(book);
+    }
+}
\ No newline at end of file
diff --git a/part_2/exercise_2/src/Id.java b/part_2/exercise_2/src/Id.java
new file mode 100644
index 0000000..b96bd5f
--- /dev/null
+++ b/part_2/exercise_2/src/Id.java
@@ -0,0 +1,7 @@
+import java.util.Objects;
+
+public record Id(Object id) {
+    public Id {
+        Objects.requireNonNull(id, "Id should not be null");
+    }
+}
diff --git a/part_2/exercise_2/src/NotEmptyString.java b/part_2/exercise_2/src/NotEmptyString.java
new file mode 100644
index 0000000..55a4263
--- /dev/null
+++ b/part_2/exercise_2/src/NotEmptyString.java
@@ -0,0 +1,26 @@
+/*
+ * @.classInvariant:
+ *     str() != null && !str.trim().isEmpty()
+ */
+public record NotEmptyString(String str) {
+    /*
+     * @.pre: true
+     * @.post: Object is constructed, throw NotEmptyStringException if string is empty
+     */
+    public NotEmptyString {
+        if (null == str || str.trim().isEmpty()) {
+            throw new NotEmptyStringException("String is null or empty.");
+        }
+    }
+
+    @Override
+    public String toString() {
+        return str;
+    }
+}
+
+class NotEmptyStringException extends RuntimeException {
+    public NotEmptyStringException(String message) {
+        super(message);
+    }
+}
diff --git a/part_2/exercise_2/src/User.java b/part_2/exercise_2/src/User.java
new file mode 100644
index 0000000..4a4988e
--- /dev/null
+++ b/part_2/exercise_2/src/User.java
@@ -0,0 +1,23 @@
+public interface User {
+    /*
+     * @.pre: true
+     * @.post: whether user is a staff or not
+     */
+    boolean isStaff();
+
+    /*
+     * @.pre: true
+     * @.post: RESULT == true
+     */
+    default boolean canSearch() {
+        return true;
+    }
+
+    /*
+     * @.pre: true
+     * @.post: RESULT == true if user is a staff, else RESULT == false
+     */
+    default boolean canUpdate(Book book) {
+        return this.isStaff();
+    }
+}
diff --git a/part_2/exercise_2/src/UserProvider.java b/part_2/exercise_2/src/UserProvider.java
new file mode 100644
index 0000000..8e756d5
--- /dev/null
+++ b/part_2/exercise_2/src/UserProvider.java
@@ -0,0 +1,8 @@
+public interface UserProvider {
+    /*
+     * @.pre: true
+     * @.post: RESULT != null,
+     *      throw Exception if no user
+     */
+    User getUser();
+}
diff --git a/part_2/exercise_2/src/Year.java b/part_2/exercise_2/src/Year.java
new file mode 100644
index 0000000..c2ea489
--- /dev/null
+++ b/part_2/exercise_2/src/Year.java
@@ -0,0 +1,21 @@
+/*
+ * @.classInvariant:
+ *     year() > 0 && year() <= 9999
+ */
+public record Year(int year) {
+    /*
+     * @.pre: true
+     * @.post: Object is constructed, throw InvalidYearException if year <= 0 || year > 9999
+     */
+    public Year {
+        if (year <= 0 || year > 9999) {
+            throw new InvalidYearException("Year is not valid");
+        }
+    }
+}
+
+class InvalidYearException extends RuntimeException {
+    public InvalidYearException(String message) {
+        super(message);
+    }
+}
\ No newline at end of file
-- 
GitLab