diff --git a/.gitignore b/.gitignore
index 9c07d4a..5cd8ac4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
*.class
*.log
+.idea/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..66bc6ee
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,47 @@
+FROM ubuntu:18.04
+
+RUN apt-get update
+
+RUN apt-get install -y wget
+
+#Java Setup
+RUN apt-get update --fix-missing
+RUN apt-get install -y default-jdk
+
+#Scala setup
+RUN apt-get remove scala-library scala
+RUN wget http://scala-lang.org/files/archive/scala-2.12.6.deb
+RUN dpkg -i scala-2.12.6.deb
+RUN apt-get update
+RUN apt-get install -y scala
+
+RUN apt-get install -y gnupg2
+RUN echo "deb https://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
+RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
+RUN apt-get update
+RUN apt-get install -y sbt
+
+#Maven Setup
+RUN apt-get install -y maven
+
+
+# Set the home directory to /root and cd into that directory
+ENV HOME /root
+WORKDIR /root
+
+
+# Copy all app files into the image
+COPY . .
+
+# Download dependancies and build the app
+RUN mvn package
+
+
+# Allow port 8080 to be accessed from outside the container
+EXPOSE 8080
+
+ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait
+RUN chmod +x /wait
+
+# Run the app
+CMD /wait && java -jar target/todo-scheduler-0.0.1-jar-with-dependencies.jar
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..4da4b27
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,19 @@
+version: '3.3'
+services:
+ nginx:
+ build: ./nginx
+ ports:
+ - '9006:80'
+ mysql:
+ image: mysql
+ environment:
+ MYSQL_ROOT_PASSWORD: 'changeme'
+ MYSQL_DATABASE: 'todo'
+ MYSQL_USER: 'sqluser'
+ MYSQL_PASSWORD: 'changeme'
+ app:
+ build: .
+ environment:
+ WAIT_HOSTS: mysql:3306
+ DB_USERNAME: 'sqluser'
+ DB_PASSWORD: 'changeme'
\ No newline at end of file
diff --git a/nginx/Dockerfile b/nginx/Dockerfile
new file mode 100644
index 0000000..184f4a0
--- /dev/null
+++ b/nginx/Dockerfile
@@ -0,0 +1,6 @@
+FROM nginx
+
+COPY ./public /usr/share/nginx/html
+COPY ./default.conf /etc/nginx/conf.d
+
+EXPOSE 80
\ No newline at end of file
diff --git a/nginx/default.conf b/nginx/default.conf
new file mode 100644
index 0000000..a449256
--- /dev/null
+++ b/nginx/default.conf
@@ -0,0 +1,17 @@
+server {
+ listen 80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ }
+
+ location /socket.io {
+ proxy_pass http://app:8080;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ }
+
+}
diff --git a/nginx/public/index.html b/nginx/public/index.html
new file mode 100644
index 0000000..959358a
--- /dev/null
+++ b/nginx/public/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+ Office Hours
+
+
+
+
+
+
+Welcome!
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nginx/public/todo.js b/nginx/public/todo.js
new file mode 100644
index 0000000..119d6db
--- /dev/null
+++ b/nginx/public/todo.js
@@ -0,0 +1,32 @@
+const socket = io.connect("http://localhost:8080", {transports: ['websocket']});
+
+socket.on('all_tasks', displayTasks);
+socket.on('message', displayMessage);
+
+function displayMessage(newMessage) {
+ document.getElementById("message").innerHTML = newMessage;
+}
+
+function displayTasks(tasksJSON) {
+ const tasks = JSON.parse(tasksJSON);
+ let formattedTasks = "";
+ for (const task of tasks) {
+ formattedTasks += "
";
+ formattedTasks += "" + task['title'] + " - " + task['description'] + "
";
+ formattedTasks += "";
+ }
+ document.getElementById("tasks").innerHTML = formattedTasks;
+}
+
+
+function addTask() {
+ let title = document.getElementById("title").value;
+ let desc = document.getElementById("desc").value;
+ socket.emit("add_task", JSON.stringify({"title": title, "description": desc}));
+ document.getElementById("title").value = "";
+ document.getElementById("desc").value = "";
+}
+
+function completeTask(taskId) {
+ socket.emit("complete_task", taskId);
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..6195959
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,88 @@
+
+ edu.buffalo.cse
+ todo-scheduler
+ 4.0.0
+ http://maven.apache.org
+ 0.0.1
+
+
+ UTF-8
+ 2.12.9
+
+
+
+
+
+
+ com.typesafe.play
+ play-json_2.12
+ 2.7.1
+
+
+
+
+ mysql
+ mysql-connector-java
+ 8.0.15
+
+
+
+ com.corundumstudio.socketio
+ netty-socketio
+ 1.7.12
+
+
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.28
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 2.4
+
+
+ jar-with-dependencies
+
+
+
+ model.TodoServer
+
+
+
+
+
+ package
+
+ single
+
+
+
+
+
+
+ org.scala-tools
+ maven-scala-plugin
+ 2.15.2
+
+
+
+ compile
+ testCompile
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/scala/model/Configuration.scala b/src/main/scala/model/Configuration.scala
new file mode 100644
index 0000000..7b8ece5
--- /dev/null
+++ b/src/main/scala/model/Configuration.scala
@@ -0,0 +1,7 @@
+package model
+
+object Configuration {
+
+ val DEV_MODE = true
+
+}
diff --git a/src/main/scala/model/Task.scala b/src/main/scala/model/Task.scala
new file mode 100644
index 0000000..f6da7b2
--- /dev/null
+++ b/src/main/scala/model/Task.scala
@@ -0,0 +1,41 @@
+package model
+
+import play.api.libs.json.{JsValue, Json}
+
+
+object Task {
+
+ var nextId: Int = 0
+
+ def cleanString(input: String, maxLength: Int = 100): String = {
+ var output = input
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ if (output.length > maxLength) {
+ output = output.slice(0, maxLength) + "..."
+ }
+ output
+ }
+
+ def apply(title: String, description: String): Task = {
+ val thisId = nextId
+ nextId += 1
+ new Task(cleanString(title), cleanString(description, 1000), thisId.toString)
+ }
+
+
+}
+
+class Task(val title: String, val description: String, val id: String) {
+
+ def asJsValue(): JsValue ={
+ val taskMap: Map[String, JsValue] = Map(
+ "title" -> Json.toJson(title),
+ "description" -> Json.toJson(description),
+ "id" -> Json.toJson(id)
+ )
+ Json.toJson(taskMap)
+ }
+
+}
diff --git a/src/main/scala/model/TodoServer.scala b/src/main/scala/model/TodoServer.scala
new file mode 100644
index 0000000..c4f34fb
--- /dev/null
+++ b/src/main/scala/model/TodoServer.scala
@@ -0,0 +1,89 @@
+package model
+
+import com.corundumstudio.socketio.listener.{ConnectListener, DataListener}
+import com.corundumstudio.socketio.{AckRequest, Configuration, SocketIOClient, SocketIOServer}
+import model.database.{Database, DatabaseAPI, TestingDatabase}
+import play.api.libs.json.{JsValue, Json}
+
+
+class TodoServer() {
+
+ val database: DatabaseAPI = if (Configuration.DEV_MODE) {
+ new TestingDatabase
+ } else {
+ new Database
+ }
+
+ setNextId()
+
+ var usernameToSocket: Map[String, SocketIOClient] = Map()
+ var socketToUsername: Map[SocketIOClient, String] = Map()
+
+ val config: Configuration = new Configuration {
+ setHostname("0.0.0.0")
+ setPort(8080)
+ }
+
+ val server: SocketIOServer = new SocketIOServer(config)
+
+ server.addConnectListener(new ConnectionListener(this))
+ server.addEventListener("add_task", classOf[String], new AddTaskListener(this))
+ server.addEventListener("complete_task", classOf[String], new CompleteTaskListener(this))
+
+ server.start()
+
+ def tasksJSON(): String = {
+ val tasks: List[Task] = database.getTasks
+ val tasksJSON: List[JsValue] = tasks.map((entry: Task) => entry.asJsValue())
+ Json.stringify(Json.toJson(tasksJSON))
+ }
+
+ def setNextId(): Unit = {
+ val tasks = database.getTasks
+ if (tasks.nonEmpty) {
+ Task.nextId = tasks.map(_.id.toInt).max + 1
+ }
+ }
+
+}
+
+object TodoServer {
+ def main(args: Array[String]): Unit = {
+ new TodoServer()
+ }
+}
+
+
+class ConnectionListener(server: TodoServer) extends ConnectListener {
+
+ override def onConnect(socket: SocketIOClient): Unit = {
+ socket.sendEvent("all_tasks", server.tasksJSON())
+ }
+
+}
+
+
+class AddTaskListener(server: TodoServer) extends DataListener[String] {
+
+ override def onData(socket: SocketIOClient, taskJSON: String, ackRequest: AckRequest): Unit = {
+ val task: JsValue = Json.parse(taskJSON)
+ val title: String = (task \ "title").as[String]
+ val description: String = (task \ "description").as[String]
+
+ server.database.addTask(Task(title, description))
+ server.server.getBroadcastOperations.sendEvent("all_tasks", server.tasksJSON())
+ }
+
+}
+
+
+class CompleteTaskListener(server: TodoServer) extends DataListener[String] {
+
+ override def onData(socket: SocketIOClient, taskId: String, ackRequest: AckRequest): Unit = {
+ server.database.completeTask(taskId)
+ server.server.getBroadcastOperations.sendEvent("all_tasks", server.tasksJSON())
+ }
+
+}
+
+
diff --git a/src/main/scala/model/database/Database.scala b/src/main/scala/model/database/Database.scala
new file mode 100644
index 0000000..1d569ab
--- /dev/null
+++ b/src/main/scala/model/database/Database.scala
@@ -0,0 +1,67 @@
+package model.database
+
+import java.sql.{Connection, DriverManager, ResultSet}
+
+import model.Task
+
+
+class Database extends DatabaseAPI{
+
+ val url = "jdbc:mysql://mysql/todo?autoReconnect=true"
+ val username: String = sys.env("DB_USERNAME")
+ val password: String = sys.env("DB_PASSWORD")
+
+ var connection: Connection = DriverManager.getConnection(url, username, password)
+ setupTable()
+
+
+ def setupTable(): Unit = {
+ val statement = connection.createStatement()
+ statement.execute("CREATE TABLE IF NOT EXISTS tasks (title TEXT, description TEXT, id TEXT)")
+ }
+
+
+ override def addTask(task: Task): Unit = {
+ val statement = connection.prepareStatement("INSERT INTO tasks VALUE (?, ?, ?)")
+
+ statement.setString(1, task.title)
+ statement.setString(2, task.description)
+ statement.setString(3, task.id)
+
+ statement.execute()
+ }
+
+
+ override def completeTask(taskId: String): Unit = {
+ val statement = connection.prepareStatement("DELETE FROM tasks WHERE id=?")
+
+ statement.setString(1, taskId)
+
+ statement.execute()
+ }
+
+
+ override def getTasks: List[Task] = {
+ val statement = connection.prepareStatement("SELECT * FROM tasks")
+ val result: ResultSet = statement.executeQuery()
+
+ var tasks: List[Task] = List()
+
+ while (result.next()) {
+ val title = result.getString("title")
+ val description = result.getString("description")
+ val id = result.getString("id")
+ tasks = new Task(title, description, id) :: tasks
+ }
+
+ tasks.reverse
+ }
+
+}
+
+
+
+
+
+
+
diff --git a/src/main/scala/model/database/DatabaseAPI.scala b/src/main/scala/model/database/DatabaseAPI.scala
new file mode 100644
index 0000000..8c4132a
--- /dev/null
+++ b/src/main/scala/model/database/DatabaseAPI.scala
@@ -0,0 +1,11 @@
+package model.database
+
+import model.Task
+
+trait DatabaseAPI {
+
+ def addTask(task: Task): Unit
+ def completeTask(taskId: String): Unit
+ def getTasks: List[Task]
+
+}
diff --git a/src/main/scala/model/database/TestingDatabase.scala b/src/main/scala/model/database/TestingDatabase.scala
new file mode 100644
index 0000000..b30a4ee
--- /dev/null
+++ b/src/main/scala/model/database/TestingDatabase.scala
@@ -0,0 +1,24 @@
+package model.database
+
+import model.Task
+
+class TestingDatabase extends DatabaseAPI {
+
+ var data: List[Task] = List()
+
+
+ override def addTask(task: Task): Unit = {
+ data ::= task
+ }
+
+
+ override def completeTask(taskId: String): Unit = {
+ data = data.filter(_.id != taskId)
+ }
+
+
+ override def getTasks: List[Task] = {
+ data.reverse
+ }
+
+}