Prerequisites:
- Java version of 17
- MongoDB URI connection
- PostgreSQL database connection
- Telegram bot username and token
- Visual Studio Code Spring Intializr
And now, let's go into the cooking step:
Initialize Spring Boot Application
Using Visual Studio Code, I clicked
ctrl
+shift
+p
altogether, choose Spring Initializr: Create Maven Project
Choose Spring Boot version of 3.0.12, I actually migrating my project from version 2.7.17 to 3.0.0.
Choose **Java **as project language.
Type your project's
group id
. For me, I choosecom.tujuhsembilan
.Type your
artifact id
, I choosescheduler
.Choose
Jar
as thepackaging type
.Choose 17 for Java version.
-
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>
-
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
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;
}
}
-
@EnableWebSecurity
annotation will let the application knows this application needs some configuration related to the security - 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("*");
}
}
Defining models and Dtos
-
First model that we will create inside
model
package isLokomotifDocument.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; }
-
The second model that we are going to create in
model
package isLokomotif.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 thecreatedDate
field isLocalDateTime
instead ofString
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; }
-
The last model that will be created in the
model
package isSummary.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; }
-
Moving on to the
dto
package, we will create a dto ofLokomotifDto.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; }
-
And last for
dto
package we will create aSummaryDto.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
-
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); }
-
LokomotifJpaRepository.java
@Repository public interface LokomotifJpaRepository extends JpaRepository<Lokomotif, String> { List<Lokomotif> findAllByCreatedDateBetween(LocalDateTime yesterday, LocalDateTime today, Pageable page); }
-
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
-
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; } }
-
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); } }
-
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
-
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); } }
-
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);
}
}
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:
And you will have a telegram summary coming in after running the application
Top comments (0)