DEV Community

Amalia Hajarani
Amalia Hajarani

Posted on

Lokomotif: Use Case (4: Implementing Spring Boot: Scheduler Report)

Prerequisites:

  1. Java version of 17
  2. MongoDB URI connection
  3. PostgreSQL database connection
  4. Telegram bot username and token
  5. Visual Studio Code Spring Intializr

And now, let's go into the cooking step:

Initialize Spring Boot Application

  1. Using Visual Studio Code, I clicked ctrl + shift + p altogether, choose Spring Initializr: Create Maven Project
    Image description

  2. Choose Spring Boot version of 3.0.12, I actually migrating my project from version 2.7.17 to 3.0.0.

  3. Choose **Java **as project language.

  4. Type your project's group id. For me, I choose com.tujuhsembilan.

  5. Type your artifact id, I choose scheduler.

  6. Choose Jar as the packaging type.

  7. Choose 17 for Java version.

  8. Add these dependency by searching them. (If you cannot find them in the search tab, you can add them manually later at pom.xml).

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.telegram</groupId>
            <artifactId>telegrambots-spring-boot-starter</artifactId>
            <version>6.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.telegram</groupId>
            <artifactId>telegrambots-abilities</artifactId>
            <version>6.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>
    
  9. After the project initialized, I create few packages. I made these packages at the same level of Application.java (SchedulerApplication.java for me)

    * configuration
    * controller
    * model
      - Dto
    * repository
    * service
    

Setting aplication.properties

First thing first, we have to make sure that our application knows all connection that we need and put it inside application.properties

spring.data.mongodb.uri=mongodb+srv://<>username:<password>@cluster0.cluster.mongodb.net/<dbname> ##Make sure the URI is including the database name

spring.datasource.url=jdbc:postgresql://localhost:5432/summary
spring.datasource.username=username
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver

# This will create table automatically in your database
spring.jpa.hibernate.ddl-auto=create

telegram.bot.username=bot_username
telegram.bot.token=bot_token
Enter fullscreen mode Exit fullscreen mode

Create configuration for application

Create a file inside configuration package called ApplicationConfig.java

import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@EnableJpaAuditing
@EnableWebSecurity
public class ApplicationConfig {

    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }

    @Bean
    protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .authorizeHttpRequests(config -> config
                        .anyRequest().permitAll())
                .csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration config = new CorsConfiguration();

        config.setAllowedOrigins(Arrays.asList("http://127.0.0.1:5173"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS", "DELETE", "PUT", "PATCH"));
        config.setAllowCredentials(true);
        config.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));

        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return source;
    }

}
Enter fullscreen mode Exit fullscreen mode
  1. @EnableWebSecurity annotation will let the application knows this application needs some configuration related to the security
  2. Security related configuration is needed to prevent some security problem like CORS policy error which I had in client-side before I have the right configuration

Create configuration for web security

As I said before, few things need to be declared to avoid CORS policy error. In the configuration package you can add another class called WebSecurityConfig.java to define cors mappings.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebSecurityConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry cors) {
        cors
                .addMapping("/**")
                .allowedOrigins("http://localhost:8080")
                .allowedMethods("*")
                .allowedHeaders("*");
    }

}
Enter fullscreen mode Exit fullscreen mode

