diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
new file mode 100644
index 0000000..808be56
--- /dev/null
+++ b/.mvn/extensions.xml
@@ -0,0 +1,9 @@
+
+
+
+ io.takari.polyglot
+ polyglot-yaml
+ 0.3.2
+
+
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..0ff3e10
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: java
+jdk: openjdk11
+script: mvn clean verify
\ No newline at end of file
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000..c30ee04
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: java $JAVA_OPTS -p ./target/parallel-java-1.0.jar:./target/dependency -m nl.jqno.paralleljava/nl.jqno.paralleljava.Main
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7a6b9de
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+# Parallel Java
+
+[](https://travis-ci.org/jqno/paralleljava)
+
+This app is written in Java from a Parallel Universe where annotations were never invented. You can check: there isn't a single annotation in this codebase!
+
+It requires a JDK 11 to build and run.
+
+It's a showcase for my talk, Java from a Parallel Universe. It's also a fully functioning [Todo Backend](https://www.todobackend.com/) (you can [run the Todo Backend test suite](https://www.todobackend.com/specs/index.html?https://parallel-java.herokuapp.com/todo
+)!), using the [Spark web framework](http://sparkjava.com/), the [Jdbi database framework](http://jdbi.org/), and **no framework** for dependency injection because you really really don't need one. It also uses Java 11 `var` declarations, [Vavr](http://www.vavr.io/) and [Polyglot for Maven](https://github.com/takari/polyglot-maven) because I think they're pretty nifty and because they make the code look a little different, as if, I dunno, as if it came from a Parallel Universe or something? Also, the application is fully modularized and has 100% test coverage because why not.
+
+Note, however, that this is still a demo app that is not production-ready. Some corners have definitely been cut. For example, the [InMemoryRepository](https://github.com/jqno/paralleljava/blob/master/src/main/java/nl/jqno/paralleljava/app/persistence/inmemory/InMemoryRepository.java) is not thread-safe.
+
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 0000000..d9c3a49
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.yml b/pom.yml
new file mode 100644
index 0000000..e5c7103
--- /dev/null
+++ b/pom.yml
@@ -0,0 +1,112 @@
+modelVersion: 4.0.0
+groupId: nl.jqno.paralleljava
+artifactId: parallel-java
+version: 1.0
+packaging: jar
+
+name: parallel-java
+description: "Todo-Backend built with Java from a Parallel Universe: a demo project to illustrate the point of my talk 'Java from a Parallel Universe'"
+
+properties: {
+ encoding: utf-8,
+ maven.compiler.source: 11,
+ maven.compiler.target: 11,
+ coverage.threshold: 1.0
+}
+
+repositories:
+ - { id: bintray-jqno-picotest-repo, url: "https://dl.bintray.com/jqno/picotest-repo" }
+
+dependencyManagement:
+ dependencies:
+ - { groupId: org.jdbi, artifactId: jdbi3-bom, version: 3.6.0, type: pom, scope: import }
+
+dependencies:
+ - { groupId: io.vavr, artifactId: vavr, version: 0.10.0 }
+ - { groupId: io.vavr, artifactId: vavr-gson, version: 0.10.0 }
+ - { groupId: com.sparkjava, artifactId: spark-core, version: 2.7.2 }
+ - { groupId: org.slf4j, artifactId: slf4j-api, version: 1.7.26 }
+ - { groupId: org.slf4j, artifactId: slf4j-simple, version: 1.7.26 }
+ - { groupId: com.google.code.gson, artifactId: gson, version: 2.8.5 }
+ - { groupId: org.jdbi, artifactId: jdbi3-core }
+ - { groupId: org.jdbi, artifactId: jdbi3-vavr }
+ - { groupId: org.postgresql, artifactId: postgresql, version: 42.2.5 }
+ - { groupId: com.h2database, artifactId: h2, version: 1.4.199 }
+
+ - { groupId: nl.jqno.picotest, artifactId: picotest, version: 0.3, scope: test }
+ - { groupId: nl.jqno.equalsverifier, artifactId: equalsverifier, version: 3.1.7, scope: test }
+ - { groupId: org.assertj, artifactId: assertj-core, version: 3.11.1, scope: test }
+ - { groupId: org.assertj, artifactId: assertj-vavr, version: 0.1.0, scope: test }
+ - { groupId: com.tngtech.archunit, artifactId: archunit, version: 0.9.3, scope: test }
+
+ # REST-Assured is useful but problematic on Java 11. We need to overrule Groovy and exclude JAXB-OSGI.
+ - { groupId: org.codehaus.groovy, artifactId: groovy, version: 2.5.6, scope: test }
+ - { groupId: org.codehaus.groovy, artifactId: groovy-xml, version: 2.5.6, scope: test }
+ - groupId: io.rest-assured
+ artifactId: rest-assured
+ version: 3.3.0
+ scope: test
+ exclusions:
+ - groupId: com.sun.xml.bind
+ artifactId: jaxb-osgi
+
+build:
+ plugins:
+ - groupId: org.apache.maven.plugins
+ artifactId: maven-compiler-plugin
+ version: 3.8.0
+
+ - groupId: org.apache.maven.plugins
+ artifactId: maven-surefire-plugin
+ version: 2.22.1
+ configuration:
+ argLine: "@{argLine} --add-opens nl.jqno.paralleljava/nl.jqno.paralleljava.app.domain=ALL-UNNAMED"
+
+ - groupId: org.apache.maven.plugins
+ artifactId: maven-dependency-plugin
+ version: 3.1.1
+ configuration:
+ includeScope: runtime
+ executions:
+ - id: default
+ phase: package
+ goals: [copy-dependencies]
+
+ - groupId: org.apache.maven.plugins
+ artifactId: maven-checkstyle-plugin
+ version: 3.0.0
+ dependencies:
+ - { groupId: com.puppycrawl.tools, artifactId: checkstyle, version: 8.18 }
+ configuration:
+ configLocation: checkstyle.xml
+ includeTestSourceDirectory: true
+ encoding: UTF-8
+ consoleOutput: true
+ excludes: "**/module-info.java"
+ executions:
+ - id: default
+ phase: verify
+ goals: [check]
+ configuration:
+ failsOnError: true
+
+ - groupId: org.jacoco
+ artifactId: jacoco-maven-plugin
+ version: 0.8.3
+ configuration:
+ excludes:
+ - "nl/jqno/paralleljava/Main.class"
+ executions:
+ - id: default-prepare-angent
+ goals: [prepare-agent]
+ - id: default-report
+ goals: [report]
+ - id: default-check
+ goals: [check]
+ configuration:
+ rules:
+ - element: BUNDLE
+ limits:
+ - counter: INSTRUCTION
+ value: COVEREDRATIO
+ minimum: ${coverage.threshold}
diff --git a/scripts/deploy b/scripts/deploy
new file mode 100755
index 0000000..a6cb42b
--- /dev/null
+++ b/scripts/deploy
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+git push -f heroku master
diff --git a/scripts/jacoco b/scripts/jacoco
new file mode 100755
index 0000000..ab43460
--- /dev/null
+++ b/scripts/jacoco
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+mvn clean verify
+mvn jacoco:report
+open target/site/jacoco/index.html
+
diff --git a/scripts/logs b/scripts/logs
new file mode 100755
index 0000000..b00a67a
--- /dev/null
+++ b/scripts/logs
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+heroku logs --tail --source app
+
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 0000000..af30d35
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +1,14 @@
+module nl.jqno.paralleljava {
+ exports nl.jqno.paralleljava;
+
+ opens nl.jqno.paralleljava.app.domain to gson;
+
+ requires io.vavr;
+ requires io.vavr.gson;
+ requires jdbi3.core;
+ requires jdbi3.vavr;
+ requires java.sql; // required for gson
+ requires gson;
+ requires slf4j.api;
+ requires spark.core;
+}
\ No newline at end of file
diff --git a/src/main/java/nl/jqno/paralleljava/Main.java b/src/main/java/nl/jqno/paralleljava/Main.java
new file mode 100644
index 0000000..e973bb5
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/Main.java
@@ -0,0 +1,41 @@
+package nl.jqno.paralleljava;
+
+import io.vavr.collection.HashMap;
+import nl.jqno.paralleljava.app.Runner;
+import nl.jqno.paralleljava.app.controller.DefaultController;
+import nl.jqno.paralleljava.app.environment.Environment;
+import nl.jqno.paralleljava.app.environment.HerokuEnvironment;
+import nl.jqno.paralleljava.app.logging.LoggerFactory;
+import nl.jqno.paralleljava.app.logging.Slf4jLogger;
+import nl.jqno.paralleljava.app.persistence.RandomIdGenerator;
+import nl.jqno.paralleljava.app.persistence.database.DatabaseRepository;
+import nl.jqno.paralleljava.app.persistence.database.JdbiEngine;
+import nl.jqno.paralleljava.app.persistence.database.TodoMapper;
+import nl.jqno.paralleljava.app.serialization.GsonSerializer;
+import nl.jqno.paralleljava.app.server.SparkServer;
+
+public class Main {
+ public static void main(String... args) {
+ LoggerFactory loggerFactory = c -> new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(c));
+
+ var processBuilder = new ProcessBuilder();
+ var environmentMap = HashMap.ofAll(processBuilder.environment());
+ var environment = new HerokuEnvironment(environmentMap);
+
+ var fullUrl = environment.hostUrl().getOrElse(Environment.DEFAULT_URL) + Environment.ENDPOINT;
+ var port = environment.port().getOrElse(Environment.DEFAULT_PORT);
+ var jdbcUrl = environment.jdbcUrl().getOrElse(Environment.DEFAULT_JDBC_URL);
+
+ var todoMapper = new TodoMapper(fullUrl);
+ var dbEngine = new JdbiEngine(jdbcUrl, todoMapper, loggerFactory);
+ var repository = new DatabaseRepository(dbEngine);
+
+ var idGenerator = new RandomIdGenerator();
+ var serializer = GsonSerializer.create(loggerFactory);
+ var controller = new DefaultController(fullUrl, repository, idGenerator, serializer, loggerFactory);
+ var server = new SparkServer(Environment.ENDPOINT, port, controller, loggerFactory);
+
+ var runner = new Runner(repository, server, loggerFactory);
+ runner.startup();
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/Runner.java b/src/main/java/nl/jqno/paralleljava/app/Runner.java
new file mode 100644
index 0000000..46acae7
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/Runner.java
@@ -0,0 +1,36 @@
+package nl.jqno.paralleljava.app;
+
+import nl.jqno.paralleljava.app.logging.Logger;
+import nl.jqno.paralleljava.app.logging.LoggerFactory;
+import nl.jqno.paralleljava.app.persistence.Repository;
+import nl.jqno.paralleljava.app.server.Server;
+
+public class Runner {
+ private final Repository repository;
+ private final Server server;
+ private final Logger logger;
+
+ public Runner(Repository repository, Server server, LoggerFactory loggerFactory) {
+ this.repository = repository;
+ this.server = server;
+ this.logger = loggerFactory.create(getClass());
+ }
+
+ public void startup() {
+ repository.initialize()
+ .onSuccess(ignored -> {
+ printBanner();
+ server.run();
+ });
+ }
+
+ private void printBanner() {
+ logger.forProduction(" _ _ _____ _ _ __ _ _");
+ logger.forProduction("| \\ | | ___ | ___| __ __ _ _ __ ___ _____ _____ _ __| | _| |\\ \\ \\ \\");
+ logger.forProduction("| \\| |/ _ \\ | |_ | '__/ _` | '_ ` _ \\ / _ \\ \\ /\\ / / _ \\| '__| |/ / | \\ \\ \\ \\");
+ logger.forProduction("| |\\ | (_) | | _|| | | (_| | | | | | | __/\\ V V / (_) | | | <|_| ) ) ) )");
+ logger.forProduction("|_| \\_|\\___/ |_| |_| \\__,_|_| |_| |_|\\___| \\_/\\_/ \\___/|_| |_|\\_(_) / / / /");
+ logger.forProduction("=======================================================================/_/_/_/");
+ logger.forProduction(" :: Built with Plain Java! :: \uD83C\uDF89");
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/controller/Controller.java b/src/main/java/nl/jqno/paralleljava/app/controller/Controller.java
new file mode 100644
index 0000000..18cb989
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/controller/Controller.java
@@ -0,0 +1,12 @@
+package nl.jqno.paralleljava.app.controller;
+
+import io.vavr.control.Try;
+
+public interface Controller {
+ Try get();
+ Try get(String id);
+ Try post(String json);
+ Try patch(String id, String json);
+ Try delete();
+ Try delete(String id);
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/controller/DefaultController.java b/src/main/java/nl/jqno/paralleljava/app/controller/DefaultController.java
new file mode 100644
index 0000000..4a2199b
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/controller/DefaultController.java
@@ -0,0 +1,97 @@
+package nl.jqno.paralleljava.app.controller;
+
+import io.vavr.Function1;
+import io.vavr.control.Try;
+import nl.jqno.paralleljava.app.domain.Todo;
+import nl.jqno.paralleljava.app.logging.Logger;
+import nl.jqno.paralleljava.app.logging.LoggerFactory;
+import nl.jqno.paralleljava.app.persistence.IdGenerator;
+import nl.jqno.paralleljava.app.persistence.Repository;
+import nl.jqno.paralleljava.app.serialization.Serializer;
+
+import java.util.UUID;
+
+public class DefaultController implements Controller {
+ private final String url;
+ private final Repository repository;
+ private final IdGenerator idGenerator;
+ private final Serializer serializer;
+ private final Logger logger;
+
+ public DefaultController(String url, Repository repository, IdGenerator idGenerator, Serializer serializer, LoggerFactory loggerFactory) {
+ this.url = url;
+ this.repository = repository;
+ this.idGenerator = idGenerator;
+ this.serializer = serializer;
+ this.logger = loggerFactory.create(getClass());
+ }
+
+ public Try get() {
+ return repository.getAll()
+ .map(serializer::serializeTodos);
+ }
+
+ public Try get(String id) {
+ var uuid = serializer.deserializeUuid(id);
+ if (uuid.isEmpty()) {
+ return Try.failure(new IllegalArgumentException("Invalid GET request: " + id));
+ }
+
+ return repository
+ .get(uuid.get())
+ .flatMap(o -> o.map(serializer::serializeTodo).toTry(() -> new IllegalArgumentException("Cannot find " + id)));
+ }
+
+ public Try post(String json) {
+ logger.forProduction("POSTed: " + json);
+ var partialTodo = serializer.deserializePartialTodo(json);
+ if (partialTodo.isEmpty() || partialTodo.get().title().isEmpty()) {
+ return Try.failure(new IllegalArgumentException("Invalid POST request: " + json));
+ }
+
+ var pt = partialTodo.get();
+ var id = idGenerator.generateId();
+ var todo = new Todo(id, pt.title().get(), buildUrlFor(id), false, pt.order().getOrElse(0));
+ return repository.create(todo)
+ .map(ignored -> serializer.serializeTodo(todo));
+ }
+
+ public Try patch(String id, String json) {
+ logger.forProduction("PATCHed: " + json);
+ var uuid = serializer.deserializeUuid(id);
+ var partialTodo = serializer.deserializePartialTodo(json);
+ if (uuid.isEmpty() || partialTodo.isEmpty()) {
+ return Try.failure(new IllegalArgumentException("Invalid PATCH request: " + id + ", " + json));
+ }
+
+ var pt = partialTodo.get();
+ Function1 updater = todo -> new Todo(
+ todo.id(),
+ pt.title().getOrElse(todo.title()),
+ todo.url(),
+ pt.completed().getOrElse(todo.completed()),
+ pt.order().getOrElse(todo.order())
+ );
+ return repository.update(uuid.get(), updater)
+ .map(serializer::serializeTodo);
+ }
+
+ public Try delete() {
+ return repository.deleteAll()
+ .map(ignored -> "");
+ }
+
+ public Try delete(String id) {
+ var uuid = serializer.deserializeUuid(id);
+ if (uuid.isEmpty()) {
+ return Try.failure(new IllegalArgumentException("Invalid DELETE request: " + id));
+ }
+
+ return repository.delete(uuid.get())
+ .map(ignored -> "");
+ }
+
+ private String buildUrlFor(UUID id) {
+ return url + "/" + id.toString();
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/domain/PartialTodo.java b/src/main/java/nl/jqno/paralleljava/app/domain/PartialTodo.java
new file mode 100644
index 0000000..44079b4
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/domain/PartialTodo.java
@@ -0,0 +1,70 @@
+package nl.jqno.paralleljava.app.domain;
+
+import io.vavr.control.Option;
+
+import java.util.Objects;
+import java.util.UUID;
+
+public final class PartialTodo {
+ private final UUID id;
+ private final String title;
+ private final String url;
+ private final Boolean completed;
+ private final Integer order;
+
+ public PartialTodo() {
+ this.id = null;
+ this.title = null;
+ this.url = null;
+ this.completed = null;
+ this.order = null;
+ }
+
+ public PartialTodo(Option id, Option title, Option url, Option completed, Option order) {
+ this.id = id.getOrNull();
+ this.title = title.getOrNull();
+ this.url = url.getOrNull();
+ this.completed = completed.getOrNull();
+ this.order = order.getOrNull();
+ }
+
+ public Option id() {
+ return Option.of(id);
+ }
+
+ public Option title() {
+ return Option.of(title);
+ }
+
+ public Option url() {
+ return Option.of(url);
+ }
+
+ public Option completed() {
+ return Option.of(completed);
+ }
+
+ public Option order() {
+ return Option.of(order);
+ }
+
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PartialTodo)) {
+ return false;
+ }
+ PartialTodo other = (PartialTodo)obj;
+ return Objects.equals(id, other.id) &&
+ Objects.equals(title, other.title) &&
+ Objects.equals(url, other.url) &&
+ Objects.equals(completed, other.completed) &&
+ Objects.equals(order, other.order);
+ }
+
+ public int hashCode() {
+ return Objects.hash(id, title, url, completed, order);
+ }
+
+ public String toString() {
+ return "PartialTodo: [id=" + id() + ", title=" + title() + ", url=" + url() + ", completed=" + completed() + ", order=" + order() + "]";
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/domain/Todo.java b/src/main/java/nl/jqno/paralleljava/app/domain/Todo.java
new file mode 100644
index 0000000..9701da0
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/domain/Todo.java
@@ -0,0 +1,76 @@
+package nl.jqno.paralleljava.app.domain;
+
+import java.util.Objects;
+import java.util.UUID;
+
+public final class Todo {
+ private final UUID id;
+ private final String title;
+ private final String url;
+ private final boolean completed;
+ private final int order;
+
+ public Todo(UUID id, String title, String url, boolean completed, int order) {
+ this.id = id;
+ this.title = title;
+ this.url = url;
+ this.completed = completed;
+ this.order = order;
+ }
+
+ public UUID id() {
+ return id;
+ }
+
+ public String title() {
+ return title;
+ }
+
+ public String url() {
+ return url;
+ }
+
+ public boolean completed() {
+ return completed;
+ }
+
+ public int order() {
+ return order;
+ }
+
+ public Todo withId(UUID id) {
+ return new Todo(id, title(), url(), completed(), order());
+ }
+
+ public Todo withTitle(String title) {
+ return new Todo(id(), title, url(), completed(), order());
+ }
+
+ public Todo withCompleted(boolean completed) {
+ return new Todo(id(), title(), url(), completed, order());
+ }
+
+ public Todo withOrder(int order) {
+ return new Todo(id(), title(), url(), completed(), order);
+ }
+
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Todo)) {
+ return false;
+ }
+ Todo other = (Todo)obj;
+ return Objects.equals(id, other.id) &&
+ Objects.equals(title, other.title) &&
+ Objects.equals(url, other.url) &&
+ completed == other.completed &&
+ order == other.order;
+ }
+
+ public int hashCode() {
+ return Objects.hash(id, title, url, completed, order);
+ }
+
+ public String toString() {
+ return "Todo: [id=" + id + ", title=" + title + ", url=" + url + ", completed=" + completed + ", order=" + order + "]";
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/environment/Environment.java b/src/main/java/nl/jqno/paralleljava/app/environment/Environment.java
new file mode 100644
index 0000000..b9105ae
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/environment/Environment.java
@@ -0,0 +1,16 @@
+package nl.jqno.paralleljava.app.environment;
+
+import io.vavr.control.Option;
+
+public interface Environment {
+
+ int DEFAULT_PORT = 4567;
+ String DEFAULT_URL = "http://localhost";
+ String DEFAULT_JDBC_URL = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1";
+
+ String ENDPOINT = "/todo";
+
+ Option port();
+ Option hostUrl();
+ Option jdbcUrl();
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/environment/HerokuEnvironment.java b/src/main/java/nl/jqno/paralleljava/app/environment/HerokuEnvironment.java
new file mode 100644
index 0000000..12dadbc
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/environment/HerokuEnvironment.java
@@ -0,0 +1,31 @@
+package nl.jqno.paralleljava.app.environment;
+
+import io.vavr.collection.Map;
+import io.vavr.control.Option;
+import io.vavr.control.Try;
+
+public class HerokuEnvironment implements Environment {
+
+ private final Map env;
+
+ public HerokuEnvironment(Map env) {
+ this.env = env;
+ }
+
+ public Option port() {
+ return env.get("PORT").flatMap(this::parse);
+ }
+
+ public Option hostUrl() {
+ // hard-coded for now
+ return Option.some("https://parallel-java.herokuapp.com");
+ }
+
+ public Option jdbcUrl() {
+ return env.get("JDBC_DATABASE_URL");
+ }
+
+ private Option parse(String port) {
+ return Try.of(() -> Integer.parseInt(port)).toOption();
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/logging/Logger.java b/src/main/java/nl/jqno/paralleljava/app/logging/Logger.java
new file mode 100644
index 0000000..5fb859c
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/logging/Logger.java
@@ -0,0 +1,10 @@
+package nl.jqno.paralleljava.app.logging;
+
+public interface Logger {
+ void forDevelopment(String message);
+ void forProduction(String message);
+ void firstThingNextMorning(String message);
+ void firstThingNextMorning(String message, Throwable t);
+ void wakeMeUp(String message);
+ void wakeMeUp(String message, Throwable t);
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/logging/LoggerFactory.java b/src/main/java/nl/jqno/paralleljava/app/logging/LoggerFactory.java
new file mode 100644
index 0000000..4a6a624
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/logging/LoggerFactory.java
@@ -0,0 +1,9 @@
+package nl.jqno.paralleljava.app.logging;
+
+import io.vavr.Function1;
+
+public interface LoggerFactory extends Function1, Logger> {
+ default Logger create(Class> c) {
+ return apply(c);
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/logging/Slf4jLogger.java b/src/main/java/nl/jqno/paralleljava/app/logging/Slf4jLogger.java
new file mode 100644
index 0000000..8c176bd
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/logging/Slf4jLogger.java
@@ -0,0 +1,33 @@
+package nl.jqno.paralleljava.app.logging;
+
+public class Slf4jLogger implements Logger {
+ private final org.slf4j.Logger logger;
+
+ public Slf4jLogger(org.slf4j.Logger logger) {
+ this.logger = logger;
+ }
+
+ public void forDevelopment(String message) {
+ logger.debug(message);
+ }
+
+ public void forProduction(String message) {
+ logger.info(message);
+ }
+
+ public void firstThingNextMorning(String message) {
+ logger.warn(message);
+ }
+
+ public void firstThingNextMorning(String message, Throwable t) {
+ logger.warn(message, t);
+ }
+
+ public void wakeMeUp(String message) {
+ logger.error(message);
+ }
+
+ public void wakeMeUp(String message, Throwable t) {
+ logger.error(message, t);
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/persistence/IdGenerator.java b/src/main/java/nl/jqno/paralleljava/app/persistence/IdGenerator.java
new file mode 100644
index 0000000..db32641
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/persistence/IdGenerator.java
@@ -0,0 +1,7 @@
+package nl.jqno.paralleljava.app.persistence;
+
+import java.util.UUID;
+
+public interface IdGenerator {
+ UUID generateId();
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/persistence/RandomIdGenerator.java b/src/main/java/nl/jqno/paralleljava/app/persistence/RandomIdGenerator.java
new file mode 100644
index 0000000..36be1c2
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/persistence/RandomIdGenerator.java
@@ -0,0 +1,9 @@
+package nl.jqno.paralleljava.app.persistence;
+
+import java.util.UUID;
+
+public class RandomIdGenerator implements IdGenerator {
+ public UUID generateId() {
+ return UUID.randomUUID();
+ }
+}
diff --git a/src/main/java/nl/jqno/paralleljava/app/persistence/Repository.java b/src/main/java/nl/jqno/paralleljava/app/persistence/Repository.java
new file mode 100644
index 0000000..3ba16ce
--- /dev/null
+++ b/src/main/java/nl/jqno/paralleljava/app/persistence/Repository.java
@@ -0,0 +1,20 @@
+package nl.jqno.paralleljava.app.persistence;
+
+import io.vavr.Function1;
+import io.vavr.collection.List;
+import io.vavr.control.Option;
+import io.vavr.control.Try;
+import nl.jqno.paralleljava.app.domain.Todo;
+
+import java.util.UUID;
+
+public interface Repository {
+ Try initialize();
+ Try create(Todo todo);
+ Try