Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/redis state management - Close #13 #27

Merged
merged 4 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ build/

### Environment Variables ###
src/main/resources/.env.properties

### DB Data ###
db/redis/
20 changes: 20 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- Caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- DataBase -->
<dependency>
<groupId>com.h2database</groupId>
Expand Down Expand Up @@ -129,6 +135,20 @@
</excludes>
</configuration>
</plugin>

<!-- This is for redis key in `TimeEntryTrackingServiceImpl#startTrackingTime(Boolean, BigDecimal)`; cuz the key is generating null -->
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-compiler-plugin</artifactId>-->
<!-- <version>3.11.0</version>-->
<!-- <configuration>-->
<!-- <source>${java.version}</source>-->
<!-- <target>${java.version}</target>-->
<!-- <compilerArgs>-->
<!-- <arg>-parameters</arg>-->
<!-- </compilerArgs>-->
<!-- </configuration>-->
<!-- </plugin>-->
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.seyed.ali.timeentryservice.config.cache;

import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfiguration {

/**
* Remember to configure the properties in application.yml or properties file.
*/
@Bean
@Primary
public RedisProperties properties() {
return new RedisProperties();
}

/**
* Lettuce is non-blocking, and Jedis is blocking.
*/
@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(this.properties().getHost());
redisStandaloneConfiguration.setPort(this.properties().getPort());

return new LettuceConnectionFactory(redisStandaloneConfiguration);
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

redisTemplate.setConnectionFactory(this.lettuceConnectionFactory());
redisTemplate.setEnableTransactionSupport(true);

redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer());
redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Object.class));

return redisTemplate;
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.seyed.ali.timeentryservice.controller;

import com.seyed.ali.timeentryservice.model.domain.TimeEntry;
import com.seyed.ali.timeentryservice.model.domain.TimeSegment;
import com.seyed.ali.timeentryservice.model.dto.TimeEntryDTO;
import com.seyed.ali.timeentryservice.model.dto.response.Result;
import com.seyed.ali.timeentryservice.model.dto.response.TimeEntryResponse;
import com.seyed.ali.timeentryservice.service.interfaces.TimeEntryService;
import com.seyed.ali.timeentryservice.util.TimeParser;
import com.seyed.ali.timeentryservice.util.converter.TimeEntryConverter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -15,6 +19,8 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

import static org.springframework.http.HttpStatus.*;

