JavaRush /Java Blog /Random EN /Building an Airline Price Monitoring System: A Step-by-St...

Building an Airline Price Monitoring System: A Step-by-Step Guide [Part 2]

Published in the Random EN group
Building an Airline Price Monitoring System: A Step-by-Step Guide [Part 1]

Content

Building an Airline Price Monitoring System: A Step-by-Step Guide [Part 2] - 1

Set up an H2 database and create a Subscription entity. Repository layer

You need to store subscription states on the application side in order to know who to send price reduction notifications to. To do this, I chose an object with all the necessary data that is needed - Subscription. Let's use JPA annotations (java persistence api), we get:
import java.io.Serializable;
import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
import lombok.ToString;

@Data
@Entity
@Table(name = "subscription")
public class Subscription implements Serializable {

   private static final long serialVersionUID = 1;

   @Id
   @GeneratedValue
   private Long id;

   @Column(name = "email")
   private String email;

   @Column(name = "country")
   private String country;

   @Column(name = "currency")
   private String currency;

   @Column(name = "locale")
   private String locale;

   @Column(name = "origin_place")
   private String originPlace;

   @Column(name = "destination_place")
   private String destinationPlace;

   @Column(name = "outbound_partial_date")
   private LocalDate outboundPartialDate;

   @Column(name = "inbound_partial_date")
   private LocalDate inboundPartialDate;

   @Column(name = "min_price")
   private Integer minPrice;
}
where we specified @Data already known to us. In addition to it, there are new ones:
  • @Entity - an annotation from JPA that says that this will be an entity from the database;
  • @Table(name = “subscription”) - also from JPA, which determines which table this entity will join with.
Next, you need to set up H2 on the project. Fortunately, we already have the dependency: we need to write a simple script to create a table and add settings in the application.properties file. Full description of how to add H2 .

Excerpt from Spring in Action 5th edition:
schema.sql - if there's a file named schema.sql in the root of the application's classpath, then the SQL in that file will be executed against the database when the application starts.

Spring Boot will understand that a file named schema.sql in the correct location means that the database needs it. As a database schema file, put it in main/resources : schema.sql
DROP TABLE IF EXISTS subscription;

CREATE TABLE subscription (
   id INT AUTO_INCREMENT PRIMARY KEY,
   email VARCHAR(250) NOT NULL,
   country VARCHAR(250) NOT NULL,
   currency VARCHAR(250) NOT NULL,
   locale VARCHAR(250) NOT NULL,
   origin_place VARCHAR(250) NOT NULL,
   destination_place VARCHAR(250) NOT NULL,
   outbound_partial_date DATE NOT NULL,
   min_price INT,
   inbound_partial_date DATE
)
Next, from the guide that I gave above, we take the following settings:
# H2
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
spring.datasource.url=jdbc:h2:mem:subscriptiondb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=flights
spring.datasource.password=flights
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
Let's add SubscriptionRespository - an interface with which it will communicate with the database from Java code.
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface SubscriptionRepository extends JpaRepository<Subscription, Long> {
   List<Subscription> findByEmail(String email);
}
With Spring Data, this is quite enough. JpaRepository has a set of necessary methods for our work. The findByEmail(String email) method was created as an additional method, which is needed in addition to the standard set. The beauty of Spring Data is that a well-written method name is enough for Spring to do everything without our implementation. The @Repository annotation is needed so that later this interface can be injected into other classes. And that's all ...) It's so easy to work with Spring Data.

EmailNofiticationService: writing an email sending service

In order for subscribers to receive notifications, you need to add sending letters to the mail. Here you can choose another way, for example, sending via instant messengers. From the experience of using gmail mail to send notifications, I will say that you need to do more additional settings in your account and disable dual authorization so that everything works fine. Here is a good solution . If you are working with Spring Boot, it is likely that the task you need to implement has already been decided and you can use it. Namely, Spring boot starter mail. The dependency was already added when the project was created, so we add the necessary settings to application.properties as indicated in the article .
# Spring Mail
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=IMAIL
spring.mail.password=PASSWORD

