diff --git a/pom.xml b/pom.xml index 3b4f05b37..9b3382795 100644 --- a/pom.xml +++ b/pom.xml @@ -192,6 +192,7 @@ leader-followers strangler arrange-act-assert + transaction-script diff --git a/transaction-script/.gitignore b/transaction-script/.gitignore new file mode 100644 index 000000000..431845ed0 --- /dev/null +++ b/transaction-script/.gitignore @@ -0,0 +1,2 @@ +/target/ +.idea/ diff --git a/transaction-script/README.md b/transaction-script/README.md new file mode 100644 index 000000000..7d784101b --- /dev/null +++ b/transaction-script/README.md @@ -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 = 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 = 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) diff --git a/transaction-script/etc/transaction-script.png b/transaction-script/etc/transaction-script.png new file mode 100644 index 000000000..6d0cffb6a Binary files /dev/null and b/transaction-script/etc/transaction-script.png differ diff --git a/transaction-script/etc/transaction-script.urm.puml b/transaction-script/etc/transaction-script.urm.puml new file mode 100644 index 000000000..e8d172377 --- /dev/null +++ b/transaction-script/etc/transaction-script.urm.puml @@ -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 {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 {abstract} + + getById(int) : Optional {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 + + getById(id : int) : Optional + - 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 diff --git a/transaction-script/pom.xml b/transaction-script/pom.xml new file mode 100644 index 000000000..e53bb4b4a --- /dev/null +++ b/transaction-script/pom.xml @@ -0,0 +1,73 @@ + + + + + java-design-patterns + com.iluwatar + 1.23.0-SNAPSHOT + + 4.0.0 + + transaction-script + + + + com.h2database + h2 + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.ashishtrivedi16.transactionscript.App + + + + + + + + + + diff --git a/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/App.java b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/App.java new file mode 100644 index 000000000..98762520d --- /dev/null +++ b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/App.java @@ -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 + * it’s perfect fit for smaller applications that don't need big architecture behind them. + * + *

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.

+ * + *

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.

+ */ +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 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); + } +} diff --git a/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/Hotel.java b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/Hotel.java new file mode 100644 index 000000000..be9000af5 --- /dev/null +++ b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/Hotel.java @@ -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"); + } + } + } +} diff --git a/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/HotelDao.java b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/HotelDao.java new file mode 100644 index 000000000..71d9860ba --- /dev/null +++ b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/HotelDao.java @@ -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 getAll() throws Exception; + + Optional getById(int id) throws Exception; + + Boolean add(Room room) throws Exception; + + Boolean update(Room room) throws Exception; + + Boolean delete(Room room) throws Exception; +} diff --git a/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/HotelDaoImpl.java b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/HotelDaoImpl.java new file mode 100644 index 000000000..e64b64699 --- /dev/null +++ b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/HotelDaoImpl.java @@ -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 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(Long.MAX_VALUE, + Spliterator.ORDERED) { + + @Override + public boolean tryAdvance(Consumer 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 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")); + } +} diff --git a/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/Room.java b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/Room.java new file mode 100644 index 000000000..c4ad7bfd6 --- /dev/null +++ b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/Room.java @@ -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 + + '}'; + } +} diff --git a/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/RoomSchemaSql.java b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/RoomSchemaSql.java new file mode 100644 index 000000000..bb2e2374b --- /dev/null +++ b/transaction-script/src/main/java/com/ashishtrivedi16/transactionscript/RoomSchemaSql.java @@ -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() { + } + +} diff --git a/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/AppTest.java b/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/AppTest.java new file mode 100644 index 000000000..755a95653 --- /dev/null +++ b/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/AppTest.java @@ -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[]{}); + } +} diff --git a/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/HotelDaoImplTest.java b/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/HotelDaoImplTest.java new file mode 100644 index 000000000..6e907fdc0 --- /dev/null +++ b/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/HotelDaoImplTest.java @@ -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; + } +} diff --git a/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/HotelTest.java b/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/HotelTest.java new file mode 100644 index 000000000..26bf4b5cd --- /dev/null +++ b/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/HotelTest.java @@ -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 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; + } +} diff --git a/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/RoomTest.java b/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/RoomTest.java new file mode 100644 index 000000000..9ea8756bd --- /dev/null +++ b/transaction-script/src/test/java/com/ashishtrivedi16/transactionscript/RoomTest.java @@ -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()); + } +}