Skip to content

Commit

Permalink
Merge pull request #2 from bifrurcated/jpa-hibernate
Browse files Browse the repository at this point in the history
Jpa hibernate
  • Loading branch information
bifrurcated authored Jan 18, 2024
2 parents 2e7a66b + 77ba3b1 commit 03bddaa
Show file tree
Hide file tree
Showing 11 changed files with 311 additions and 4 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
<artifactId>spring-aspects</artifactId>
<version>6.0.11</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bifurcated.wallet.controller;

import com.bifurcated.wallet.errors.UnsupportedOperationTypeError;
import com.bifurcated.wallet.operation.OperationType;
import com.bifurcated.wallet.service.WalletEntityService;
import com.bifurcated.wallet.service.WalletService;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.Optional;
import java.util.UUID;

@RestController
@RequestMapping("/api/v2")
public class WalletEntityController {

private final WalletEntityService walletService;
private final Map<String, OperationType> operations;


@Autowired
public WalletEntityController(WalletEntityService walletEntityService, Map<String, OperationType> operationTypeMap) {
this.walletService = walletEntityService;
this.operations = operationTypeMap;
}

public record WalletResponse(UUID id, Float amount){}
public record WalletRequest(
@JsonProperty("valletId") UUID walletId,
String operationType,
Float amount
){}
@PostMapping("/wallet")
public WalletResponse wallet(@RequestBody WalletRequest request) {
var operationType = request.operationType().toLowerCase();
var wallet = walletService.addAmount(request.walletId(), request.amount());
return new WalletResponse(wallet.getId(), wallet.getAmount());
}

public record BalanceResponse(Float amount){}
@GetMapping("/wallets/{WALLET_UUID}")
public BalanceResponse balance(@PathVariable(value = "WALLET_UUID") UUID id) {
Float amount = walletService.amount(id);
return new BalanceResponse(amount);
}
}
26 changes: 26 additions & 0 deletions src/main/java/com/bifurcated/wallet/data/UseIdOrGenerate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.bifurcated.wallet.data;

import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.generator.BeforeExecutionGenerator;
import org.hibernate.generator.EventType;

import java.util.EnumSet;
import java.util.UUID;

import static org.hibernate.generator.EventType.INSERT;

public class UseIdOrGenerate implements BeforeExecutionGenerator {

@Override
public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
Object id = session.getEntityPersister(null, owner).getIdentifier(owner, session);
return id != null ? id : UUID.randomUUID();
}


@Override
public EnumSet<EventType> getEventTypes() {
return EnumSet.of(INSERT);
}

}
30 changes: 30 additions & 0 deletions src/main/java/com/bifurcated/wallet/data/WalletEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.bifurcated.wallet.data;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

import java.util.UUID;

