classDiagram direction BT %% Interface class PublicationOperations { <<interface>> + publish() void } %% Abstract class class Publication { <<abstract>> - String id - String title - Author author - LocalDate publicationDate - String format + getId() String + getTitle() String + getAuthor() Author + getPublicationDate() LocalDate + getFormat() String } Publication ..> PublicationOperations %% Publication Entity classes class Book { - String ISBN - int pages - int genre + getISBN() String + getPages() int + getGenre() String } class CD { - int duration - int numberOfTracks + getDuration() int + getNumberOfTracks() int } class DVD { - int duration + getDuration() int } Book ..> Publication CD ..> Publication DVD ..> Publication
LAB#SE03-2: Library/Book, Expand Model
Basic understanding of Java programming language is required, as well as some familiarity with Maven or Gradle for managing dependencies and building the project.
Use of Interfaces and Abstract classes to implement features.
Describe the required models that might define the solution to a Library implementation. Create multiple classes in Java using the most convenient entity relationship between them to implement the solution.
Test these classes using JUnit.
- Create a new Maven or Gradle project and setting up the project structure
- Modify the project’s
pom.xml
orbuild.gradle
file to import necessary dependencies, including JUnit for testing - Implement the required classes in Java
- Implement User interactive interfaces for each manager
- Write JUnit tests to verify that classes work as expected
- Allow the user to input data via the console, rather than using
hard-coded test data
in JUnit tests
Introduction
In LibraryProject
, the User
and Staff
classes could have different operations
that they are authorized/able to perform.
The User
class could have the following operations:
- Search for books
- Borrow a book
- Return a book
- Check the status of borrowed books
- Update personal information
The Staff
class could have the following operations:
- Add a new book
- Remove a book
- Update the book details
- Register a new user
- Remove a user
- Update the user details
- Generate reports on library operations
To implement
these operations, we could define three separate interfaces
:
- UserOperations
- StaffOperations
- GeneralOperations
Remember, Interfaces have no state, thus they don’t have variables (any attribute defined in an Interface is treated as a constant), and they are responsible of telling the classes that implement it what methods to support.
If you need to keep track of the state of your instances (i.e. attributes), then go for the abstract class and extend it to the required subclasses.
Here are some good points regarding Interfaces and Abstract classes:
- Instance variables in interface: It’s a risky approach because they aren’t treated as variables but
final static
(i.e, constants). If you need to define variables for your classes as well as mandatory methods implementation, then go for the Abstract class approach
Interfaces
The three interfaces GeneralOperations
, UserOperations
and StaffOperations
provide a clear separation of concerns and responsibilities within the library management system.
GeneralOperations
interface
provides methods that are commonly used by both users and staff, such as searching for books and creating an account.
UserOperations
interface
includes methods that are specific to users, such as updating personal information, borrowing and returning books, and checking their borrowed books.
StaffOperations
interface
includes methods that are specific to staff members, such as adding, removing, and updating book details, as well as registering and updating user details and generationg reports.
By separating these different functionailities into interfaces, it becomes easier to implement and maintain the system.
Interfaces allow the developers to implement their features knowing beforehand the expected available methods of future classes.
Abstract class
Finally, the LibraryMember
abstract class is a parent class for users and staff members, which includes common fields such as id, address, and phoneNumber.
By defining these fields in an abstract
class, it allows for different types of users and staff members to inherit them and avoid duplication of code.
UML
Previous drafts
The two UML diagrams describe the core features of a libraryProject, specifically the use cases for borrowing and issuing books, as well as the roles and responsibilities of users and librarians/staff members.
The first diagram,
Core features: borrow and issue
, depicts the interaction between the user and the system, as well as the system and the librarian:
Create link to the previous UML
The second diagram,
Core features: user and librarian (staff)
, expands on the roles and responsibilities of users and librarians/staff members.
Create link to the previous UML
Current UML
Create new UML
Publications
Library Members
classDiagram direction BT %% Relationships LibraryMember ..> GeneralOperations LibraryMember ..> Person Staff ..o StaffMember StaffMember ..> StaffOperations StaffMember ..> LibraryMember User ..o LibraryMember User ..> BorrowOperations Librarian ..o StaffMember Librarian ..> BorrowOperations %% Interfaces class GeneralOperations { <<interface>> + createAccount(): void + searchPublications(query: String): void } class BorrowOperations { <<interface>> + createBorrow(item: Publication) void + returnBorrow(item: Publication) void + updateBorrow(item: Publication) void } class StaffOperations { <<interface>> + addPublication(item: Publication) void + removePublication(item: Publication) void + updatePublication(item: Publication) void + registerMember(member: LibraryMember) void + removeMember(member: LibraryMember) void + updateMember(member: LibraryMember) void + generateReports() void } %% Abstract class class Person { <<abstract>> - firstName: String - lastName: String - nationality: String - birthdate: LocalDate } class LibraryMember { <<abstract>> - id: String - address: String - phoneNumber: String } class StaffMember { <<abstract>> - double: salary } %% Entities class User { - String: mail - String: password } class Staff { } class Librarian { }
Migrating to SpringBoot
With the core model defined and its functionalities, let’s migrate the project to a SpringBoot project to enable Web features, so we can use REST controllers to access the application and its databases.
User interface
Let’s define the different domains that are accessible through a user interface.
- In a web interface, domains could be:
/user
with a private access, only available to Library Users/librarian
with a private access, only available to Librarians/web
with a public access, available to any person that access the webpage
Controllers
In the backend, we need controllers for user
and librarian
, with access to the Library db
.
Requests can be of multiple forms, such as API Rest, HTML, websockets…
Required methods to resolve the requests coming from the frontend must be implemented into each actor (user
and library
). These methods invoke other classes like @Service
and @Repository
, that have the utils
required to resolve the requests.
@Service
and @Repository
here are like the Manager
classes
We treat them as the source of all utilities that execute the response logic
Databases
For this project, we’ll be using H2. H2 is a Java SQL database with the following main features:
- Very fast, open source, JDBC API
- Embedded and serer modes; in-memory databases
- Browser based Console application
- Small footprint: around 2.5 MB jar file size
Creating entities
We need to define entities that will persist inside DB: - Any Publication
subclass object - Any LibraryMember
subclass object: - This includes User
, Librarian
and Staff
- Borrows
objects
Each entity must define its primary key with the @Id
annotation. When inheritance is present, then the parent class should define it as well as the @Inheritance
type.
- If the parent class isn’t going to be instantiated, only define the
@MappedSuperClass
annotation to allow subclasses to inherit its JPA attibutes - Each first class that defines a new DB table where to store this’ or their subclasses’ instances must define the following annotations:
@Entity
, even though this class doesn’t create instances (mandatori for its subclasses)@Table
name where to store all instances@Inheritance
strategy, such asSINGLE_TABLE
to store all subclass entities into this single table- If
SINGLE_TABLE
is selected, then the parent class must define@DiscriminatorColumn
with a name and its value type. Child classes must then define the proper@DiscriminatorValue
that will be inserted in the previous column when inserted into the table - If
TABLE_PER_CLASS
is selected, then each subclass will create its own@Table
inside the DB. If no@Table
annotation is defined in subclasses, then the subclass name is used as its table name
- If
When there’s a composition relationship between classes, then we have to define its multiplicity using the @ManyToMany
, @OneToOne
, @ManyToOne
or @OneToMany
annotations.
@ManyToOne
relation
j.LocalContainerEntityManagerFactoryBean : Failed to initialize JPA EntityManagerFactory: Association 'com.springbootlab0.approach_1.domain.Publication.author' targets an unknown entity named 'com.springbootlab0.approach_1.domain.Author'
This error is due to bad annotations between inherited classes, not a @ManyToOne
issue. Publication
class couldn’t find the Author
entity because Person
superclass and its subclass Author
were conflicting with the @Inheritance
and @MappedSuperClass
annotations.
Once fixed, an AUTHOR_TABLE
is created in the H2 DB and Publication
instance can correctly map the Author
attribute to the AUTHOR_ID
field in the previous table.
The final JPA entity and inheritance diagram is as follows:
classDiagram direction BT %% Abstract class class Publication { \@Entity(name = "Publication") \@Table(name = "PUBLICATION_TABLE") \@Inheritance(strategy = InheritanceType.SINGLE_TABLE) \@DiscriminatorColumn(name = "PUBLICATION_TYPE", discriminatorType = DiscriminatorType.STRING) } %% Publication Entity classes class Book { \@Entity(name = "Book") \@DiscriminatorValue(value = "BOOK") } class CD { \@Entity(name = "CD") \@DiscriminatorValue(value = "CD") } class DVD { \@Entity(name = "DVD") \@DiscriminatorValue(value = "DVD") } Book ..> Publication CD ..> Publication DVD ..> Publication
Publication JPA annotations
classDiagram direction BT %% Relationships LibraryMember ..> Person Author ..> Person Staff ..o StaffMember StaffMember ..> LibraryMember User ..o LibraryMember Librarian ..o StaffMember class Person { @MappedSuperClass } class Author { \@Entity(name = "Author") \@Table(name = "AUTHOR_TABLE") } class LibraryMember { \@Entity(name = "LibraryMember") \@Table(name = "LIBRARYMEMBER_TABLE") \@Inheritance(strategy = InheritanceType.SINGLE_TABLE) \@DiscriminatorColumn(name = "MEMBER_TYPE", discriminatorType = discriminatorType.STRING) } class StaffMember { @MappedSuperClass } class User { \@Entity(name = "User") \@DiscriminatorValue(value = "USER") } class Staff { \@Entity(name = "Staff") \@DiscriminatorValue(value = "STAFF") } class Librarian { \@Entity(name = "Librarian") \@DiscriminatorValue(value = "LIBRARIAN") }
LibraryMember JPA annotations
The resulting tables in the DB are as follow:
AUTHOR_TABLE
: InheritsPerson
fields and adds its ownId
andpenName
LIBRARY_MEMBER
: Here it inherits:Person
fields- Adds its own
Id
,address
andphoneNumber
fields - Adds the
StaffMember
’ssalary
field - And also adds the
User
’smail
andpassword
fields
PUBLICATION_TABLE
: Here the inheritance is as follows:- Adds its own
Publication
fields, including theId
field - Adds the
Book
’s fields - Adds the
CD
’s fields - Adds the
DVD
fields, even though they overlap with theCD
ones - And also adds the
Author ID
of that publication, coming from theAuthor
entity. The@ManyToOne
annotation can be verified in the Indexes ofPUBLICATION_TABLE
, whereAUTHOR_ID
appears as Not unique
- Adds its own
Defining the use cases
Creating a publication
It seems that we’ll have to work with particular publications instead of creating a one-size-fits-all solution, but the inheritance is well solved in the Thymeleaf form and the object is correctly composed when the request reaches the PublicationController
.
The Spring boot controller sends a new empty Publication object to the thymeleaf engine with its already generated ID, but if the ID isn’t assigned again in the creation form, then the Controller recreates the object with a new ID!
Make sure to add a th:field=*{id}
entry in the form to make sure that later @Setter
methods will re-assign that value to the object
Creating a Libary Member
Inheritance is causing issues when trying to save the attributes of child classes:
- Only
Person
attributes and theid
created in the abstractLibraryMember
are considered when Thymeleaf sends the form values to the@Controller
, still don’t know why - This doesn’t happen with the current
Publication
implementation, where all fields, including@Many-to-one
andenum
types when creating a newBook
from the Publication Form View
@Setter
annotations in all @Entity
classes!!
I forgot to add the @Setter
annotation in all @Entity
classes except the parent one! 😥
Once added the @Setter
annotation, tables are correctly populated!
As discussed in this StackOverflow thread,the only way to do this is by removing the inheritance from these classes and mark the previously parent class as @Embeddable
, while defining new parent class attribute to each of the previously called child classes and annotating them as @Embedded
.
This doesn’t make it easier to maintain as shown in this Baeldung example, and in the OOP realm we would have to access a new Person object for each derivated class.
Also, this Baeldung article explains that when using @MappedSuperclass
, ancestors cannot contain associations with other entities. This might be the reason why it doesn’t work well with Librarian
and User
?
Updating publications or library members
Optional<Subclass>
when dealing with Publication updates
This requires further investigation because, while using a very similar form, it looks like the Parent class ID is safely transfered when recreating the object on the @Controller
side. This doesn’t happen when the recreated class doesn’t inherit the ID from another class.
Also, this ID problem also appears on subclasses creation forms. Does it have something to do with the GET/POST or RequestMapping annotations?
Creating Borrows
The Borrow
class contains the information related to a Publication
that has been borrowed by a LibraryMember
. The instantiated object contains the following fields:
id
: the unique ID of thatBorrow
borrowUser
: the user of thisBorrow
borrowedPublication
: the publication borrowedstartBorrowDate
: the date where theBorrow
was created. It is automatically initialized onBorrow
creationdueBorrowDate
: the date where theBorrow
should already be returned. It is automatically initialized onBorrow
creation, 15 days from thestartBorrowDate
returnedBorrowDate
: the real date where theBorrow
has been returned. Initialized asnull
onBorrow
creation.borrowStatus
: The currentBorrow
status, eitherIN_PROGRESS
,LATE
orCLOSED
. TheborrowStatus
should be automatically updated depending on thedueBorrowDate
and the current date (IN_PROGRESS
orLATE
), as well as thereturnedBorrowDate
(CLOSED
)
classDiagram direction LR Borrow --> Publication Borrow --> LibraryMember Borrow --> BorrowStatus %% Abstract class class Publication { \@Entity(name = "Publication") \@Table(name = "PUBLICATION_TABLE") } class LibraryMember { \@Entity(name = "LibraryMember") \@Table(name = "LIBRARYMEMBER_TABLE") } class Borrow { @Id - id: String @Column - borrowUser: LibraryMember @Column - borrowedPublication: Publication @DateTimeFormat - startBorrowDate: LocalDate @DateTimeFormat - dueBorrowDate: LocalDate @DateTimeFormat - returnedBorrowDate: LocalDate @Enumerate - borrowStatus: BorrowStatus } class BorrowStatus {GameStats IN_PROGRESS LATE CLOSED }
Create Borrow Entity and Repository
Borrow
entities are named Borrow
and stored in a @Table
named BORROW_TABLE
. The repository interface is a CrudRepository
that will be extended with additional methods whenever required.
Create Borrow Service and Controller
The BorrowController
has the following methods:
index
: renders the Borrows main pagecreateBorrow
(GET): aGetMapping
that preparesModel
object with the required attributes for Thymeleaf to render the Borrow creation form.- If the request doesn’t send a
LibraryMember
ID, then the response includes a list ofLibraryMember
objects so Thymeleaf renders a selection list - If the request sends a
LibraryMember
ID, then the response sends again that ID and Thymeleaf doesn’t render a list ofLibraryMember
objects
- If the request doesn’t send a
createBorrow
(POST): aPostMapping
that takes an ID, either as aPathVariable
or aRequestParam
, and a list ofPublication
IDs, and creates a Borrow for each one- If the request doesn’t send a
PathVariable
, then the received request is from aBorrow
form with selectableLibraryMember
, and thus the method will redirect to the same selectableLibraryMember
form - Else, the method will redirect to the Borrow form with the same
PathVariable
ID
- If the request doesn’t send a
updateBorrow
(GET): aGetMapping
that retrieves theBorrow
object that matches the retrievedid
byPathVariable
and inserts it into theModel
object for Thymeleaf to render.updateBorrow
(POST): aPostMapping
that takes an ID as aPathVariable
and anOptional<Borrow>
object and proceeds to update it in the DataBase, if it exists.deleteBorrow
(GET): aGetMapping
that takes an ID as aPathVariable
and deletes its correspondingBorrow
from the database if it exists.
The BorrowService
has the following methods:
getBorrowById
: return aBorrow
if the provided ID exists in the databasecreateBorrow
: insert aBorrow
in the database if its ID doesn’t exist in the databasecreateMultipleBorrows
: given aUser
ID and a list ofPublication
IDs, create and save into the database aBorrow
for eachPublication
assigned to thatUser
and return the list of createdBorrows
deleteBorrowById
: delete aBorrow
from the database if the provided ID exists. When deleting theBorrow
, itsPublication
status is set toAVAILABLE
updateBorrow
: update aBorrow
in the database with the updated information from the client request.- Only
borrowStatus
should be available for update.
- Only
Updating a Borrow
When updating a Borrow
, a similar form as when creating one shall appear, but only Borrow status should be updatable at the moment (there’s no reason to change neither the borrow user nor the borrowed publication, as well as the borrow dates, which should be automatically updated based on the Borrow status). - If Borrow.borrowStatus
is set to IN_PROGRESS
and previous value was different, then the dueBorrowDate
is set to 15 days from the current time - If Borrow.borrowStatus
is set to CLOSED
and previous value was different, then the returnedBorrowDate
is set to the current time
Using HTTP session to store User information
We’ll use an HTTP session to show all borrows of a specific user. The user information will be stored in the HTTP session instead of transferred on each request-response cycle as @PathVariable
or @RequestParam
On login stage, we’ll store the following session attributes:
isLoggedIn
: whether the user has already logged in or notmemberId
: the ID of the Library Member logged inmemberClass
: the class of the Library Member (User, LibraryMember, Staff…)memberName
: the full name of the Library Member for rendering purposes