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. +