@RestController
Expand All @@ -24,6 +30,8 @@
public class TimeEntryController {

private final TimeEntryService timeEntryService;
private final TimeEntryConverter timeEntryConverter;
private final TimeParser timeParser;

@GetMapping
@Operation(summary = "Get all time entries", responses = {
Expand All @@ -34,21 +42,36 @@ public class TimeEntryController {
)
})
public ResponseEntity<Result> getTimeEntries() {
List<TimeEntryResponse> timeEntryResponseList = this.timeEntryConverter.convertToTimeEntryResponseList(this.timeEntryService.getTimeEntries());

return ResponseEntity.ok(new Result(
true,
OK,
"List of time entries.",
this.timeEntryService.getTimeEntries()
timeEntryResponseList
));
}

@GetMapping("/{userId}")
@GetMapping("/user/{userId}")
public ResponseEntity<Result> getUsersTimeEntry(@PathVariable String userId) {
TimeEntryResponse timeEntryResponse = this.timeEntryConverter.convertToTimeEntryResponse(this.timeEntryService.getUsersTimeEntry(userId));

return ResponseEntity.ok(new Result(
true,
OK,
"Time entry for user: '" + userId + "' :",
this.timeEntryService.getUsersTimeEntry(userId)
timeEntryResponse
));
}

@GetMapping("/{timeEntryId}")
public ResponseEntity<Result> getSpecificTimeEntry(@PathVariable String timeEntryId) {
TimeEntryResponse timeEntryResponse = this.timeEntryConverter.convertToTimeEntryResponse(this.timeEntryService.getTimeEntryById(timeEntryId));
return ResponseEntity.ok(new Result(
true,
OK,
"Time entry: '" + timeEntryId + "'.",
timeEntryResponse
));
}

Expand All @@ -64,11 +87,16 @@ public ResponseEntity<Result> addTimeEntryManually(@Valid @RequestBody TimeEntry

@PutMapping("/{timeEntryId}")
public ResponseEntity<Result> updateTimeEntryManually(@Valid @PathVariable String timeEntryId, @RequestBody TimeEntryDTO timeEntryDTO) {
TimeEntry timeEntry = this.timeEntryService.updateTimeEntryManually(timeEntryId, timeEntryDTO);
TimeSegment lastTimeSegment = timeEntry.getTimeSegmentList().getLast();
String startTimeString = this.timeParser.parseLocalDateTimeToString(lastTimeSegment.getStartTime());
TimeEntryDTO timeEntryDTOResponse = this.timeEntryConverter.createTimeEntryDTO(timeEntry, lastTimeSegment, startTimeString);

return ResponseEntity.ok(new Result(
true,
OK,
"Time entry for user: -> " + timeEntryId + " <- updated successfully.",
this.timeEntryService.updateTimeEntryManually(timeEntryId, timeEntryDTO)
timeEntryDTOResponse
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,11 @@ public class TimeEntryTrackingController {

@PostMapping("/start")
public ResponseEntity<Result> startTrackingTimeEntry(@Valid @RequestBody TimeBillingDTO timeBillingDTO) {
boolean billable = false;
BigDecimal hourlyRate = BigDecimal.ZERO;

if (timeBillingDTO != null) {
billable = timeBillingDTO.billable();
hourlyRate = timeBillingDTO.hourlyRate();
}
return ResponseEntity.status(CREATED).body(new Result(
true,
CREATED,
"Time tracking started...",
this.timeEntryService.startTrackingTimeEntry(billable, hourlyRate)
this.timeEntryService.startTrackingTimeEntry(timeBillingDTO)
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.persistence.OneToMany;
import lombok.*;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -17,7 +18,7 @@
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class TimeEntry {
public class TimeEntry implements Serializable {

@Id
private String timeEntryId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.persistence.ManyToOne;
import lombok.*;

import java.io.Serializable;
import java.time.Duration;
import java.time.LocalDateTime;

Expand All @@ -15,7 +16,7 @@
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class TimeSegment {
public class TimeSegment implements Serializable {

@Id
private String timeSegmentId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import com.seyed.ali.timeentryservice.model.domain.TimeEntry;
import com.seyed.ali.timeentryservice.model.domain.TimeSegment;
import com.seyed.ali.timeentryservice.model.dto.TimeEntryDTO;
import com.seyed.ali.timeentryservice.model.dto.response.TimeEntryResponse;
import com.seyed.ali.timeentryservice.repository.TimeEntryRepository;
import com.seyed.ali.timeentryservice.service.cache.TimeEntryCacheManager;
import com.seyed.ali.timeentryservice.service.interfaces.TimeEntryService;
import com.seyed.ali.timeentryservice.util.TimeEntryUtility;
import com.seyed.ali.timeentryservice.util.TimeParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -24,24 +26,46 @@ public class TimeEntryServiceImpl implements TimeEntryService {
private final TimeEntryRepository timeEntryRepository;
private final TimeParser timeParser;
private final TimeEntryUtility timeEntryUtility;
private final TimeEntryCacheManager timeEntryCacheManager;

/**
* {@inheritDoc}
*/
@Override
public List<TimeEntryResponse> getTimeEntries() {
List<TimeEntry> timeEntryList = this.timeEntryRepository.findAll();
return this.timeEntryUtility.convertToTimeEntryResponseList(timeEntryList);
public List<TimeEntry> getTimeEntries() {
return this.timeEntryRepository.findAll();
}

/**
* {@inheritDoc}
*/
@Override
public TimeEntryResponse getUsersTimeEntry(String userId) {
TimeEntry timeEntry = this.timeEntryRepository.findByUserId(userId)
@Cacheable(
cacheNames = TimeEntryCacheManager.TIME_ENTRY_CACHE,
key = "#userId",
unless = "#result == null"
)
public TimeEntry getUsersTimeEntry(String userId) {
return this.timeEntryRepository.findByUserId(userId)
.orElseThrow(() -> new ResourceNotFoundException("User with id " + userId + " not found"));
return this.timeEntryUtility.convertToTimeEntryResponse(timeEntry);
}

/**
* {@inheritDoc}
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
@Cacheable(
cacheNames = TimeEntryCacheManager.TIME_ENTRY_CACHE,
key = "#timeEntryId",
unless = "#result == null"
)
public TimeEntry getTimeEntryById(String timeEntryId) {
log.info("Db call.");
TimeEntry timeEntry = this.timeEntryRepository.findById(timeEntryId)
.orElseThrow(()-> new ResourceNotFoundException("Time entry with ID: '" + timeEntryId +"' was not found."));
timeEntry.getTimeSegmentList().size(); // This will initialize the timeSegmentList: otherwise we'll get hibernate's LazyLoadingException.
return timeEntry;
}

/**
Expand All @@ -51,7 +75,11 @@ public TimeEntryResponse getUsersTimeEntry(String userId) {
@Transactional
public String addTimeEntryManually(TimeEntryDTO timeEntryDTO) {
TimeEntry timeEntry = this.timeEntryUtility.createTimeEntry(timeEntryDTO);
this.timeEntryRepository.save(timeEntry);
TimeEntry savedTimeEntry = this.timeEntryRepository.save(timeEntry);

// cache the saved `TimeEntry` to redis
this.timeEntryCacheManager.cacheTimeEntry(savedTimeEntry.getTimeEntryId(), savedTimeEntry);

TimeSegment lastTimeSegment = timeEntry.getTimeSegmentList().getLast();
return this.timeParser.parseTimeToString(lastTimeSegment.getStartTime(), lastTimeSegment.getEndTime(), lastTimeSegment.getDuration());
}
Expand All @@ -61,21 +89,26 @@ public String addTimeEntryManually(TimeEntryDTO timeEntryDTO) {
*/
@Override
@Transactional
public TimeEntryDTO updateTimeEntryManually(String timeEntryId, TimeEntryDTO timeEntryDTO) {
public TimeEntry updateTimeEntryManually(String timeEntryId, TimeEntryDTO timeEntryDTO) {
TimeEntry timeEntry = this.timeEntryRepository.findById(timeEntryId)
.orElseThrow(() -> new IllegalArgumentException("The provided timeEntryId does not exist"));
.orElseThrow(() -> new ResourceNotFoundException("The provided timeEntryId does not exist"));
this.timeEntryUtility.updateTimeEntry(timeEntry, timeEntryDTO, this.timeParser);
this.timeEntryRepository.save(timeEntry);
TimeSegment lastTimeSegment = timeEntry.getTimeSegmentList().getLast();
String startTimeString = this.timeParser.parseLocalDateTimeToString(lastTimeSegment.getStartTime());
return this.timeEntryUtility.createTimeEntryDTO(timeEntry, lastTimeSegment, startTimeString);
TimeEntry savedTimeEntry = this.timeEntryRepository.save(timeEntry);

// cache the saved `TimeEntry` to redis
this.timeEntryCacheManager.cacheTimeEntry(timeEntryId, savedTimeEntry);
return savedTimeEntry;
}

/**
* {@inheritDoc}
*/
@Override
@Transactional
@CacheEvict(
cacheNames = TimeEntryCacheManager.TIME_ENTRY_CACHE,
key = "#timeEntryId"
)
public void deleteTimeEntry(String timeEntryId) {
this.timeEntryRepository.deleteById(timeEntryId);
}
Expand Down
Loading