# Other properties
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.writetimeout=5000

# TLS , port 587
spring.mail.properties.mail.smtp.starttls.enable=true
and the actual service. You need to send a notification about the registration of a subscription and about a decrease in the price of a flight:
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;

/**
* Sends email notification.
*/
public interface EmailNotifierService {

   /**
    * Notifies subscriber, that the minPrice has decreased.
    *
    * @param subscription the {@link Subscription} object.
    * @param oldMinPrice minPrice before recount.
    * @param newMinPrice minPrice after recount.
    */
   void notifySubscriber(Subscription subscription, Integer oldMinPrice, Integer newMinPrice);

   /**
    * Notifies subscriber, that subscription has added.
    *
    * @param subscription the {@link Subscription} object.
    */
   void notifyAddingSubscription(Subscription subscription);
}
and implementation:
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import com.github.romankh3.flightsmonitoring.service.EmailNotifierService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

/**
* {@inheritDoc}
*/
@Slf4j
@Service
public class EmailNotifierServiceImpl implements EmailNotifierService {

   @Autowired
   private JavaMailSender javaMailSender;

   /**
    * {@inheritDoc}
    */
   @Override
   public void notifySubscriber(Subscription subscription, Integer oldMinPrice, Integer newMinPrice) {
       log.debug("method notifySubscriber STARTED");
       SimpleMailMessage msg = new SimpleMailMessage();
       msg.setTo(subscription.getEmail());
       msg.setSubject("Flights Monitoring Service");
       msg.setText(String.format("Hello, dear! \n "
               + "the price for your flight has decreased \n"
               + "Old min price = %s,\n new min price = %s,\n Subscription details = %s", oldMinPrice, newMinPrice, subscription.toString()));
       javaMailSender.send(msg);
       log.debug("method notifySubscriber FINISHED");
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void notifyAddingSubscription(Subscription subscription) {
       log.debug("method notifyAddingSubscription STARTED");
       SimpleMailMessage msg = new SimpleMailMessage();
       msg.setTo(subscription.getEmail());
       msg.setSubject("Flights Monitoring Service");
       msg.setText(String.format("Hello, dear! \n "
               + "Subscription has been successfully added. \n"
               + "Subscription details = %s", subscription.toString()));
       javaMailSender.send(msg);
       log.debug("method notifyAddingSubscription FINISHED");
   }
}

FlightPriceService - connection of client services with application services

To connect the work of our FlightPricesClient and the service for processing subscriptions, we need to create a service that, based on the Subscription object, will issue complete information about the flight with a minimum cost. There is a FlightPriceService for this:
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;

/**
* Service, for getting details based on {@link Subscription} object.
*/
public interface FlightPriceService {

   /**
    * Finds minPrice based on {@link Subscription}.
    *
    * @param flightPricesDto provided {@link FlightPricesDto} object.
    * @return
    */
   Integer findMinPrice(FlightPricesDto flightPricesDto);

   /**
    * Finds all the flight data related to {@link Subscription} object.
    *
    * @param subscription provided {@link Subscription} object
    * @return {@link FlightPricesDto} with all the data related to flight specific in {@link Subscription}.
    */
   FlightPricesDto findFlightPrice(Subscription subscription);
}
and implementation:
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
import com.github.romankh3.flightsmonitoring.client.service.FlightPricesClient;
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import com.github.romankh3.flightsmonitoring.service.FlightPriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* {@inheritDoc}
*/
@Service
public class FlightPriceServiceImpl implements FlightPriceService {

   @Autowired
   private FlightPricesClient flightPricesClient;

