The objective of the project is to create API-REST services with Spring Boot
The java version used in this project is at least version 8
- [Download and setup] (https://www.java.com/es/download/help/download_options.xml)
Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project's build, reporting and documentation from a central piece of information.
- [Download and setup] (https://maven.apache.org/download.cgi)
The project uses Lombok. Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.
The project was carried out with the eclipse IDE, so it was necessary to install Lombok. The download and installation can be obtained from this link:
- Spring Boot 2.1.4.RELEASE - Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run"
- Lombok - Project Lombok is a small library that can be used to reduce the amount of boilerplate Java code that is commonly written for Java classes
- H2 Database - H2 database can be configured to run as inmemory database, which means that data will not persist on the disk
- Swagger 2 - Using Swagger makes documenting your RESTful services much easier
- Maven - Maven is a software tool for project management and construction
- Java 8 - Java is an object-oriented programming language
We decided to use H2 as a database in memory due to its characteristics such as its integration with Spring Boot. In order to configure H2 database correctly, the following properties had to be added in the application.properties configuration file:
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
We configure swagger by adding a configuration class, here the example:
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.orangebank.codechallenge")).paths(PathSelectors.any())
.build();
}
}At the root of the project:
mvn clean install
This command attempts to clean the files and directories generated by Maven during its build. The Install Plugin is used during the install phase to add artifact(s) to the local repository. Before the construction and generation of the project all the tests will be executed and the jar will only be generated if these tests are executed successfully.
You can run your application using java -jar, as shown in the following example:
$ java -jar target/orangebank-codechallenge-0.0.1-SNAPSHOT.jar
The Spring Boot Maven plugin includes a run goal that can be used to quickly compile and run the application. The following example shows a typical Maven command to run a Spring Boot application:
$ mvn spring-boot:run
The application runs over port number 9000:
Postman is a HTTP client that allows us to test our endpoints.
Some requests are added so that they can be imported and executed in Postman.
Here an example:
curl -X POST \
http://localhost:9000/api/transactions \
-H 'Accept: */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Content-Length: 221' \
-H 'Content-Type: application/json' \
-H 'Cookie: JSESSIONID=6A7916BF24330FDA19E815FC39D35E99; JSESSIONID.091d8ed4=node018k7jagmg4wap1l4duvpxmc21n0.node0' \
-H 'Host: localhost:9000' \
-H 'Postman-Token: fd534148-de64-4111-bcaa-4df326f8e406,048ff1a5-6b82-4785-8faf-f3a5905c7b9a' \
-H 'User-Agent: PostmanRuntime/7.15.2' \
-H 'cache-control: no-cache' \
-d '{
"reference": "123456A",
"account_iban": "ES9820385778983000760236",
"date": "2019-07-16T16:55:42.000Z",
"amount": 7643,
"fee": 3.18,
"description": "Restaurant payment",
"channel": "CLIENT"
}'
By default, Spring Boot will get and download the Hibernate Validator automatically. Annotate the bean with javax.validation.constraints.* annotations.
import java.util.Date;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@NoArgsConstructor
@ToString
public class RequestBodyCustom {
private String reference;
@JsonProperty("account_iban")
@NotEmpty(message = "account_iban is mandatory")
private String accountIban;
private Date date;
@NotNull(message = "amount is mandatory")
private Double amount;
private Double fee;
private String description;
private String channel;
private String status;
}Add @Valid to @RequestBody. Bean validation is enabled now
@RestController
@RequestMapping(path = "api", produces = { MediaType.APPLICATION_JSON_VALUE })
@Validated
@Api(value = "This is the controller for the application")
public class TransactionController {
private TransactionCreateService transactionCreateService;
private TransactionSearchService transactionSearchService;
@JsonView(View.Summary.class)
@PostMapping("/transactions")
@ApiOperation(value = "Create a transaction", notes = "If any error occurs, it will be intercepted by an error handler")
public ResponseEntity<ResponseBodyCustom> createTransaction(@RequestBody @Valid RequestBodyCustom request) {
TransactionDTO transactionDTO = requestToDto(request);
TransactionDTO t = transactionCreateService.createTransaction(transactionDTO);
ResponseBodyCustom response = new ResponseBodyCustom();
response.setMessage("Transaction created");
response.setStatus(HttpStatus.OK);
response.setTimestamp(LocalDateTime.now());
List<TransactionDTO> transactions = new ArrayList<>();
transactions.add(t);
response.setTransactions(transactions);
return ResponseEntity.ok().body(response);
}By default, Spring Boot provides a BasicErrorController controller for /error mapping that handles all errors, and getErrorAttributes to produce a JSON response with details of the error, the HTTP status, and the exception message. In Spring Boot, we can use @ControllerAdvice to handle custom exceptions, here the example:
@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
private static final String ERRORS = "errors";
private static final String STATUS = "status";
private static final String TIMESTAMP = "timestamp";
private static final String BAD_REQUEST = "BAD_REQUEST";
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public final ResponseEntity<ErrorResponse> handleConstraintViolation(ConstraintViolationException ex,
WebRequest request) {
List<String> details = ex.getConstraintViolations().stream().map(e -> e.getMessage())
.collect(Collectors.toList());
ErrorResponse error = new ErrorResponse(BAD_REQUEST, details);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}The project has two characteristics of CRUD operations, Read and Create / Update. The service layer could have defined both characteristics but we have decided to separate them and create an interface (and their implementation) for each of them.
Therefore, the structure is as follows:
- com.orangebank.codechallenge.service
- TransactionCreateService.java
- TransactionCreateServiceImpl.java
- TransactionSearchService.java
- TransactionSearchServiceImpl.java
To carry out the business test cases we have decided to use black box tests using Apache HttpClient to send HTTP GET/POST request.
All business cases are in class /orangebank-codechallenge/src/test/java/com/orangenank/codechallenge/acceptance/AcceptanceTest.java, here an example:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = OrangebankCodechallengeApplication.class, webEnvironment = WebEnvironment.DEFINED_PORT)
public class AcceptanceTest {
@LocalServerPort
private Integer port;
/**
* @Given: A transaction that is not stored in our system.
* @When: I check the status from any channel
* @Then: The system returns the status 'INVALID'
*/
@Test
public void givenNotTransactionBD_whenStatusAnyChannel_thenReturnINVALIDStatus() {
try {
String uri = "http://localhost:".concat(port.toString()).concat("/api/transactions/status/XXX111/CLIENT");
HttpGet request = new HttpGet(uri);
HttpClient httpclient = HttpClientBuilder.create().build();
HttpResponse response = httpclient.execute(request);
BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
StringBuilder result = new StringBuilder();
String line = "";
while ((line = rd.readLine()) != null) {
result.append(line);
}
String jsonExpected = "{\"reference\":\"XXX111\",\"status\":\"INVALID\"}";
JSONObject jsonResult = new JSONObject(result.toString());
JSONAssert.assertEquals(jsonExpected, jsonResult, false);
} catch (Exception e) {
fail();
}
}