@Setter
@Getter
@ToString
@Entity(name = "wallet")
public class WalletEntity {

@Id
@GeneratedValue(generator="UseIdOrGenerate")
@GenericGenerator(name="UseIdOrGenerate", type = UseIdOrGenerate.class)
@Column(name = "id", nullable = false)
@JdbcTypeCode(SqlTypes.UUID)
private UUID id;

private Float amount;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@

public class UnsupportedOperationTypeError extends ResponseStatusException {
public UnsupportedOperationTypeError() {
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "operation type is not supported");
super(HttpStatus.BAD_REQUEST, "operation type is not supported");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.bifurcated.wallet.repository;

import com.bifurcated.wallet.data.WalletEntity;
import jakarta.annotation.Nonnull;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.*;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;
import java.util.UUID;

public interface WalletEntityRepository extends JpaRepository<WalletEntity, UUID>, JpaSpecificationExecutor<WalletEntity> {
@Transactional
@Modifying
@Query("update wallet w set w.amount = ?1 where w.id = ?2")
int updateAmountById(Float amount, UUID id);

@Transactional
@Modifying
@Query("update wallet w set w.amount = w.amount + ?2 where w.id = ?1")
void updateAddAmountById(UUID id, Float amount);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.bifurcated.wallet.service;

import com.bifurcated.wallet.data.WalletEntity;
import com.bifurcated.wallet.errors.WalletNotFoundError;
import com.bifurcated.wallet.repository.WalletEntityRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.UUID;

@Service
public class WalletEntityService {

private final WalletEntityRepository repository;
private final EntityManager entityManager;

@Autowired
public WalletEntityService(WalletEntityRepository walletEntityRepository, EntityManager entityManager) {
this.repository = walletEntityRepository;
this.entityManager = entityManager;
}

@Transactional
public WalletEntity addAmount(UUID id, Float amount) {
var entity = entityManager.find(WalletEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
var wallet = Optional.ofNullable(entity).orElseThrow(WalletNotFoundError::new);
wallet.setAmount(wallet.getAmount() + amount);
return repository.save(wallet);
}

public WalletEntity addAmountUsingUpdate(UUID id, Float amount) {
repository.updateAddAmountById(id, amount);
return repository.findById(id).orElseThrow(WalletNotFoundError::new);
}

public Float amount(UUID id) {
return repository.findById(id).orElseThrow(WalletNotFoundError::new).getAmount();
}
}
5 changes: 4 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ spring:
username: ${POSTGRES_NAME:postgres}
password: ${POSTGRES_PASSWORD:123}
liquibase:
enabled: true
enabled: true

jpa:
open-in-view: false
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ record WalletRequest(UUID valletId, String operationType, Float amount){}
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andDo(print())
.andExpect(status().isUnsupportedMediaType());
.andExpect(status().isBadRequest());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.bifurcated.wallet.controller;

import com.bifurcated.wallet.data.Wallet;
import com.bifurcated.wallet.data.WalletEntity;
import com.bifurcated.wallet.repository.WalletEntityRepository;
import com.bifurcated.wallet.repository.WalletRepo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class WalletEntityControllerTest {
private static final Logger logger = LoggerFactory.getLogger(WalletEntityControllerTest.class);
private static final String END_POINT_PATH = "/api/v2";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private WalletEntityRepository repository;

@BeforeEach
public void setup() {
repository.deleteAll();
WalletEntity wallet1 = new WalletEntity();
wallet1.setId(UUID.fromString("b3919077-79e6-4570-bfe0-980ef18f3731"));
wallet1.setAmount(2000F);
repository.save(wallet1);
WalletEntity wallet2 = new WalletEntity();
wallet2.setId(UUID.fromString("bc10fad7-94f1-4047-be4e-311247eed5fb"));
wallet2.setAmount(500F);
repository.save(wallet2);
WalletEntity wallet3 = new WalletEntity();
wallet3.setId(UUID.fromString("9ebef4de-68e3-43ad-a812-42193919ff02"));
wallet3.setAmount(302000F);
repository.save(wallet3);
}
@AfterEach
public void clear() {
repository.deleteAll();
}

@Test
public void testWalletGetAmount() throws Exception {
record BalanceResponse(Float amount){}
BalanceResponse balanceResponse = new BalanceResponse(2000F);
String response = objectMapper.writeValueAsString(balanceResponse);
String id = "b3919077-79e6-4570-bfe0-980ef18f3731";
this.mockMvc.perform(get(END_POINT_PATH+"/wallets/"+id))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().json(response));
}

@Test
public void testWalletGetAmountNotFound() throws Exception {
String id = "b3919077-79e6-4570-bfe0-980ef18f3700";
this.mockMvc.perform(get(END_POINT_PATH+"/wallets/"+id))
.andDo(print())
.andExpect(status().isNotFound());
}

@Test
public void testWalletDepositDDOSAttack() throws Exception {
record WalletRequest(UUID valletId, String operationType, Float amount){}
record BalanceResponse(Float amount){}
UUID id = UUID.fromString("b3919077-79e6-4570-bfe0-980ef18f3731");
WalletRequest walletRequest = new WalletRequest(
id, "DEPOSIT", 1000F);

String requestBody = objectMapper.writeValueAsString(walletRequest);

ExecutorService executorService = Executors.newFixedThreadPool(300);
List<Future<ResultActions>> futures = new ArrayList<>();
for (int i = 0; i < 300; i++) {
Callable<ResultActions> callable = () -> {
ResultActions perform = this.mockMvc.perform(post(END_POINT_PATH + "/wallet")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody));
return perform.andExpect(status().isOk());
};

Future<ResultActions> future = executorService.submit(callable);
futures.add(future);
}
for (Future<ResultActions> future : futures) {
future.get();
}

BalanceResponse balanceResponse = new BalanceResponse(302000F);
String response = objectMapper.writeValueAsString(balanceResponse);
this.mockMvc.perform(get(END_POINT_PATH+"/wallets/"+id))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().json(response));
}

}
5 changes: 4 additions & 1 deletion src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ spring:
connectionTimeout: 300000
liquibase:
enabled: true
contexts: test
contexts: test

jpa:
open-in-view: false

0 comments on commit 03bddaa

Please sign in to comment.