   /**
    * {@inheritDoc}
    */
   @Override
   public Integer findMinPrice(FlightPricesDto flightPricesDto) {
       return flightPricesDto.getQuotas().get(0).getMinPrice();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public FlightPricesDto findFlightPrice(Subscription subscription) {
       if (subscription.getInboundPartialDate() == null) {
           return flightPricesClient
                   .browseQuotes(subscription.getCountry(), subscription.getCurrency(), subscription.getLocale(),
                           subscription.getOriginPlace(), subscription.getDestinationPlace(),
                           subscription.getOutboundPartialDate().toString());
       } else {
           return flightPricesClient
                   .browseQuotes(subscription.getCountry(), subscription.getCurrency(), subscription.getLocale(),
                           subscription.getOriginPlace(), subscription.getDestinationPlace(),
                           subscription.getOutboundPartialDate().toString(), subscription.getInboundPartialDate().toString());
       }
   }
}
Here we have two methods: one returns the full information about the flight with the minimum price, and the other accepts this information and returns the value of the minimum price. This could be done every time and with the result, but I find it more convenient to use.

Create SubscriptionService and CRUD operations in it

To fully manage subscriptions, you need to create a service with CRUD operations. CRUD stands for create, read, update, delete. That is, you need to be able to create a subscription, read it by ID, edit and delete it. The only difference is that we will receive subscriptions not by ID, but by email, since this is exactly what we need. After all, why do we need subscriptions for an incomprehensible ID, but all user subscriptions via his mail are really needed. So SubscriptionService:
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionCreateDto;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionDto;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionUpdateDto;
import java.util.List;

/**
* Manipulates with  subscriptions.
*/
public interface SubscriptionService {

   /**
    * Add new subscription.
    * @param dto the dto of the subscription.
    */
   SubscriptionDto create(SubscriptionCreateDto dto);

   /**
    * Get all subscription based on email.
    *
    * @param email provided email;
    * @return the collection of the {@link SubscriptionDto} objects.
    */
   List<SubscriptionDto> findByEmail(String email);

   /**
    * Remove subscription based on it ID
    *
    * @param subscriptionId the ID of the {@link Subscription}.
    */
   void delete(Long subscriptionId);

   /**
    * Update subscription based on ID
    *
    *
    * @param subscriptionId the ID of the subscription to be updated.
    * @param dto the data to be updated.
    * @return updated {@link SubscriptionDto}.
    */
   SubscriptionDto update(Long subscriptionId, SubscriptionUpdateDto dto);
}
Three new DTO objects can be seen in the arguments:
  • SubscriptionDto - contains all the information to display;
  • SubscriptionCreateDto - data for creating a subscription;
  • SubscriptionUpdateDto - Data that can be updated in a subscription.
The Create, Update DTO did not include such fields as ID, minPrice, since the user has read-only access to them. The ID is set by the database, and minPrice is obtained from a request to the Skyscanner API. And the implementation of this service, SubscriptionServiceImpl:
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
import com.github.romankh3.flightsmonitoring.repository.SubscriptionRepository;
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionCreateDto;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionDto;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionUpdateDto;
import com.github.romankh3.flightsmonitoring.service.EmailNotifierService;
import com.github.romankh3.flightsmonitoring.service.FlightPriceService;
import com.github.romankh3.flightsmonitoring.service.SubscriptionService;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;

/**
* {@inheritDoc}
*/
@Slf4j
@Service
public class SubscriptionServiceImpl implements SubscriptionService {

   @Autowired
   private SubscriptionRepository subscriptionRepository;

   @Autowired
   private FlightPriceService flightPriceService;

   @Autowired
   private EmailNotifierService emailNotifierService;

