From 02faed4edd05be19c992edc228cf80fb2798c74e Mon Sep 17 00:00:00 2001 From: dohyoung rim Date: Thu, 29 Dec 2016 16:49:36 +0900 Subject: [PATCH 1/7] init commit. build architecture. much to-do --- pom.xml | 224 ++++++++++++++++++ .../zeplchallenge/todo/ResponseBuilder.java | 38 +++ .../java/dhrim/zeplchallenge/todo/Task.java | 24 ++ .../java/dhrim/zeplchallenge/todo/Todo.java | 15 ++ .../dhrim/zeplchallenge/todo/TodoRestApi.java | 100 ++++++++ .../dhrim/zeplchallenge/todo/TodoServer.java | 96 ++++++++ .../dhrim/zeplchallenge/todo/TodoService.java | 64 +++++ src/main/resources/logback.xml | 32 +++ .../zeplchallenge/todo/TodoRestApiTest.java | 93 ++++++++ src/test/resources/logback-test.xml | 32 +++ 10 files changed, 718 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/Task.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/Todo.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/TodoServer.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/TodoService.java create mode 100644 src/main/resources/logback.xml create mode 100644 src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java create mode 100644 src/test/resources/logback-test.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4c50006 --- /dev/null +++ b/pom.xml @@ -0,0 +1,224 @@ + + + 4.0.0 + + dhrim + todo-challenge + 1.0-SNAPSHOT + + + + + 3.1 + + + 1.9.11 + 2.25 + 1.7.22 + 1.1.8 + 1.16.12 + + + 4.12 + 3.1 + + + + + + + maven-compiler-plugin + ${plugin.compiler.version} + + 1.8 + 1.8 + + + + + + + + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + org.eclipse.jetty + jetty-server + 9.2.3.v20140905 + + + + + org.eclipse.jetty + jetty-servlet + 9.2.3.v20140905 + + + + + org.glassfish.jersey.core + jersey-server + 2.7 + + + + + org.glassfish.jersey.containers + jersey-container-servlet-core + 2.7 + + + + + org.glassfish.jersey.containers + jersey-container-jetty-http + 2.7 + + + + + com.sun.jersey + jersey-json + 1.19.3 + + + + + + + + + + + + com.sun.jersey.contribs + jersey-guice + 1.19.3 + + + javax.ws.rs + jsr311-api + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.codehaus.jackson + jackson-jaxrs + ${jackson.version} + + + + + com.google.inject + guice + 3.0 + + + + + com.google.guava + guava + 20.0 + + + + + + + org.mapdb + mapdb + 3.0.2 + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + ch.qos.logback + logback-core + ${logback.version} + + + + + + commons-httpclient + commons-httpclient + ${httpclient.version} + test + + + + + junit + junit + ${junit.version} + test + + + + + + + \ No newline at end of file diff --git a/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java b/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java new file mode 100644 index 0000000..563c51a --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java @@ -0,0 +1,38 @@ +package dhrim.zeplchallenge.todo; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.annotate.JsonSerialize; + +import javax.ws.rs.core.Response; +import java.io.IOException; + +public class ResponseBuilder { + + private javax.ws.rs.core.Response.Status status; + private Object body; + + // set as static so as to always use same instance to reuse parsing result. + // ObjectMapper is thread safe by itself. + private static ObjectMapper objectMapper = new ObjectMapper(); + static { + objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL); + } + + public ResponseBuilder(javax.ws.rs.core.Response.Status status) { + this.status = status; + this.body = null; + } + + public ResponseBuilder(javax.ws.rs.core.Response.Status status, Object body) { + this.status = status; + this.body = body; + } + + + public javax.ws.rs.core.Response build() throws IOException { + String json = objectMapper.writeValueAsString(body); + Response.ResponseBuilder r = Response.status(status).entity(json); + return r.build(); + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/Task.java b/src/main/java/dhrim/zeplchallenge/todo/Task.java new file mode 100644 index 0000000..cde2809 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/Task.java @@ -0,0 +1,24 @@ +package dhrim.zeplchallenge.todo; + +import lombok.Data; + +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Date; + +@Data +@XmlRootElement +public class Task implements Serializable { + + private String id; + private String name; + private String description; + private Status status; + private Date created; + + public enum Status { + DONE, + NOT_DONE, + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/Todo.java b/src/main/java/dhrim/zeplchallenge/todo/Todo.java new file mode 100644 index 0000000..d2ce8b1 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/Todo.java @@ -0,0 +1,15 @@ +package dhrim.zeplchallenge.todo; + +import lombok.Data; + +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Date; + +@Data +@XmlRootElement +public class Todo implements Serializable { + private String id; + private String name; + private Date created; +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java new file mode 100644 index 0000000..6d61756 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java @@ -0,0 +1,100 @@ +package dhrim.zeplchallenge.todo; + +import lombok.extern.slf4j.Slf4j; + +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.List; + +@Path("/") +@Slf4j +public class TodoRestApi { + + @Inject + private TodoService todoService; + + @GET + @Path("/todos") + @Produces("application/json") + public Response getTodos() throws IOException { + List todoList = todoService.getTodoList(); + return new ResponseBuilder(Response.Status.OK, todoList).build(); + } + + @GET + @Path("/todos/{todoId}") + @Produces("application/json") + public Response getTodoTasks(@PathParam("todoId") String todoId) throws IOException { + List taskList = todoService.getTaskList(todoId); + return new ResponseBuilder(Response.Status.OK, taskList).build(); + } + + @GET + @Path("/todos/{todoId}/tasks/{taskId}") + @Produces("application/json") + public Response getTodoTask(@PathParam("todoId") String todoId, @PathParam("taskId") String taskId) throws IOException { + Task task = todoService.getTodoTask(todoId, taskId); + return new ResponseBuilder(Response.Status.OK, task).build(); + } + + @GET + @Path("/todos/{todoId}/tasks/done}") + @Produces("application/json") + public Response getTodoTasksOfStatusDone(@PathParam("todoId") String todoId) throws IOException { + List taskList = todoService.getTodoTaskList(todoId, Task.Status.DONE); + return new ResponseBuilder(Response.Status.OK, taskList).build(); + } + + @GET + @Path("/todos/{todoId}/tasks/not-done}") + @Produces("application/json") + public Response getTodoTasksOfStatusNotDone(@PathParam("todoId") String todoId) throws IOException { + List taskList = todoService.getTodoTaskList(todoId, Task.Status.NOT_DONE); + return new ResponseBuilder(Response.Status.OK, taskList).build(); + } + + @POST + @Path("/todos") + @Consumes("application/json") + @Produces("application/json") + public Response createTodo(Todo todo) throws IOException { + Todo newTodo = todoService.createTodo(todo); + return new ResponseBuilder(Response.Status.OK, newTodo).build(); + } + + @POST + @Path("/todos/{todoId}/tasks") + @Consumes("application/json") + @Produces("application/json") + public Response createTask(@PathParam("todoId") String todoId, Task task) throws IOException { + Task newTask = todoService.createTask(todoId, task); + return new ResponseBuilder(Response.Status.OK, newTask).build(); + } + + @PUT + @Path("/todos/{todoId}") + @Produces("application/json") + public Response deleteTodo(@PathParam("todoId") String todoId) throws IOException { + todoService.deleteTodo(todoId); + return new ResponseBuilder(Response.Status.OK).build(); + } + + @DELETE + @Path("/todos/{todoId}/tasks/{taskId}") + @Produces("application/json") + public Response deleteTodoTask(@PathParam("todoId") String todoId, @PathParam("taskId") String taskId) throws IOException { + todoService.deleteTask(todoId, taskId); + return new ResponseBuilder(Response.Status.OK).build(); + } + + // TODO : remove + @GET + @Path("/hello") + public String hello() { + return "hello"; + } + + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java b/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java new file mode 100644 index 0000000..8bd7bd2 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java @@ -0,0 +1,96 @@ +package dhrim.zeplchallenge.todo; + +import com.google.common.annotations.VisibleForTesting; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.servlet.GuiceFilter; +import com.google.inject.util.Modules; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; + + +// TODO : treat exception +@Slf4j +public class TodoServer { + + private void RestServer() { } + + private Server server; + + private static Module[] GUICE_DI_MODULES = { + new GuiceDiBinding() + }; + + + + public void start(int port) throws Exception { + + server = new Server(port); + ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/"); + // GuiceFilter is added. to inject instance when http requested + servletContextHandler.addFilter(GuiceFilter.class, "/*", null); + servletContextHandler.addServlet(DefaultServlet.class, "/"); + + try { + server.start(); + log.info("TodoServer started with port {}.", port); + } catch(Throwable e) { + log.error("Starting TodoServer failed. port={}", port, e); + } + + } + + public void shutdown() { + + if(server==null || !server.isStarted()) { + log.info("Server not started. Cancel shutdown."); + return; + } + + try { + server.stop(); + } catch (Throwable e) { + log.error("Something failed during shutting down TodoServer.", e); + } finally { + server.destroy(); + } + + } + + + public boolean isStarted() { + if(server==null) { return false; } + return server.isStarted(); + } + + public void join() throws InterruptedException { + if(!isStarted()) { return; } + server.join(); + } + + + public static TodoServer getInstance() { + return getInstanceWithMockBinding(); + } + + + private static Injector injector; + + @VisibleForTesting + static TodoServer getInstanceWithMockBinding(Module... additionalModules) { + Module module = Modules.override(GUICE_DI_MODULES).with(additionalModules); + injector = Guice.createInjector(module); + return injector.getInstance(TodoServer.class); + } + + @VisibleForTesting + static Object getDiInstance(Class clazz) { + return injector.getInstance(clazz); + } + +} + + diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoService.java b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java new file mode 100644 index 0000000..e03e8b4 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java @@ -0,0 +1,64 @@ +package dhrim.zeplchallenge.todo; + +import com.google.inject.Singleton; +import lombok.extern.slf4j.Slf4j; + +import javax.inject.Inject; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Singleton +@Slf4j +public class TodoService { + + @Inject + private TodoRepo todoRepo; + + public List getTodoList() { + return todoRepo.getTodoList(); + } + + public Todo getTodoList(String todoId) { + throw new RuntimeException("not implemented"); + } + + public List getTaskList(String todoId) { + throw new RuntimeException("not implemented"); + } + + public Task getTodoTask(String todoId, String taskId) { + throw new RuntimeException("not implemented"); + } + + public List getTodoTaskList(String todoId, Task.Status done) { + throw new RuntimeException("not implemented"); + } + + /** + * Create new Todo instance, + * + * id and created are overwritten with auto generated. + * + * @param todo + * @return created new instance + */ + public Todo createTodo(Todo todo) { + if(todo==null) { throw new IllegalArgumentException("todo instance is null."); } + todo.setId(UUID.randomUUID().toString()); + todo.setCreated(new Date()); + return todoRepo.saveOrUpdate(todo); + } + + public Task createTask(String todoId, Task task) { + throw new RuntimeException("not implemented"); + } + + public void deleteTodo(String todoId) { + throw new RuntimeException("not implemented"); + } + + public void deleteTask(String todoId, String taskId) { + throw new RuntimeException("not implemented"); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..98a6e3c --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n + + + + + logFile.log + + logFile.%d{yyyy-MM-dd}.log + 30 + 3GB + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java new file mode 100644 index 0000000..9e8e0bd --- /dev/null +++ b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java @@ -0,0 +1,93 @@ +package dhrim.zeplchallenge.todo; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.GetMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TodoRestApiTest extends AbstractTestBase { + + @Before + public void before() throws Exception { + super.before(); + } + + @After + public void after() { + super.after(); + } + + private Module getMockBinding() { + return new AbstractModule() { + @Override + protected void configure() { + bind(TodoRepo.class).toInstance(new MockMemoryTodoRepo()); + } + }; + } + + private static class MockMemoryTodoRepo extends AbstractMapBasedTodoRepo { + + @Override + protected Map getTodoMapInstance() { + return new HashMap(); + } + + @Override + protected Map> getTasksMapInstance() { + return new HashMap>(); + } + + + } + + + @Test + public void test_createTodo() throws Exception { + + // GIVEN + + // WHEN + Todo todo = new TodoBuilder().name("name1").build(); + String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", todo); + + // THEN + // {"id":"1e0840ec-6cbf-41d4-8c83-83fb6f8bf8f7","name":"name1","created":1482992669764} + assertNotNull(responseBodyString); + Todo actualTodo = objectMapper.readValue(responseBodyString, Todo.class); + + assertNotNull(actualTodo.getId()); + assertEquals(todo.getName(), actualTodo.getName()); + assertNotNull(actualTodo.getCreated()); + + } + + @Test + public void test_getTodos() throws Exception { + + // GIVEN + + // WHEN + String url = BASE_URL+"/todos"; + HttpClient httpClient = new HttpClient(); + GetMethod method = new GetMethod(url); + httpClient.executeMethod(method); + + // THEN + assertEquals(HttpStatus.OK_200, method.getStatusCode()); + + + } + + +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..9906bdc --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,32 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n + + + + + logFile.log + + logFile.%d{yyyy-MM-dd}.log + 30 + 3GB + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n + + + + + + + + + + + + + \ No newline at end of file From 9b3f8145b76764754e611f44e1f9bd3ba1f83290 Mon Sep 17 00:00:00 2001 From: dohyoung rim Date: Thu, 29 Dec 2016 17:10:12 +0900 Subject: [PATCH 2/7] commit again. add ommited files. --- .../todo/AbstractMapBasedTodoRepo.java | 79 +++++++++++++++++ .../zeplchallenge/todo/GuiceDiBinding.java | 40 +++++++++ .../java/dhrim/zeplchallenge/todo/Main.java | 26 ++++++ .../zeplchallenge/todo/MapDbTodoRepo.java | 84 +++++++++++++++++++ .../dhrim/zeplchallenge/todo/TodoRepo.java | 13 +++ .../zeplchallenge/todo/AbstractTestBase.java | 72 ++++++++++++++++ .../zeplchallenge/todo/IntegrationTest.java | 83 ++++++++++++++++++ .../zeplchallenge/todo/MapDbTodRepoTest.java | 44 ++++++++++ .../dhrim/zeplchallenge/todo/TodoBuilder.java | 34 ++++++++ .../zeplchallenge/todo/TodoRestApiTest.java | 3 +- 10 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/GuiceDiBinding.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/Main.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java create mode 100644 src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java create mode 100644 src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java create mode 100644 src/test/java/dhrim/zeplchallenge/todo/MapDbTodRepoTest.java create mode 100644 src/test/java/dhrim/zeplchallenge/todo/TodoBuilder.java diff --git a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java new file mode 100644 index 0000000..4315585 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java @@ -0,0 +1,79 @@ +package dhrim.zeplchallenge.todo; + +import com.google.common.annotations.VisibleForTesting; +import com.google.inject.Singleton; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +@Singleton +@Slf4j +public abstract class AbstractMapBasedTodoRepo implements TodoRepo { + + // todo.id -> Todo + private Map todoMap; + + // todo.id -> List + private Map> tasksMap; + + protected abstract Map getTodoMapInstance(); + protected abstract Map> getTasksMapInstance(); + + protected void initIfNot() { + if(todoMap!=null) { return; } + todoMap = getTodoMapInstance(); + tasksMap = getTasksMapInstance(); + } + + @VisibleForTesting + void clear_for_test() { + todoMap.clear(); + tasksMap.clear(); + } + + @Override + public List getTodoList() { + initIfNot(); + return new ArrayList(todoMap.values()); + } + + @Override + public Todo getTodo(String todoId) { + initIfNot(); + return todoMap.get(todoId); + } + + @Override + public Todo saveOrUpdate(Todo todo) { + initIfNot(); + todoMap.put(todo.getId(), todo); + return getTodo(todo.getId()); + } + + + @Override + public List getTaskList(String todoId) { + initIfNot(); + return new ArrayList(tasksMap.get(todoId).values()); + } + + @Override + public Task getTask(String todoId, String taskId) { + initIfNot(); + Map aTaskMap = tasksMap.get(todoId); + return aTaskMap.get(taskId); + } + + @Override + public Task saveOrUpdate(String todoId, Task task) { + initIfNot(); + Map aTaskMap = tasksMap.get(todoId); + if(aTaskMap==null) { return null; } + aTaskMap.put(task.getId(), task); + return getTask(todoId, task.getId()); + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/GuiceDiBinding.java b/src/main/java/dhrim/zeplchallenge/todo/GuiceDiBinding.java new file mode 100644 index 0000000..c230dfc --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/GuiceDiBinding.java @@ -0,0 +1,40 @@ +package dhrim.zeplchallenge.todo; + +import com.sun.jersey.api.core.PackagesResourceConfig; +import com.sun.jersey.guice.JerseyServletModule; +import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; + +/** + * Bind jersey resource classes and other service classes for Guice DI injection. + * + * Jersey resource classes are auto scanned from BASE_PACKAGE recursively. + * Other service classes are configured manually not scanned. + */ +public class GuiceDiBinding extends JerseyServletModule { + + // value is like "dhrim.zeplchallenge.todo" + private static final String BASE_PACKAGE = Main.class.getPackage().getName(); + + @Override + protected void configureServlets() { + + configureResourceClasses(); + configureServiceClasses(); + + serve("/*").with(GuiceContainer.class); + + } + + private void configureResourceClasses() { + PackagesResourceConfig resourceConfig = new PackagesResourceConfig(BASE_PACKAGE); + for (Class resource : resourceConfig.getClasses()) { + bind(resource); + } + } + + private void configureServiceClasses() { + bind(TodoService.class); + bind(TodoRepo.class).to(MapDbTodoRepo.class); + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/Main.java b/src/main/java/dhrim/zeplchallenge/todo/Main.java new file mode 100644 index 0000000..6909367 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/Main.java @@ -0,0 +1,26 @@ +package dhrim.zeplchallenge.todo; + +/** + * TODO : describe + */ +public class Main { + + public static final int DEFAULT_PORT = 2222; + + public static void main(String[] args) throws Exception { + + // TODO : read from args + int port = DEFAULT_PORT; + + TodoServer todoServer = TodoServer.getInstance(); + + try { + todoServer.start(port); + todoServer.join(); + } finally { + todoServer.shutdown(); + } + + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java new file mode 100644 index 0000000..1b927d8 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java @@ -0,0 +1,84 @@ +package dhrim.zeplchallenge.todo; + +import com.google.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.mapdb.*; + +import java.io.*; +import java.util.Map; + + +@Singleton +@Slf4j +/** + * TodoRepo using file based MadDb. + * + * This class use library MapDb. http://www.mapdb.org/ + * + * Map is created from file and stored into the file. + * + */ +class MapDbTodoRepo extends AbstractMapBasedTodoRepo { + + static final String DB_FILE_NAME = "todo_data.db"; + + public MapDbTodoRepo() { + super.initIfNot(); + } + + private static DB db; + private Serializer serializer = new ObjectSerializer(); + + private void initDbIfNot() { + if(db!=null) { return; } + db = DBMaker + .fileDB(DB_FILE_NAME) + .closeOnJvmShutdown() + .make(); + log.info("File DbMap is initialized with file "+DB_FILE_NAME); + } + + @Override + protected Map getTodoMapInstance() { + initDbIfNot(); + return db.hashMap("todoMap", Serializer.STRING, serializer).createOrOpen(); + } + + @Override + protected Map> getTasksMapInstance() { + initDbIfNot(); + return db.hashMap("tasksMap", Serializer.STRING, serializer).createOrOpen(); + } + + protected void clear() { + new File(DB_FILE_NAME).delete(); + } + + public class ObjectSerializer implements Serializer, Serializable { + + @Override + public void serialize(@NotNull DataOutput2 out, @NotNull Object value) throws IOException { + if(!(value instanceof Serializable)) { + throw new IOException("value class is not implements Serializable. value="+value); + } + // TODO : check if value is Serializable + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutput objectOut = new ObjectOutputStream(bos)) { + objectOut.writeObject(value); + out.write(bos.toByteArray()); + } + } + + @Override + public Object deserialize(@NotNull DataInput2 input, int available) throws IOException { + try (ByteArrayInputStream bis = new ByteArrayInputStream(input.internalByteArray()); + ObjectInput objectInput = new ObjectInputStream(bis)) { + return objectInput.readObject(); + } catch (ClassNotFoundException e) { + throw new IOException("deserialization failed.", e); + } + } + + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java new file mode 100644 index 0000000..4996209 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java @@ -0,0 +1,13 @@ +package dhrim.zeplchallenge.todo; + +import java.util.List; + +public interface TodoRepo { + List getTodoList(); + Todo getTodo(String todoId); + Todo saveOrUpdate(Todo todo); + + List getTaskList(String todoId); + Task getTask(String todoId, String taskId); + Task saveOrUpdate(String todoId, Task task); +} diff --git a/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java new file mode 100644 index 0000000..82570c8 --- /dev/null +++ b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java @@ -0,0 +1,72 @@ +package dhrim.zeplchallenge.todo; + +import com.google.inject.Module; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public abstract class AbstractTestBase { + + protected static final int PORT = 2222; + protected static final String BASE_URL = "http://localhost:+"+PORT; + + private static TodoServer todoServer; + + protected ObjectMapper objectMapper = new ObjectMapper(); + + /** return mock binding configured google Guice module. **/ + protected abstract Module getMockBinding(); + + protected void before() throws Exception { + startServerIfNotStarted(); + initObjectMapperIfNotCreated(); + } + + protected void initObjectMapperIfNotCreated() { + if(objectMapper!=null) { return; } + objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL); + } + + protected void after() { + shutdownServer(); + } + + protected void startServerIfNotStarted() throws Exception { + todoServer = TodoServer.getInstanceWithMockBinding(getMockBinding()); + todoServer.start(PORT); + } + + protected void shutdownServer() { + if(todoServer ==null || !todoServer.isStarted()) { return; } + todoServer.shutdown(); + todoServer = null; + } + + + + protected String sendAndGetResponseBody(String url, Object requestBodyObject) throws IOException { + + HttpClient httpClient = new HttpClient(); + PostMethod method = new PostMethod(url); + StringRequestEntity stringRequestEntity = new StringRequestEntity(objectMapper.writeValueAsString(requestBodyObject), "application/json", "UTF-8"); + method.setRequestHeader(HttpHeader.CONTENT_TYPE.asString(), "application/json"); + method.setRequestEntity(stringRequestEntity); + httpClient.executeMethod(method); + + assertEquals(HttpStatus.OK_200, method.getStatusCode()); + + return method.getResponseBodyAsString(); + + } + + +} diff --git a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java new file mode 100644 index 0000000..7de6832 --- /dev/null +++ b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java @@ -0,0 +1,83 @@ +package dhrim.zeplchallenge.todo; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.GetMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Test only all integrated are working. + * + * RestApi - Service - Repo + * + * Detail functions are tested in other testcase. + * + */ +public class IntegrationTest extends AbstractTestBase { + + + @Before + public void before() throws Exception { + super.before(); + } + + @After + public void after() { + super.after(); + } + + @Override + protected Module getMockBinding() { + return new AbstractModule() { + @Override + protected void configure() { } + }; + } + + @Test + public void test_createTodo() throws Exception { + + // GIVEN + + // WHEN + Todo todo = new TodoBuilder().name("name1").build(); + String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", todo); + + // THEN + // {"id":"1e0840ec-6cbf-41d4-8c83-83fb6f8bf8f7","name":"name1","created":1482992669764} + assertNotNull(responseBodyString); + Todo actualTodo = objectMapper.readValue(responseBodyString, Todo.class); + + assertNotNull(actualTodo.getId()); + assertEquals(todo.getName(), actualTodo.getName()); + assertNotNull(actualTodo.getCreated()); + + } + + @Test + public void test_getTodos() throws Exception { + + // GIVEN + + // WHEN + String url = BASE_URL+"/todos"; + HttpClient httpClient = new HttpClient(); + GetMethod method = new GetMethod(url); + httpClient.executeMethod(method); + + // THEN + assertEquals(HttpStatus.OK_200, method.getStatusCode()); + + + } + + + +} diff --git a/src/test/java/dhrim/zeplchallenge/todo/MapDbTodRepoTest.java b/src/test/java/dhrim/zeplchallenge/todo/MapDbTodRepoTest.java new file mode 100644 index 0000000..5ee2b5f --- /dev/null +++ b/src/test/java/dhrim/zeplchallenge/todo/MapDbTodRepoTest.java @@ -0,0 +1,44 @@ +package dhrim.zeplchallenge.todo; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class MapDbTodRepoTest { + + private MapDbTodoRepo repo = new MapDbTodoRepo(); + + @Before + public void before() { + repo.clear(); + } + + + @Test + public void test_createTodo_success () { + + // GIVEN + Todo orgTodo = new TodoBuilder().id("id1").name("name1").build(); + + { + // WHEN + Todo actualTodo = repo.saveOrUpdate(orgTodo); + + // THEN + assertEquals(orgTodo, actualTodo); + } + + + { + // WHEN + Todo actualTodo = repo.getTodo(orgTodo.getId()); + + // THEN + assertEquals(orgTodo, actualTodo); + } + + } + + +} diff --git a/src/test/java/dhrim/zeplchallenge/todo/TodoBuilder.java b/src/test/java/dhrim/zeplchallenge/todo/TodoBuilder.java new file mode 100644 index 0000000..44cdbc5 --- /dev/null +++ b/src/test/java/dhrim/zeplchallenge/todo/TodoBuilder.java @@ -0,0 +1,34 @@ +package dhrim.zeplchallenge.todo; + +import java.util.Date; + +public class TodoBuilder { + + private String id = null; + private String name = "default_name"; + private Date created = null; + + public Todo build() { + Todo todo = new Todo(); + todo.setId(id); + todo.setName(name); + todo.setCreated(created); + return todo; + } + + public TodoBuilder id(String id) { + this.id = id; + return this; + } + + public TodoBuilder name(String name) { + this.name = name; + return this; + } + + public TodoBuilder created(Date created) { + this.created = created; + return this; + } + +} diff --git a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java index 9e8e0bd..0e78093 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java @@ -27,7 +27,8 @@ public void after() { super.after(); } - private Module getMockBinding() { + @Override + protected Module getMockBinding() { return new AbstractModule() { @Override protected void configure() { From bb2a06f11a4417f4b38b0e70d8048b588037f80d Mon Sep 17 00:00:00 2001 From: dohyoung rim Date: Thu, 29 Dec 2016 18:11:20 +0900 Subject: [PATCH 3/7] add exception handling way. --- .../todo/AbstractMapBasedTodoRepo.java | 7 ++++++ .../dhrim/zeplchallenge/todo/TodoRestApi.java | 2 ++ .../dhrim/zeplchallenge/todo/TodoService.java | 4 +++- src/main/resources/logback.xml | 2 +- .../zeplchallenge/todo/AbstractTestBase.java | 5 ++--- .../zeplchallenge/todo/IntegrationTest.java | 2 +- .../zeplchallenge/todo/TodoRestApiTest.java | 22 +++++++++++++++++-- src/test/resources/logback-test.xml | 2 +- 8 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java index 4315585..70e0cac 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java +++ b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java @@ -43,12 +43,14 @@ public List getTodoList() { @Override public Todo getTodo(String todoId) { initIfNot(); + if(todoId==null) { throw new IllegalArgumentException("todoId is null"); } return todoMap.get(todoId); } @Override public Todo saveOrUpdate(Todo todo) { initIfNot(); + if(todo.getId()==null) { throw new IllegalArgumentException("todo.id is null. todo="+todo); } todoMap.put(todo.getId(), todo); return getTodo(todo.getId()); } @@ -57,12 +59,15 @@ public Todo saveOrUpdate(Todo todo) { @Override public List getTaskList(String todoId) { initIfNot(); + if(todoId==null) { throw new IllegalArgumentException("todoId is null"); } return new ArrayList(tasksMap.get(todoId).values()); } @Override public Task getTask(String todoId, String taskId) { initIfNot(); + if(todoId==null) { throw new IllegalArgumentException("todoId is null"); } + if(taskId==null) { throw new IllegalArgumentException("taskId is null"); } Map aTaskMap = tasksMap.get(todoId); return aTaskMap.get(taskId); } @@ -70,6 +75,8 @@ public Task getTask(String todoId, String taskId) { @Override public Task saveOrUpdate(String todoId, Task task) { initIfNot(); + if(todoId==null) { throw new IllegalArgumentException("todoId is null"); } + if(task.getId()==null) { throw new IllegalArgumentException("task.id is null. task="+task); } Map aTaskMap = tasksMap.get(todoId); if(aTaskMap==null) { return null; } aTaskMap.put(task.getId(), task); diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java index 6d61756..41f99c3 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java @@ -60,7 +60,9 @@ public Response getTodoTasksOfStatusNotDone(@PathParam("todoId") String todoId) @Consumes("application/json") @Produces("application/json") public Response createTodo(Todo todo) throws IOException { + System.out.println("ASDF : todo="+todo); Todo newTodo = todoService.createTodo(todo); + System.out.println("ASDF : newTodo="+newTodo); return new ResponseBuilder(Response.Status.OK, newTodo).build(); } diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoService.java b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java index e03e8b4..01d3c27 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoService.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java @@ -42,9 +42,11 @@ public List getTodoTaskList(String todoId, Task.Status done) { * * @param todo * @return created new instance + * @throws throw IllegalArgumentException when todo is null or todo.name is null */ public Todo createTodo(Todo todo) { - if(todo==null) { throw new IllegalArgumentException("todo instance is null."); } + if(todo==null) { throw new IllegalArgumentException("todo is null."); } + if(todo.getName()==null) { throw new IllegalArgumentException("todo.name is null. todo="+todo); } todo.setId(UUID.randomUUID().toString()); todo.setCreated(new Date()); return todoRepo.saveOrUpdate(todo); diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 98a6e3c..689e14b 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -24,7 +24,7 @@ - + diff --git a/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java index 82570c8..f7335fb 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java +++ b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java @@ -7,7 +7,6 @@ import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; import java.io.IOException; @@ -53,7 +52,7 @@ protected void shutdownServer() { - protected String sendAndGetResponseBody(String url, Object requestBodyObject) throws IOException { + protected String sendAndGetResponseBody(String url, Object requestBodyObject, int expectedStatusCode) throws IOException { HttpClient httpClient = new HttpClient(); PostMethod method = new PostMethod(url); @@ -62,7 +61,7 @@ protected String sendAndGetResponseBody(String url, Object requestBodyObject) th method.setRequestEntity(stringRequestEntity); httpClient.executeMethod(method); - assertEquals(HttpStatus.OK_200, method.getStatusCode()); + assertEquals(expectedStatusCode, method.getStatusCode()); return method.getResponseBodyAsString(); diff --git a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java index 7de6832..1daa15f 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java @@ -48,7 +48,7 @@ public void test_createTodo() throws Exception { // WHEN Todo todo = new TodoBuilder().name("name1").build(); - String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", todo); + String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", todo, HttpStatus.OK_200); // THEN // {"id":"1e0840ec-6cbf-41d4-8c83-83fb6f8bf8f7","name":"name1","created":1482992669764} diff --git a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java index 0e78093..c76b01f 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java @@ -9,6 +9,7 @@ import org.junit.Before; import org.junit.Test; +import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -57,10 +58,10 @@ protected Map> getTasksMapInstance() { public void test_createTodo() throws Exception { // GIVEN + Todo todo = new TodoBuilder().name("name1").build(); // WHEN - Todo todo = new TodoBuilder().name("name1").build(); - String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", todo); + String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", todo, HttpStatus.OK_200); // THEN // {"id":"1e0840ec-6cbf-41d4-8c83-83fb6f8bf8f7","name":"name1","created":1482992669764} @@ -73,6 +74,23 @@ public void test_createTodo() throws Exception { } + + @Test + public void test_createTodo_failed_when_empty_input() throws IOException { + + // GIVEN + Object emptyString = ""; + + // WHEN + String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", emptyString, HttpStatus.BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage =objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + } + + @Test public void test_getTodos() throws Exception { diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 9906bdc..ef0e27a 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -24,7 +24,7 @@ - + From 46c52d4e2358e39722a17537363d3a3122a546dc Mon Sep 17 00:00:00 2001 From: dohyougn rim Date: Fri, 30 Dec 2016 18:09:19 +0900 Subject: [PATCH 4/7] finish architecture building - error handling - testcaes template - fix date formate in json --- pom.xml | 78 ++++-------- .../todo/AbstractExceptionMapper.java | 19 +++ .../todo/AbstractMapBasedTodoRepo.java | 2 + .../zeplchallenge/todo/FailedMessage.java | 11 ++ .../todo/IllegalArgumentExceptionMapper.java | 23 ++++ .../todo/NotFoundExceptionMapper.java | 24 ++++ .../zeplchallenge/todo/ResponseBuilder.java | 2 + .../todo/ThrowableExceptionMapper.java | 23 ++++ .../dhrim/zeplchallenge/todo/TodoRestApi.java | 2 - .../dhrim/zeplchallenge/todo/TodoServer.java | 2 - .../dhrim/zeplchallenge/todo/TodoService.java | 4 +- .../zeplchallenge/todo/AbstractTestBase.java | 54 ++++++-- .../zeplchallenge/todo/IntegrationTest.java | 2 +- .../zeplchallenge/todo/TodoRestApiTest.java | 115 ++++++++++++++---- src/test/resources/logback-test.xml | 2 +- 15 files changed, 271 insertions(+), 92 deletions(-) create mode 100644 src/main/java/dhrim/zeplchallenge/todo/AbstractExceptionMapper.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/FailedMessage.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/IllegalArgumentExceptionMapper.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/NotFoundExceptionMapper.java create mode 100644 src/main/java/dhrim/zeplchallenge/todo/ThrowableExceptionMapper.java diff --git a/pom.xml b/pom.xml index 4c50006..8a19835 100644 --- a/pom.xml +++ b/pom.xml @@ -15,14 +15,21 @@ 1.9.11 + 9.2.3.v20140905 2.25 + 1.19.3 + 1.19.3 + 3.0 1.7.22 1.1.8 1.16.12 + 3.0.2 + 20.0 4.12 3.1 + 2.0.0.0 @@ -55,56 +62,48 @@ org.eclipse.jetty jetty-server - 9.2.3.v20140905 + ${jetty-server.version} org.eclipse.jetty jetty-servlet - 9.2.3.v20140905 + ${jetty-server.version} org.glassfish.jersey.core jersey-server - 2.7 + ${jersy.version} org.glassfish.jersey.containers jersey-container-servlet-core - 2.7 + ${jersy.version} org.glassfish.jersey.containers jersey-container-jetty-http - 2.7 + ${jersy.version} com.sun.jersey jersey-json - 1.19.3 + ${jersey-json.version} - - - - - - - - com.sun.jersey.contribs jersey-guice - 1.19.3 + ${jersey-json.version} javax.ws.rs @@ -114,43 +113,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.codehaus.jackson jackson-jaxrs @@ -161,14 +123,14 @@ com.google.inject guice - 3.0 + ${guice.version} com.google.guava guava - 20.0 + ${guava.version} @@ -177,7 +139,7 @@ org.mapdb mapdb - 3.0.2 + ${mapdb.version} @@ -217,6 +179,12 @@ test + + org.hamcrest + hamcrest-junit + ${hamcrest-junit.version} + test + diff --git a/src/main/java/dhrim/zeplchallenge/todo/AbstractExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/AbstractExceptionMapper.java new file mode 100644 index 0000000..bfac92d --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/AbstractExceptionMapper.java @@ -0,0 +1,19 @@ +package dhrim.zeplchallenge.todo; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +public abstract class AbstractExceptionMapper { + + protected Response buildResponse(int statusCode, String message) { + FailedMessage failedMessage = new FailedMessage(); + failedMessage.setMessage(message); + + return Response + .status(statusCode) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(failedMessage) + .build(); + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java index 70e0cac..ed2d630 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java +++ b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java @@ -30,6 +30,8 @@ protected void initIfNot() { @VisibleForTesting void clear_for_test() { + // could be null if initIfNot() not called. + if(todoMap==null) { return; } todoMap.clear(); tasksMap.clear(); } diff --git a/src/main/java/dhrim/zeplchallenge/todo/FailedMessage.java b/src/main/java/dhrim/zeplchallenge/todo/FailedMessage.java new file mode 100644 index 0000000..19b3ba2 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/FailedMessage.java @@ -0,0 +1,11 @@ +package dhrim.zeplchallenge.todo; + +import lombok.Data; + +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@XmlRootElement +public class FailedMessage { + private String message; +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/IllegalArgumentExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/IllegalArgumentExceptionMapper.java new file mode 100644 index 0000000..d96f596 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/IllegalArgumentExceptionMapper.java @@ -0,0 +1,23 @@ +package dhrim.zeplchallenge.todo; + + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpStatus; + +import javax.inject.Singleton; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +@Singleton +@Slf4j +public class IllegalArgumentExceptionMapper extends AbstractExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(IllegalArgumentException e) { + log.debug("Invalid request.", e); + return buildResponse(HttpStatus.BAD_REQUEST_400, e.getMessage()); + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/NotFoundExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/NotFoundExceptionMapper.java new file mode 100644 index 0000000..8a50548 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/NotFoundExceptionMapper.java @@ -0,0 +1,24 @@ +package dhrim.zeplchallenge.todo; + + +import com.sun.jersey.api.NotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpStatus; + +import javax.inject.Singleton; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +@Singleton +@Slf4j +public class NotFoundExceptionMapper extends AbstractExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(NotFoundException e) { + log.debug("Invalid request.", e); + return buildResponse(HttpStatus.NOT_FOUND_404, e.getMessage()); + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java b/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java index 563c51a..e968594 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java +++ b/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java @@ -5,6 +5,7 @@ import javax.ws.rs.core.Response; import java.io.IOException; +import java.text.SimpleDateFormat; public class ResponseBuilder { @@ -16,6 +17,7 @@ public class ResponseBuilder { private static ObjectMapper objectMapper = new ObjectMapper(); static { objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS")); } public ResponseBuilder(javax.ws.rs.core.Response.Status status) { diff --git a/src/main/java/dhrim/zeplchallenge/todo/ThrowableExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/ThrowableExceptionMapper.java new file mode 100644 index 0000000..b2c96b9 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/ThrowableExceptionMapper.java @@ -0,0 +1,23 @@ +package dhrim.zeplchallenge.todo; + + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpStatus; + +import javax.inject.Singleton; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +@Singleton +@Slf4j +public class ThrowableExceptionMapper extends AbstractExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(Throwable e) { + log.error("Server failed.", e); + return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR_500, e.getMessage()); + } + +} diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java index 41f99c3..6d61756 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java @@ -60,9 +60,7 @@ public Response getTodoTasksOfStatusNotDone(@PathParam("todoId") String todoId) @Consumes("application/json") @Produces("application/json") public Response createTodo(Todo todo) throws IOException { - System.out.println("ASDF : todo="+todo); Todo newTodo = todoService.createTodo(todo); - System.out.println("ASDF : newTodo="+newTodo); return new ResponseBuilder(Response.Status.OK, newTodo).build(); } diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java b/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java index 8bd7bd2..0736d64 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java @@ -8,7 +8,6 @@ import com.google.inject.util.Modules; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -32,7 +31,6 @@ public void start(int port) throws Exception { ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/"); // GuiceFilter is added. to inject instance when http requested servletContextHandler.addFilter(GuiceFilter.class, "/*", null); - servletContextHandler.addServlet(DefaultServlet.class, "/"); try { server.start(); diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoService.java b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java index 01d3c27..1f86487 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoService.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java @@ -49,7 +49,9 @@ public Todo createTodo(Todo todo) { if(todo.getName()==null) { throw new IllegalArgumentException("todo.name is null. todo="+todo); } todo.setId(UUID.randomUUID().toString()); todo.setCreated(new Date()); - return todoRepo.saveOrUpdate(todo); + todo = todoRepo.saveOrUpdate(todo); + log.debug("New Todo created. todo={}", todo); + return todo; } public Task createTask(String todoId, Task task) { diff --git a/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java index f7335fb..d197c60 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java +++ b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java @@ -2,24 +2,31 @@ import com.google.inject.Module; import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.methods.*; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.eclipse.jetty.http.HttpHeader; import java.io.IOException; +import java.text.SimpleDateFormat; import static org.junit.Assert.assertEquals; public abstract class AbstractTestBase { + + protected static final String GET = "GET"; + protected static final String POST = "POST"; + protected static final String PUT = "PUT"; + protected static final String DELETE = "DELETE"; + protected static final int PORT = 2222; protected static final String BASE_URL = "http://localhost:+"+PORT; private static TodoServer todoServer; - protected ObjectMapper objectMapper = new ObjectMapper(); + protected ObjectMapper objectMapper; /** return mock binding configured google Guice module. **/ protected abstract Module getMockBinding(); @@ -33,6 +40,7 @@ protected void initObjectMapperIfNotCreated() { if(objectMapper!=null) { return; } objectMapper = new ObjectMapper(); objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS")); } protected void after() { @@ -51,14 +59,44 @@ protected void shutdownServer() { } + protected String sendAndGetResponseBody(String httpMethod, String path, int expectedStatusCode) throws IOException { + return sendAndGetResponseBody(httpMethod, path, null, expectedStatusCode); + } - protected String sendAndGetResponseBody(String url, Object requestBodyObject, int expectedStatusCode) throws IOException { + protected String sendAndGetResponseBody(String httpMethod, String path, Object requestBodyObject, int expectedStatusCode) throws IOException { + + String url = BASE_URL+path; + HttpMethod method = null; + switch (httpMethod) { + case GET : + method = new GetMethod(url); + break; + case POST : + method = new PostMethod(url); + break; + case PUT : + method = new PutMethod(url); + break; + case DELETE : + method = new DeleteMethod(url); + break; + default : + throw new RuntimeException("invalid httpMethod. '"+httpMethod+"'"); + } + switch(httpMethod) { + case POST: + case PUT: + if(requestBodyObject!=null) { + StringRequestEntity stringRequestEntity = new StringRequestEntity(objectMapper.writeValueAsString(requestBodyObject), "application/json", "UTF-8"); + method.setRequestHeader(HttpHeader.CONTENT_TYPE.asString(), "application/json"); + ((EntityEnclosingMethod)method).setRequestEntity(stringRequestEntity); + } + break; + default: + break; + } HttpClient httpClient = new HttpClient(); - PostMethod method = new PostMethod(url); - StringRequestEntity stringRequestEntity = new StringRequestEntity(objectMapper.writeValueAsString(requestBodyObject), "application/json", "UTF-8"); - method.setRequestHeader(HttpHeader.CONTENT_TYPE.asString(), "application/json"); - method.setRequestEntity(stringRequestEntity); httpClient.executeMethod(method); assertEquals(expectedStatusCode, method.getStatusCode()); diff --git a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java index 1daa15f..e3aa4c2 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java @@ -48,7 +48,7 @@ public void test_createTodo() throws Exception { // WHEN Todo todo = new TodoBuilder().name("name1").build(); - String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", todo, HttpStatus.OK_200); + String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, HttpStatus.OK_200); // THEN // {"id":"1e0840ec-6cbf-41d4-8c83-83fb6f8bf8f7","name":"name1","created":1482992669764} diff --git a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java index c76b01f..cfb5748 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java @@ -2,22 +2,30 @@ import com.google.inject.AbstractModule; import com.google.inject.Module; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.methods.GetMethod; +import lombok.Data; +import org.codehaus.jackson.type.TypeReference; import org.eclipse.jetty.http.HttpStatus; +import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; +import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; public class TodoRestApiTest extends AbstractTestBase { + + private MockMemoryTodoRepo mockTodoRepo = new MockMemoryTodoRepo(); + @Before public void before() throws Exception { super.before(); @@ -26,6 +34,7 @@ public void before() throws Exception { @After public void after() { super.after(); + mockTodoRepo.clear_for_test(); } @Override @@ -33,44 +42,83 @@ protected Module getMockBinding() { return new AbstractModule() { @Override protected void configure() { - bind(TodoRepo.class).toInstance(new MockMemoryTodoRepo()); + bind(TodoRepo.class).toInstance(mockTodoRepo); } }; } + @Data private static class MockMemoryTodoRepo extends AbstractMapBasedTodoRepo { + public Map todoMap = new HashMap(); + public Map> tasksMap = new HashMap>(); + @Override protected Map getTodoMapInstance() { - return new HashMap(); + return todoMap; } @Override protected Map> getTasksMapInstance() { - return new HashMap>(); + return tasksMap; } - } + @Test + public void test_when_invalid_path() throws IOException { + + // GIVEN + Object emptyString = ""; + + // WHEN, THEN + sendAndGetResponseBody(GET, "/invalid_path", emptyString, HttpStatus.NOT_FOUND_404); + + } + @Test public void test_createTodo() throws Exception { // GIVEN Todo todo = new TodoBuilder().name("name1").build(); - // WHEN - String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", todo, HttpStatus.OK_200); + // create one + { + // WHEN + String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, HttpStatus.OK_200); - // THEN - // {"id":"1e0840ec-6cbf-41d4-8c83-83fb6f8bf8f7","name":"name1","created":1482992669764} - assertNotNull(responseBodyString); - Todo actualTodo = objectMapper.readValue(responseBodyString, Todo.class); + // THEN + // {"id":"ad070105-64d4-4646-a06b-6c150114af32","name":"name1","created":"2016-12-30 04:27:22.090"} + assertNotNull(responseBodyString); + Todo actualTodo = objectMapper.readValue(responseBodyString, Todo.class); + + assertNotNull(actualTodo.getId()); + assertEquals(todo.getName(), actualTodo.getName()); + assertNotNull(actualTodo.getCreated()); + + // expect 1 todo in repo + assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, 1, mockTodoRepo.todoMap.size()); + } - assertNotNull(actualTodo.getId()); - assertEquals(todo.getName(), actualTodo.getName()); - assertNotNull(actualTodo.getCreated()); + + // create again + { + // WHEN + String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, HttpStatus.OK_200); + + // THEN + // {"id":"ad070105-64d4-4646-a06b-6c150114af32","name":"name1","created":"2016-12-30 04:27:22.090"} + assertNotNull(responseBodyString); + Todo actualTodo = objectMapper.readValue(responseBodyString, Todo.class); + + assertNotNull(actualTodo.getId()); + assertEquals(todo.getName(), actualTodo.getName()); + assertNotNull(actualTodo.getCreated()); + + // expect 2 todos in repo + assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, 2, mockTodoRepo.todoMap.size()); + } } @@ -82,29 +130,52 @@ public void test_createTodo_failed_when_empty_input() throws IOException { Object emptyString = ""; // WHEN - String responseBodyString = sendAndGetResponseBody(BASE_URL+"/todos", emptyString, HttpStatus.BAD_REQUEST_400); + String responseBodyString = sendAndGetResponseBody(POST, "/todos", emptyString, HttpStatus.BAD_REQUEST_400); // THEN - FailedMessage failedMessage =objectMapper.readValue(responseBodyString, FailedMessage.class); + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); assertNotNull(failedMessage.getMessage()); } + @Test + public void test_getTodos_when_empty() throws Exception { + + // GIVEN + + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos", HttpStatus.OK_200); + + // THEN + + // just assert '[]' + List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); + assertEquals(0, todoList.size()); + + } + @Test public void test_getTodos() throws Exception { // GIVEN + Todo expectedTodo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + expectedTodo1.setCreated(null); + mockTodoRepo.todoMap.put(expectedTodo1.getId(), expectedTodo1); + Todo expectedTodo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build(); + expectedTodo2.setCreated(null); + mockTodoRepo.todoMap.put(expectedTodo2.getId(), expectedTodo2); // WHEN - String url = BASE_URL+"/todos"; - HttpClient httpClient = new HttpClient(); - GetMethod method = new GetMethod(url); - httpClient.executeMethod(method); + String responseBodyString = sendAndGetResponseBody(GET, "/todos", HttpStatus.OK_200); // THEN - assertEquals(HttpStatus.OK_200, method.getStatusCode()); + List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); + // THEN + assertEquals(2, todoList.size()); + assertThat(todoList, CoreMatchers.hasItem(expectedTodo1)); + assertThat(todoList, CoreMatchers.hasItem(expectedTodo2)); } diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index ef0e27a..b142738 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -24,7 +24,7 @@ - + From 1b12320680d668db60d32a4ecb0e870f368da617 Mon Sep 17 00:00:00 2001 From: dohyougn rim Date: Sun, 1 Jan 2017 19:20:40 +0900 Subject: [PATCH 5/7] implement all rest api and all logic. --- .../todo/AbstractMapBasedTodoRepo.java | 64 +- .../java/dhrim/zeplchallenge/todo/Main.java | 31 +- .../zeplchallenge/todo/MapDbTodoRepo.java | 9 +- .../zeplchallenge/todo/ResponseBuilder.java | 11 +- .../dhrim/zeplchallenge/todo/TodoRepo.java | 2 + .../dhrim/zeplchallenge/todo/TodoRestApi.java | 48 +- .../dhrim/zeplchallenge/todo/TodoServer.java | 7 - .../dhrim/zeplchallenge/todo/TodoService.java | 108 ++- .../AbstractExceptionMapper.java | 4 +- .../IllegalArgumentExceptionMapper.java | 2 +- .../NotFoundExceptionMapper.java | 2 +- .../ThrowableExceptionMapper.java | 2 +- .../WebApplicationExceptionMapper.java | 23 + .../zeplchallenge/todo/AbstractTestBase.java | 30 +- .../zeplchallenge/todo/IntegrationTest.java | 3 +- ...odRepoTest.java => MapDbTodoRepoTest.java} | 4 +- .../dhrim/zeplchallenge/todo/TaskBuilder.java | 49 ++ .../zeplchallenge/todo/TodoRestApiTest.java | 643 +++++++++++++++++- todo-challenge-explanation.md | 32 + 19 files changed, 964 insertions(+), 110 deletions(-) rename src/main/java/dhrim/zeplchallenge/todo/{ => exceptionhandler}/AbstractExceptionMapper.java (82%) rename src/main/java/dhrim/zeplchallenge/todo/{ => exceptionhandler}/IllegalArgumentExceptionMapper.java (91%) rename src/main/java/dhrim/zeplchallenge/todo/{ => exceptionhandler}/NotFoundExceptionMapper.java (92%) rename src/main/java/dhrim/zeplchallenge/todo/{ => exceptionhandler}/ThrowableExceptionMapper.java (91%) create mode 100644 src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/WebApplicationExceptionMapper.java rename src/test/java/dhrim/zeplchallenge/todo/{MapDbTodRepoTest.java => MapDbTodoRepoTest.java} (89%) create mode 100644 src/test/java/dhrim/zeplchallenge/todo/TaskBuilder.java create mode 100644 todo-challenge-explanation.md diff --git a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java index ed2d630..bb831f2 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java +++ b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,24 +17,27 @@ public abstract class AbstractMapBasedTodoRepo implements TodoRepo { // todo.id -> Todo private Map todoMap; - // todo.id -> List - private Map> tasksMap; + // todo.id -> Map + private Map> taskMapMap; + /** return Map which store todoId to Todo */ protected abstract Map getTodoMapInstance(); - protected abstract Map> getTasksMapInstance(); + + /** return Map which store todoId to Map */ + protected abstract Map> getTaskMapMapInstance(); protected void initIfNot() { - if(todoMap!=null) { return; } + if(todoMap !=null) { return; } todoMap = getTodoMapInstance(); - tasksMap = getTasksMapInstance(); + taskMapMap = getTaskMapMapInstance(); } @VisibleForTesting void clear_for_test() { // could be null if initIfNot() not called. - if(todoMap==null) { return; } + if(todoMap ==null) { return; } todoMap.clear(); - tasksMap.clear(); + taskMapMap.clear(); } @Override @@ -45,7 +49,7 @@ public List getTodoList() { @Override public Todo getTodo(String todoId) { initIfNot(); - if(todoId==null) { throw new IllegalArgumentException("todoId is null"); } + if(todoId==null) { throw new IllegalArgumentException("todoId is null."); } return todoMap.get(todoId); } @@ -61,28 +65,52 @@ public Todo saveOrUpdate(Todo todo) { @Override public List getTaskList(String todoId) { initIfNot(); - if(todoId==null) { throw new IllegalArgumentException("todoId is null"); } - return new ArrayList(tasksMap.get(todoId).values()); + if(todoId==null) { throw new IllegalArgumentException("todoId is null."); } + return new ArrayList(taskMapMap.get(todoId).values()); } @Override public Task getTask(String todoId, String taskId) { initIfNot(); - if(todoId==null) { throw new IllegalArgumentException("todoId is null"); } - if(taskId==null) { throw new IllegalArgumentException("taskId is null"); } - Map aTaskMap = tasksMap.get(todoId); - return aTaskMap.get(taskId); + if(todoId==null) { throw new IllegalArgumentException("todoId is null."); } + if(taskId==null) { throw new IllegalArgumentException("taskId is null."); } + Map taskMap = taskMapMap.get(todoId); + if(taskMap==null) { return null; } + Task task = taskMap.get(taskId); + if(task==null) { throw new IllegalArgumentException("task not found. todoId="+todoId+", taskId="+taskId); } + return task; } @Override public Task saveOrUpdate(String todoId, Task task) { initIfNot(); - if(todoId==null) { throw new IllegalArgumentException("todoId is null"); } + if(todoId==null) { throw new IllegalArgumentException("todoId is null."); } if(task.getId()==null) { throw new IllegalArgumentException("task.id is null. task="+task); } - Map aTaskMap = tasksMap.get(todoId); - if(aTaskMap==null) { return null; } - aTaskMap.put(task.getId(), task); + Map taskMap = taskMapMap.get(todoId); + if(taskMap==null) { + taskMap = new HashMap<>(); + taskMapMap.put(todoId, taskMap); + } + taskMap.put(task.getId(), task); return getTask(todoId, task.getId()); } + + @Override + public void removeTodo(String todoId) { + initIfNot(); + if(todoId==null) { throw new IllegalArgumentException("todoId is null."); } + todoMap.remove(todoId); + } + + @Override + public void removeTask(String todoId, String taskId) { + initIfNot(); + if(todoId==null) { throw new IllegalArgumentException("todoId is null."); } + if(taskId==null) { throw new IllegalArgumentException("taskId is null."); } + Map taskMap = taskMapMap.get(todoId); + if(taskMap==null) { return; } + taskMap.remove(taskId); + } + } diff --git a/src/main/java/dhrim/zeplchallenge/todo/Main.java b/src/main/java/dhrim/zeplchallenge/todo/Main.java index 6909367..5e35136 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/Main.java +++ b/src/main/java/dhrim/zeplchallenge/todo/Main.java @@ -1,16 +1,19 @@ package dhrim.zeplchallenge.todo; /** - * TODO : describe + * Main start class. + * + * Port could be configured with option '-p' or default port 8080 is used. + * */ public class Main { - public static final int DEFAULT_PORT = 2222; + public static final int DEFAULT_PORT = 8080; + private static final String OPTIONS_PORT = "-p"; public static void main(String[] args) throws Exception { - // TODO : read from args - int port = DEFAULT_PORT; + int port = parsePort(args); TodoServer todoServer = TodoServer.getInstance(); @@ -23,4 +26,24 @@ public static void main(String[] args) throws Exception { } + private static int parsePort(String[] args) { + if(args.length==0) { return DEFAULT_PORT; } + if(args.length==2 && OPTIONS_PORT.equals(args[0])) { + try { + return Integer.parseInt(args[1]); + } catch(NumberFormatException e) { + // ignore + } + } + showUsage(); + System.exit(1); + return 0; + } + + private static void showUsage() { + System.out.println("USAGE"); + System.out.println(" java "+Main.class.getName()+" : with default port "+DEFAULT_PORT); + System.out.println(" java "+Main.class.getName()+" "+OPTIONS_PORT+" port"); + } + } diff --git a/src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java index 1b927d8..1b171d5 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java +++ b/src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java @@ -34,8 +34,10 @@ private void initDbIfNot() { if(db!=null) { return; } db = DBMaker .fileDB(DB_FILE_NAME) - .closeOnJvmShutdown() + .closeOnJvmShutdownWeakReference() + .checksumHeaderBypass() .make(); + log.info("File DbMap is initialized with file "+DB_FILE_NAME); } @@ -46,7 +48,7 @@ protected Map getTodoMapInstance() { } @Override - protected Map> getTasksMapInstance() { + protected Map> getTaskMapMapInstance() { initDbIfNot(); return db.hashMap("tasksMap", Serializer.STRING, serializer).createOrOpen(); } @@ -55,14 +57,13 @@ protected void clear() { new File(DB_FILE_NAME).delete(); } - public class ObjectSerializer implements Serializer, Serializable { + private class ObjectSerializer implements Serializer, Serializable { @Override public void serialize(@NotNull DataOutput2 out, @NotNull Object value) throws IOException { if(!(value instanceof Serializable)) { throw new IOException("value class is not implements Serializable. value="+value); } - // TODO : check if value is Serializable try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutput objectOut = new ObjectOutputStream(bos)) { objectOut.writeObject(value); out.write(bos.toByteArray()); diff --git a/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java b/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java index e968594..6ff545f 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java +++ b/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java @@ -7,6 +7,9 @@ import java.io.IOException; import java.text.SimpleDateFormat; +/** + * Build http rest response. + */ public class ResponseBuilder { private javax.ws.rs.core.Response.Status status; @@ -15,9 +18,10 @@ public class ResponseBuilder { // set as static so as to always use same instance to reuse parsing result. // ObjectMapper is thread safe by itself. private static ObjectMapper objectMapper = new ObjectMapper(); + static { objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL); - objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS")); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")); } public ResponseBuilder(javax.ws.rs.core.Response.Status status) { @@ -32,7 +36,10 @@ public ResponseBuilder(javax.ws.rs.core.Response.Status status, Object body) { public javax.ws.rs.core.Response build() throws IOException { - String json = objectMapper.writeValueAsString(body); + String json = "{}"; + if(body!=null) { + json = objectMapper.writeValueAsString(body); + } Response.ResponseBuilder r = Response.status(status).entity(json); return r.build(); } diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java index 4996209..193a844 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java @@ -6,8 +6,10 @@ public interface TodoRepo { List getTodoList(); Todo getTodo(String todoId); Todo saveOrUpdate(Todo todo); + void removeTodo(String todoId); List getTaskList(String todoId); Task getTask(String todoId, String taskId); Task saveOrUpdate(String todoId, Task task); + void removeTask(String todoId, String taskId); } diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java index 6d61756..e3e9254 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java @@ -18,7 +18,7 @@ public class TodoRestApi { @GET @Path("/todos") @Produces("application/json") - public Response getTodos() throws IOException { + public Response getTodoList() throws IOException { List todoList = todoService.getTodoList(); return new ResponseBuilder(Response.Status.OK, todoList).build(); } @@ -26,7 +26,15 @@ public Response getTodos() throws IOException { @GET @Path("/todos/{todoId}") @Produces("application/json") - public Response getTodoTasks(@PathParam("todoId") String todoId) throws IOException { + public Response getTodoAsList(@PathParam("todoId") String todoId) throws IOException { + List todoList = todoService.getTodoAsList(todoId); + return new ResponseBuilder(Response.Status.OK, todoList).build(); + } + + @GET + @Path("/todos/{todoId}/tasks") + @Produces("application/json") + public Response getTaskList(@PathParam("todoId") String todoId) throws IOException { List taskList = todoService.getTaskList(todoId); return new ResponseBuilder(Response.Status.OK, taskList).build(); } @@ -34,24 +42,24 @@ public Response getTodoTasks(@PathParam("todoId") String todoId) throws IOExcept @GET @Path("/todos/{todoId}/tasks/{taskId}") @Produces("application/json") - public Response getTodoTask(@PathParam("todoId") String todoId, @PathParam("taskId") String taskId) throws IOException { - Task task = todoService.getTodoTask(todoId, taskId); + public Response getTask(@PathParam("todoId") String todoId, @PathParam("taskId") String taskId) throws IOException { + Task task = todoService.getTask(todoId, taskId); return new ResponseBuilder(Response.Status.OK, task).build(); } @GET - @Path("/todos/{todoId}/tasks/done}") + @Path("/todos/{todoId}/tasks/done") @Produces("application/json") - public Response getTodoTasksOfStatusDone(@PathParam("todoId") String todoId) throws IOException { - List taskList = todoService.getTodoTaskList(todoId, Task.Status.DONE); + public Response getTaskListOfStatusDone(@PathParam("todoId") String todoId) throws IOException { + List taskList = todoService.getTaskList(todoId, Task.Status.DONE); return new ResponseBuilder(Response.Status.OK, taskList).build(); } @GET - @Path("/todos/{todoId}/tasks/not-done}") + @Path("/todos/{todoId}/tasks/not-done") @Produces("application/json") - public Response getTodoTasksOfStatusNotDone(@PathParam("todoId") String todoId) throws IOException { - List taskList = todoService.getTodoTaskList(todoId, Task.Status.NOT_DONE); + public Response getTaskListOfStatusNotDone(@PathParam("todoId") String todoId) throws IOException { + List taskList = todoService.getTaskList(todoId, Task.Status.NOT_DONE); return new ResponseBuilder(Response.Status.OK, taskList).build(); } @@ -74,6 +82,16 @@ public Response createTask(@PathParam("todoId") String todoId, Task task) throws } @PUT + @Path("/todos/{todoId}/tasks/{taskId}") + @Consumes("application/json") + @Produces("application/json") + public Response updateTask(@PathParam("todoId") String todoId, @PathParam("taskId") String taskId, Task task) throws IOException { + Task newTask = todoService.updateTask(todoId, taskId, task); + return new ResponseBuilder(Response.Status.OK, newTask).build(); + } + + + @DELETE @Path("/todos/{todoId}") @Produces("application/json") public Response deleteTodo(@PathParam("todoId") String todoId) throws IOException { @@ -84,17 +102,9 @@ public Response deleteTodo(@PathParam("todoId") String todoId) throws IOExceptio @DELETE @Path("/todos/{todoId}/tasks/{taskId}") @Produces("application/json") - public Response deleteTodoTask(@PathParam("todoId") String todoId, @PathParam("taskId") String taskId) throws IOException { + public Response deleteTask(@PathParam("todoId") String todoId, @PathParam("taskId") String taskId) throws IOException { todoService.deleteTask(todoId, taskId); return new ResponseBuilder(Response.Status.OK).build(); } - // TODO : remove - @GET - @Path("/hello") - public String hello() { - return "hello"; - } - - } diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java b/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java index 0736d64..76cf97d 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java @@ -11,7 +11,6 @@ import org.eclipse.jetty.servlet.ServletContextHandler; -// TODO : treat exception @Slf4j public class TodoServer { @@ -74,7 +73,6 @@ public static TodoServer getInstance() { return getInstanceWithMockBinding(); } - private static Injector injector; @VisibleForTesting @@ -84,11 +82,6 @@ static TodoServer getInstanceWithMockBinding(Module... additionalModules) { return injector.getInstance(TodoServer.class); } - @VisibleForTesting - static Object getDiInstance(Class clazz) { - return injector.getInstance(clazz); - } - } diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoService.java b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java index 1f86487..3a8525f 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoService.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java @@ -4,9 +4,11 @@ import lombok.extern.slf4j.Slf4j; import javax.inject.Inject; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; @Singleton @Slf4j @@ -19,50 +21,112 @@ public List getTodoList() { return todoRepo.getTodoList(); } - public Todo getTodoList(String todoId) { - throw new RuntimeException("not implemented"); + public List getTodoAsList(String todoId) { + validateTodoId(todoId); + Todo todo = todoRepo.getTodo(todoId); + List todoList = new ArrayList<>(); + if(todo!=null) { todoList.add(todo); } + return todoList; } - public List getTaskList(String todoId) { - throw new RuntimeException("not implemented"); + public Task getTask(String todoId, String taskId) { + validateTodoExist(todoId); + validateTaskId(taskId); + return todoRepo.getTask(todoId, taskId); } - public Task getTodoTask(String todoId, String taskId) { - throw new RuntimeException("not implemented"); + public List getTaskList(String todoId, Task.Status status) { + List taskList = getTaskList(todoId); + validateStatus(status); + return taskList.stream().filter(task -> task.getStatus()==status).collect(Collectors.toList()); } - public List getTodoTaskList(String todoId, Task.Status done) { - throw new RuntimeException("not implemented"); + + public List getTaskList(String todoId) { + validateTodoExist(todoId); + return todoRepo.getTaskList(todoId); } - /** - * Create new Todo instance, - * - * id and created are overwritten with auto generated. - * - * @param todo - * @return created new instance - * @throws throw IllegalArgumentException when todo is null or todo.name is null - */ public Todo createTodo(Todo todo) { - if(todo==null) { throw new IllegalArgumentException("todo is null."); } - if(todo.getName()==null) { throw new IllegalArgumentException("todo.name is null. todo="+todo); } + + validateTodo(todo); + todo.setId(UUID.randomUUID().toString()); todo.setCreated(new Date()); todo = todoRepo.saveOrUpdate(todo); log.debug("New Todo created. todo={}", todo); + return todo; + } public Task createTask(String todoId, Task task) { - throw new RuntimeException("not implemented"); + + validateTodoExist(todoId); + validateTask(task); + + task.setId(UUID.randomUUID().toString()); + task.setCreated(new Date()); + task.setStatus(Task.Status.NOT_DONE); + task = todoRepo.saveOrUpdate(todoId, task); + + log.debug("New Task created. task={}", task); + + return task; + + } + + public Task updateTask(String todoId, String taskId, Task newTask) { + validateTask(newTask); + Task oldTask = getTask(todoId, taskId); + oldTask.setName(newTask.getName()); + oldTask.setDescription(newTask.getDescription()); + oldTask.setStatus(newTask.getStatus()); + todoRepo.saveOrUpdate(todoId, oldTask); + return todoRepo.getTask(todoId, taskId); } public void deleteTodo(String todoId) { - throw new RuntimeException("not implemented"); + validateTodoExist(todoId); + todoRepo.removeTodo(todoId); } public void deleteTask(String todoId, String taskId) { - throw new RuntimeException("not implemented"); + Task task = getTask(todoId, taskId); + if(task==null) { throw new IllegalArgumentException("task not exist for todoId "+todoId+" and taskId "+taskId); } + todoRepo.removeTask(todoId, taskId); } + + + + private void validateTodoId(String todoId) { + if(todoId==null) { throw new IllegalArgumentException("todoId is null."); } + } + + private void validateTaskId(String taskId) { + if(taskId==null) { throw new IllegalArgumentException("taskId is null."); } + } + + private void validateTodoExist(String todoId) { + validateTodoId(todoId); + Todo todo = todoRepo.getTodo(todoId); + if(todo==null) { throw new IllegalArgumentException("todo not exist for todoId "+todoId); } + } + + private void validateTodo(Todo todo) { + if(todo==null) { throw new IllegalArgumentException("todo is null."); } + if(todo.getName()==null) { throw new IllegalArgumentException("todo is empty."); } + } + + private void validateTask(Task task) { + if(task==null) { throw new IllegalArgumentException("task is null."); } + if(task.getName()==null && task.getDescription()==null && task.getStatus()==null) { throw new IllegalArgumentException("task is empty."); } + } + + private void validateStatus(Task.Status status) { + if (status == null) { + throw new IllegalArgumentException("status is null."); + } + } + } diff --git a/src/main/java/dhrim/zeplchallenge/todo/AbstractExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/AbstractExceptionMapper.java similarity index 82% rename from src/main/java/dhrim/zeplchallenge/todo/AbstractExceptionMapper.java rename to src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/AbstractExceptionMapper.java index bfac92d..4aed35b 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/AbstractExceptionMapper.java +++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/AbstractExceptionMapper.java @@ -1,4 +1,6 @@ -package dhrim.zeplchallenge.todo; +package dhrim.zeplchallenge.todo.exceptionhandler; + +import dhrim.zeplchallenge.todo.FailedMessage; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; diff --git a/src/main/java/dhrim/zeplchallenge/todo/IllegalArgumentExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/IllegalArgumentExceptionMapper.java similarity index 91% rename from src/main/java/dhrim/zeplchallenge/todo/IllegalArgumentExceptionMapper.java rename to src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/IllegalArgumentExceptionMapper.java index d96f596..8e356f3 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/IllegalArgumentExceptionMapper.java +++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/IllegalArgumentExceptionMapper.java @@ -1,4 +1,4 @@ -package dhrim.zeplchallenge.todo; +package dhrim.zeplchallenge.todo.exceptionhandler; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/dhrim/zeplchallenge/todo/NotFoundExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/NotFoundExceptionMapper.java similarity index 92% rename from src/main/java/dhrim/zeplchallenge/todo/NotFoundExceptionMapper.java rename to src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/NotFoundExceptionMapper.java index 8a50548..8f29fee 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/NotFoundExceptionMapper.java +++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/NotFoundExceptionMapper.java @@ -1,4 +1,4 @@ -package dhrim.zeplchallenge.todo; +package dhrim.zeplchallenge.todo.exceptionhandler; import com.sun.jersey.api.NotFoundException; diff --git a/src/main/java/dhrim/zeplchallenge/todo/ThrowableExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/ThrowableExceptionMapper.java similarity index 91% rename from src/main/java/dhrim/zeplchallenge/todo/ThrowableExceptionMapper.java rename to src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/ThrowableExceptionMapper.java index b2c96b9..c205004 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/ThrowableExceptionMapper.java +++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/ThrowableExceptionMapper.java @@ -1,4 +1,4 @@ -package dhrim.zeplchallenge.todo; +package dhrim.zeplchallenge.todo.exceptionhandler; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/WebApplicationExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/WebApplicationExceptionMapper.java new file mode 100644 index 0000000..8950c44 --- /dev/null +++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/WebApplicationExceptionMapper.java @@ -0,0 +1,23 @@ +package dhrim.zeplchallenge.todo.exceptionhandler; + + +import lombok.extern.slf4j.Slf4j; + +import javax.inject.Singleton; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +@Singleton +@Slf4j +public class WebApplicationExceptionMapper extends AbstractExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(WebApplicationException e) { + log.debug("Invalid request.", e); + return buildResponse(e.getResponse().getStatus(), e.getMessage()); + } + +} diff --git a/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java index d197c60..2808e22 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java +++ b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java @@ -10,8 +10,11 @@ import java.io.IOException; import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public abstract class AbstractTestBase { @@ -40,7 +43,7 @@ protected void initObjectMapperIfNotCreated() { if(objectMapper!=null) { return; } objectMapper = new ObjectMapper(); objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL); - objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS")); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")); } protected void after() { @@ -86,9 +89,10 @@ protected String sendAndGetResponseBody(String httpMethod, String path, Object r switch(httpMethod) { case POST: case PUT: + method.setRequestHeader(HttpHeader.CONTENT_TYPE.asString(), "application/json"); if(requestBodyObject!=null) { - StringRequestEntity stringRequestEntity = new StringRequestEntity(objectMapper.writeValueAsString(requestBodyObject), "application/json", "UTF-8"); - method.setRequestHeader(HttpHeader.CONTENT_TYPE.asString(), "application/json"); + String jsonBody = objectMapper.writeValueAsString(requestBodyObject); + StringRequestEntity stringRequestEntity = new StringRequestEntity(jsonBody, "application/json", "UTF-8"); ((EntityEnclosingMethod)method).setRequestEntity(stringRequestEntity); } break; @@ -101,8 +105,26 @@ protected String sendAndGetResponseBody(String httpMethod, String path, Object r assertEquals(expectedStatusCode, method.getStatusCode()); - return method.getResponseBodyAsString(); + String responseBody = method.getResponseBodyAsString(); + System.out.println("responseBody=" + responseBody); + return responseBody; + + } + + + protected void assertEmpty(Todo todo) { + assertNull("todo is not empty. todo="+todo, todo.getId()); + assertNull("todo is not empty. todo="+todo, todo.getName()); + assertNull("todo is not empty. todo="+todo, todo.getCreated()); + } + + protected void assertEmpty(Task task) { + assertNull("todo is not empty. todo="+task, task.getId()); + assertNull("todo is not empty. todo="+task, task.getDescription()); + assertNull("todo is not empty. todo="+task, task.getName()); + assertNull("todo is not empty. todo="+task, task.getStatus()); + assertNull("todo is not empty. todo="+task, task.getCreated()); } diff --git a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java index e3aa4c2..cd809fa 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java @@ -51,7 +51,6 @@ public void test_createTodo() throws Exception { String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, HttpStatus.OK_200); // THEN - // {"id":"1e0840ec-6cbf-41d4-8c83-83fb6f8bf8f7","name":"name1","created":1482992669764} assertNotNull(responseBodyString); Todo actualTodo = objectMapper.readValue(responseBodyString, Todo.class); @@ -62,7 +61,7 @@ public void test_createTodo() throws Exception { } @Test - public void test_getTodos() throws Exception { + public void test_getTodoList() throws Exception { // GIVEN diff --git a/src/test/java/dhrim/zeplchallenge/todo/MapDbTodRepoTest.java b/src/test/java/dhrim/zeplchallenge/todo/MapDbTodoRepoTest.java similarity index 89% rename from src/test/java/dhrim/zeplchallenge/todo/MapDbTodRepoTest.java rename to src/test/java/dhrim/zeplchallenge/todo/MapDbTodoRepoTest.java index 5ee2b5f..06b0762 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/MapDbTodRepoTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/MapDbTodoRepoTest.java @@ -5,7 +5,7 @@ import static org.junit.Assert.assertEquals; -public class MapDbTodRepoTest { +public class MapDbTodoRepoTest { private MapDbTodoRepo repo = new MapDbTodoRepo(); @@ -16,7 +16,7 @@ public void before() { @Test - public void test_createTodo_success () { + public void test_saveOrUpdate_and_getTodo () { // GIVEN Todo orgTodo = new TodoBuilder().id("id1").name("name1").build(); diff --git a/src/test/java/dhrim/zeplchallenge/todo/TaskBuilder.java b/src/test/java/dhrim/zeplchallenge/todo/TaskBuilder.java new file mode 100644 index 0000000..8de1627 --- /dev/null +++ b/src/test/java/dhrim/zeplchallenge/todo/TaskBuilder.java @@ -0,0 +1,49 @@ +package dhrim.zeplchallenge.todo; + +import java.util.Date; + +public class TaskBuilder { + + private String id = null; + private String name ="default_task_name"; + private String description; + private Task.Status status; + private Date created; + + + public Task build() { + Task task = new Task(); + task.setId(id); + task.setName(name); + task.setDescription(description); + task.setStatus(status); + task.setCreated(created); + return task; + } + + public TaskBuilder id(String id) { + this.id = id; + return this; + } + + public TaskBuilder name(String name) { + this.name = name; + return this; + } + + public TaskBuilder description(String description) { + this.description = description; + return this; + } + + public TaskBuilder status(Task.Status status) { + this.status = status; + return this; + } + + public TaskBuilder created(Date created) { + this.created = created; + return this; + } + +} diff --git a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java index cfb5748..b685cac 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java @@ -16,10 +16,9 @@ import java.util.List; import java.util.Map; -import static org.hamcrest.collection.IsIterableContainingInOrder.contains; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; +import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400; +import static org.eclipse.jetty.http.HttpStatus.OK_200; +import static org.junit.Assert.*; public class TodoRestApiTest extends AbstractTestBase { @@ -50,8 +49,8 @@ protected void configure() { @Data private static class MockMemoryTodoRepo extends AbstractMapBasedTodoRepo { - public Map todoMap = new HashMap(); - public Map> tasksMap = new HashMap>(); + public Map todoMap = new HashMap<>(); + public Map> tasksMap = new HashMap<>(); @Override protected Map getTodoMapInstance() { @@ -59,7 +58,7 @@ protected Map getTodoMapInstance() { } @Override - protected Map> getTasksMapInstance() { + protected Map> getTaskMapMapInstance() { return tasksMap; } @@ -86,7 +85,7 @@ public void test_createTodo() throws Exception { // create one { // WHEN - String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, HttpStatus.OK_200); + String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, OK_200); // THEN // {"id":"ad070105-64d4-4646-a06b-6c150114af32","name":"name1","created":"2016-12-30 04:27:22.090"} @@ -97,8 +96,8 @@ public void test_createTodo() throws Exception { assertEquals(todo.getName(), actualTodo.getName()); assertNotNull(actualTodo.getCreated()); - // expect 1 todo in repo - assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, 1, mockTodoRepo.todoMap.size()); + final int EXPECTED_TODO_SIZE = 1; + assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, EXPECTED_TODO_SIZE, mockTodoRepo.todoMap.size()); } @@ -116,8 +115,8 @@ public void test_createTodo() throws Exception { assertEquals(todo.getName(), actualTodo.getName()); assertNotNull(actualTodo.getCreated()); - // expect 2 todos in repo - assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, 2, mockTodoRepo.todoMap.size()); + final int EXPECTED_TODO_SIZE = 2; + assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, EXPECTED_TODO_SIZE, mockTodoRepo.todoMap.size()); } } @@ -130,7 +129,7 @@ public void test_createTodo_failed_when_empty_input() throws IOException { Object emptyString = ""; // WHEN - String responseBodyString = sendAndGetResponseBody(POST, "/todos", emptyString, HttpStatus.BAD_REQUEST_400); + String responseBodyString = sendAndGetResponseBody(POST, "/todos", emptyString, BAD_REQUEST_400); // THEN FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); @@ -140,15 +139,14 @@ public void test_createTodo_failed_when_empty_input() throws IOException { @Test - public void test_getTodos_when_empty() throws Exception { + public void test_getTodoList_when_empty() throws Exception { // GIVEN // WHEN - String responseBodyString = sendAndGetResponseBody(GET, "/todos", HttpStatus.OK_200); + String responseBodyString = sendAndGetResponseBody(GET, "/todos", OK_200); // THEN - // just assert '[]' List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); assertEquals(0, todoList.size()); @@ -156,28 +154,629 @@ public void test_getTodos_when_empty() throws Exception { } @Test - public void test_getTodos() throws Exception { + public void test_getTodoList() throws Exception { // GIVEN Todo expectedTodo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); - expectedTodo1.setCreated(null); mockTodoRepo.todoMap.put(expectedTodo1.getId(), expectedTodo1); Todo expectedTodo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build(); - expectedTodo2.setCreated(null); mockTodoRepo.todoMap.put(expectedTodo2.getId(), expectedTodo2); // WHEN - String responseBodyString = sendAndGetResponseBody(GET, "/todos", HttpStatus.OK_200); + String responseBodyString = sendAndGetResponseBody(GET, "/todos", OK_200); // THEN List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); - - // THEN assertEquals(2, todoList.size()); assertThat(todoList, CoreMatchers.hasItem(expectedTodo1)); assertThat(todoList, CoreMatchers.hasItem(expectedTodo2)); } + @Test + public void test_getTodoAsList() throws IOException { + + // GIVEN + Todo todo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo1.getId(), todo1); + Todo todo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo2.getId(), todo2); + + { + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo1.getId(), HttpStatus.OK_200); + + // THEN + List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); + assertEquals(1, todoList.size()); + assertEquals(todo1, todoList.get(0)); + } + + + { + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo2.getId(), HttpStatus.OK_200); + + // THEN + List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); + assertEquals(1, todoList.size()); + assertEquals(todo2, todoList.get(0)); + } + + } + + @Test + public void test_getTodoAsList_when_todo_not_exist() throws IOException { + + // GIVEN + Todo todo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo1.getId(), todo1); + Todo todo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo2.getId(), todo2); + + { + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+"NOT_EXISTING_TODO_ID", HttpStatus.OK_200); + + // THEN + List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); + assertEquals(0, todoList.size()); + } + + } + + + @Test + public void test_deleteTodo() throws IOException { + + // GIVEN + Todo todo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo1.getId(), todo1); + Todo todo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo2.getId(), todo2); + + { + // WHEN + Todo targetTodo = todo1; + String responseBodyString = sendAndGetResponseBody(DELETE, "/todos/"+targetTodo.getId(), HttpStatus.OK_200); + + // THEN + Todo todo = objectMapper.readValue(responseBodyString, Todo.class); + assertEmpty(todo); + assertNull(mockTodoRepo.todoMap.get(targetTodo.getId())); + } + + { + // WHEN + Todo targetTodo = todo2; + String responseBodyString = sendAndGetResponseBody(DELETE, "/todos/"+targetTodo.getId(), HttpStatus.OK_200); + + // THEN + Todo todo = objectMapper.readValue(responseBodyString, Todo.class); + assertEmpty(todo); + assertNull(mockTodoRepo.todoMap.get(targetTodo.getId())); + } + + } + + @Test + public void test_deleteTodo_when_invalid_todoId() throws IOException { + + // GIVEN + Todo todo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo1.getId(), todo1); + Todo todo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo2.getId(), todo2); + + { + // WHEN, THEN + String responseBodyString = sendAndGetResponseBody(DELETE, "/todos/INVALID_TODO_ID", BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + } + + } + + + @Test + public void test_createTask() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + + // create one + { + // WHEN + Task task = new TaskBuilder().name("taskName1").description("task description 1").build(); + String responseBodyString = sendAndGetResponseBody(POST, "/todos/"+todo.getId()+"/tasks", task, HttpStatus.OK_200); + + // THEN + assertNotNull(responseBodyString); + Task actualTask = objectMapper.readValue(responseBodyString, Task.class); + + assertNotNull(actualTask.getId()); + assertEquals(task.getName(), actualTask.getName()); + assertEquals(task.getDescription(), actualTask.getDescription()); + assertEquals(Task.Status.NOT_DONE, actualTask.getStatus()); + assertNotNull(actualTask.getCreated()); + + final int EXPECTED_TASK_SIZE = 1; + assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, EXPECTED_TASK_SIZE, mockTodoRepo.tasksMap.get(todo.getId()).size()); + } + + // create anothter + { + // WHEN + Task task = new TaskBuilder().name("taskName2").description("task description 2").build(); + String responseBodyString = sendAndGetResponseBody(POST, "/todos/"+todo.getId()+"/tasks", task, HttpStatus.OK_200); + + // THEN + assertNotNull(responseBodyString); + Task actualTask = objectMapper.readValue(responseBodyString, Task.class); + + assertNotNull(actualTask.getId()); + assertEquals(task.getName(), actualTask.getName()); + assertEquals(task.getDescription(), actualTask.getDescription()); + assertEquals(Task.Status.NOT_DONE, actualTask.getStatus()); + assertNotNull(actualTask.getCreated()); + + final int EXPECTED_TASK_SIZE = 2; + assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, EXPECTED_TASK_SIZE, mockTodoRepo.tasksMap.get(todo.getId()).size()); + } + + } + + @Test + public void test_createTask_when_invalid_todoId () throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + // WHEN + Task task = null; + String responseBodyString = sendAndGetResponseBody(POST, "/todos/INVALID_TODO_ID/tasks", task, BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + + } + + @Test + public void test_createTask_when_no_input () throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + // WHEN + Task task = null; + String responseBodyString = sendAndGetResponseBody(POST, "/todos/"+todo.getId()+"/tasks", task, BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + } + + + @Test + public void test_getTask() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo.getId()+"/tasks/"+targetTask.getId(), HttpStatus.OK_200); + + // THEN + Task task = objectMapper.readValue(responseBodyString, Task.class); + assertEquals(targetTask, task); + } + + { + Task targetTask = task2; + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo.getId()+"/tasks/"+targetTask.getId(), HttpStatus.OK_200); + + // THEN + Task task = objectMapper.readValue(responseBodyString, Task.class); + assertEquals(targetTask, task); + } + + } + + + @Test + public void test_getTask_when_invalid_todoId() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/INVALID_TODO_ID/tasks/"+targetTask.getId(), BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + } + + } + + @Test + public void test_getTask_when_invalid_taskId() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo.getId()+"/tasks/INVALID_TASK_ID", HttpStatus.BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + } + + } + + @Test + public void test_getTaskList() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo.getId()+"/tasks", HttpStatus.OK_200); + + // THEN + List taskList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); + assertEquals(2, taskList.size()); + + assertThat(taskList, CoreMatchers.hasItem(task1)); + assertThat(taskList, CoreMatchers.hasItem(task2)); + } + + } + + + @Test + public void test_getTaskList_when_invalid_todoId() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/INVALID_TODO_ID/tasks", HttpStatus.BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + } + + } + + @Test + public void test_getTaskList_with_status_done() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + Task task3 = new TaskBuilder().id("taskId3").name("task name3").description("description3").status(Task.Status.DONE).created(new Date()).build(); + taskMap.put(task3.getId(), task3); + Task task4 = new TaskBuilder().id("taskId4").name("task name4").description("description4").status(Task.Status.DONE).created(new Date()).build(); + taskMap.put(task4.getId(), task4); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo.getId()+"/tasks/not-done", HttpStatus.OK_200); + + // THEN + List taskList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); + assertEquals(2, taskList.size()); + + assertThat(taskList, CoreMatchers.hasItem(task1)); + assertThat(taskList, CoreMatchers.hasItem(task2)); + } + + { + // WHEN + String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo.getId()+"/tasks/done", HttpStatus.OK_200); + + // THEN + List taskList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); + assertEquals(2, taskList.size()); + + assertThat(taskList, CoreMatchers.hasItem(task3)); + assertThat(taskList, CoreMatchers.hasItem(task4)); + } + + } + + @Test + public void test_deleteTask() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + // WHEN + String responseBodyString = sendAndGetResponseBody(DELETE, "/todos/"+todo.getId()+"/tasks/"+targetTask.getId(), HttpStatus.OK_200); + + // THEN + Task task = objectMapper.readValue(responseBodyString, Task.class); + assertEmpty(task); + assertNull(mockTodoRepo.tasksMap.get(todo.getId()).get(targetTask.getId())); + } + + { + Task targetTask = task2; + // WHEN + String responseBodyString = sendAndGetResponseBody(DELETE, "/todos/"+todo.getId()+"/tasks/"+targetTask.getId(), HttpStatus.OK_200); + + // THEN + Task task = objectMapper.readValue(responseBodyString, Task.class); + assertEmpty(task); + assertNull(mockTodoRepo.tasksMap.get(todo.getId()).get(targetTask.getId())); + } + + } + + + @Test + public void test_deleteTask_when_invalid_todoId() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + // WHEN + String responseBodyString = sendAndGetResponseBody(DELETE, "/todos/INVALID_TODO_ID/tasks/" + targetTask.getId(), HttpStatus.BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + } + + } + + @Test + public void test_deleteTask_when_invalid_taskId() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + // WHEN + String responseBodyString = sendAndGetResponseBody(DELETE, "/todos/"+todo.getId()+"/tasks/INVALID_TASK_ID", HttpStatus.BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + } + + } + + + @Test + public void test_updateTask() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + targetTask.setName("modified task name1"); + targetTask.setDescription("modified description1"); + targetTask.setStatus(Task.Status.DONE); + + // WHEN + String responseBodyString = sendAndGetResponseBody(PUT, "/todos/"+todo.getId()+"/tasks/"+targetTask.getId(), targetTask, HttpStatus.OK_200); + + // THEN + Task task = objectMapper.readValue(responseBodyString, Task.class); + assertEquals(targetTask, task); + Task actualTask = taskMap.get(targetTask.getId()); + assertEquals(targetTask, actualTask); + } + + } + + + @Test + public void test_updateTask_when_invalid_todoId () throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + + // WHEN + String responseBodyString = sendAndGetResponseBody(PUT, "/todos/INVALID_TODO_ID/tasks/"+targetTask.getId(), targetTask, HttpStatus.BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + } + + } + + + @Test + public void test_updateTask_when_invalid_taskId () throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + + // WHEN + String responseBodyString = sendAndGetResponseBody(PUT, "/todos/"+todo.getId()+"/tasks/INVALID_TASK_ID", targetTask, HttpStatus.BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + } + + } + + @Test + public void test_updateTask_when_empty_task () throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); + mockTodoRepo.todoMap.put(todo.getId(), todo); + + Map taskMap = new HashMap<>(); + Task task1 = new TaskBuilder().id("taskId1").name("task name1").description("description1").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task1.getId(), task1); + Task task2 = new TaskBuilder().id("taskId2").name("task name2").description("description2").status(Task.Status.NOT_DONE).created(new Date()).build(); + taskMap.put(task2.getId(), task2); + + mockTodoRepo.tasksMap.put(todo.getId(), taskMap); + + { + Task targetTask = task1; + + // WHEN + String responseBodyString = sendAndGetResponseBody(PUT, "/todos/"+todo.getId()+"/tasks/"+targetTask.getId(), HttpStatus.BAD_REQUEST_400); + + // THEN + FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class); + assertNotNull(failedMessage.getMessage()); + + } + + } + } diff --git a/todo-challenge-explanation.md b/todo-challenge-explanation.md new file mode 100644 index 0000000..c633cf9 --- /dev/null +++ b/todo-challenge-explanation.md @@ -0,0 +1,32 @@ +Todo Challenge Explanation +========================= + +### Build and Run +``` +$ mvn compile +$ mvn exec:java -Dexec.mainClass=dhrim.zeplchallenge.todo.Main -Dexec.args="-p 8080" +``` + +### Architecture +Resource --> Service --> Repo + - TodoRestApi : Jersey resource file. endpoint is defined. call TodoService + - TodoService : treat business logic. use TodoRepo for storing + - TodoRepo : repository interface + -- AbstractMapBaseTodoRepo : abstract repo class which use map + -- MapDbTodoRepo : concret repo class which extends AbstractMapBaseTodoRepo. + - TodoServer : Server Main. provide getInstance() which create instance by Guice + +### Repository +Use MapDb(http://www.mapdb.org/) for storing which use local file for persistence. + +### Limitation +- Didn't consider expandability. It means only valid for single server beacuse of repository +- Didn't consider performance +- Didn't treat detail exception case. Not defined in requirement as like duplicated name. + +### TestCase +3 test case exist. +- TodoRestApiTest : call REST api by http request. It use mock Repo instead of real Repo. +- IntegrationTest : test with real Repo. +- MapDbTodoRepoTest : test working with MapDb. + From 9780a518dbb8ceb57d8de3bd0f26bbb82d3d410f Mon Sep 17 00:00:00 2001 From: dohyougn rim Date: Sun, 1 Jan 2017 19:59:41 +0900 Subject: [PATCH 6/7] remove incorrect endpoint GET /todos/:todo_id to get task list. --- .../dhrim/zeplchallenge/todo/TodoRestApi.java | 8 --- .../zeplchallenge/todo/TodoRestApiTest.java | 52 ------------------- todo-challenge-explanation.md | 18 +++++-- 3 files changed, 13 insertions(+), 65 deletions(-) diff --git a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java index e3e9254..8b34791 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java +++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java @@ -23,14 +23,6 @@ public Response getTodoList() throws IOException { return new ResponseBuilder(Response.Status.OK, todoList).build(); } - @GET - @Path("/todos/{todoId}") - @Produces("application/json") - public Response getTodoAsList(@PathParam("todoId") String todoId) throws IOException { - List todoList = todoService.getTodoAsList(todoId); - return new ResponseBuilder(Response.Status.OK, todoList).build(); - } - @GET @Path("/todos/{todoId}/tasks") @Produces("application/json") diff --git a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java index b685cac..27f7b4d 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java @@ -173,58 +173,6 @@ public void test_getTodoList() throws Exception { } - @Test - public void test_getTodoAsList() throws IOException { - - // GIVEN - Todo todo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); - mockTodoRepo.todoMap.put(todo1.getId(), todo1); - Todo todo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build(); - mockTodoRepo.todoMap.put(todo2.getId(), todo2); - - { - // WHEN - String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo1.getId(), HttpStatus.OK_200); - - // THEN - List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); - assertEquals(1, todoList.size()); - assertEquals(todo1, todoList.get(0)); - } - - - { - // WHEN - String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+todo2.getId(), HttpStatus.OK_200); - - // THEN - List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); - assertEquals(1, todoList.size()); - assertEquals(todo2, todoList.get(0)); - } - - } - - @Test - public void test_getTodoAsList_when_todo_not_exist() throws IOException { - - // GIVEN - Todo todo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build(); - mockTodoRepo.todoMap.put(todo1.getId(), todo1); - Todo todo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build(); - mockTodoRepo.todoMap.put(todo2.getId(), todo2); - - { - // WHEN - String responseBodyString = sendAndGetResponseBody(GET, "/todos/"+"NOT_EXISTING_TODO_ID", HttpStatus.OK_200); - - // THEN - List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {}); - assertEquals(0, todoList.size()); - } - - } - @Test public void test_deleteTodo() throws IOException { diff --git a/todo-challenge-explanation.md b/todo-challenge-explanation.md index c633cf9..c4a2612 100644 --- a/todo-challenge-explanation.md +++ b/todo-challenge-explanation.md @@ -7,6 +7,15 @@ $ mvn compile $ mvn exec:java -Dexec.mainClass=dhrim.zeplchallenge.todo.Main -Dexec.args="-p 8080" ``` +### TestCase +``` +mvn test +``` +3 test case exist. +- TodoRestApiTest : call REST api by http request. It use mock Repo instead of real Repo. +- IntegrationTest : test with real Repo. +- MapDbTodoRepoTest : test working with MapDb. + ### Architecture Resource --> Service --> Repo - TodoRestApi : Jersey resource file. endpoint is defined. call TodoService @@ -24,9 +33,8 @@ Use MapDb(http://www.mapdb.org/) for storing which use local file for persistenc - Didn't consider performance - Didn't treat detail exception case. Not defined in requirement as like duplicated name. -### TestCase -3 test case exist. -- TodoRestApiTest : call REST api by http request. It use mock Repo instead of real Repo. -- IntegrationTest : test with real Repo. -- MapDbTodoRepoTest : test working with MapDb. +### Change Endpoint +GET /todos/:todo_id for get task list is not correct. + +I implemented with GET /todos/:todl_id/tasks instead. From 4045eb6a1080c14eb6d02b1220dc1b3a883f048c Mon Sep 17 00:00:00 2001 From: dohyougn rim Date: Fri, 6 Jan 2017 12:32:34 +0900 Subject: [PATCH 7/7] fix creating task failed bug. cause : returned instance from MapDb is clone. --- .../todo/AbstractMapBasedTodoRepo.java | 4 +++- .../zeplchallenge/todo/IntegrationTest.java | 21 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java index bb831f2..2a12fd0 100644 --- a/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java +++ b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java @@ -89,9 +89,11 @@ public Task saveOrUpdate(String todoId, Task task) { Map taskMap = taskMapMap.get(todoId); if(taskMap==null) { taskMap = new HashMap<>(); - taskMapMap.put(todoId, taskMap); } taskMap.put(task.getId(), task); + // TODO : it's not good specific code related with MapDb. + // taskMapMap is clone instance when using MapDb. + taskMapMap.put(todoId, taskMap); return getTask(todoId, task.getId()); } diff --git a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java index cd809fa..7eff322 100644 --- a/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java +++ b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java @@ -9,6 +9,9 @@ import org.junit.Before; import org.junit.Test; +import java.io.IOException; + +import static org.eclipse.jetty.http.HttpStatus.OK_200; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -48,7 +51,7 @@ public void test_createTodo() throws Exception { // WHEN Todo todo = new TodoBuilder().name("name1").build(); - String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, HttpStatus.OK_200); + String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, OK_200); // THEN assertNotNull(responseBodyString); @@ -72,11 +75,23 @@ public void test_getTodoList() throws Exception { httpClient.executeMethod(method); // THEN - assertEquals(HttpStatus.OK_200, method.getStatusCode()); - + assertEquals(OK_200, method.getStatusCode()); } + @Test + public void bug_fix_of_task_creating_failed() throws IOException { + + // GIVEN + Todo todo = new TodoBuilder().name("name1").build(); + String responseBodyString = sendAndGetResponseBody(POST, "/todos", todo, OK_200); + todo = objectMapper.readValue(responseBodyString, Todo.class); + + // WHEN + Task task = new TaskBuilder().name("taskName1").description("task description 1").build(); + sendAndGetResponseBody(POST, "/todos/"+todo.getId()+"/tasks", task, HttpStatus.OK_200); + + } }