Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
c89c27b
Adds a build script
jqno Mar 8, 2019
ea1889c
Adds Hello World app
jqno Mar 8, 2019
6879ae4
Enables Heroku deployment
jqno Mar 9, 2019
dbdf7b7
Adds logging
jqno Mar 9, 2019
69776a4
Adds Travis CI script
jqno Mar 10, 2019
ce96cc2
Disallows annotations
jqno Mar 10, 2019
244b081
Modularise this bad boy
jqno Mar 11, 2019
2a3cfe7
Applies architecture
jqno Mar 11, 2019
d04d16c
Applies more architecture
jqno Mar 12, 2019
d28d955
Applies architecture to logger
jqno Mar 12, 2019
25d6292
Adds Architecture tests (in JUnit 3 for now)
jqno Mar 14, 2019
53ab5ed
Adds tests for Heroku and Endpoints
jqno Mar 14, 2019
5cd05af
Adds test for SparkServer
jqno Mar 15, 2019
1d9baf0
Don't deploy test dependencies
jqno Mar 15, 2019
6346823
Switches to PicoTest with AssertJ
jqno Mar 15, 2019
d6a21dc
Enables CORS
jqno Mar 15, 2019
b40b5bf
Sets up before/afterAll for SparkServerTest
jqno Mar 15, 2019
46813ca
Adds JaCoCo to build
jqno Mar 15, 2019
5344445
Adds unit test for CORS
jqno Mar 17, 2019
15226ca
Moves tests to correct package
jqno Mar 18, 2019
f1d86d9
Adds test for Slf4jLogger
jqno Mar 18, 2019
9c0af8b
Makes the build less chatty
jqno Mar 18, 2019
e8dd87a
Extracts Endpoints interface
jqno Mar 18, 2019
a202469
Adds Todo domain class
jqno Mar 19, 2019
0e0eefc
Adds Serializer
jqno Mar 19, 2019
2b3e2c4
Refactors ArchitectureTest
jqno Mar 19, 2019
493515a
Renames hello endpoint to todo
jqno Mar 19, 2019
f73dcb0
Extracts Request to its own class
jqno Mar 19, 2019
b278cc0
Implements POST endpoint
jqno Mar 19, 2019
96edf5a
Small refactoring
jqno Mar 19, 2019
aedd1bb
Adds logs script
jqno Mar 19, 2019
e3a14b7
Renames EndpointsTest to DefaultEndpointsTest
jqno Mar 19, 2019
6ba04d2
Implements DELETE endpoint
jqno Mar 19, 2019
f79bed0
Extracts assertSingleCall method
jqno Mar 19, 2019
23bbdda
Adds toString to Todo
jqno Mar 19, 2019
fb97542
Implements serializer for lists of Todos
jqno Mar 19, 2019
91083fc
Implements GET endpoint to replace helloworld
jqno Mar 20, 2019
5e9928f
Fixes NoClassDefError in gson
jqno Mar 20, 2019
e49b557
Adds InMemoryRepository
jqno Mar 20, 2019
242db69
Uses Repository to store Todos in Endpoints
jqno Mar 20, 2019
bfe5b7c
Introduces PartialTodo
jqno Mar 21, 2019
8f23dd3
Reorganises TestData
jqno Mar 21, 2019
2abadc8
Fixes serialization for PartialTodos
jqno Mar 21, 2019
ec0054a
Adds support for partial Todo to POST
jqno Mar 21, 2019
3e97c50
Adds logging to Endpoints
jqno Mar 21, 2019
cf8167d
Makes all PartialTodo fields optional
jqno Mar 21, 2019
0913cc9
Adds logging to InMemoryRepository
jqno Mar 21, 2019
36878f7
Adds logging to GsonSerializer
jqno Mar 21, 2019
bf31eda
ಠ_ಠ
jqno Mar 21, 2019
4007193
Responds with the actual Todo after a POST
jqno Mar 21, 2019
53dfc6f
Returns correct URL from POST
jqno Mar 22, 2019
fb1244f
Switches from int to UUID for ids
jqno Mar 22, 2019
296ab19
Renames Endpoints to Controller
jqno Mar 22, 2019
e16970e
Simplifies Controller by removing Route and Request
jqno Mar 22, 2019
4a59e37
Adds GET endpoint with id
jqno Mar 22, 2019
f7b4af5
Cleans up wiring of loggers
jqno Mar 22, 2019
98fb1d5
Refactors WiredApplication
jqno Mar 22, 2019
911686f
Updates coverage threshold
jqno Mar 22, 2019
e3f0cab
Implements PATCH endpoint
jqno Mar 22, 2019
b480478
Implements DELETE with id endpoint
jqno Mar 22, 2019
8447500
Implements dealing with order
jqno Mar 22, 2019
7feaa1f
Adds CORS tests for all endpoints
jqno Mar 22, 2019
ec60781
Simplifies patch logic
jqno Mar 22, 2019
f3a8f08
Start with adding error handling
jqno Mar 24, 2019
ea5b42a
Adds error handling to controller
jqno Mar 24, 2019
26d756f
Differentiates between invalid requests and internal server errors
jqno Mar 24, 2019
1c94c72
Allows for failures in Repository
jqno Mar 25, 2019
7405d56
Improves naming
jqno Mar 25, 2019
2cb7356
Flips ifs around for readability
jqno Mar 25, 2019
9d2a4f0
Restructures wiring
jqno Mar 25, 2019
450430a
Restructures test wiring
jqno Mar 25, 2019
57ba636
Renames DefaultWiring to Wiring
jqno Mar 25, 2019
341d5c3
Turns Heroku into a proper interface with implementation
jqno Mar 25, 2019
e9dd055
Adds invalid data to TestData
jqno Mar 25, 2019
f8d4cb5
Adds tests for failure cases in DefaultController
jqno Mar 26, 2019
16661d0
Sets coverage of everything except wiring to 100%
jqno Mar 26, 2019
ab14483
Makes slf4j-api dependency explicit
jqno Mar 26, 2019
fd481ba
Moves InMemoryRepository to a separate package
jqno Mar 26, 2019
c0d5f6e
Adds Database dependencies
jqno Mar 26, 2019
b68df97
Adds initial empty DatabaseRepository
jqno Mar 26, 2019
1963da3
Wires in jdbc url and logger
jqno Mar 26, 2019
089b440
Adds architecture test for Jdbi
jqno Mar 26, 2019
69f5877
Integrates AssertJ-vavr
jqno Mar 28, 2019
4a08f10
Adds test to assert that repo initialization is idempotent
jqno Mar 28, 2019
8987d5a
Implements initial database round-trip
jqno Mar 28, 2019
04d72b1
Generates URL from todo and implements GET
jqno Mar 28, 2019
6ec72ca
Implements the rest of the database queries
jqno Mar 28, 2019
5c21db4
Removes placeholder tests
jqno Mar 28, 2019
6fc48f9
Adds some failure logging to DatabaseRepository
jqno Mar 28, 2019
ad1d211
Adds PostgreSQL dependency
jqno Mar 29, 2019
a65ba50
Fixes PostgreSQL conversion issue
jqno Mar 29, 2019
94d4076
Adds test for failure case
jqno Mar 29, 2019
25b90a0
Consolidates environment default constants
jqno Mar 29, 2019
1b579b3
Factors out Wired class
jqno Mar 29, 2019
d92b924
Names Repository methods more consistently
jqno Mar 29, 2019
2750172
Adds Runner
jqno Mar 29, 2019
721e1ca
Adds a thread safety warning
jqno Mar 29, 2019
d64fe9e
Adds a README.md
jqno Mar 29, 2019
31ab938
Fixes architecture test when run from IntelliJ
jqno Mar 29, 2019
2a8e82e
Makes it possible to run locally with H2
jqno Mar 29, 2019
af90ac2
Makes various small improvements
jqno Apr 1, 2019
cb4c8f8
Makes repo initialization failures testable
jqno Apr 1, 2019
a16bb91
Adds a banner
jqno Apr 7, 2019
44ba2a5
Adds transaction support for updates
jqno Apr 11, 2019
81ab5df
Renames Jdbi to Engine
jqno Apr 14, 2019
fe7004f
Update README.md
jqno Apr 25, 2019
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
9 changes: 9 additions & 0 deletions .mvn/extensions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<extensions>
<extension>
<groupId>io.takari.polyglot</groupId>
<artifactId>polyglot-yaml</artifactId>
<version>0.3.2</version>
</extension>
</extensions>