   /**
    * {@inheritDoc}
    */
   @Override
   public SubscriptionDto create(SubscriptionCreateDto dto) {
       Subscription subscription = toEntity(dto);
       Optional<Subscription> one = subscriptionRepository.findOne(Example.of(subscription));

       if (one.isPresent()) {
           log.info("The same subscription has been found for Subscription={}", subscription);
           Subscription fromDatabase = one.get();
           FlightPricesDto flightPriceResponse = flightPriceService.findFlightPrice(subscription);
           subscription.setMinPrice(flightPriceService.findMinPrice(flightPriceResponse));
           return toDto(fromDatabase, flightPriceResponse);
       } else {
           FlightPricesDto flightPriceResponse = flightPriceService.findFlightPrice(subscription);
           subscription.setMinPrice(flightPriceService.findMinPrice(flightPriceResponse));
           Subscription saved = subscriptionRepository.save(subscription);
           log.info("Added new subscription={}", saved);
           emailNotifierService.notifyAddingSubscription(subscription);
           return toDto(saved, flightPriceResponse);
       }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public List<SubscriptionDto> findByEmail(String email) {
       return subscriptionRepository.findByEmail(email).stream()
               .map(subscription -> {
                   FlightPricesDto flightPriceResponse = flightPriceService.findFlightPrice(subscription);
                   if (subscription.getMinPrice() != flightPriceService.findMinPrice(flightPriceResponse)) {
                       subscription.setMinPrice(flightPriceService.findMinPrice(flightPriceResponse));
                       subscriptionRepository.save(subscription);
                   }
                   return toDto(subscription, flightPriceResponse);
               })
               .collect(Collectors.toList());
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void delete(Long subscriptionId) {
       subscriptionRepository.deleteById(subscriptionId);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public SubscriptionDto update(Long subscriptionId, SubscriptionUpdateDto dto) {
       Subscription subscription = subscriptionRepository.getOne(subscriptionId);
       subscription.setDestinationPlace(dto.getDestinationPlace());
       subscription.setOriginPlace(dto.getOriginPlace());
       subscription.setLocale(dto.getLocale());
       subscription.setCurrency(dto.getCurrency());
       subscription.setCountry(dto.getCountry());
       subscription.setEmail(dto.getEmail());
       subscription.setOutboundPartialDate(dto.getOutboundPartialDate());
       subscription.setInboundPartialDate(dto.getInboundPartialDate());

       FlightPricesDto flightPriceResponse = flightPriceService.findFlightPrice(subscription);
       subscription.setMinPrice(flightPriceService.findMinPrice(flightPriceResponse));
       return toDto(subscriptionRepository.save(subscription), flightPriceResponse);
   }

   private Subscription toEntity(SubscriptionCreateDto dto) {
       Subscription subscription = new Subscription();
       subscription.setCountry(dto.getCountry());
       subscription.setCurrency(dto.getCurrency());
       subscription.setDestinationPlace(dto.getDestinationPlace());
       subscription.setInboundPartialDate(dto.getInboundPartialDate());
       subscription.setOutboundPartialDate(dto.getOutboundPartialDate());
       subscription.setLocale(dto.getLocale());
       subscription.setOriginPlace(dto.getOriginPlace());
       subscription.setEmail(dto.getEmail());

       return subscription;
   }

   private SubscriptionDto toDto(Subscription entity, FlightPricesDto response) {
       SubscriptionDto dto = new SubscriptionDto();
       dto.setEmail(entity.getEmail());
       dto.setCountry(entity.getCountry());
       dto.setCurrency(entity.getCurrency());
       dto.setLocale(entity.getLocale());
       dto.setOriginPlace(entity.getOriginPlace());
       dto.setDestinationPlace(entity.getDestinationPlace());
       dto.setOutboundPartialDate(entity.getOutboundPartialDate());
       dto.setInboundPartialDate(entity.getInboundPartialDate());
       dto.setMinPrice(entity.getMinPrice());
       dto.setId(entity.getId());
       dto.setFlightPricesDto(response);
       return dto;
   }
}

Writing a Spring Scheduler to Check Ticket Status

To know if the price of air tickets has changed, you need to run requests from time to time on the created subscriptions and check with the saved state the minimum price that lies in the database. For this, there is Spring Scheduler, which will help us with this. Here's a great description . Like everything in Spring, it doesn't take much:
  • Add the @EnableScheduling annotation;

  • Create a SchedulerTasks object and place it in the Application Context

    import com.github.romankh3.flightsmonitoring.service.RecountMinPriceService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    @Slf4j
    @Component
    public class SchedulerTasks {
    
       @Autowired
       private RecountMinPriceService recountMinPriceService;
    
       private static final long TEN_MINUTES = 1000 * 60 * 10;
    
       @Scheduled(fixedRate = TEN_MINUTES)
       public void recountMinPrice() {
           log.debug("recount minPrice Started");
           recountMinPriceService.recount();
           log.debug("recount minPrice finished");
       }
    }
  • We write RecountMinPriceService, which will perform all the logic:

    /**
    * Recounts minPrice for all the subscriptions.
    */
    public interface RecountMinPriceService {
    
       /**
        * Recounts minPrice for all the subscriptions.
        */
       void recount();
    }

    and implementation:

    import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
    import com.github.romankh3.flightsmonitoring.repository.SubscriptionRepository;
    import com.github.romankh3.flightsmonitoring.service.EmailNotifierService;
    import com.github.romankh3.flightsmonitoring.service.FlightPriceService;
    import com.github.romankh3.flightsmonitoring.service.RecountMinPriceService;
    import java.time.LocalDate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
    * {@inheritDoc}
    */
    @Service
    public class RecountMinPriceServiceImpl implements RecountMinPriceService {
    
       @Autowired
       private SubscriptionRepository subscriptionRepository;
    
       @Autowired
       private FlightPriceService flightPriceService;
    
       @Autowired
       private EmailNotifierService emailNotifierService;
    
       //todo add async
       /**
        * {@inheritDoc}
        */
       @Override
       public void recount() {
           subscriptionRepository.findAll().forEach(subscription -> {
               if(subscription.getOutboundPartialDate().isAfter(LocalDate.now().minusDays(1))) {
                   FlightPricesDto flightPricesDto = flightPriceService.findFlightPrice(subscription);
                   Integer newNumPrice = flightPriceService.findMinPrice(flightPricesDto);
                   if (subscription.getMinPrice() > newNumPrice) {
                       emailNotifierService.notifySubscriber(subscription, subscription.getMinPrice(), newNumPrice);
                       subscription.setMinPrice(newNumPrice);
                       subscriptionRepository.save(subscription);
                   }
               } else {
                   subscriptionRepository.delete(subscription);
               }
           });
       }
    }
and that's it, you can use it. Now every 30 minutes (this figure is set in SchedulerTasks) minPrice will be recalculated without our participation. If the price goes down, a notification will be sent to the user and stored in the database. This will happen on a daemon thread. Building an Airline Price Monitoring System: A Step-by-Step Guide [Part 2] - 2

Adding Swagger and Swagger UI to the Application

Before writing controllers, let's add Swagger and Swagger UI. Excellent article on this topic.

Swagger is an open source software framework backed by a rich ecosystem of tools that helps developers design, build, document, and consume RESTful web services.

Swagger UI provides a web page with information about the REST API to show what requests are there, what data it accepts, and what it returns. Add descriptions to fields, show examples of fields that the application expects. Also, using the Swagger UI, you can send requests to the application without the help of third-party tools. This is an important part, since no front-end is provided. We figured out why, now how to add it:
  • add two dependencies to pom.xml

    <dependency>
        <groupId>io.springfox</groupId>
         <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
  • A new class for the SwaggerConfig configuration, with settings for what will be shown on the UI (user interface).

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
       @Bean
       public Docket api() {
           return new Docket(DocumentationType.SWAGGER_2)
                   .select()
                   .apis(RequestHandlerSelectors.basePackage("com.github.romankh3.flightsmonitoring.rest.controller"))
                   .paths(PathSelectors.any())
                   .build();
       }
    }
And that's it: now when you start the application, if you follow the link swagger-ui.html , all information about the controllers will be visible. Since there is no frontend in the application, there will be an error when hitting the main page. Let's add a redirect from the main page to the Swagger UI. For this, ye;ty is another WebConfig configuration class:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* Web configuration class.
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {

   @Override
   public void addViewControllers(ViewControllerRegistry registry) {
       registry.addViewController("/").setViewName("redirect:/swagger-ui.html");
   }
}
Building an Airline Price Monitoring System: A Step-by-Step Guide [Part 3]
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION