diff --git a/src/com/showbooking/Client.java b/src/com/showbooking/Client.java new file mode 100644 index 0000000..d47a986 --- /dev/null +++ b/src/com/showbooking/Client.java @@ -0,0 +1,58 @@ +package com.showbooking; + +import com.showbooking.bookings.Booking; +import com.showbooking.bookings.BookingService; +import com.showbooking.events.EventService; +import com.showbooking.events.Show; +import com.showbooking.payments.Payable; +import com.showbooking.payments.PaymentService; +import com.showbooking.repository.BookingRepository; +import com.showbooking.repository.EventRepository; + +import java.util.List; +import java.util.Map; + +public class Client { + EventRepository eventRepository; + BookingRepository bookingRepository; + Payable paymentService; + + public Client() { + eventRepository = EventRepository.getInstance(); + bookingRepository = BookingRepository.getInstance(); + paymentService = new PaymentService(); + } + public List fetchEvents(Map filter){ + return EventService.fetchShows(eventRepository, filter); + } + + public Boolean addOrUpdateEvents(List> events){ + for(Map event: events) { + if(!EventService.addOrUpdateEvent(eventRepository, event)) + return false; + } + + return true; + } + + public Boolean addLayout(Boolean[][] layout, Long screenId) { + return EventService.addScreenLayout(eventRepository, screenId, layout); + } + + public Boolean[][] fetchEventSeatLayout(Long eventId) { + return EventService.fetchEventSeatLayout(eventRepository, eventId); + } + + public Long bookEvent(List seatIds, Long eventId, Long screenId, Long userId) { + return BookingService.bookEvent(paymentService, bookingRepository, eventRepository, userId, eventId, + screenId, seatIds); + } + + public Booking fetchBooking(Long bookingId) { + return BookingService.fetchBooking(bookingRepository, bookingId); + } + + public Boolean cancelBooking(Long bookingId) { + return BookingService.cancelBooking(paymentService, bookingRepository, bookingId); + } +} diff --git a/src/com/showbooking/bookings/Booking.java b/src/com/showbooking/bookings/Booking.java new file mode 100644 index 0000000..2a695ab --- /dev/null +++ b/src/com/showbooking/bookings/Booking.java @@ -0,0 +1,60 @@ +package com.showbooking.bookings; + +import com.showbooking.events.Show; +import com.showbooking.payments.Payment; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class Booking { + Show show; + Payment payment; + + Long id; + Long userId; + Boolean isActive; + List seatIds; + private final Lock lock = new ReentrantLock(); + + Booking(Long userId, Show show, Payment payment, List bookedSeatIds) { + Random rand = new Random(); + + this.id = rand.nextLong(); + this.isActive = true; + this.userId = userId; + this.show = show; + this.seatIds = new ArrayList<>(bookedSeatIds); + this.payment = payment; + } + + public Long getId(){ + return this.id; + } + + public Lock getLock() { + return this.lock; + } + + public Boolean getIsActive(){ + return this.isActive; + } + + public Show getShow() { + return this.show; + } + + public Payment getPayment() { + return this.payment; + } + + public List getSeatIds() { + return this.seatIds; + } + + public void setIsActiveFalse() { + this.isActive = false; + } +} diff --git a/src/com/showbooking/bookings/BookingService.java b/src/com/showbooking/bookings/BookingService.java new file mode 100644 index 0000000..105e613 --- /dev/null +++ b/src/com/showbooking/bookings/BookingService.java @@ -0,0 +1,68 @@ +package com.showbooking.bookings; + +import com.showbooking.bookings.exception.SeatUnavailableException; +import com.showbooking.payments.Payable; +import com.showbooking.events.Show; +import com.showbooking.payments.Payment; +import com.showbooking.payments.PaymentStatus; +import com.showbooking.payments.exception.PaymentFailedException; +import com.showbooking.repository.BookingRepository; +import com.showbooking.repository.EventRepository; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class BookingService { + public static Long bookEvent(Payable paymentService, BookingRepository bookingRepository, EventRepository eventRepository, + Long userId, Long eventId, Long screenId, List seatIds) { + List availableShows = eventRepository.fetchAllShows(eventId).stream().filter( + show -> show.getScreenId().equals(screenId)).collect(Collectors.toList()); + Show selectedShow = Collections.min(availableShows, Comparator.comparing(Show::getId)); + + Collections.sort(seatIds); + try { + selectedShow.bookSeats(seatIds); + Payment payment = paymentService.makePayment(userId, selectedShow.getPrice()); + if(payment.getStatus().equals(PaymentStatus.SUCCESS)) { + Booking booking = bookingRepository.addBooking(new Booking(userId, selectedShow, payment, seatIds)); + return booking.getId(); + } else { + throw new PaymentFailedException(); + } + } catch (SeatUnavailableException exc) { + System.out.println("Selected seat no longer available."); + return null; + } catch (PaymentFailedException exc) { + selectedShow.unBookSeats(seatIds); + return null; + } + } + + public static Boolean cancelBooking(Payable paymentService, BookingRepository bookingRepository, Long bookingId) { + Booking booking = bookingRepository.fetchBooking(bookingId); + if(!booking.getIsActive()) { + return false; + } + + List bookedSeats = booking.getSeatIds(); + Show bookedShow = booking.getShow(); + booking.getLock().lock(); + try { + if (booking.getIsActive()) { + paymentService.refundPayment(booking.getPayment()); + bookedShow.unBookSeats(bookedSeats); + booking.setIsActiveFalse(); + return true; + } + return false; + } finally { + booking.getLock().unlock(); + } + } + + public static Booking fetchBooking(BookingRepository bookingRepository, long bookingId) { + return bookingRepository.fetchBooking(bookingId); + } +} diff --git a/src/com/showbooking/bookings/exception/SeatUnavailableException.java b/src/com/showbooking/bookings/exception/SeatUnavailableException.java new file mode 100644 index 0000000..2315749 --- /dev/null +++ b/src/com/showbooking/bookings/exception/SeatUnavailableException.java @@ -0,0 +1,7 @@ +package com.showbooking.bookings.exception; + +public class SeatUnavailableException extends Exception { + public SeatUnavailableException() { + super("Selected seat not available!"); + } +} diff --git a/src/com/showbooking/events/Event.java b/src/com/showbooking/events/Event.java new file mode 100644 index 0000000..a49cfc0 --- /dev/null +++ b/src/com/showbooking/events/Event.java @@ -0,0 +1,12 @@ +package com.showbooking.events; + +public interface Event { + void setTitle(String title); + void setGenre(String genre); + void setLanguage(String genre); + + Long getEventId(); + String getEventTitle(); + String getEventGenre(); + String getEventLanguage(); +} diff --git a/src/com/showbooking/events/EventService.java b/src/com/showbooking/events/EventService.java new file mode 100644 index 0000000..497267d --- /dev/null +++ b/src/com/showbooking/events/EventService.java @@ -0,0 +1,80 @@ +package com.showbooking.events; + +import com.showbooking.repository.EventRepository; + +import java.util.*; + +public class EventService { + public static Boolean addOrUpdateEvent(EventRepository repository, Map data) { + try { + Event event = repository.fetchMovie((Long) data.get("eventId")); + Screen screen = repository.fetchScreen((Long) data.get("screenId")); + if (screen == null) { + screen = repository.addScreen(new Screen((Long) data.get("screenId"))); + } + + if (event == null) { + event = repository.addMovie(new Movie((Long) data.get("eventId"), (String) data.get("title"), + (String) data.get("genre"), (String) data.get("language"))); + } else { + event.setTitle((String) data.get("title")); + event.setGenre((String) data.get("genre")); + event.setLanguage((String) data.get("language")); + repository.updateMovie(event); + } + + repository.removeShows(event, screen); + List showDetails = (List) data.get("showTimePriceMap"); + for (Object showDetail : showDetails) { + Map detail = (Map) showDetail; + repository.addShow(new Show(event, screen, (Integer) detail.get("time"), (Integer) detail.get("price"))); + } + } catch (Exception exc) { + System.out.println("Failed adding/updating event, due to: " + exc.getMessage()); + return false; + } + return true; + } + + public static List fetchShows(EventRepository repository, Map filter) { + List allEvents = repository.fetchAllMovies(); + + Filter filterObj = new Filter(filter.getOrDefault("title", null), + filter.getOrDefault("genre", null), + filter.getOrDefault("language", null), + filter.getOrDefault("price", null)); + List filteredEvents = FilterService.filterEvents(allEvents, filterObj); + + List eventShows = new ArrayList(); + for(Event event: filteredEvents) { + eventShows.addAll(repository.fetchAllShows(event.getEventId())); + } + + List filteredShows = FilterService.filterShows(eventShows, filterObj); + return filteredShows; + } + + public static Boolean addScreenLayout(EventRepository repository, Long screenId, Boolean[][] layout) { + try { + Screen screen = repository.fetchScreen(screenId); + screen.addLayout(layout); + repository.updateScreen(screen); + } catch(Exception exc) { + System.out.println("Unable to add screen layout"); + return false; + } + + return true; + } + + public static Boolean[][] fetchEventSeatLayout(EventRepository repository, Long eventId) { + Event event = repository.fetchMovie(eventId); + List shows = repository.fetchAllShows(event.getEventId()); + if(shows.isEmpty()) + return null; + + Show selectedShow = Collections.min(shows, Comparator.comparing(Show::getId)); + Screen screen = repository.fetchScreen(selectedShow.getScreenId()); + return selectedShow.getSeatingLayout(screen); + } +} diff --git a/src/com/showbooking/events/Filter.java b/src/com/showbooking/events/Filter.java new file mode 100644 index 0000000..a369c60 --- /dev/null +++ b/src/com/showbooking/events/Filter.java @@ -0,0 +1,46 @@ +package com.showbooking.events; + +public class Filter { + String title; + String genre; + String language; + Integer minPrice; + Integer maxPrice; + + Filter(String title, String genre, String language, String price) { + if(title != null) + this.title = title; + if(genre != null) + this.genre = genre; + if(language != null) + this.language = language; + + if(price != null) { + String[] prices = price.split(":", 2); + if(!prices[0].isEmpty()) + this.minPrice = Integer.valueOf(prices[0]); + if(!prices[1].isEmpty()) + this.maxPrice = Integer.valueOf(prices[1]); + } + } + + public String getTitle() { + return this.title; + } + + public String getGenre() { + return this.genre; + } + + public String getLanguage() { + return this.language; + } + + public Integer getMinPrice() { + return this.minPrice; + } + + public Integer getMaxPrice() { + return this.maxPrice; + } +} diff --git a/src/com/showbooking/events/FilterService.java b/src/com/showbooking/events/FilterService.java new file mode 100644 index 0000000..242faff --- /dev/null +++ b/src/com/showbooking/events/FilterService.java @@ -0,0 +1,35 @@ +package com.showbooking.events; + +import java.util.ArrayList; +import java.util.List; + +public class FilterService { + public static List filterEvents(List events, Filter filter) { + List result = new ArrayList(events); + + if(filter.getTitle() != null) { + result.removeIf(event -> !event.getEventTitle().equalsIgnoreCase(filter.getTitle())); + } + if(filter.getGenre() != null) { + result.removeIf(event -> !event.getEventGenre().equalsIgnoreCase(filter.getGenre())); + } + if(filter.getLanguage() != null) { + result.removeIf(event -> !event.getEventLanguage().equalsIgnoreCase(filter.getLanguage())); + } + + return result; + } + + public static List filterShows(List shows, Filter filter) { + List result = new ArrayList(shows); + + if(filter.getMinPrice() != null) { + result.removeIf(show -> show.getPrice() < filter.getMinPrice()); + } + if(filter.getMaxPrice() != null) { + result.removeIf(show -> show.getPrice() > filter.getMaxPrice()); + } + + return result; + } +} diff --git a/src/com/showbooking/events/Movie.java b/src/com/showbooking/events/Movie.java new file mode 100644 index 0000000..76e2b0a --- /dev/null +++ b/src/com/showbooking/events/Movie.java @@ -0,0 +1,55 @@ +package com.showbooking.events; + +public class Movie implements Event { + Long id; + String title; + String genre; + String language; + + Movie(Long id, String title, String genre, String language) { + this.id = id; + this.title = title; + this.genre = genre.toUpperCase(); + this.language = language.toUpperCase(); + } + + @Override + public void setTitle(String title) { + this.title = title; + } + + @Override + public void setGenre(String genre) { + this.genre = genre.toUpperCase(); + } + + @Override + public void setLanguage(String genre) { + this.language = language.toUpperCase(); + } + + @Override + public Long getEventId() { + return this.id; + } + + @Override + public String getEventTitle() { + return this.title; + } + + @Override + public String getEventGenre() { + return this.genre; + } + + @Override + public String getEventLanguage() { + return this.language; + } + + @Override + public String toString() { + return "Id: " + id + ", Title: " + title + ", Genre: " + genre +", Language: " + language; + } +} diff --git a/src/com/showbooking/events/Screen.java b/src/com/showbooking/events/Screen.java new file mode 100644 index 0000000..bb55dbb --- /dev/null +++ b/src/com/showbooking/events/Screen.java @@ -0,0 +1,42 @@ +package com.showbooking.events; + +public class Screen { + Long id; + Integer totalSeats; + Boolean[][] seatingLayout; + + Screen(Long id) { + this.id = id; + this.totalSeats = 0; + this.seatingLayout = null; + } + + public void addLayout(Boolean[][] layout) { + int seatCount = 0; + this.seatingLayout = new Boolean[layout.length][]; + for (int i = 0; i < this.seatingLayout.length; ++i) { + this.seatingLayout[i] = new Boolean[layout[i].length]; + System.arraycopy(layout[i], 0, this.seatingLayout[i], 0, this.seatingLayout[i].length); + + seatCount += this.seatingLayout[i].length; + } + this.totalSeats = seatCount; + } + + public Long getId() { + return this.id; + } + + public Integer getTotalSeats() { + return this.totalSeats; + } + + public Boolean[][] getSeatingLayout(){ + return this.seatingLayout; + } + + @Override + public String toString() { + return "Id: " + id + ", total seats: " + totalSeats; + } +} diff --git a/src/com/showbooking/events/Show.java b/src/com/showbooking/events/Show.java new file mode 100644 index 0000000..127f9be --- /dev/null +++ b/src/com/showbooking/events/Show.java @@ -0,0 +1,109 @@ +package com.showbooking.events; + +import com.showbooking.bookings.exception.SeatUnavailableException; +import com.showbooking.events.exception.InvalidShowTimingException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +public class Show { + Long id; + Long eventId; + Long screenId; + Integer timing; + Integer price; + List bookedSeats; + Map lockObjects; + + Show(Event event, Screen screen, Integer timing, Integer price) throws InvalidShowTimingException { + if(timing < 0 ||timing > 23) { + throw new InvalidShowTimingException(); + } + + Random rand = new Random(); + this.bookedSeats = new ArrayList<>(); + this.id = rand.nextLong(); + this.eventId = event.getEventId(); + this.screenId = screen.getId(); + this.timing = timing; + this.price = price; + this.lockObjects = new ConcurrentHashMap<>(); + } + + public Long getId() { + return this.id; + } + + public Long getEventId() { + return this.eventId; + } + + public Long getScreenId() { + return this.screenId; + } + + public Integer getTiming() { + return this.timing; + } + + public Integer getPrice() { + return this.price; + } + + public Boolean[][] getSeatingLayout(Screen screen) { + int seatCount = 0; + Boolean[][] seatingLayout = new Boolean[screen.getSeatingLayout().length][]; + for (int i = 0; i < seatingLayout.length; ++i) { + int rowLength = screen.getSeatingLayout()[i].length; + seatingLayout[i] = new Boolean[rowLength]; + System.arraycopy(screen.getSeatingLayout()[i], 0, seatingLayout[i], 0, seatingLayout[i].length); + + for(Long bookedSeatNo: this.bookedSeats){ + if(seatCount < bookedSeatNo && bookedSeatNo <= (seatCount + rowLength)) { + int j = (int)(bookedSeatNo - seatCount - 1); + seatingLayout[i][j] = false; + } + } + seatCount += rowLength; + } + + return seatingLayout; + } + + public void bookSeats(List seatIds) throws SeatUnavailableException{ + List locks = new ArrayList<>(); + try { + for (Long seatId : seatIds) { + ReentrantLock lock = lockObjects.computeIfAbsent(seatId, k -> new ReentrantLock()); + if (lock.tryLock()) { + locks.add(lock); + } else { + throw new SeatUnavailableException(); + } + } + + for (Long seatId : seatIds) { + if(this.bookedSeats.contains(seatId)) + throw new SeatUnavailableException(); + } + this.bookedSeats.addAll(seatIds); + } finally { + for(ReentrantLock lock: locks) { + lock.unlock(); + } + } + } + + public void unBookSeats(List seatIds) { + this.bookedSeats.removeAll(seatIds); + } + + @Override + public String toString() { + return "Id: " + id + ", eventId: " + eventId + ", screenId: " + screenId +", bookedSeats: " + bookedSeats; + } +} \ No newline at end of file diff --git a/src/com/showbooking/events/exception/InvalidShowTimingException.java b/src/com/showbooking/events/exception/InvalidShowTimingException.java new file mode 100644 index 0000000..d60f863 --- /dev/null +++ b/src/com/showbooking/events/exception/InvalidShowTimingException.java @@ -0,0 +1,7 @@ +package com.showbooking.events.exception; + +public class InvalidShowTimingException extends Exception { + public InvalidShowTimingException(){ + super("Invalid show timing!!"); + }; +} diff --git a/src/com/showbooking/payments/Payable.java b/src/com/showbooking/payments/Payable.java new file mode 100644 index 0000000..a8e32e6 --- /dev/null +++ b/src/com/showbooking/payments/Payable.java @@ -0,0 +1,8 @@ +package com.showbooking.payments; + +import com.showbooking.payments.exception.PaymentFailedException; + +public interface Payable { + Payment makePayment(Long userId, Integer amount) throws PaymentFailedException; + void refundPayment(Payment payment); +} diff --git a/src/com/showbooking/payments/Payment.java b/src/com/showbooking/payments/Payment.java new file mode 100644 index 0000000..eac4a72 --- /dev/null +++ b/src/com/showbooking/payments/Payment.java @@ -0,0 +1,29 @@ +package com.showbooking.payments; + + +public class Payment { + final Long id; + final Long userId; + final Integer amount; + final PaymentStatus status; + + + Payment(Long id, Long userId, Integer amount, PaymentStatus status) { + this.id = id; + this.userId = userId; + this.amount = amount; + this.status = status; + } + + public PaymentStatus getStatus() { + return this.status; + } + + public Integer getAmount() { + return this.amount; + } + + public Long getUserId(){ + return this.userId; + } +} diff --git a/src/com/showbooking/payments/PaymentService.java b/src/com/showbooking/payments/PaymentService.java new file mode 100644 index 0000000..f071703 --- /dev/null +++ b/src/com/showbooking/payments/PaymentService.java @@ -0,0 +1,33 @@ +package com.showbooking.payments; + +import com.showbooking.payments.exception.PaymentFailedException; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class PaymentService implements Payable { + + @Override + public Payment makePayment(Long userId, Integer amount) throws PaymentFailedException { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException exception) { + System.out.println(exception.getMessage()); + } + + Random rand = new Random(); + int paymentProbability = rand.nextInt(100); + if(paymentProbability >= 25) { + System.out.println("User " + userId + ", made successful payment of Rs. " + amount); + return new Payment(rand.nextLong(), userId, amount, PaymentStatus.SUCCESS); + } + + System.out.println("User " + userId + ", payment of Rs. " + amount + " failed"); + throw new PaymentFailedException(); + } + + @Override + public void refundPayment(Payment payment) { + System.out.println("Refund of Rs. " + payment.getAmount() + ", successful for user " + payment.getUserId()); + } +} diff --git a/src/com/showbooking/payments/PaymentStatus.java b/src/com/showbooking/payments/PaymentStatus.java new file mode 100644 index 0000000..4477f83 --- /dev/null +++ b/src/com/showbooking/payments/PaymentStatus.java @@ -0,0 +1,6 @@ +package com.showbooking.payments; + +public enum PaymentStatus { + SUCCESS, + FAILED +} diff --git a/src/com/showbooking/payments/exception/PaymentFailedException.java b/src/com/showbooking/payments/exception/PaymentFailedException.java new file mode 100644 index 0000000..4e76a39 --- /dev/null +++ b/src/com/showbooking/payments/exception/PaymentFailedException.java @@ -0,0 +1,7 @@ +package com.showbooking.payments.exception; + +public class PaymentFailedException extends Exception { + public PaymentFailedException(){ + super("Your payment could not be processed."); + }; +} diff --git a/src/com/showbooking/repository/BookingRepository.java b/src/com/showbooking/repository/BookingRepository.java new file mode 100644 index 0000000..be4d356 --- /dev/null +++ b/src/com/showbooking/repository/BookingRepository.java @@ -0,0 +1,40 @@ +package com.showbooking.repository; + + +import com.showbooking.bookings.Booking; + +import java.util.HashMap; +import java.util.Map; + +public class BookingRepository { + private volatile static BookingRepository instance; + private final Map allBookings; + + private BookingRepository() { + allBookings = new HashMap<>(); + } + + public static BookingRepository getInstance() { + if(instance == null) { + synchronized (BookingRepository.class) { + if(instance == null) { + instance = new BookingRepository(); + } + } + } + return instance; + } + + public Booking fetchBooking(Long bookingId) { + return allBookings.getOrDefault(bookingId, null); + } + + public Booking addBooking(Booking userBooking) { + allBookings.put(userBooking.getId(), userBooking); + return userBooking; + } + + public void cancelBooking(Booking userBooking) { + allBookings.put(userBooking.getId(), userBooking); + } +} diff --git a/src/com/showbooking/repository/EventRepository.java b/src/com/showbooking/repository/EventRepository.java new file mode 100644 index 0000000..ee8b98d --- /dev/null +++ b/src/com/showbooking/repository/EventRepository.java @@ -0,0 +1,81 @@ +package com.showbooking.repository; + +import com.showbooking.events.Event; +import com.showbooking.events.Screen; +import com.showbooking.events.Show; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class EventRepository { + private volatile static EventRepository instance; + private final Map movieMap; + private final Map screenMap; + private Map> showStore; + + private EventRepository() { + this.movieMap = new HashMap<>(); + this.screenMap = new HashMap<>(); + this.showStore = new HashMap<>(); + } + + public static EventRepository getInstance() { + if (instance == null) { + synchronized (EventRepository.class) { + if (instance == null) { + instance = new EventRepository(); + } + } + } + return instance; + } + + public List fetchAllMovies() { + List allMovies = new ArrayList(movieMap.values()); + return allMovies; + } + + public Event fetchMovie(Long id) { + return movieMap.get(id); + } + + public Event addMovie(Event event) { + movieMap.put(event.getEventId(), event); + return event; + } + + public void updateMovie(Event event) { + movieMap.put(event.getEventId(), event); + } + + public Screen fetchScreen(Long id) { + return screenMap.get(id); + } + + public Screen addScreen(Screen screen) { + screenMap.put(screen.getId(), screen); + return screen; + } + + public void updateScreen(Screen screen) { + screenMap.put(screen.getId(), screen); + } + + public List fetchAllShows(Long eventId) { + return showStore.getOrDefault(eventId, new ArrayList()); + } + + public Show addShow(Show show) { + List eventShows = showStore.getOrDefault(show.getEventId(), new ArrayList<>()); + eventShows.add(show); + showStore.put(show.getEventId(), eventShows); + return show; + } + + public void removeShows(Event event, Screen screen) { + List eventShows = showStore.getOrDefault(event.getEventId(), new ArrayList<>()); + eventShows.removeIf(show -> show.getScreenId().equals(screen.getId())); + } +}