Defining models and Dtos

  1. First model that we will create inside model package is LokomotifDocument.java. This model is presenting response that we will get from MongoDB, hence the content of the model is the same as Spring Boot: Info Scheduler service

    import jakarta.persistence.Column;
    import jakarta.persistence.Entity;
    import jakarta.persistence.EntityListeners;
    import jakarta.persistence.Id;
    
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    import org.springframework.data.mongodb.core.mapping.Document;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    @Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Document(collection = "lokomotifs")
    @EntityListeners(AuditingEntityListener.class)
    public class LokomotifDocument {
    
        @Id
        private String _id;
    
        @Column
        private String kodeLoko;
    
        @Column
        private String namaLoko;
    
        @Column
        private String dimensiLoko;
    
        @Column
        private String status;
    
        @Column
        private String createdDate;
    
    }
    
  2. The second model that we are going to create in model package is Lokomotif.java. Different thing about this model and the previous one is that this model will be use to communicate with PostgreSQL database and also the type of the createdDate field is LocalDateTime instead of String

    import java.time.LocalDateTime;
    
    import jakarta.persistence.Column;
    import jakarta.persistence.Entity;
    import jakarta.persistence.EntityListeners;
    import jakarta.persistence.Id;
    import jakarta.persistence.JoinColumn;
    import jakarta.persistence.ManyToOne;
    import jakarta.persistence.Table;
    
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    @Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity
    @Table(name = "lokomotif")
    @EntityListeners(AuditingEntityListener.class)
    public class Lokomotif {
    
        @Id
        private String _id;
    
        @Column
        private String kodeLoko;
    
        @Column
        private String namaLoko;
    
        @Column
        private String dimensiLoko;
    
        @Column
        private String status;
    
        @Column(columnDefinition = "TIMESTAMP")
        private LocalDateTime createdDate;
    
        @ManyToOne
        @JoinColumn(name = "summary_id", nullable = true)
        private Summary summary;
    
    }
    
  3. The last model that will be created in the model package is Summary.java which represents the data structure of summary that will be use to provide summary report

    import java.time.LocalDateTime;
    import java.util.List;
    
    import jakarta.persistence.Column;
    import jakarta.persistence.Entity;
    import jakarta.persistence.EntityListeners;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    import jakarta.persistence.OneToMany;
    import jakarta.persistence.Table;
    
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    @Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity
    @Table(name = "summary", schema = "public")
    @EntityListeners(AuditingEntityListener.class)
    public class Summary {
    
        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE)
        private Integer id;
    
        @Column
        private Integer totalLokomotif;
    
        @Column
        @CreatedDate
        private LocalDateTime createdDate;
    
        @Column
        @OneToMany(mappedBy = "summary")
        private List<Lokomotif> lokomotifs;
    }
    
  4. Moving on to the dto package, we will create a dto of LokomotifDto.java

    import java.time.LocalDateTime;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class LokomotifDto {
    
        private String _id;
        private String kodeLoko;
        private String namaLoko;
        private String dimensiLoko;
        private String status;
        private LocalDateTime createdDate;
    
    }
    
  5. And last for dto package we will create a SummaryDto.java

    import java.time.LocalDateTime;
    import java.util.List;
    
    import com.tujuhsembilan.scheduler.model.Lokomotif;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class SummaryDto {
    
        private Integer id;
        private Integer totalLokomotif;
        private LocalDateTime createdDate;
        private List<Lokomotif> lokomotifs;
    
        @Override
        public String toString() {
            return "Report created at " + createdDate.toLocalDate().toString() + "\r\n" + "Total of lokomotifs generated into database are: " + totalLokomotif.toString();
        }
    
    }
    

Defining repositories

We will create about 3 repositories inside repository package

  1. LokomotifMongoRepository.java

    import org.springframework.data.mongodb.repository.MongoRepository;
    import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
    
    import com.tujuhsembilan.scheduler.model.LokomotifDocument;
    import java.util.List;
    
    @EnableMongoRepositories
    public interface LokomotifMongoRepository extends MongoRepository<LokomotifDocument, String> {
        List<LokomotifDocument> findByCreatedDateBetween(String createdDate, String today);
    }
    
  2. LokomotifJpaRepository.java

    @Repository
    public interface LokomotifJpaRepository extends JpaRepository<Lokomotif, String> {
        List<Lokomotif> findAllByCreatedDateBetween(LocalDateTime yesterday, LocalDateTime today, Pageable page);
    
    }
    
  3. SummaryRepository.java

    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    import com.tujuhsembilan.scheduler.model.Summary;
    
    @Repository
    public interface SummaryRepository extends JpaRepository<Summary, Integer> {
    
    }
    

Defining services

