diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..8a19835
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,192 @@
+
+
+ 4.0.0
+
+ dhrim
+ todo-challenge
+ 1.0-SNAPSHOT
+
+
+
+
+ 3.1
+
+
+ 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
+
+
+
+
+
+
+ maven-compiler-plugin
+ ${plugin.compiler.version}
+
+ 1.8
+ 1.8
+
+
+
+
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty-server.version}
+
+
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ ${jetty-server.version}
+
+
+
+
+ org.glassfish.jersey.core
+ jersey-server
+ ${jersy.version}
+
+
+
+
+ org.glassfish.jersey.containers
+ jersey-container-servlet-core
+ ${jersy.version}
+
+
+
+
+ org.glassfish.jersey.containers
+ jersey-container-jetty-http
+ ${jersy.version}
+
+
+
+
+ com.sun.jersey
+ jersey-json
+ ${jersey-json.version}
+
+
+
+ com.sun.jersey.contribs
+ jersey-guice
+ ${jersey-json.version}
+
+
+ javax.ws.rs
+ jsr311-api
+
+
+
+
+
+
+ org.codehaus.jackson
+ jackson-jaxrs
+ ${jackson.version}
+
+
+
+
+ com.google.inject
+ guice
+ ${guice.version}
+
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+
+
+
+ org.mapdb
+ mapdb
+ ${mapdb.version}
+
+
+
+
+
+ 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
+
+
+
+ org.hamcrest
+ hamcrest-junit
+ ${hamcrest-junit.version}
+ test
+
+
+
+
+
+
\ No newline at end of file
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..2a12fd0
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/AbstractMapBasedTodoRepo.java
@@ -0,0 +1,118 @@
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+@Singleton
+@Slf4j
+public abstract class AbstractMapBasedTodoRepo implements TodoRepo {
+
+ // todo.id -> Todo
+ private Map todoMap;
+
+ // todo.id -> Map
+ private Map> taskMapMap;
+
+ /** return Map which store todoId to Todo */
+ protected abstract Map getTodoMapInstance();
+
+ /** return Map which store todoId to Map */
+ protected abstract Map> getTaskMapMapInstance();
+
+ protected void initIfNot() {
+ if(todoMap !=null) { return; }
+ todoMap = getTodoMapInstance();
+ taskMapMap = getTaskMapMapInstance();
+ }
+
+ @VisibleForTesting
+ void clear_for_test() {
+ // could be null if initIfNot() not called.
+ if(todoMap ==null) { return; }
+ todoMap.clear();
+ taskMapMap.clear();
+ }
+
+ @Override
+ public List getTodoList() {
+ initIfNot();
+ return new ArrayList(todoMap.values());
+ }
+
+ @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());
+ }
+
+
+ @Override
+ public List getTaskList(String todoId) {
+ initIfNot();
+ 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 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(task.getId()==null) { throw new IllegalArgumentException("task.id is null. task="+task); }
+ Map taskMap = taskMapMap.get(todoId);
+ if(taskMap==null) {
+ taskMap = new HashMap<>();
+ }
+ 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());
+ }
+
+
+ @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/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/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..5e35136
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/Main.java
@@ -0,0 +1,49 @@
+package dhrim.zeplchallenge.todo;
+
+/**
+ * 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 = 8080;
+ private static final String OPTIONS_PORT = "-p";
+
+ public static void main(String[] args) throws Exception {
+
+ int port = parsePort(args);
+
+ TodoServer todoServer = TodoServer.getInstance();
+
+ try {
+ todoServer.start(port);
+ todoServer.join();
+ } finally {
+ todoServer.shutdown();
+ }
+
+ }
+
+ 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
new file mode 100644
index 0000000..1b171d5
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/MapDbTodoRepo.java
@@ -0,0 +1,85 @@
+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)
+ .closeOnJvmShutdownWeakReference()
+ .checksumHeaderBypass()
+ .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> getTaskMapMapInstance() {
+ initDbIfNot();
+ return db.hashMap("tasksMap", Serializer.STRING, serializer).createOrOpen();
+ }
+
+ protected void clear() {
+ new File(DB_FILE_NAME).delete();
+ }
+
+ 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);
+ }
+ 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/ResponseBuilder.java b/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java
new file mode 100644
index 0000000..6ff545f
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/ResponseBuilder.java
@@ -0,0 +1,47 @@
+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;
+import java.text.SimpleDateFormat;
+
+/**
+ * Build http rest response.
+ */
+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);
+ objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
+ }
+
+ 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 = "{}";
+ 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/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/TodoRepo.java b/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java
new file mode 100644
index 0000000..193a844
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRepo.java
@@ -0,0 +1,15 @@
+package dhrim.zeplchallenge.todo;
+
+import java.util.List;
+
+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
new file mode 100644
index 0000000..8b34791
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/TodoRestApi.java
@@ -0,0 +1,102 @@
+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 getTodoList() throws IOException {
+ List todoList = todoService.getTodoList();
+ 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();
+ }
+
+ @GET
+ @Path("/todos/{todoId}/tasks/{taskId}")
+ @Produces("application/json")
+ 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")
+ @Produces("application/json")
+ 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")
+ @Produces("application/json")
+ 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();
+ }
+
+ @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}/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 {
+ todoService.deleteTodo(todoId);
+ return new ResponseBuilder(Response.Status.OK).build();
+ }
+
+ @DELETE
+ @Path("/todos/{todoId}/tasks/{taskId}")
+ @Produces("application/json")
+ public Response deleteTask(@PathParam("todoId") String todoId, @PathParam("taskId") String taskId) throws IOException {
+ todoService.deleteTask(todoId, taskId);
+ return new ResponseBuilder(Response.Status.OK).build();
+ }
+
+}
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..76cf97d
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/TodoServer.java
@@ -0,0 +1,87 @@
+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.ServletContextHandler;
+
+
+@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);
+
+ 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);
+ }
+
+}
+
+
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..3a8525f
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/TodoService.java
@@ -0,0 +1,132 @@
+package dhrim.zeplchallenge.todo;
+
+import com.google.inject.Singleton;
+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
+public class TodoService {
+
+ @Inject
+ private TodoRepo todoRepo;
+
+ public List getTodoList() {
+ return todoRepo.getTodoList();
+ }
+
+ 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 Task getTask(String todoId, String taskId) {
+ validateTodoExist(todoId);
+ validateTaskId(taskId);
+ return todoRepo.getTask(todoId, taskId);
+ }
+
+ 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 getTaskList(String todoId) {
+ validateTodoExist(todoId);
+ return todoRepo.getTaskList(todoId);
+ }
+
+ public Todo createTodo(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) {
+
+ 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) {
+ validateTodoExist(todoId);
+ todoRepo.removeTodo(todoId);
+ }
+
+ public void deleteTask(String todoId, String taskId) {
+ 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/exceptionhandler/AbstractExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/AbstractExceptionMapper.java
new file mode 100644
index 0000000..4aed35b
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/AbstractExceptionMapper.java
@@ -0,0 +1,21 @@
+package dhrim.zeplchallenge.todo.exceptionhandler;
+
+import dhrim.zeplchallenge.todo.FailedMessage;
+
+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/exceptionhandler/IllegalArgumentExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/IllegalArgumentExceptionMapper.java
new file mode 100644
index 0000000..8e356f3
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/IllegalArgumentExceptionMapper.java
@@ -0,0 +1,23 @@
+package dhrim.zeplchallenge.todo.exceptionhandler;
+
+
+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/exceptionhandler/NotFoundExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/NotFoundExceptionMapper.java
new file mode 100644
index 0000000..8f29fee
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/NotFoundExceptionMapper.java
@@ -0,0 +1,24 @@
+package dhrim.zeplchallenge.todo.exceptionhandler;
+
+
+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/exceptionhandler/ThrowableExceptionMapper.java b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/ThrowableExceptionMapper.java
new file mode 100644
index 0000000..c205004
--- /dev/null
+++ b/src/main/java/dhrim/zeplchallenge/todo/exceptionhandler/ThrowableExceptionMapper.java
@@ -0,0 +1,23 @@
+package dhrim.zeplchallenge.todo.exceptionhandler;
+
+
+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/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/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000..689e14b
--- /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/AbstractTestBase.java b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java
new file mode 100644
index 0000000..2808e22
--- /dev/null
+++ b/src/test/java/dhrim/zeplchallenge/todo/AbstractTestBase.java
@@ -0,0 +1,131 @@
+package dhrim.zeplchallenge.todo;
+
+import com.google.inject.Module;
+import org.apache.commons.httpclient.HttpClient;
+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 java.util.Date;
+import java.util.TimeZone;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+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;
+
+ /** 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);
+ objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
+ }
+
+ 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 httpMethod, String path, int expectedStatusCode) throws IOException {
+ return sendAndGetResponseBody(httpMethod, path, null, expectedStatusCode);
+ }
+
+ 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:
+ method.setRequestHeader(HttpHeader.CONTENT_TYPE.asString(), "application/json");
+ if(requestBodyObject!=null) {
+ String jsonBody = objectMapper.writeValueAsString(requestBodyObject);
+ StringRequestEntity stringRequestEntity = new StringRequestEntity(jsonBody, "application/json", "UTF-8");
+ ((EntityEnclosingMethod)method).setRequestEntity(stringRequestEntity);
+ }
+ break;
+ default:
+ break;
+ }
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.executeMethod(method);
+
+ assertEquals(expectedStatusCode, method.getStatusCode());
+
+ 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
new file mode 100644
index 0000000..7eff322
--- /dev/null
+++ b/src/test/java/dhrim/zeplchallenge/todo/IntegrationTest.java
@@ -0,0 +1,97 @@
+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.io.IOException;
+
+import static org.eclipse.jetty.http.HttpStatus.OK_200;
+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(POST, "/todos", todo, OK_200);
+
+ // THEN
+ assertNotNull(responseBodyString);
+ Todo actualTodo = objectMapper.readValue(responseBodyString, Todo.class);
+
+ assertNotNull(actualTodo.getId());
+ assertEquals(todo.getName(), actualTodo.getName());
+ assertNotNull(actualTodo.getCreated());
+
+ }
+
+ @Test
+ public void test_getTodoList() throws Exception {
+
+ // GIVEN
+
+ // WHEN
+ String url = BASE_URL+"/todos";
+ HttpClient httpClient = new HttpClient();
+ GetMethod method = new GetMethod(url);
+ httpClient.executeMethod(method);
+
+ // THEN
+ 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);
+
+ }
+
+}
diff --git a/src/test/java/dhrim/zeplchallenge/todo/MapDbTodoRepoTest.java b/src/test/java/dhrim/zeplchallenge/todo/MapDbTodoRepoTest.java
new file mode 100644
index 0000000..06b0762
--- /dev/null
+++ b/src/test/java/dhrim/zeplchallenge/todo/MapDbTodoRepoTest.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 MapDbTodoRepoTest {
+
+ private MapDbTodoRepo repo = new MapDbTodoRepo();
+
+ @Before
+ public void before() {
+ repo.clear();
+ }
+
+
+ @Test
+ public void test_saveOrUpdate_and_getTodo () {
+
+ // 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/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/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
new file mode 100644
index 0000000..27f7b4d
--- /dev/null
+++ b/src/test/java/dhrim/zeplchallenge/todo/TodoRestApiTest.java
@@ -0,0 +1,730 @@
+package dhrim.zeplchallenge.todo;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+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.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 {
+
+
+ private MockMemoryTodoRepo mockTodoRepo = new MockMemoryTodoRepo();
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+ }
+
+ @After
+ public void after() {
+ super.after();
+ mockTodoRepo.clear_for_test();
+ }
+
+ @Override
+ protected Module getMockBinding() {
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ 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 todoMap;
+ }
+
+ @Override
+ protected Map> getTaskMapMapInstance() {
+ 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();
+
+ // create one
+ {
+ // WHEN
+ 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"}
+ assertNotNull(responseBodyString);
+ Todo actualTodo = objectMapper.readValue(responseBodyString, Todo.class);
+
+ assertNotNull(actualTodo.getId());
+ assertEquals(todo.getName(), actualTodo.getName());
+ assertNotNull(actualTodo.getCreated());
+
+ final int EXPECTED_TODO_SIZE = 1;
+ assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, EXPECTED_TODO_SIZE, mockTodoRepo.todoMap.size());
+ }
+
+
+ // 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());
+
+ final int EXPECTED_TODO_SIZE = 2;
+ assertEquals("incorrect todo size. todoRepo="+mockTodoRepo, EXPECTED_TODO_SIZE, mockTodoRepo.todoMap.size());
+ }
+
+ }
+
+
+ @Test
+ public void test_createTodo_failed_when_empty_input() throws IOException {
+
+ // GIVEN
+ Object emptyString = "";
+
+ // WHEN
+ String responseBodyString = sendAndGetResponseBody(POST, "/todos", emptyString, BAD_REQUEST_400);
+
+ // THEN
+ FailedMessage failedMessage = objectMapper.readValue(responseBodyString, FailedMessage.class);
+ assertNotNull(failedMessage.getMessage());
+
+ }
+
+
+ @Test
+ public void test_getTodoList_when_empty() throws Exception {
+
+ // GIVEN
+
+ // WHEN
+ String responseBodyString = sendAndGetResponseBody(GET, "/todos", OK_200);
+
+ // THEN
+ // just assert '[]'
+ List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {});
+ assertEquals(0, todoList.size());
+
+ }
+
+ @Test
+ public void test_getTodoList() throws Exception {
+
+ // GIVEN
+ Todo expectedTodo1 = new TodoBuilder().id("id1").name("name1").created(new Date()).build();
+ mockTodoRepo.todoMap.put(expectedTodo1.getId(), expectedTodo1);
+ Todo expectedTodo2 = new TodoBuilder().id("id2").name("name2").created(new Date()).build();
+ mockTodoRepo.todoMap.put(expectedTodo2.getId(), expectedTodo2);
+
+ // WHEN
+ String responseBodyString = sendAndGetResponseBody(GET, "/todos", OK_200);
+
+ // THEN
+ List todoList = objectMapper.readValue(responseBodyString, new TypeReference>() {});
+ assertEquals(2, todoList.size());
+ assertThat(todoList, CoreMatchers.hasItem(expectedTodo1));
+ assertThat(todoList, CoreMatchers.hasItem(expectedTodo2));
+
+ }
+
+
+ @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/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..b142738
--- /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
diff --git a/todo-challenge-explanation.md b/todo-challenge-explanation.md
new file mode 100644
index 0000000..c4a2612
--- /dev/null
+++ b/todo-challenge-explanation.md
@@ -0,0 +1,40 @@
+Todo Challenge Explanation
+=========================
+
+### Build and Run
+```
+$ 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
+ - 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.
+
+### Change Endpoint
+GET /todos/:todo_id for get task list is not correct.
+
+I implemented with GET /todos/:todl_id/tasks instead.
+