Commit f62ed388 authored by Unknown's avatar Unknown

init

parent 6b8c963f
Pipeline #11521 failed with stage
stages:
- build
build:
stage: build
image: maven:3-jdk-8-alpine
script:
- echo "hello world"
# moneytracker
# Money Tracker
The Money Tracker is an application to track your monthly expenses. It provides a true RESTful HTTP API, which is documented using Spring Rest Docs.
Visit `http://localhost:8080` to use the application.
Visit `http://localhost:8080/docs/api-guide.html` to view the documentation.
## Build and Run
```
mvn clean package -P tomcat
java -jar target/*.war
```
## Build and Run at WildFly
```
mvn clean package
```
\ No newline at end of file
This is my file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<groupId>me.stritzke</groupId>
<artifactId>moneytracker</artifactId>
<version>0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
</parent>
<properties>
<snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.8</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4.1209</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<version>1.0.2.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- To build an executable war use one of the profiles below -->
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>tomcat</id>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Documentation.java</include>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.2</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
<attributes>
<snippets>${project.build.directory}/generated-snippets</snippets>
</attributes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.outputDirectory}/static/docs
</outputDirectory>
<resources>
<resource>
<directory>
${project.build.directory}/generated-docs
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
= Money Tracker API Guide
The Money Tracker API is implemented as a true RESTful HTTP API, so hypermedia is a thing here!
== API Root
include::{snippets}/root/http-request.adoc[]
=== Response
include::{snippets}/root/http-response.adoc[]
=== Links
include::{snippets}/root/links.adoc[]
== Expenses
An expense is a single purchase, which is spent in a particular month.
=== Index
include::{snippets}/expenses/index/http-request.adoc[]
==== Response
include::{snippets}/expenses/index/http-response.adoc[]
==== Links
include::{snippets}/expenses/index/links.adoc[]
=== List Expenses
include::{snippets}/expenses/get/http-request.adoc[]
include::{snippets}/expenses/get/path-parameters.adoc[]
==== Response
include::{snippets}/expenses/get/http-response.adoc[]
include::{snippets}/expenses/get/response-fields.adoc[]
include::{snippets}/expenses/get/links.adoc[]
=== Create an Expense
An expense can be created at the expense root, which will add the expense to the current month.
include::{snippets}/expenses/creation_atRoot/http-request.adoc[]
==== Response
Looking at the Location header you are able to see that the expense was created in the month and year, in which this version of the software was released.
include::{snippets}/expenses/creation_atRoot/http-response.adoc[]
==== Response Headers
include::{snippets}/expenses/creation_atRoot/response-headers.adoc[]
=== Create an Expense in a specific Month
An expense can also be created in a specific month.
include::{snippets}/expenses/creation/http-request.adoc[]
include::{snippets}/expenses/creation/path-parameters.adoc[]
==== Response
include::{snippets}/expenses/creation/http-response.adoc[]
==== Response Headers
include::{snippets}/expenses/creation/response-headers.adoc[]
== Backup & Restore
Backup und restore operations can be triggered through the API. While backups can be created whenever you want, there are some preconditions, which need to be met, if you want to restore application data.
=== Create a Backup
include::{snippets}/backup/create/http-request.adoc[]
==== Response
include::{snippets}/backup/create/http-response.adoc[]
include::{snippets}/backup/create/response-fields.adoc[]
=== Restore a Backup
If the applications contains no data, a backup can be restored directly through the restore endpoint.
include::{snippets}/backup/restore/http-request.adoc[]
==== Response
include::{snippets}/backup/restore/http-response.adoc[]
If the application already contains expenses, the API response is as follows, with the same request as above.
include::{snippets}/backup/restore_dataExisting/http-response.adoc[]
If you want to ignore the warning, you can use query parameters as follows to do so. The response will be just like above.
include::{snippets}/backup/restore_force/http-request.adoc[]
include::{snippets}/backup/restore_force/request-parameters.adoc[]
\ No newline at end of file
package me.stritzke.moneytracker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
package me.stritzke.moneytracker;
import org.springframework.data.rest.webmvc.RepositoryLinksResource;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.stereotype.Component;
@Component
public class DocumentationRootLink implements ResourceProcessor<RepositoryLinksResource> {
@Override
public RepositoryLinksResource process(RepositoryLinksResource resource) {
resource.add(new Link("/doc/api-guide.html").withRel("doc"));
return resource;
}
}
package me.stritzke.moneytracker;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
import org.springframework.stereotype.Component;
@Component
public class SpringDataRestConfiguration extends RepositoryRestConfigurerAdapter {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration configuration) {
configuration.setBasePath("/api");
}
}
package me.stritzke.moneytracker.backup;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collection;
@Data
@NoArgsConstructor
@AllArgsConstructor
class Backup {
private final int version = 1;
Collection<ExpenseBackupDTO> expenses;
}
package me.stritzke.moneytracker.backup;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
class BackupEndpoint {
private final BackupService backupService;
@RequestMapping(method = RequestMethod.GET, value = "backup")
public ResponseEntity<?> doBackup() {
Backup backup = backupService.create();
return ResponseEntity.ok(backup);
}
@RequestMapping(method = RequestMethod.POST, value = "restore")
public ResponseEntity<?> doRestore(@RequestBody Backup backup,
@RequestParam(value = "force", required = false) boolean useForce) {
backupService.restore(backup, useForce);
return ResponseEntity.ok().build();
}
}
package me.stritzke.moneytracker.backup;
import org.springframework.data.rest.webmvc.RepositoryLinksResource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.stereotype.Component;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
@Component
class BackupRootLinks implements ResourceProcessor<RepositoryLinksResource> {
@Override
public RepositoryLinksResource process(RepositoryLinksResource resource) {
resource.add(linkTo(methodOn(BackupEndpoint.class).doBackup()).withRel("backup"));
resource.add(linkTo(methodOn(BackupEndpoint.class).doRestore(null, true)).withRel("restore"));
return resource;
}
}
package me.stritzke.moneytracker.backup;
import lombok.RequiredArgsConstructor;
import me.stritzke.moneytracker.expenses.ExpenseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
class BackupService {
private final ExpenseService expenseService;
Backup create() {
Collection<ExpenseBackupDTO> expenseBackupDTOs = expenseService.findAll().stream()
.map(ExpenseBackupDTO::new)
.collect(Collectors.toList());
return new Backup(expenseBackupDTOs);
}
void restore(Backup backup, boolean ignoreDataExistence) throws DataExistingWarning {
if (applicationContainsData() && !ignoreDataExistence) {
throw new DataExistingWarning();
}
backup.getExpenses().forEach(expense -> {
expenseService.save(expense.getAmount(), expense.getComment(), expense.getYear(), expense.getMonth());
});
}
private boolean applicationContainsData() {
return expenseService.findAll().size() > 0;
}
}
package me.stritzke.moneytracker.backup;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.CONFLICT, reason = "The application already contains expenses. Override by adding ?force=true")
class DataExistingWarning extends RuntimeException {
}
package me.stritzke.moneytracker.backup;
import lombok.Data;
import lombok.NoArgsConstructor;
import me.stritzke.moneytracker.expenses.Expense;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
class ExpenseBackupDTO {
ExpenseBackupDTO(Expense expense) {
this.amount = expense.getAmount();
this.comment = expense.getComment();
this.year = expense.getYear();
this.month = expense.getMonth();
}
private BigDecimal amount;
private String comment;
private int year;
private int month;
}
package me.stritzke.moneytracker.expenses;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import java.util.Calendar;
import java.util.Date;
@Data
@RequiredArgsConstructor
class DateWrapper {
private final Integer year;
private final Integer month;
DateWrapper() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
this.year = calendar.get(Calendar.YEAR);
this.month = calendar.get(Calendar.MONTH) + 1;
}
DateWrapper getPreviousMonth() {
if (getMonth() == 1) {
return new DateWrapper(getYear() - 1, 12);
} else {
return new DateWrapper(getYear(), getMonth() - 1);
}
}
DateWrapper getNextMonth() {
if (getMonth() == 12) {
return new DateWrapper(getYear() + 1, 1);
} else {
return new DateWrapper(getYear(), getMonth() + 1);
}
}
}
package me.stritzke.moneytracker.expenses;
import lombok.Data;
import org.springframework.hateoas.ResourceSupport;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.math.BigDecimal;
@Data
@Entity
public class Expense extends ResourceSupport {
@Id
@GeneratedValue
private long numericId;
private BigDecimal amount;
private String comment;
private int year;
private int month;
}
\ No newline at end of file
package me.stritzke.moneytracker.expenses;
import lombok.Data;
import java.math.BigDecimal;
import java.math.RoundingMode;
@Data
class ExpenseCreationDTO {
private BigDecimal amount;
private String comment;
void setAmount(BigDecimal amount) {
this.amount = amount.setScale(2, RoundingMode.CEILING);
}
}
package me.stritzke.moneytracker.expenses;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.RepositoryLinksResource;
import org.springframework.hateoas.*;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
@RestController
@RequestMapping("/api/expenses")
@ExposesResourceFor(Expense.class)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
class ExpenseEndpoint implements ResourceProcessor<RepositoryLinksResource> {
private final ExpenseService expenseService;
@RequestMapping(method = RequestMethod.GET)
public ResourceSupport getExpenseRoot() {
ResourceSupport expenseIndex = new ResourceSupport();
expenseIndex.add(getLinkToExpenses("current", new DateWrapper()));
return expenseIndex;
}
private Link getLinkToExpenses(String rel, DateWrapper date) {
return ControllerLinkBuilder.linkTo(methodOn(ExpenseEndpoint.class).getExpensesOfMonth(date.getYear(), date.getMonth())).withRel(rel);
}
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> addExpense(@RequestBody ExpenseCreationDTO expenseCreationDTO) {
DateWrapper date = new DateWrapper();
Expense expense = expenseService.save(expenseCreationDTO.getAmount(), expenseCreationDTO.getComment(), date.getYear(), date.getMonth());
URI location = ControllerLinkBuilder.linkTo(methodOn(ExpenseEndpoint.class).getExpense(date.getYear(), date.getMonth(), expense.getNumericId())).toUri();
return ResponseEntity.created(location).build();
}
@RequestMapping(method = RequestMethod.GET, value = "/{year}/{month}")
public ResponseEntity<Resources<Expense>> getExpensesOfMonth(@PathVariable("year") Integer year, @PathVariable("month") Integer month) {
Collection<Expense> byYearAndMonth = expenseService.find(new DateWrapper(year, month));
byYearAndMonth.forEach(this::addSelfLink);
Resources<Expense> expenseResource = new Resources<>(byYearAndMonth);
DateWrapper date = new DateWrapper(year, month);
expenseResource.add(getLinkToExpenses("self", date));
expenseResource.add(getLinkToExpenses("previous", date.getPreviousMonth()));
expenseResource.add(getLinkToExpenses("next", date.getNextMonth()));
return ResponseEntity.ok(expenseResource);
}
private void addSelfLink(Expense expense) {
expense.add(linkTo(methodOn(ExpenseEndpoint.class)
.getExpense(expense.getYear(), expense.getMonth(), expense.getNumericId()))
.withSelfRel());
}
@RequestMapping(method = RequestMethod.POST, value = "/{year}/{month}")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<?> addExpenseInMonth(@PathVariable("year") Integer year,
@PathVariable("month") Integer month,
@RequestBody ExpenseCreationDTO expenseCreationDTO) throws URISyntaxException {
Expense expense = expenseService.save(expenseCreationDTO.getAmount(), expenseCreationDTO.getComment(), year, month);
URI location = ControllerLinkBuilder.linkTo(methodOn(ExpenseEndpoint.class).getExpense(year, month, expense.getNumericId())).toUri();
return ResponseEntity.created(location).build();
}
@RequestMapping(method = RequestMethod.GET, value = "/{year}/{month}/{expenseId}")
public ResponseEntity<?> getExpense(@PathVariable("year") Integer year,
@PathVariable("month") Integer month,
@PathVariable("expenseId") Long expenseId) {
Expense expense = expenseService.findOne(expenseId);
if (expense == null) {
return ResponseEntity.notFound().build();
} else {
return ResponseEntity.ok(expense);
}
}
@RequestMapping(method = RequestMethod.DELETE, value = "/{year}/{month}/{expenseId}")
public ResponseEntity<?> deleteExpense(@PathVariable("year") Integer year,
@PathVariable("month") Integer month,
@PathVariable("expenseId") Long expenseId) {
expenseService.delete(expenseId);
return ResponseEntity.noContent().build();