We are going to make three services in a service package

  1. LokomotifService.java in this service we define a method to get lokomotif data from database that was created for the last 24 hours

    import java.time.LocalDateTime;
    import java.util.List;
    import java.util.stream.Collectors;
    
    import org.modelmapper.ModelMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import com.tujuhsembilan.scheduler.model.Lokomotif;
    import com.tujuhsembilan.scheduler.model.LokomotifDocument;
    import com.tujuhsembilan.scheduler.repository.LokomotifJpaRepository;
    import com.tujuhsembilan.scheduler.repository.LokomotifMongoRepository;
    import com.tujuhsembilan.scheduler.utils.LocalDateTimeToStringConverter;
    import com.tujuhsembilan.scheduler.utils.StringToLocalDateTimeConverter;
    
    import lombok.RequiredArgsConstructor;
    
    @Service
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class LokomotifService {
    
        private final ModelMapper modelMapper;
    
        private final LokomotifMongoRepository lokomotifMongoRepository;
    
        private final LokomotifJpaRepository lokomotifJpaRepository;
    
        public List<Lokomotif> getAllLokomotif() {
    
            modelMapper.addConverter(new StringToLocalDateTimeConverter());
    
            List<LokomotifDocument> data = lokomotifMongoRepository.findAll();
    
            List<Lokomotif> lokomotifs = data
                .stream()
                .map(element -> modelMapper.map(element, Lokomotif.class))
                .collect(Collectors.toList());
    
            return lokomotifs;
    
        }
    
        public List<Lokomotif> getLokomotifCreatedYesterday() {
    
            LocalDateTimeToStringConverter converter = new LocalDateTimeToStringConverter();
    
            modelMapper.addConverter(new StringToLocalDateTimeConverter());
    
            LocalDateTime yesterday = LocalDateTime.now().minusDays(1);
            LocalDateTime today = LocalDateTime.now();
            String yesterdayDate = converter.convert(yesterday);
            String todayDate = converter.convert(today);
    
            List<LokomotifDocument> data = lokomotifMongoRepository.findByCreatedDateBetween(yesterdayDate, todayDate);
    
            List<Lokomotif> lokomotifs = data
                .stream()
                .map(element -> modelMapper.map(element, Lokomotif.class))
                .collect(Collectors.toList());
    
            return lokomotifs;
    
        }
    
    }
    
  2. SummaryService.java in this service we will create an instance of Summary model

    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import com.tujuhsembilan.scheduler.model.Lokomotif;
    import com.tujuhsembilan.scheduler.model.Summary;
    import com.tujuhsembilan.scheduler.repository.SummaryRepository;
    
    import lombok.RequiredArgsConstructor;
    
    @Service
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class SummaryService {
    
        private final SummaryRepository summaryRepository;
    
        public Summary createSummary(List<Lokomotif> lokomotifs) {
            var dailySummary = Summary
                .builder()
                .totalLokomotif(lokomotifs.size())
                .lokomotifs(lokomotifs)
                .build();
    
                return summaryRepository.save(dailySummary);
        }
    }
    
  3. BotSummaryService.java in this service we define methods to communicate with telegram bot API

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.telegram.telegrambots.bots.TelegramLongPollingBot;
    import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
    import org.telegram.telegrambots.meta.api.objects.Update;
    import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
    
    import com.tujuhsembilan.scheduler.model.dto.SummaryDto;
    
    @Service
    public class BotSummaryService extends TelegramLongPollingBot {
    
        @Value("${telegram.bot.username}")
        private String botUsername;
    
        @Value("${telegram.bot.token}")
        private String botToken;
    
        @Value("${telegram.bot.chatId}")
        private String chatId;
    
        @Override
        public String getBotUsername() {
            return this.botUsername;
        }
    
        @Override
        public String getBotToken() {
            return this.botToken;
        }
    
        public String getChatId() {
            return this.chatId;
        }
    
        @Override
        public void onUpdateReceived(Update update) {
            if (update.hasMessage()) {
                var message = update.getMessage();
                var chatId = message.getChatId();
                try {
                    var reply = "hi";
                    sendNotification(String.valueOf(chatId), reply);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    
        public void startScheduledReporting(SummaryDto summaryDto) {
            try {
                String reply = summaryDto.toString();
                sendNotification(this.chatId, reply);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private void sendNotification(String chatId, String message) throws TelegramApiException {
            var response = new SendMessage(chatId, message);
            execute(response);
        }
    
    }
    

Working with controllers

We are going to make two controllers inside controller package

  1. LokomotifController.java where we have method for API endpoint call from client side and also a method to save random locomotive data for the last 24 hours into PostgreSQL for every one hour (scheduled)

    import java.time.LocalDateTime;
    import java.util.List;
    import java.util.stream.Collectors;
    
    import org.modelmapper.ModelMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.PageRequest;
    import org.springframework.data.domain.Pageable;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tujuhsembilan.scheduler.model.Lokomotif;
    import com.tujuhsembilan.scheduler.model.dto.LokomotifDto;
    import com.tujuhsembilan.scheduler.repository.LokomotifJpaRepository;
    import com.tujuhsembilan.scheduler.service.LokomotifService;
    
    import lombok.RequiredArgsConstructor;
    
    @RestController
    @RequestMapping("/lokomotifs")
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class LokomotifController {
    
        private final ModelMapper modelMapper;
    
        private final LokomotifService lokomotifService;
    
        private final LokomotifJpaRepository lokomotifJpaRepository;
    
        @GetMapping("/")
        public ResponseEntity<List<LokomotifDto>> getAllLokomotif(@RequestParam int page) {
    
            LocalDateTime yesterday = LocalDateTime.now().minusDays(3);
            LocalDateTime today = LocalDateTime.now();
    
            Pageable pageRequest = PageRequest.of(page, Integer.MAX_VALUE);
    
            List<Lokomotif> data = lokomotifJpaRepository.findAllByCreatedDateBetween(yesterday, today, pageRequest);
    
            List<LokomotifDto> dtos = data
                .stream()
                .map(element -> modelMapper.map(element, LokomotifDto.class))
                .collect(Collectors.toList());
    
            return ResponseEntity.status(HttpStatus.OK).body(dtos);
        }
    
        @PostMapping("/")
        @Scheduled(fixedRate = 3600000)
        public ResponseEntity<List<LokomotifDto>> saveLokomotifCreatedYesterday() {
    
            List<Lokomotif> data = lokomotifService.getLokomotifCreatedYesterday();
    
            List<Lokomotif> savedData = lokomotifJpaRepository.saveAll(data);
    
            List<LokomotifDto> dtos = savedData
                .stream()
                .map(element -> modelMapper.map(element, LokomotifDto.class))
                .collect(Collectors.toList());
    
            return ResponseEntity.status(HttpStatus.CREATED).body(dtos);
    
        }
    
    }
    
  2. SummaryController.java in this controller we have method to save summary into PostgreSQL for every one hour (scheduled)

    import java.time.LocalDateTime;
    import java.util.List;
    
    import org.modelmapper.ModelMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.Pageable;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tujuhsembilan.scheduler.model.Lokomotif;
    import com.tujuhsembilan.scheduler.model.dto.SummaryDto;
    import com.tujuhsembilan.scheduler.repository.LokomotifJpaRepository;
    import com.tujuhsembilan.scheduler.service.BotSummaryService;
    import com.tujuhsembilan.scheduler.service.SummaryService;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    @RestController
    @RequestMapping("/summary")
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class SummaryController {
    
        private final ModelMapper modelMapper;
    
        private final LokomotifJpaRepository lokomotifJpaRepository;
    
        private final SummaryService summaryService;
    
        private final BotSummaryService botSummaryService;
    
        @ResponseBody
        @PostMapping("/")
        @Scheduled(fixedRate = 3600000)
        public ResponseEntity<SummaryDto> createSummary() {
    
            LocalDateTime yesterday = LocalDateTime.now().minusDays(3);
            LocalDateTime today = LocalDateTime.now();
    
            List<Lokomotif> lokomotifs = lokomotifJpaRepository.findAllByCreatedDateBetween(yesterday, today, Pageable.unpaged());
    
            var savedSummary = summaryService.createSummary(lokomotifs);
    
            log.info("Summary is saved to database");
    
            var dto = modelMapper.map(savedSummary, SummaryDto.class);
    
            botSummaryService.startScheduledReporting(dto);
    
            log.info("Summary is sent to telegram");
    
            return ResponseEntity.status(HttpStatus.CREATED).body(dto);
        }
    
    }
    

Updating Application.java (SchedulerApplication.java for me)

To make sure that this application is implementing scheduling feature we need to add an annotation of @Enablescheduling so the class will look like this:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class SchedulerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SchedulerApplication.class, args);
    }

}
Enter fullscreen mode Exit fullscreen mode

Running the application

Since I already have extension for springboot in my visual studio code, I could just press run button and the correct log when the code works properly will look like this:

Image description

And you will have a telegram summary coming in after running the application

Image description

Top comments (0)