@@ -14,7 +14,7 @@ import org.apache.pekko.util.ByteString
1414import org .joda .time .DateTime
1515import org .springframework .beans .BeanUtils
1616import play .api .Logging
17- import play .api .libs .json .{JsValue , Json }
17+ import play .api .libs .json .{JsResultException , JsValue , Json }
1818import play .api .libs .ws .{WSClient , WSResponse }
1919import play .mvc .Http
2020import schema .ExternalCourseValidator .{CourseUnitInfo , GradeScale as ExtGradeScale }
@@ -26,6 +26,7 @@ import javax.inject.Inject
2626import scala .collection .immutable .TreeSet
2727import scala .concurrent .{ExecutionContext , Future }
2828import scala .jdk .CollectionConverters .*
29+ import scala .util .Try
2930
3031class 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
0 commit comments