Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions apps/stuff/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ targetCompatibility = 1.8

dependencies {
// 3rd party
compile 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.5'
compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.5'
compile 'com.google.guava:guava:24.1-jre'
compile 'org.hsqldb:hsqldb:2.2.9'
compile 'org.slf4j:slf4j-api:1.7.25'
Expand Down
18 changes: 17 additions & 1 deletion apps/stuff/src/main/java/sparkles/StuffApp.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package sparkles;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import org.hsqldb.jdbc.JDBCDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -13,7 +18,7 @@
import javax.sql.DataSource;

import io.javalin.Javalin;

import io.javalin.json.JavalinJackson;
import sparkles.support.common.Environment;
import sparkles.support.javalin.JavalinApp;
import sparkles.support.javalin.flyway.FlywayExtension;
Expand All @@ -22,6 +27,7 @@
import sparkles.support.javalin.spring.data.auditing.Auditing;
import sparkles.support.javalin.spring.data.auditing.AuditingExtension;
import sparkles.support.javalin.spring.data.SpringDataExtension;
import sparkles.support.json.resources.JacksonResourcesModule;

import static io.javalin.apibuilder.ApiBuilder.crud;
import static sparkles.support.javalin.security.Security.requires;
Expand All @@ -42,6 +48,14 @@ public static void main(String[] args) {
public Javalin init() {
final DataSource dataSource = createDataSource();

JavalinJackson.getObjectMapper()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.registerModule(new JacksonResourcesModule())
.registerModule(new JavaTimeModule());

return JavalinApp.create()
.register(FlywayExtension.create(() -> dataSource,
"persistence/migrations/flyway"))
Expand All @@ -51,6 +65,8 @@ public Javalin init() {
// TODO: resolve auditor from request context
return "foo";
}))
// CRUD handling for StuffEntity - w/ RESTful resources
.register(new StuffHandler())
.accessManager(KeycloakAccessManager.create(
Environment.value("KEYCLOAK_URL", "https://foobar"),
Environment.value("KEYCLOAK_REALM", "realm"),
Expand Down
4 changes: 2 additions & 2 deletions apps/stuff/src/main/java/sparkles/StuffEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;

import java.time.LocalDateTime;
import java.time.Instant;
import java.util.UUID;

import javax.persistence.Column;
Expand All @@ -30,7 +30,7 @@ public class StuffEntity {

@CreatedDate
@Column(name = "created_at", updatable = false, nullable = false)
public LocalDateTime createdAt;
public Instant createdAt;

@CreatedBy
@Column(name = "created_by", updatable = false, nullable = false)
Expand Down
24 changes: 24 additions & 0 deletions apps/stuff/src/main/java/sparkles/StuffHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package sparkles;

import java.util.UUID;

import io.javalin.Context;
import sparkles.support.javalin.spring.data.rest.RestRepositoryHandler;

public class StuffHandler extends RestRepositoryHandler<StuffRepository, StuffEntity, UUID> {

public StuffHandler() {
super("foo/:id", StuffRepository.class, StuffEntity.class);
}

@Override
protected UUID toId(Context ctx) {
return UUID.fromString(ctx.pathParam(":id"));
}

@Override
protected String toUrl(StuffEntity entity) {
return "/foo/" + entity.id.toString();
}

}
14 changes: 8 additions & 6 deletions apps/stuff/src/test/java/sparkles/StuffCrudHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import io.javalin.Javalin;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import sparkles.support.javalin.testing.HttpClient;
import sparkles.support.javalin.testing.JavalinTestRunner;
import sparkles.support.javalin.testing.TestApp;
Expand All @@ -28,8 +29,9 @@ public Javalin setUpTestApp() {

@Test
public void post_shouldCreateEntity() {
testClient.enableLogging(HttpLoggingInterceptor.Level.BODY);
final Response response = testClient
.post("/stuff")
.post("/foo")
.json("{ \"name\": \"foo\" }")
.send();

Expand Down Expand Up @@ -57,20 +59,20 @@ public void getId_shouldReturnEntity() {
@Test
public void get_shouldReturnEntityList() {
testClient
.post("/stuff")
.post("/foo")
.json("{ \"name\": \"foo\" }")
.send();
testClient
.post("/stuff")
.post("/foo")
.json("{ \"name\": \"bar\" }")
.send();

final Response response = testClient
.get("/stuff")
.get("/foo")
.send();
final List<StuffEntity> responseEntities = testClient.jsonResponse(List.class);
// final List<StuffEntity> responseEntities = testClient.jsonResponse(List.class);
assertThat(response.code()).isEqualTo(200);
assertThat(responseEntities.size()).isGreaterThan(2);
assertThat(testClient.stringResponse()).isNull();
}

}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ include ':support:javalin:jwt'
include ':support:javalin:keycloak-security'
include ':support:javalin:spring-data'
include ':support:javalin:testing'
include ':support:json:json-resources'
include ':support:json:schema'
include ':support:keycloak'
include ':support:ok:moshi'
Expand Down
7 changes: 7 additions & 0 deletions support/javalin/spring-data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ archivesBaseName = "${group}-${name}"
dependencies {
processor 'org.projectlombok:lombok:1.16.20'

compile project(':support:common')
compile project(':support:javalin:common')
compile project(':support:json:json-resources')
compile 'com.fasterxml.jackson.core:jackson-databind:2.9.0'
compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.0'
compile 'com.fasterxml.jackson.core:jackson-core:2.9.0'
compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final'
compile 'org.hibernate:hibernate-core:5.2.14.Final'
compile 'org.hibernate:hibernate-entitymanager:5.2.14.Final'
compile 'org.slf4j:slf4j-api:1.7.25'
compile 'org.springframework.data:spring-data-commons:2.0.5.RELEASE'
compile 'org.springframework.data:spring-data-jpa:2.0.5.RELEASE'

testCompile project(':support:testing')
}

testlogger {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
package sparkles.support.javalin.spring.data;

import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.repository.Repository;

import javax.persistence.EntityManager;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;

@RequiredArgsConstructor
@Accessors(fluent = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class SpringData {
private final EntityManager entityManager;
private final JpaRepositoryFactory jpaRepositoryFactory;

public <T extends Repository> T repository(Class<T> repoClz) {
return jpaRepositoryFactory.getRepository(repoClz);
private final SpringDataExtension.SpringDataContext wrapped;

public EntityManager entityManager() {
return wrapped.entityManager();
}

public JpaRepositoryFactory jpaRepositoryFactory() {
return wrapped.jpaRepositoryFactory();
}

public <T> T createRepository(Class<T> repositoryClazz) {
return wrapped.createRepository(repositoryClazz);
}

public <T> T repository(Class<T> repositoryClz) {
return wrapped.repository(repositoryClz);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import io.javalin.Javalin;
import io.javalin.JavalinEvent;
import lombok.AccessLevel;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class SpringDataExtension implements Extension {
Expand All @@ -36,7 +38,7 @@ public void registerOnJavalin(Javalin app) {

ctx.register(EntityManager.class, entityManager);
ctx.register(JpaRepositoryFactory.class, jpaRepositoryFactory);
ctx.register(SpringData.class, new SpringData(entityManager, jpaRepositoryFactory));
ctx.register(SpringData.class, new SpringData(new SpringDataContext(entityManager, jpaRepositoryFactory)));

// Begin a transaction per request (transaction boundary is request boundary)
entityManager.getTransaction().begin();
Expand All @@ -59,42 +61,44 @@ public static SpringDataExtension create(Supplier<EntityManagerFactory> entityMa
return new SpringDataExtension(entityManagerFactory);
}

public static SpringDataContext springData(Context ctx) {
return new SpringDataContext(ctx);
public static SpringData springData(Context ctx) {
return new SpringData(new SpringDataContext(ctx.use(EntityManager.class), ctx.use(JpaRepositoryFactory.class)));
}

@Data
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Accessors(fluent = true)
public static class SpringDataContext {
private final Context ctx;

private final Map<Class, Object> repos = new HashMap<>();

/**
* Returns the EntityManager for the request context
* @return EntityManager
*/
public EntityManager entityManager() {
return ctx.use(EntityManager.class);
}
private final EntityManager entityManager;

/**
* Returns the JpaRepositoryFactory the request context
* @return
* Returns the JpaRepositoryFactory for the request context
*/
public JpaRepositoryFactory jpaRepositoryFactory() {
return ctx.use(JpaRepositoryFactory.class);
}
private final JpaRepositoryFactory jpaRepositoryFactory;

@SuppressWarnings("unchecked")
public <T> T createRepository(Class<T> repositoryClazz) {
if (repos.containsKey(repositoryClazz)) {
return (T) repos.get(repositoryClazz);
return repository(repositoryClazz);
}

@SuppressWarnings("unchecked")
public <T> T repository(Class<T> repositoryClz) {
if (repos.containsKey(repositoryClz)) {
return (T) repos.get(repositoryClz);
} else {
T repo = jpaRepositoryFactory().getRepository(repositoryClazz);
repos.put(repositoryClazz, repo);
T repo = jpaRepositoryFactory.getRepository(repositoryClz);
repos.put(repositoryClz, repo);

return repo;
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package sparkles.support.javalin.spring.data.rest;

import java.util.ArrayList;
import java.util.List;

import sparkles.support.json.resources.Embedded;

/**
* JSON resource representation for a collection of entities.
*
* @param <Entity>
*/
public class EntityCollectionResource<Entity> extends EntityResource<EntityCollectionResource.Metadata> {

private List<EntityResource<Entity>> content = new ArrayList<>();

@Embedded("content")
public List<EntityResource<Entity>> getContent() {
return content;
}

public void setContent(List<EntityResource<Entity>> content) {
this.content = content;
}

public static <Entity> EntityCollectionResource<Entity> from(List<EntityResource<Entity>> entities) {
EntityCollectionResource<Entity> resource = new EntityCollectionResource<>();
resource.entity = new Metadata();
resource.entity.count = entities.size();
resource.content.addAll(entities);

return resource;
}

public static class Metadata {
public long count;
public long total;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package sparkles.support.javalin.spring.data.rest;

import com.fasterxml.jackson.annotation.JsonUnwrapped;

import sparkles.support.json.resources.Link;
import sparkles.support.json.resources.Links;
import sparkles.support.json.resources.LinkCollection;
import sparkles.support.json.resources.Resource;

/**
* JSON resource representation for an entity.
*
* @param <Entity>
*/
@Resource
public class EntityResource<Entity> {

private final LinkCollection links = new LinkCollection();

@JsonUnwrapped
public Entity entity;

@Links
public LinkCollection getLinks() {
return links;
}

public EntityResource<Entity> withEntity(Entity entity) {
this.entity = entity;

return this;
}

public EntityResource<Entity> withSelfRel(String href) {
links.add(new Link().rel("self").href(href));

return this;
}

}
Loading