3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
language: java
jdk: openjdk11
script: mvn clean verify
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: java $JAVA_OPTS -p ./target/parallel-java-1.0.jar:./target/dependency -m nl.jqno.paralleljava/nl.jqno.paralleljava.Main
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Parallel Java

[![Build Status](https://img.shields.io/travis/jqno/paralleljava.svg?style=plastic)](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.

16 changes: 16 additions & 0 deletions checkstyle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">

<module name = "Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="error"/>

<module name="TreeWalker">
<module name="Regexp">
<property name="format" value="@"/>
<property name="illegalPattern" value="true"/>
<property name="ignoreComments" value="true"/>
<property name="message" value="Annotations are not allowed!"/>
</module>
</module>
</module>
112 changes: 112 additions & 0 deletions pom.yml
Original file line number Diff line number Diff line change
@@ -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}
3 changes: 3 additions & 0 deletions scripts/deploy
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

git push -f heroku master
6 changes: 6 additions & 0 deletions scripts/jacoco
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

mvn clean verify
mvn jacoco:report
open target/site/jacoco/index.html

4 changes: 4 additions & 0 deletions scripts/logs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash

heroku logs --tail --source app

14 changes: 14 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 41 additions & 0 deletions src/main/java/nl/jqno/paralleljava/Main.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
36 changes: 36 additions & 0 deletions src/main/java/nl/jqno/paralleljava/app/Runner.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
12 changes: 12 additions & 0 deletions src/main/java/nl/jqno/paralleljava/app/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package nl.jqno.paralleljava.app.controller;

import io.vavr.control.Try;

public interface Controller {
Try<String> get();
Try<String> get(String id);
Try<String> post(String json);
Try<String> patch(String id, String json);
Try<String> delete();
Try<String> delete(String id);
}
Original file line number Diff line number Diff line change
@@ -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<String> get() {
return repository.getAll()
.map(serializer::serializeTodos);
}

public Try<String> 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<String> 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<String> 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<Todo, Todo> 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<String> delete() {
return repository.deleteAll()
.map(ignored -> "");
}

public Try<String> 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();
}
}
Loading