Merge pull request #1480 from ashishtrivedi16/master

#1321 Transaction script model implementation
This commit is contained in:
Ilkka Seppälä 2020-08-11 16:59:48 +03:00 committed by GitHub
commit d091e369ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1411 additions and 0 deletions

View File

@ -192,6 +192,7 @@
<module>leader-followers</module>
<module>strangler</module>
<module>arrange-act-assert</module>
<module>transaction-script</module>
</modules>
<repositories>

2
transaction-script/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target/
.idea/

View File

@ -0,0 +1,111 @@
---
layout: pattern
title: Transaction Script
folder: transaction-script
permalink: /patterns/transaction-script/
categories: Behavioral
tags:
- Data access
---
## Intent
Transaction Script organizes business logic by procedures where each procedure handles a single request from the presentation.
## Explanation
Real world example
> You need to create a hotel room booking system. Since the requirements are quite simple we intend to use the Transaction Script pattern here.
In plain words
> Transaction Script organizes business logic into transactions that the system needs to carry out.
Programmatic example
The `Hotel` class takes care of booking and cancelling room reservations.
```java
public class Hotel {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
private HotelDaoImpl hotelDao;
public Hotel(HotelDaoImpl hotelDao) {
this.hotelDao = hotelDao;
}
public void bookRoom(int roomNumber) throws Exception {
Optional<Room> room = hotelDao.getById(roomNumber);
if (room.isEmpty()) {
throw new Exception("Room number: " + roomNumber + " does not exist");
} else {
if (room.get().isBooked()) {
throw new Exception("Room already booked!");
} else {
Room updateRoomBooking = room.get();
updateRoomBooking.setBooked(true);
hotelDao.update(updateRoomBooking);
}
}
}
public void cancelRoomBooking(int roomNumber) throws Exception {
Optional<Room> room = hotelDao.getById(roomNumber);
if (room.isEmpty()) {
throw new Exception("Room number: " + roomNumber + " does not exist");
} else {
if (room.get().isBooked()) {
Room updateRoomBooking = room.get();
updateRoomBooking.setBooked(false);
int refundAmount = updateRoomBooking.getPrice();
hotelDao.update(updateRoomBooking);
LOGGER.info("Booking cancelled for room number: " + roomNumber);
LOGGER.info(refundAmount + " is refunded");
} else {
throw new Exception("No booking for the room exists");
}
}
}
}
```
The `Hotel` class has two methods, one for booking and cancelling a room respectively. Each one of them handles a single transaction in the system, making `Hotel` implement the Transaction Script pattern.
```
public void bookRoom(int roomNumber);
```
The book room method consolidates all the needed steps like checking if the room is already booked
or not, if not booked then books the rooma nd updates the database by using the DAO.
```
public void cancelRoomBooking(int roomNumber)
```
The cancel room method consolidates steps like checking if the room is booked or not,
if booked then calculates the refund amount and updates the database using the DAO.
## Class diagram
![alt text](./etc/transaction-script.png "Transaction script model")
## Applicability
Use the Transaction Script pattern when the application has only a small amount of logic and that logic won't be extended in the future.
## Consequences
* As the business logic gets more complicated,
it gets progressively harder to keep the transaction script
in a well-designed state.
* Code duplication between transaction scripts can occur.
* Normally not easy to refactor transactions script to other domain logic
patterns.
## Related patterns
* Domain Model
* Table Module
* Service Layer
## Credits
* [Transaction Script Pattern](https://dzone.com/articles/transaction-script-pattern#:~:text=Transaction%20Script%20(TS)%20is%20the,need%20big%20architecture%20behind%20them.)
* [Transaction Script](https://www.informit.com/articles/article.aspx?p=1398617)
* [Patterns of Enterprise Application Architecture](https://www.amazon.com/gp/product/0321127420?ie=UTF8&tag=gupesasnebl-20&linkCode=as2&camp=1789&creative=9325&creativeASIN=0321127420)

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,65 @@
@startuml
package com.ashishtrivedi16.transaction-script {
class App {
- H2_DB_URL : String {static}
- LOGGER : Logger {static}
- addRooms(hotelDaoImpl : HotelDaoImpl) {static}
- createDataSource() : DataSource {static}
- createSchema(dataSource : DataSource) {static}
- deleteSchema(dataSource : DataSource) {static}
- getRoomsStatus(hotelDaoImpl : HotelDaoImpl) {static}
- generateSampleRooms() : List<Room> {static}
+ main(args : String[]) {static}
}
class Room {
- id: Int
- roomType: String
- price: Int
- booked: Boolean
+ Customer(id : int, roomType : String, price: Int, booked: Boolean)
+ getId() : int
+ getRoomType() : String
+ getPrice() : Int
+ isBooked() : Boolean
+ setId(id : int)
+ setRoomType(roomType : String)
+ setPrice(price : Int)
+ setBooked(booked : boolean)
+ equals(that : Object) : boolean
+ hashCode() : int
+ toString() : String
}
interface HotelDao {
+ add(Room) : boolean {abstract}
+ delete(Room) : boolean {abstract}
+ getAll() : Stream<Room> {abstract}
+ getById(int) : Optional<Room> {abstract}
+ update(Room) : boolean {abstract}
}
class RoomSchemaSql {
+ CREATE_SCHEMA_SQL : String {static}
+ DELETE_SCHEMA_SQL : String {static}
- RoomSchemaSql()
}
class HotelDaoImpl {
- dataSource : DataSource
+ HotelDaoImpl(dataSource : DataSource)
+ add(room : Room) : boolean
- createRoom(resultSet : ResultSet) : Room
+ delete(room : Room) : boolean
+ getAll() : Stream<Room>
+ getById(id : int) : Optional<Room>
- getConnection() : Connection
- mutedClose(connection : Connection, statement : PreparedStatement, resultSet : ResultSet)
+ update(room : Room) : boolean
}
class Hotel {
- LOGGER : Logger {static}
- hotelDao: HotelDaoImpl
+ Hotel(hotelDao: HotelDaoImpl)
+ bookRoom(roomNumber: Int)
+ cancelRoomBooking(roomNumber: Int)
}
}
HotelDaoImpl ..|> HotelDao
@enduml

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
The MIT License
Copyright © 2014-2019 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>java-design-patterns</artifactId>
<groupId>com.iluwatar</groupId>
<version>1.23.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>transaction-script</artifactId>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<configuration>
<archive>
<manifest>
<mainClass>com.ashishtrivedi16.transactionscript.App</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,145 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
import java.util.List;
import javax.sql.DataSource;
import org.h2.jdbcx.JdbcDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Transaction Script (TS) is one of the simplest domain logic pattern.
* It needs less work to implement than other domain logic patterns and therefore
* its perfect fit for smaller applications that don't need big architecture behind them.
*
* <p>In this example we will use the TS pattern to implement booking and cancellation
* methods for a Hotel management App. The main method will initialise an instance of
* {@link Hotel} and add rooms to it. After that it will book and cancel a couple of rooms
* and that will be printed by the logger.</p>
*
* <p>The thing we have to note here is that all the operations related to booking or cancelling
* a room like checking the database if the room exists, checking the booking status or the
* room, calculating refund price are all clubbed inside a single transaction script method.</p>
*/
public class App {
private static final String H2_DB_URL = "jdbc:h2:~/test";
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
/**
* Program entry point.
* Initialises an instance of Hotel and adds rooms to it.
* Carries out booking and cancel booking transactions.
* @param args command line arguments
* @throws Exception if any error occurs
*/
public static void main(String[] args) throws Exception {
final var dataSource = createDataSource();
deleteSchema(dataSource);
createSchema(dataSource);
final var dao = new HotelDaoImpl(dataSource);
// Add rooms
addRooms(dao);
// Print room booking status
getRoomStatus(dao);
var hotel = new Hotel(dao);
// Book rooms
hotel.bookRoom(1);
hotel.bookRoom(2);
hotel.bookRoom(3);
hotel.bookRoom(4);
hotel.bookRoom(5);
hotel.bookRoom(6);
// Cancel booking for a few rooms
hotel.cancelRoomBooking(1);
hotel.cancelRoomBooking(3);
hotel.cancelRoomBooking(5);
getRoomStatus(dao);
deleteSchema(dataSource);
}
private static void getRoomStatus(HotelDaoImpl dao) throws Exception {
try (var customerStream = dao.getAll()) {
customerStream.forEach((customer) -> LOGGER.info(customer.toString()));
}
}
private static void deleteSchema(DataSource dataSource) throws java.sql.SQLException {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.DELETE_SCHEMA_SQL);
}
}
private static void createSchema(DataSource dataSource) throws Exception {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.CREATE_SCHEMA_SQL);
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
/**
* Get database.
*
* @return h2 datasource
*/
private static DataSource createDataSource() {
var dataSource = new JdbcDataSource();
dataSource.setUrl(H2_DB_URL);
return dataSource;
}
private static void addRooms(HotelDaoImpl hotelDao) throws Exception {
for (var room : generateSampleRooms()) {
hotelDao.add(room);
}
}
/**
* Generate rooms.
*
* @return list of rooms
*/
private static List<Room> generateSampleRooms() {
final var room1 = new Room(1, "Single", 50, false);
final var room2 = new Room(2, "Double", 80, false);
final var room3 = new Room(3, "Queen", 120, false);
final var room4 = new Room(4, "King", 150, false);
final var room5 = new Room(5, "Single", 50, false);
final var room6 = new Room(6, "Double", 80, false);
return List.of(room1, room2, room3, room4, room5, room6);
}
}

View File

@ -0,0 +1,87 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Hotel {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
private final HotelDaoImpl hotelDao;
public Hotel(HotelDaoImpl hotelDao) {
this.hotelDao = hotelDao;
}
/**
* Book a room.
*
* @param roomNumber room to book
* @throws Exception if any error
*/
public void bookRoom(int roomNumber) throws Exception {
var room = hotelDao.getById(roomNumber);
if (room.isEmpty()) {
throw new Exception("Room number: " + roomNumber + " does not exist");
} else {
if (room.get().isBooked()) {
throw new Exception("Room already booked!");
} else {
var updateRoomBooking = room.get();
updateRoomBooking.setBooked(true);
hotelDao.update(updateRoomBooking);
}
}
}
/**
* Cancel a room booking.
*
* @param roomNumber room to cancel booking
* @throws Exception if any error
*/
public void cancelRoomBooking(int roomNumber) throws Exception {
var room = hotelDao.getById(roomNumber);
if (room.isEmpty()) {
throw new Exception("Room number: " + roomNumber + " does not exist");
} else {
if (room.get().isBooked()) {
var updateRoomBooking = room.get();
updateRoomBooking.setBooked(false);
int refundAmount = updateRoomBooking.getPrice();
hotelDao.update(updateRoomBooking);
LOGGER.info("Booking cancelled for room number: " + roomNumber);
LOGGER.info(refundAmount + " is refunded");
} else {
throw new Exception("No booking for the room exists");
}
}
}
}

View File

@ -0,0 +1,40 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
import java.util.Optional;
import java.util.stream.Stream;
public interface HotelDao {
Stream<Room> getAll() throws Exception;
Optional<Room> getById(int id) throws Exception;
Boolean add(Room room) throws Exception;
Boolean update(Room room) throws Exception;
Boolean delete(Room room) throws Exception;
}

View File

@ -0,0 +1,172 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HotelDaoImpl implements HotelDao {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
private final DataSource dataSource;
public HotelDaoImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Stream<Room> getAll() throws Exception {
try {
var connection = getConnection();
var statement = connection.prepareStatement("SELECT * FROM ROOMS");
var resultSet = statement.executeQuery(); // NOSONAR
return StreamSupport.stream(new Spliterators.AbstractSpliterator<Room>(Long.MAX_VALUE,
Spliterator.ORDERED) {
@Override
public boolean tryAdvance(Consumer<? super Room> action) {
try {
if (!resultSet.next()) {
return false;
}
action.accept(createRoom(resultSet));
return true;
} catch (Exception e) {
throw new RuntimeException(e); // NOSONAR
}
}
}, false).onClose(() -> {
try {
mutedClose(connection, statement, resultSet);
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
});
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
@Override
public Optional<Room> getById(int id) throws Exception {
ResultSet resultSet = null;
try (var connection = getConnection();
var statement = connection.prepareStatement("SELECT * FROM ROOMS WHERE ID = ?")) {
statement.setInt(1, id);
resultSet = statement.executeQuery();
if (resultSet.next()) {
return Optional.of(createRoom(resultSet));
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
} finally {
if (resultSet != null) {
resultSet.close();
}
}
}
@Override
public Boolean add(Room room) throws Exception {
if (getById(room.getId()).isPresent()) {
return false;
}
try (var connection = getConnection();
var statement = connection.prepareStatement("INSERT INTO ROOMS VALUES (?,?,?,?)")) {
statement.setInt(1, room.getId());
statement.setString(2, room.getRoomType());
statement.setInt(3, room.getPrice());
statement.setBoolean(4, room.isBooked());
statement.execute();
return true;
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
@Override
public Boolean update(Room room) throws Exception {
try (var connection = getConnection();
var statement =
connection
.prepareStatement("UPDATE ROOMS SET ROOM_TYPE = ?, PRICE = ?, BOOKED = ?"
+ " WHERE ID = ?")) {
statement.setString(1, room.getRoomType());
statement.setInt(2, room.getPrice());
statement.setBoolean(3, room.isBooked());
statement.setInt(4, room.getId());
return statement.executeUpdate() > 0;
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
@Override
public Boolean delete(Room room) throws Exception {
try (var connection = getConnection();
var statement = connection.prepareStatement("DELETE FROM ROOMS WHERE ID = ?")) {
statement.setInt(1, room.getId());
return statement.executeUpdate() > 0;
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
private Connection getConnection() throws Exception {
return dataSource.getConnection();
}
private void mutedClose(Connection connection, PreparedStatement statement, ResultSet resultSet)
throws Exception {
try {
resultSet.close();
statement.close();
connection.close();
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
private Room createRoom(ResultSet resultSet) throws Exception {
return new Room(resultSet.getInt("ID"),
resultSet.getString("ROOM_TYPE"),
resultSet.getInt("PRICE"),
resultSet.getBoolean("BOOKED"));
}
}

View File

@ -0,0 +1,123 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
/**
* A room POJO that represents the data that will be read from the data source.
*/
public class Room {
private int id;
private String roomType;
private int price;
private boolean booked;
/**
* Create an instance of room.
* @param id room id
* @param roomType room type
* @param price room price
* @param booked room booking status
*/
public Room(int id, String roomType, int price, boolean booked) {
this.id = id;
this.roomType = roomType;
this.price = price;
this.booked = booked;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getRoomType() {
return roomType;
}
public void setRoomType(String roomType) {
this.roomType = roomType;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public boolean isBooked() {
return booked;
}
public void setBooked(boolean booked) {
this.booked = booked;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Room room = (Room) o;
if (id != room.id) {
return false;
}
if (price != room.price) {
return false;
}
if (booked != room.booked) {
return false;
}
return roomType.equals(room.roomType);
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + roomType.hashCode();
result = 31 * result + price;
result = 31 * result + (booked ? 1 : 0);
return result;
}
@Override
public String toString() {
return "Room{"
+ "id=" + id
+ ", roomType=" + roomType
+ ", price=" + price
+ ", booked=" + booked
+ '}';
}
}

View File

@ -0,0 +1,38 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
/**
* Customer Schema SQL Class.
*/
public final class RoomSchemaSql {
public static final String CREATE_SCHEMA_SQL =
"CREATE TABLE ROOMS (ID NUMBER, ROOM_TYPE VARCHAR(100), PRICE INT(100), BOOKED VARCHAR(100))";
public static final String DELETE_SCHEMA_SQL = "DROP TABLE ROOMS IF EXISTS";
private RoomSchemaSql() {
}
}

View File

@ -0,0 +1,36 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
import org.junit.jupiter.api.Test;
/**
* Tests that Transaction script example runs without errors.
*/
public class AppTest {
@Test
public void test() throws Exception {
App.main(new String[]{});
}
}

View File

@ -0,0 +1,272 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.Mockito.*;
/**
* Tests {@link HotelDaoImpl}.
*/
public class HotelDaoImplTest {
private static final String DB_URL = "jdbc:h2:~/test";
private HotelDaoImpl dao;
private Room existingRoom = new Room(1, "Single", 50, false);
/**
* Creates rooms schema.
*
* @throws SQLException if there is any error while creating schema.
*/
@BeforeEach
public void createSchema() throws SQLException {
try (var connection = DriverManager.getConnection(DB_URL);
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.DELETE_SCHEMA_SQL);
statement.execute(RoomSchemaSql.CREATE_SCHEMA_SQL);
}
}
/**
* Represents the scenario where DB connectivity is present.
*/
@Nested
public class ConnectionSuccess {
/**
* Setup for connection success scenario.
*
* @throws Exception if any error occurs.
*/
@BeforeEach
public void setUp() throws Exception {
var dataSource = new JdbcDataSource();
dataSource.setURL(DB_URL);
dao = new HotelDaoImpl(dataSource);
var result = dao.add(existingRoom);
assertTrue(result);
}
/**
* Represents the scenario when DAO operations are being performed on a non existing room.
*/
@Nested
public class NonExistingRoom {
@Test
public void addingShouldResultInSuccess() throws Exception {
try (var allRooms = dao.getAll()) {
assumeTrue(allRooms.count() == 1);
}
final var nonExistingRoom = new Room(2, "Double", 80, false);
var result = dao.add(nonExistingRoom);
assertTrue(result);
assertRoomCountIs(2);
assertEquals(nonExistingRoom, dao.getById(nonExistingRoom.getId()).get());
}
@Test
public void deletionShouldBeFailureAndNotAffectExistingRooms() throws Exception {
final var nonExistingRoom = new Room(2, "Double", 80, false);
var result = dao.delete(nonExistingRoom);
assertFalse(result);
assertRoomCountIs(1);
}
@Test
public void updationShouldBeFailureAndNotAffectExistingRooms() throws Exception {
final var nonExistingId = getNonExistingRoomId();
final var newRoomType = "Double";
final var newPrice = 80;
final var room = new Room(nonExistingId, newRoomType, newPrice, false);
var result = dao.update(room);
assertFalse(result);
assertFalse(dao.getById(nonExistingId).isPresent());
}
@Test
public void retrieveShouldReturnNoRoom() throws Exception {
assertFalse(dao.getById(getNonExistingRoomId()).isPresent());
}
}
/**
* Represents a scenario where DAO operations are being performed on an already existing
* room.
*/
@Nested
public class ExistingRoom {
@Test
public void addingShouldResultInFailureAndNotAffectExistingRooms() throws Exception {
var existingRoom = new Room(1, "Single", 50, false);
var result = dao.add(existingRoom);
assertFalse(result);
assertRoomCountIs(1);
assertEquals(existingRoom, dao.getById(existingRoom.getId()).get());
}
@Test
public void deletionShouldBeSuccessAndRoomShouldBeNonAccessible() throws Exception {
var result = dao.delete(existingRoom);
assertTrue(result);
assertRoomCountIs(0);
assertFalse(dao.getById(existingRoom.getId()).isPresent());
}
@Test
public void updationShouldBeSuccessAndAccessingTheSameRoomShouldReturnUpdatedInformation() throws
Exception {
final var newRoomType = "Double";
final var newPrice = 80;
final var newBookingStatus = false;
final var Room = new Room(existingRoom.getId(), newRoomType, newPrice, newBookingStatus);
var result = dao.update(Room);
assertTrue(result);
final var room = dao.getById(existingRoom.getId()).get();
assertEquals(newRoomType, room.getRoomType());
assertEquals(newPrice, room.getPrice());
assertEquals(newBookingStatus, room.isBooked());
}
}
}
/**
* Represents a scenario where DB connectivity is not present due to network issue, or DB service
* unavailable.
*/
@Nested
public class ConnectivityIssue {
private static final String EXCEPTION_CAUSE = "Connection not available";
/**
* setup a connection failure scenario.
*
* @throws SQLException if any error occurs.
*/
@BeforeEach
public void setUp() throws SQLException {
dao = new HotelDaoImpl(mockedDatasource());
}
private DataSource mockedDatasource() throws SQLException {
var mockedDataSource = mock(DataSource.class);
var mockedConnection = mock(Connection.class);
var exception = new SQLException(EXCEPTION_CAUSE);
doThrow(exception).when(mockedConnection).prepareStatement(Mockito.anyString());
doReturn(mockedConnection).when(mockedDataSource).getConnection();
return mockedDataSource;
}
@Test
public void addingARoomFailsWithExceptionAsFeedbackToClient() {
assertThrows(Exception.class, () -> {
dao.add(new Room(2, "Double", 80, false));
});
}
@Test
public void deletingARoomFailsWithExceptionAsFeedbackToTheClient() {
assertThrows(Exception.class, () -> {
dao.delete(existingRoom);
});
}
@Test
public void updatingARoomFailsWithFeedbackToTheClient() {
final var newRoomType = "Double";
final var newPrice = 80;
final var newBookingStatus = false;
assertThrows(Exception.class, () -> {
dao.update(new Room(existingRoom.getId(), newRoomType, newPrice, newBookingStatus));
});
}
@Test
public void retrievingARoomByIdFailsWithExceptionAsFeedbackToClient() {
assertThrows(Exception.class, () -> {
dao.getById(existingRoom.getId());
});
}
@Test
public void retrievingAllRoomsFailsWithExceptionAsFeedbackToClient() {
assertThrows(Exception.class, () -> {
dao.getAll();
});
}
}
/**
* Delete room schema for fresh setup per test.
*
* @throws SQLException if any error occurs.
*/
@AfterEach
public void deleteSchema() throws SQLException {
try (var connection = DriverManager.getConnection(DB_URL);
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.DELETE_SCHEMA_SQL);
}
}
private void assertRoomCountIs(int count) throws Exception {
try (var allRooms = dao.getAll()) {
assertEquals(count, allRooms.count());
}
}
/**
* An arbitrary number which does not correspond to an active Room id.
*
* @return an int of a room id which doesn't exist
*/
private int getNonExistingRoomId() {
return 999;
}
}

View File

@ -0,0 +1,151 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests {@link Hotel}
*/
public class HotelTest {
private static final String H2_DB_URL = "jdbc:h2:~/test";
private Hotel hotel;
private HotelDaoImpl dao;
@BeforeEach
public void setUp() throws Exception {
final var dataSource = createDataSource();
deleteSchema(dataSource);
createSchema(dataSource);
dao = new HotelDaoImpl(dataSource);
addRooms(dao);
hotel = new Hotel(dao);
}
@Test
public void bookingRoomShouldChangeBookedStatusToTrue() throws Exception {
hotel.bookRoom(1);
assertTrue(dao.getById(1).get().isBooked());
}
@Test()
public void bookingRoomWithInvalidIdShouldRaiseException() {
assertThrows(Exception.class, () -> {
hotel.bookRoom(getNonExistingRoomId());
});
}
@Test()
public void bookingRoomAgainShouldRaiseException() {
assertThrows(Exception.class, () -> {
hotel.bookRoom(1);
hotel.bookRoom(1);
});
}
@Test
public void NotBookingRoomShouldNotChangeBookedStatus() throws Exception {
assertFalse(dao.getById(1).get().isBooked());
}
@Test
public void cancelRoomBookingShouldChangeBookedStatus() throws Exception {
hotel.bookRoom(1);
assertTrue(dao.getById(1).get().isBooked());
hotel.cancelRoomBooking(1);
assertFalse(dao.getById(1).get().isBooked());
}
@Test
public void cancelRoomBookingWithInvalidIdShouldRaiseException() {
assertThrows(Exception.class, () -> {
hotel.cancelRoomBooking(getNonExistingRoomId());
});
}
@Test
public void cancelRoomBookingForUnbookedRoomShouldRaiseException() {
assertThrows(Exception.class, () -> {
hotel.cancelRoomBooking(1);
});
}
private static void deleteSchema(DataSource dataSource) throws java.sql.SQLException {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.DELETE_SCHEMA_SQL);
}
}
private static void createSchema(DataSource dataSource) throws Exception {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.CREATE_SCHEMA_SQL);
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
public static DataSource createDataSource() {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setUrl(H2_DB_URL);
return dataSource;
}
private static void addRooms(HotelDaoImpl hotelDao) throws Exception {
for (var room : generateSampleRooms()) {
hotelDao.add(room);
}
}
public static List<Room> generateSampleRooms() {
final var room1 = new Room(1, "Single", 50, false);
final var room2 = new Room(2, "Double", 80, false);
final var room3 = new Room(3, "Queen", 120, false);
final var room4 = new Room(4, "King", 150, false);
final var room5 = new Room(5, "Single", 50, false);
final var room6 = new Room(6, "Double", 80, false);
return List.of(room1, room2, room3, room4, room5, room6);
}
/**
* An arbitrary number which does not correspond to an active Room id.
*
* @return an int of a room id which doesn't exist
*/
private int getNonExistingRoomId() {
return 999;
}
}

View File

@ -0,0 +1,95 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.ashishtrivedi16.transactionscript;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/**
* Tests {@link Room}.
*/
public class RoomTest {
private Room room;
private static final int ID = 1;
private static final String ROOMTYPE = "Single";
private static final int PRICE = 50;
private static final boolean BOOKED = false;
@BeforeEach
public void setUp() {
room = new Room(ID, ROOMTYPE, PRICE, BOOKED);
}
@Test
public void getAndSetId() {
final var newId = 2;
room.setId(newId);
assertEquals(newId, room.getId());
}
@Test
public void getAndSetRoomType() {
final var newRoomType = "Double";
room.setRoomType(newRoomType);
assertEquals(newRoomType, room.getRoomType());
}
@Test
public void getAndSetLastName() {
final var newPrice = 60;
room.setPrice(newPrice);
assertEquals(newPrice, room.getPrice());
}
@Test
public void notEqualWithDifferentId() {
final var newId = 2;
final var otherRoom = new Room(newId, ROOMTYPE, PRICE, BOOKED);
assertNotEquals(room, otherRoom);
assertNotEquals(room.hashCode(), otherRoom.hashCode());
}
@Test
public void equalsWithSameObjectValues() {
final var otherRoom = new Room(ID, ROOMTYPE, PRICE, BOOKED);
assertEquals(room, otherRoom);
assertEquals(room.hashCode(), otherRoom.hashCode());
}
@Test
public void equalsWithSameObjects() {
assertEquals(room, room);
assertEquals(room.hashCode(), room.hashCode());
}
@Test
public void testToString() {
assertEquals(String.format("Room{id=%s, roomType=%s, price=%s, booked=%s}",
room.getId(), room.getRoomType(), room.getPrice(), room.isBooked()), room.toString());
}
}