Skip to content

Commit 17c9759

Browse files
committed
CSCEXAM-1551 Support alternative format of importing course language
1 parent a5b3633 commit 17c9759

File tree

4 files changed

+42
-21
lines changed

4 files changed

+42
-21
lines changed

app/impl/ExternalCourseHandlerImpl.scala

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import org.apache.pekko.util.ByteString
1414
import org.joda.time.DateTime
1515
import org.springframework.beans.BeanUtils
1616
import play.api.Logging
17-
import play.api.libs.json.{JsValue, Json}
17+
import play.api.libs.json.{JsResultException, JsValue, Json}
1818
import play.api.libs.ws.{WSClient, WSResponse}
1919
import play.mvc.Http
2020
import schema.ExternalCourseValidator.{CourseUnitInfo, GradeScale as ExtGradeScale}
@@ -26,6 +26,7 @@ import javax.inject.Inject
2626
import scala.collection.immutable.TreeSet
2727
import scala.concurrent.{ExecutionContext, Future}
2828
import scala.jdk.CollectionConverters.*
29+
import scala.util.Try
2930

3031
class ExternalCourseHandlerImpl @Inject (
3132
private val wsClient: WSClient,
@@ -93,34 +94,49 @@ class ExternalCourseHandlerImpl @Inject (
9394
external.setId(local.getId)
9495
case _ => external.save()
9596

96-
private def stripBom(response: WSResponse) =
97-
val bomCandidate = response.bodyAsBytes.splitAt(3)
98-
if bomCandidate._1 == BOM then
99-
logger.warn("BOM character detected in the beginning of response body")
100-
Json.parse(bomCandidate._2.toArray)
101-
else response.json
97+
private def parseResponseBody(response: WSResponse): Option[JsValue] =
98+
val bytes = response.bodyAsBytes
99+
val bodyBytes =
100+
if bytes.length >= 3 && bytes.splitAt(3)._1 == BOM then
101+
logger.warn("BOM character detected in the beginning of response body")
102+
bytes.drop(3)
103+
else bytes
104+
val body = bodyBytes.utf8String.trim
105+
if body.startsWith("<") then
106+
logger.warn("Response is not JSON (e.g. HTML error page). Body starts with '<'.")
107+
None
108+
else
109+
Try(Json.parse(bodyBytes.toArray)).toOption match
110+
case None =>
111+
logger.warn("Response was not valid JSON (e.g. HTML error page).")
112+
None
113+
case some => some
102114

103115
private def downloadCourses(url: URL) =
104116
queryRequest(url)
105117
.get()
106118
.map(response =>
107119
val status = response.status
108120
if status == Http.Status.OK then
109-
val root = stripBom(response)
110-
parseCourses(root).flatMap(parseCourse)
121+
parseResponseBody(response) match
122+
case Some(root) => parseCourses(root).flatMap(parseCourse)
123+
case None => Seq.empty
111124
else
112125
logger.info(s"Non-OK response received for URL: %url. Status: $status")
113126
Seq.empty
114127
)
115-
.recover { case e: Exception =>
128+
.recover { case e: JsResultException =>
129+
logger.error("Unable to parse course data: JSON structure did not match expected format", e)
130+
Seq.empty
131+
case e: Exception =>
116132
logger.error("Unable to download course data due to exception in network connection", e)
117133
Seq.empty
118134
}
119135

120136
private def parseCourses(root: JsValue): Seq[CourseUnitInfo] =
121-
val single = (root \\ "CourseUnitInfo").map(_.asOpt[CourseUnitInfo])
122-
if single.head.nonEmpty then single.flatten.toSeq
123-
else (root \\ "CourseUnitInfo").flatMap(_.as[Seq[CourseUnitInfo]]).toSeq
137+
(root \\ "CourseUnitInfo").flatMap { node =>
138+
node.asOpt[CourseUnitInfo].toSeq ++ node.asOpt[Seq[CourseUnitInfo]].getOrElse(Seq.empty)
139+
}.toSeq
124140

125141
private def parseCourse(cui: CourseUnitInfo): Option[Course] =
126142
(validateStart(cui.startDate), validateEnd(cui.endDate)) match

app/schema/ExternalCourseValidator.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,13 @@ object ExternalCourseValidator:
6464
case class CreditLanguage(name: String)
6565

6666
object CourseUnitInfo:
67-
val asScales: Reads[Seq[GradeScale]] = implicitly[Reads[GradeScale]].map(Seq(_))
68-
val readScale: Reads[Seq[GradeScale]] = implicitly[Reads[Seq[GradeScale]]].orElse(asScales)
67+
private val asScales: Reads[Seq[GradeScale]] = implicitly[Reads[GradeScale]].map(Seq(_))
68+
private val readScale: Reads[Seq[GradeScale]] =
69+
implicitly[Reads[Seq[GradeScale]]].orElse(asScales)
70+
private val creditsLanguageAsString: Reads[Seq[CreditLanguage]] =
71+
implicitly[Reads[String]].map(lang => Seq(CreditLanguage(lang)))
72+
private val readCreditsLanguage: Reads[Seq[CreditLanguage]] =
73+
creditsLanguageAsString.orElse(implicitly[Reads[Seq[CreditLanguage]]])
6974
implicit val cuiReads: Reads[CourseUnitInfo] = (
7075
(JsPath \ "identifier").read[String](using readInt) and
7176
(JsPath \ "courseUnitCode").read[String] and
@@ -84,7 +89,7 @@ object ExternalCourseValidator:
8489
(JsPath \ "department").readNullable[Seq[Department]] and
8590
(JsPath \ "lecturerResponsible").readNullable[Seq[LecturerResponsible]] and
8691
(JsPath \ "lecturer").readNullable[Seq[Lecturer]] and
87-
(JsPath \ "creditsLanguage").readNullable[Seq[CreditLanguage]] and
92+
(JsPath \ "creditsLanguage").readNullable[Seq[CreditLanguage]](using readCreditsLanguage) and
8893
(JsPath \ "gradeScale").readNullable[Seq[GradeScale]](using readScale)
8994
)(CourseUnitInfo.apply)
9095
case class CourseUnitInfo(

conf/integrationtest.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ play.mailer.host = localhost
1010
play.mailer.port = 3025
1111
play.mailer.ssl = no
1212
play.mailer.tls = no
13+
# Pin credentials so env (e.g. act/GitHub Actions SMTP_USER) does not override;
14+
# avoids GreenMail "Mailbox X already exists" when many concurrent SMTP connections AUTH.
15+
play.mailer.user = "user"
16+
play.mailer.password = "password"
1317

1418
play.evolutions.db.default.autoApply = false
1519
play.evolutions.db.default.enabled = false

test/resources/courseUnitInfo.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
"courseUnitType" : 1,
88
"courseImplementation": "abcdefghijklmnop",
99
"credits" : 3,
10-
"creditsLanguage" : [
11-
{
12-
"name" : "fi"
13-
}
14-
],
10+
"creditsLanguage" : "fi",
1511
"gradeScale": [
1612
{
1713
"name" : "0-5",

0 commit comments

Comments
 (0)