diff options
Diffstat (limited to 'Scala/objsets/project/CourseraHttp.scala')
-rw-r--r-- | Scala/objsets/project/CourseraHttp.scala | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/Scala/objsets/project/CourseraHttp.scala b/Scala/objsets/project/CourseraHttp.scala new file mode 100644 index 0000000..5f55b12 --- /dev/null +++ b/Scala/objsets/project/CourseraHttp.scala @@ -0,0 +1,223 @@ +import dispatch.{Request, Http, NoLogging, StatusCode, url} +import cc.spray.json.{JsNull, JsonParser, DefaultJsonProtocol, JsValue} +import RichJsValue._ +import org.apache.commons.codec.digest.DigestUtils +import org.apache.commons.codec.binary.{Hex, Base64} +import java.io.{IOException, File, FileInputStream} +import scalaz.Scalaz.{mkIdentity, ValidationNEL} + +import Settings._ + +case class JsonSubmission(api_state: String, user_info: JsValue, submission_metadata: JsValue, solutions: JsValue, submission_encoding: String, submission: String) +//case class JsonQueueResult(submission: JsonSubmission) +object SubmitJsonProtocol extends DefaultJsonProtocol { + implicit val jsonSubmissionFormat = jsonFormat6(JsonSubmission) +// implicit val jsonQueueResultFormat = jsonFormat1(JsonQueueResult) +} + +object CourseraHttp { + private lazy val http = new Http with NoLogging + + private def executeRequest[T](req: Request)(parse: String => ValidationNEL[String, T]): ValidationNEL[String, T] = { + try { + http(req >- { res => + parse(res) + }) + } catch { + case ex: IOException => + ("Connection failed\n"+ ex.toString()).failNel + case StatusCode(code, message) => + ("HTTP failed with status "+ code +"\n"+ message).failNel + } + } + + + /****************************** + * SUBMITTING + */ + + def getChallenge(email: String, submitProject: ProjectDetails): ValidationNEL[String, Challenge] = { + val baseReq = url(challengeUrl(submitProject.courseId)) + val withArgs = baseReq << Map("email_address" -> email, + "assignment_part_sid" -> submitProject.assignmentPartId, + "response_encoding" -> "delim") + + executeRequest(withArgs) { res => + // example result. there might be an `aux_data` value at the end. + // |email_address|a@b.com|challenge_key|XXYYXXYYXXYY|state|XXYYXXYYXXYY|challenge_aux_data| + val parts = res.split('|').filterNot(_.isEmpty) + if (parts.length < 7) + ("Unexpected challenge format: \n"+ res).failNel + else + Challenge(parts(1), parts(3), parts(5)).successNel + } + } + + def submitSolution(sourcesJar: File, submitProject: ProjectDetails, challenge: Challenge, chResponse: String): ValidationNEL[String, String] = { + val fileLength = sourcesJar.length() + if (!sourcesJar.exists()) { + ("Sources jar archive does not exist\n"+ sourcesJar.getAbsolutePath).failNel + } else if (fileLength == 0l) { + ("Sources jar archive is empty\n"+ sourcesJar.getAbsolutePath).failNel + } else if (fileLength > maxSubmitFileSize) { + ("Sources jar archive is too big. Allowed size: "+ + maxSubmitFileSize +" bytes, found "+ fileLength +" bytes.\n"+ + sourcesJar.getAbsolutePath).failNel + } else { + val bytes = new Array[Byte](fileLength.toInt) + val sizeRead = try { + val is = new FileInputStream(sourcesJar) + val read = is.read(bytes) + is.close() + read + } catch { + case ex: IOException => + ("Failed to read sources jar archive\n"+ ex.toString()).failNel + } + if (sizeRead != bytes.length) { + ("Failed to read the sources jar archive, size read: "+ sizeRead).failNel + } else { + val fileData = encodeBase64(bytes) + val baseReq = url(submitUrl(submitProject.courseId)) + val withArgs = baseReq << Map("assignment_part_sid" -> submitProject.assignmentPartId, + "email_address" -> challenge.email, + "submission" -> fileData, + "submission_aux" -> "", + "challenge_response" -> chResponse, + "state" -> challenge.state) + executeRequest(withArgs) { res => + // the API returns HTTP 200 even if there are failures, how impolite... + if (res.contains("Your submission has been accepted")) + res.successNel + else + res.failNel + } + } + } + } + + def challengeResponse(challenge: Challenge, otPassword: String): String = + shaHexDigest(challenge.challengeKey + otPassword) + + + /******************************** + * DOWNLOADING SUBMISSIONS + */ + + // def downloadFromQueue(queue: String, targetJar: File, apiKey: String): ValidationNEL[String, QueueResult] = { + // val baseReq = url(Settings.submitQueueUrl) + // val withArgsAndHeader = baseReq << Map("queue" -> queue) <:< Map("X-api-key" -> apiKey) + + // executeRequest(withArgsAndHeader) { res => + // extractJson(res, targetJar) + // } + // } + + def readJsonFile(jsonFile: File, targetJar: File): ValidationNEL[String, QueueResult] = { + extractJson(sbt.IO.read(jsonFile), targetJar) + } + + def extractJson(jsonData: String, targetJar: File): ValidationNEL[String, QueueResult] = { + import SubmitJsonProtocol._ + for { + jsonSubmission <- { + try { + val parsed = JsonParser(jsonData) + val submission = parsed \ "submission" + if (submission == JsNull) { + ("Nothing to grade, queue is empty.").failNel + } else { + submission.convertTo[JsonSubmission].successNel + } + } catch { + case e: Exception => + ("Could not parse submission\n"+ jsonData +"\n"+ fullExceptionString(e)).failNel + } + } + queueResult <- { + val encodedFile = jsonSubmission.submission + val jarContent = decodeBase64(encodedFile) + try { + sbt.IO.write(targetJar, jarContent) + QueueResult(jsonSubmission.api_state).successNel + } catch { + case e: IOException => + ("Failed to write jar file to "+ targetJar.getAbsolutePath +"\n"+ e.toString).failNel + } + } + } yield queueResult + } + + def unpackJar(file: File, targetDirectory: File): ValidationNEL[String, Unit] = { + try { + val files = sbt.IO.unzip(file, targetDirectory) + if (files.isEmpty) + ("No files found when unpacking jar file "+ file.getAbsolutePath).failNel + else + ().successNel + } catch { + case e: IOException => + val msg = "Error while unpacking the jar file "+ file.getAbsolutePath +" to "+ targetDirectory.getAbsolutePath +"\n"+ e.toString + if (Settings.offlineMode) { + println("[offline mode] "+ msg) + ().successNel + } else { + msg.failNel + } + } + } + + + /******************************** + * SUBMITTING GRADES + */ + + def submitGrade(feedback: String, score: String, apiState: String, apiKey: String, gradeProject: ProjectDetails): ValidationNEL[String, Unit] = { + import DefaultJsonProtocol._ + val baseReq = url(Settings.uploadFeedbackUrl(gradeProject.courseId)) + val withArgs = baseReq << Map("api_state" -> apiState, "score" -> score, "feedback" -> feedback) <:< Map("X-api-key" -> apiKey) + executeRequest(withArgs) { res => + try { + val js = JsonParser(res) + val status = (js \ "status").convertTo[String] + if (status == "202") + ().successNel + else + ("Unexpected result from submit request: "+ status).failNel + } catch { + case e: Exception => + ("Failed to parse response while submitting grade\n"+ res +"\n"+ fullExceptionString(e)).failNel + } + } + } + + + /********************************* + * TOOLS AND STUFF + */ + + def shaHexDigest(s: String): String = { + val chars = Hex.encodeHex(DigestUtils.sha(s)) + new String(chars) + } + + + def fullExceptionString(e: Throwable) = + e.toString +"\n"+ e.getStackTrace.map(_.toString).mkString("\n") + + + /* Base 64 tools */ + + def encodeBase64(bytes: Array[Byte]): String = + new String(Base64.encodeBase64(bytes)) + + def decodeBase64(str: String): Array[Byte] = { + // codecs 1.4 has a version accepting a string, but not 1.2; jar hell. + Base64.decodeBase64(str.getBytes) + } +} + +case class Challenge(email: String, challengeKey: String, state: String) + +case class QueueResult(apiState: String) + |