From ed830954a51f18a701e29c8181d578be1523b228 Mon Sep 17 00:00:00 2001 From: Y2Nk4 <1147678303@qq.com> Date: Wed, 10 Mar 2021 01:58:32 -0500 Subject: [PATCH 1/4] fix(OfficeHoursServer): Duplicate Configuration class while import --- src/main/scala/model/OfficeHoursServer.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/scala/model/OfficeHoursServer.scala b/src/main/scala/model/OfficeHoursServer.scala index 09ef61b..b98465e 100644 --- a/src/main/scala/model/OfficeHoursServer.scala +++ b/src/main/scala/model/OfficeHoursServer.scala @@ -1,14 +1,15 @@ package model +import model.{Configuration => LocalConfiguration} import com.corundumstudio.socketio.listener.{DataListener, DisconnectListener} -import com.corundumstudio.socketio.{AckRequest, Configuration, SocketIOClient, SocketIOServer} +import com.corundumstudio.socketio.{AckRequest, Configuration => SocketIOConfiguration, SocketIOClient, SocketIOServer} import model.database.{Database, DatabaseAPI, TestingDatabase} import play.api.libs.json.{JsValue, Json} class OfficeHoursServer() { - val database: DatabaseAPI = if(Configuration.DEV_MODE){ + val database: DatabaseAPI = if(LocalConfiguration.DEV_MODE){ new TestingDatabase }else{ new Database @@ -17,7 +18,7 @@ class OfficeHoursServer() { var usernameToSocket: Map[String, SocketIOClient] = Map() var socketToUsername: Map[SocketIOClient, String] = Map() - val config: Configuration = new Configuration { + val config: SocketIOConfiguration = new SocketIOConfiguration { setHostname("0.0.0.0") setPort(8080) } From e102cd861ca98cd451b54925491a013a68021932 Mon Sep 17 00:00:00 2001 From: Y2Nk4 <1147678303@qq.com> Date: Wed, 10 Mar 2021 03:46:03 -0500 Subject: [PATCH 2/4] feat(Configuration, Dotenv): Using dotenv to control the project environments Instead of hardcoding the configurations in the project, using dotenv could help the developers to set up their local dev environment without revise anything in the project. Also, for production deployment, environment variables could be loaded by dockerfile. I think it's the best way to configure the environment. --- .gitignore | 3 ++ README.md | 17 ++++++ pom.xml | 7 +++ src/main/resources/.env.example | 24 +++++++++ .../scala/helper/DirtyEnvironmentHack.scala | 52 +++++++++++++++++++ src/main/scala/helper/Dotenv.scala | 37 +++++++++++++ src/main/scala/model/Configuration.scala | 17 ++++++ src/main/scala/model/OfficeHoursServer.scala | 11 ++-- src/main/scala/model/database/Database.scala | 2 +- 9 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/.env.example create mode 100644 src/main/scala/helper/DirtyEnvironmentHack.scala create mode 100644 src/main/scala/helper/Dotenv.scala diff --git a/.gitignore b/.gitignore index 54f139b..649faed 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties + +#dotenv +src/main/resources/.env \ No newline at end of file diff --git a/README.md b/README.md index c1a6587..372262f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # OfficeHours A web-based system to manage office hours + +### Known Issues +1. [Failed to compile with Scala 2.13](https://github.com/UB-CSE/OfficeHours/issues/174) + +### Contribution Guide + +#### 1. How to Build this Project +1. Set up Scala SDK +2. Mark `src/main/scala` as `Source Root` if need +3. Set up the local configurations + - Rename `src/main/resources/.env.example` to `src/main/resources/.env` + + Revise anything you need to change (Ex: change DB configurations. By default, it will + use `List` as DB Driver, that means you don't really need to care about the DB connection + if you're not familiar with it, but data(Ex: Queue data), will lose once you restart the project). + +4. Build it. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 44e6ab8..d2c1055 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,13 @@ + + + src/main/resources + + src/main/resources/.env + + \ No newline at end of file diff --git a/src/main/resources/.env.example b/src/main/resources/.env.example new file mode 100644 index 0000000..9dea8cb --- /dev/null +++ b/src/main/resources/.env.example @@ -0,0 +1,24 @@ +# src/main/resources/.env.example +# Rename this file into `.env` +# to make this project work + +# ENV +# +# Not used by anything yet +ENV=DEV + +# DB_TYPE +# Type of the database the server gonna use +# Types are implemented in model.database, +# Loaded in src/main/scala/model/Configuration +# +# "MySQL" || "List" +# Default: "List" +DB_TYPE=List + +# DB Connection Informations +# Only need if you're using MySQL as + +# DB_URL=jdbc:mysql://localhost/officehours?autoReconnect=true&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC +# DB_USERNAME=root +# DB_PASSWORD=root diff --git a/src/main/scala/helper/DirtyEnvironmentHack.scala b/src/main/scala/helper/DirtyEnvironmentHack.scala new file mode 100644 index 0000000..cca9049 --- /dev/null +++ b/src/main/scala/helper/DirtyEnvironmentHack.scala @@ -0,0 +1,52 @@ +package helper + + +import java.util.Collections +import scala.util.{ Failure, Success, Try } +import scala.util.control.NonFatal + +/** + * Rewrite the runtime Environment, embedding entries from the .env file. + * + * Taken from: https://github.com/mefellows/sbt-dotenv/blob/master/src/main/scala/au/com/onegeek/sbtdotenv/DirtyEnvironmentHack.scala + * Original from: http://stackoverflow.com/questions/318239/how-do-i-set-environment-variables-from-java/7201825#7201825 + * + * Created by mfellows on 20/07/2014. + */ +object DirtyEnvironmentHack { + def setEnv(newEnv: java.util.Map[String, String]): Unit = { + Try({ + val processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment") + + val theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment") + theEnvironmentField.setAccessible(true) + val env = theEnvironmentField.get(null).asInstanceOf[java.util.Map[String, String]] // scalastyle:off null + env.putAll(newEnv) + + val theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment") + theCaseInsensitiveEnvironmentField.setAccessible(true) + val ciEnv = theCaseInsensitiveEnvironmentField.get(null).asInstanceOf[java.util.Map[String, String]] // scalastyle:off null + ciEnv.putAll(newEnv) + }) match { + case Failure(_: NoSuchFieldException) => + Try({ + val classes = classOf[Collections].getDeclaredClasses + val env = System.getenv + classes.filter(_.getName == "java.util.Collections$UnmodifiableMap").foreach(cl => { + val field = cl.getDeclaredField("m") + field.setAccessible(true) + val map = field.get(env).asInstanceOf[java.util.Map[String, String]] + map.clear() + map.putAll(newEnv) + }) + }) match { + case Failure(NonFatal(e2)) => + e2.printStackTrace() + case Success(_) => + } + case Failure(NonFatal(e1)) => + e1.printStackTrace() + case Success(_) => + } + } +} \ No newline at end of file diff --git a/src/main/scala/helper/Dotenv.scala b/src/main/scala/helper/Dotenv.scala new file mode 100644 index 0000000..11f4a1a --- /dev/null +++ b/src/main/scala/helper/Dotenv.scala @@ -0,0 +1,37 @@ +package helper + +import scala.io.Source +import scala.collection.JavaConverters._ +import scala.util.{Failure, Success, Try} + +object Dotenv { + // To load .env file to the Environmental Variables for development + def loadEnv (envFilePath: String = ".env"): Unit = { + var envLines: List[String] = Try(Source.fromResource(envFilePath).getLines().toList) match { + case Success(lines) => lines + case Failure(e) => { + print("[Warn] Failed to load .env: ") + println(e) + + // return empty list if .env not exist + List() + } + } + + for (line <- envLines) { + // skip the lines which are commented out + if (line.trim != "" && !line.trim.startsWith("#")) { + // Split the line by character `=` once + var lineContent: Array[String] = line.split("=", 2) + if (lineContent.length > 0) { + if (lineContent.length == 2) { + DirtyEnvironmentHack.setEnv((sys.env ++ Map( + lineContent(0) -> lineContent(1) + )).asJava) + } + } + } + } + } +} + diff --git a/src/main/scala/model/Configuration.scala b/src/main/scala/model/Configuration.scala index 7b8ece5..3f0a58f 100644 --- a/src/main/scala/model/Configuration.scala +++ b/src/main/scala/model/Configuration.scala @@ -1,7 +1,24 @@ package model +import scala.util.{Failure, Success, Try} + object Configuration { val DEV_MODE = true + /** + * DB_TYPE + * + * Type of the database the server gonna use + * Types are implemented in model.database + * + * "MySQL" || "List" + * + * default: "List" + * */ + var DB_TYPE: String = Try(sys.env("DB_TYPE")) match { + case Success(dbType) => dbType + case Failure(e) => "List" + } + } diff --git a/src/main/scala/model/OfficeHoursServer.scala b/src/main/scala/model/OfficeHoursServer.scala index b98465e..f750e21 100644 --- a/src/main/scala/model/OfficeHoursServer.scala +++ b/src/main/scala/model/OfficeHoursServer.scala @@ -5,14 +5,16 @@ import com.corundumstudio.socketio.listener.{DataListener, DisconnectListener} import com.corundumstudio.socketio.{AckRequest, Configuration => SocketIOConfiguration, SocketIOClient, SocketIOServer} import model.database.{Database, DatabaseAPI, TestingDatabase} import play.api.libs.json.{JsValue, Json} +import helper.Dotenv class OfficeHoursServer() { - val database: DatabaseAPI = if(LocalConfiguration.DEV_MODE){ - new TestingDatabase - }else{ + val database: DatabaseAPI = if(LocalConfiguration.DB_TYPE == "MySQL") { new Database + } else { + // fallback to using List as DB if it's "List" or others + new TestingDatabase } var usernameToSocket: Map[String, SocketIOClient] = Map() @@ -41,6 +43,9 @@ class OfficeHoursServer() { object OfficeHoursServer { def main(args: Array[String]): Unit = { + // Load environmental variables + Dotenv.loadEnv() + new OfficeHoursServer() } } diff --git a/src/main/scala/model/database/Database.scala b/src/main/scala/model/database/Database.scala index bbc9f7a..6f0d035 100644 --- a/src/main/scala/model/database/Database.scala +++ b/src/main/scala/model/database/Database.scala @@ -7,7 +7,7 @@ import model.StudentInQueue class Database extends DatabaseAPI{ - val url = "jdbc:mysql://mysql/officehours?autoReconnect=true" + val url: String = sys.env("DB_URL") val username: String = sys.env("DB_USERNAME") val password: String = sys.env("DB_PASSWORD") From a4a624cacdac9f7ff58d4a34eff840ddc7b27acd Mon Sep 17 00:00:00 2001 From: Y2Nk4 <1147678303@qq.com> Date: Wed, 10 Mar 2021 03:58:51 -0500 Subject: [PATCH 3/4] refactor(Dotenv): put Dotenv related objects into helpers.dotenv --- .../scala/helper/{ => dotenv}/DirtyEnvironmentHack.scala | 7 +++---- src/main/scala/helper/{ => dotenv}/Dotenv.scala | 6 ++---- src/main/scala/model/OfficeHoursServer.scala | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) rename src/main/scala/helper/{ => dotenv}/DirtyEnvironmentHack.scala (96%) rename src/main/scala/helper/{ => dotenv}/Dotenv.scala (89%) diff --git a/src/main/scala/helper/DirtyEnvironmentHack.scala b/src/main/scala/helper/dotenv/DirtyEnvironmentHack.scala similarity index 96% rename from src/main/scala/helper/DirtyEnvironmentHack.scala rename to src/main/scala/helper/dotenv/DirtyEnvironmentHack.scala index cca9049..77ac7de 100644 --- a/src/main/scala/helper/DirtyEnvironmentHack.scala +++ b/src/main/scala/helper/dotenv/DirtyEnvironmentHack.scala @@ -1,9 +1,8 @@ -package helper - +package helper.dotenv import java.util.Collections -import scala.util.{ Failure, Success, Try } import scala.util.control.NonFatal +import scala.util.{Failure, Success, Try} /** * Rewrite the runtime Environment, embedding entries from the .env file. @@ -49,4 +48,4 @@ object DirtyEnvironmentHack { case Success(_) => } } -} \ No newline at end of file +} diff --git a/src/main/scala/helper/Dotenv.scala b/src/main/scala/helper/dotenv/Dotenv.scala similarity index 89% rename from src/main/scala/helper/Dotenv.scala rename to src/main/scala/helper/dotenv/Dotenv.scala index 11f4a1a..5545fa0 100644 --- a/src/main/scala/helper/Dotenv.scala +++ b/src/main/scala/helper/dotenv/Dotenv.scala @@ -1,12 +1,11 @@ -package helper +package helper.dotenv import scala.io.Source -import scala.collection.JavaConverters._ import scala.util.{Failure, Success, Try} object Dotenv { // To load .env file to the Environmental Variables for development - def loadEnv (envFilePath: String = ".env"): Unit = { + def loadEnv(envFilePath: String = ".env"): Unit = { var envLines: List[String] = Try(Source.fromResource(envFilePath).getLines().toList) match { case Success(lines) => lines case Failure(e) => { @@ -34,4 +33,3 @@ object Dotenv { } } } - diff --git a/src/main/scala/model/OfficeHoursServer.scala b/src/main/scala/model/OfficeHoursServer.scala index f750e21..cdbc16a 100644 --- a/src/main/scala/model/OfficeHoursServer.scala +++ b/src/main/scala/model/OfficeHoursServer.scala @@ -2,10 +2,10 @@ package model import model.{Configuration => LocalConfiguration} import com.corundumstudio.socketio.listener.{DataListener, DisconnectListener} -import com.corundumstudio.socketio.{AckRequest, Configuration => SocketIOConfiguration, SocketIOClient, SocketIOServer} +import com.corundumstudio.socketio.{AckRequest, SocketIOClient, SocketIOServer, Configuration => SocketIOConfiguration} +import helper.dotenv.Dotenv import model.database.{Database, DatabaseAPI, TestingDatabase} import play.api.libs.json.{JsValue, Json} -import helper.Dotenv class OfficeHoursServer() { From 506c0f9bf112477e0c2f5cc483497d9870476606 Mon Sep 17 00:00:00 2001 From: Y2Nk4 <1147678303@qq.com> Date: Fri, 12 Mar 2021 14:31:45 -0500 Subject: [PATCH 4/4] fix(Dotenv): add JavaConverters for asJava method --- src/main/scala/helper/dotenv/Dotenv.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/helper/dotenv/Dotenv.scala b/src/main/scala/helper/dotenv/Dotenv.scala index 5545fa0..3368bd0 100644 --- a/src/main/scala/helper/dotenv/Dotenv.scala +++ b/src/main/scala/helper/dotenv/Dotenv.scala @@ -2,6 +2,7 @@ package helper.dotenv import scala.io.Source import scala.util.{Failure, Success, Try} +import collection.JavaConverters._ object Dotenv { // To load .env file to the Environmental Variables for development