diff --git a/.gitignore b/.gitignore index 9c07d4a..9f29af7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.class *.log +.idea/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e138150 --- /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/office-hours-0.0.1-jar-with-dependencies.jar diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e816900 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.3' +services: + nginx: + build: ./nginx + ports: + - '9005:80' + mysql: + image: mysql + environment: + MYSQL_ROOT_PASSWORD: 'changeme' + MYSQL_DATABASE: 'officehours' + 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..5443216 --- /dev/null +++ b/nginx/public/index.html @@ -0,0 +1,29 @@ + + + + + Office Hours + + + + +
+ +

Welcome!

+ +

Name

+ + +

+ + + +
+ +
+ + + + + + \ No newline at end of file diff --git a/nginx/public/officeHours.js b/nginx/public/officeHours.js new file mode 100644 index 0000000..f0c0c95 --- /dev/null +++ b/nginx/public/officeHours.js @@ -0,0 +1,28 @@ +const socket = io.connect({transports: ['websocket']}); + +socket.on('queue', displayQueue); +socket.on('message', displayMessage); + +function displayMessage(newMessage) { + document.getElementById("message").innerHTML = newMessage; +} + +function displayQueue(queueJSON) { + const queue = JSON.parse(queueJSON); + let formattedQueue = ""; + for (const student of queue) { + formattedQueue += student['username'] + " has been waiting since " + student['timestamp'] + "
" + } + document.getElementById("queue").innerHTML = formattedQueue; +} + + +function enterQueue() { + let name = document.getElementById("name").value; + socket.emit("enter_queue", name); + document.getElementById("name").value = ""; +} + +function readyToHelp() { + socket.emit("ready_for_student"); +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..62790e0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,88 @@ + + edu.buffalo.cse + office-hours + 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.OfficeHoursServer + + + + + + 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/Database.scala b/src/main/scala/model/Database.scala new file mode 100644 index 0000000..c3c5c78 --- /dev/null +++ b/src/main/scala/model/Database.scala @@ -0,0 +1,55 @@ +package model + +import java.sql.{Connection, DriverManager, ResultSet} + +object Database { + + val url = "jdbc:mysql://mysql/officehours" + 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 queue (username TEXT, timestamp BIGINT)") + } + + + def addStudentToQueue(student: StudentInQueue): Unit = { + val statement = connection.prepareStatement("INSERT INTO queue VALUE (?, ?)") + + statement.setString(1, student.username) + statement.setLong(2, student.timestamp) + + statement.execute() + } + + + def removeStudentFromQueue(username: String): Unit = { + val statement = connection.prepareStatement("DELETE FROM queue WHERE username=?") + + statement.setString(1, username) + + statement.execute() + } + + + def getQueue(): List[StudentInQueue] = { + val statement = connection.prepareStatement("SELECT * FROM queue") + val result: ResultSet = statement.executeQuery() + + var queue: List[StudentInQueue] = List() + + while (result.next()) { + val username = result.getString("username") + val timestamp = result.getLong("timestamp") + queue = new StudentInQueue(username, timestamp) :: queue + } + + queue + } + +} diff --git a/src/main/scala/model/OfficeHoursServer.scala b/src/main/scala/model/OfficeHoursServer.scala new file mode 100644 index 0000000..8ea6167 --- /dev/null +++ b/src/main/scala/model/OfficeHoursServer.scala @@ -0,0 +1,79 @@ +package model + +import com.corundumstudio.socketio.listener.{DataListener, DisconnectListener} +import com.corundumstudio.socketio.{AckRequest, Configuration, SocketIOClient, SocketIOServer} +import play.api.libs.json.{JsValue, Json} + + +class OfficeHoursServer() { + + 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.addDisconnectListener(new DisconnectionListener(this)) + server.addEventListener("enter_queue", classOf[String], new EnterQueueListener(this)) + server.addEventListener("ready_for_student", classOf[Nothing], new ReadyForStudentListener(this)) + + server.start() + + def queueJSON(): String = { + val queue: List[StudentInQueue] = Database.getQueue() + val queueJSON: List[JsValue] = queue.map((entry: StudentInQueue) => entry.asJsValue()) + Json.stringify(Json.toJson(queueJSON)) + } + +} + +object OfficeHoursServer { + def main(args: Array[String]): Unit = { + new OfficeHoursServer() + } +} + + +class DisconnectionListener(server: OfficeHoursServer) extends DisconnectListener { + override def onDisconnect(socket: SocketIOClient): Unit = { + if (server.socketToUsername.contains(socket)) { + val username = server.socketToUsername(socket) + server.socketToUsername -= socket + if (server.usernameToSocket.contains(username)) { + server.usernameToSocket -= username + } + } + } +} + + +class EnterQueueListener(server: OfficeHoursServer) extends DataListener[String] { + override def onData(socket: SocketIOClient, username: String, ackRequest: AckRequest): Unit = { + Database.addStudentToQueue(StudentInQueue(username, System.nanoTime())) + server.socketToUsername += (socket -> username) + server.usernameToSocket += (username -> socket) + server.server.getBroadcastOperations.sendEvent("queue", server.queueJSON()) + } +} + + +class ReadyForStudentListener(server: OfficeHoursServer) extends DataListener[Nothing] { + override def onData(socket: SocketIOClient, dirtyMessage: Nothing, ackRequest: AckRequest): Unit = { + val queue = Database.getQueue().sortBy(_.timestamp) + if(queue.nonEmpty){ + val studentToHelp = queue.head + Database.removeStudentFromQueue(studentToHelp.username) + socket.sendEvent("message", "You are now helping " + studentToHelp.username) + if(server.usernameToSocket.contains(studentToHelp.username)){ + server.usernameToSocket(studentToHelp.username).sendEvent("message", "A TA is ready to help you") + } + server.server.getBroadcastOperations.sendEvent("queue", server.queueJSON()) + } + } +} + + diff --git a/src/main/scala/model/StudentInQueue.scala b/src/main/scala/model/StudentInQueue.scala new file mode 100644 index 0000000..464108f --- /dev/null +++ b/src/main/scala/model/StudentInQueue.scala @@ -0,0 +1,36 @@ +package model + +import play.api.libs.json.{JsValue, Json} + + +object StudentInQueue { + + def cleanString(input: String): String = { + var output = input + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + if (output.length > 20) { + output = output.slice(0, 20) + "..." + } + output + } + + def apply(username: String, timestamp: Long): StudentInQueue = { + new StudentInQueue(cleanString(username), timestamp) + } + + +} + +class StudentInQueue(val username: String, val timestamp: Long) { + + def asJsValue(): JsValue ={ + val messageMap: Map[String, JsValue] = Map( + "username" -> Json.toJson(username), + "timestamp" -> Json.toJson(timestamp) + ) + Json.toJson(messageMap) + } + +}