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