From c152c86081f8aa6ec0604f9f470bdc19554a3c23 Mon Sep 17 00:00:00 2001 From: anil Date: Wed, 2 Aug 2023 16:45:10 +0530 Subject: [PATCH 01/33] client side evaluation changes --- .../sunbird/utils/AssessmentContants.scala | 31 + .../org/sunbird/v5/actors/QuestionActor.scala | 37 +- .../v5/actors/QuestionSetAssessActor.scala | 32 + .../v5/managers/AssessmentV5Manager.scala | 163 +++- .../controllers/v5/QuestionController.scala | 54 +- .../v5/QuestionSetAssessController.scala | 19 + .../v5/QuestionSetController.scala | 34 +- .../app/utils/ActorNames.scala | 1 + .../assessment-service/app/utils/ApiId.scala | 1 + .../app/utils/QuestionSetOperations.scala | 2 +- .../assessment-service/conf/application.conf | 11 +- assessment-api/assessment-service/conf/routes | 8 +- .../sunbird/managers/HierarchyManager.scala | 105 ++- .../scala/org/sunbird/managers/KeyData.scala | 25 + .../org/sunbird/managers/KeyManager.scala | 114 +++ .../scala/org/sunbird/utils/Base64Util.java | 722 ++++++++++++++++++ .../scala/org/sunbird/utils/CryptoUtil.scala | 25 + .../sunbird/utils/HierarchyConstants.scala | 17 + .../org/sunbird/utils/JWTTokenType.scala | 19 + .../scala/org/sunbird/utils/JsonUtil.scala | 30 + .../scala/org/sunbird/utils/JwtUtil.scala | 70 ++ schemas/question/1.0/schema.json | 9 + schemas/question/1.1/schema.json | 9 + schemas/questionset/1.0/schema.json | 9 + schemas/questionset/1.1/schema.json | 9 + 25 files changed, 1516 insertions(+), 40 deletions(-) create mode 100644 assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetAssessActor.scala create mode 100644 assessment-api/assessment-service/app/controllers/v5/QuestionSetAssessController.scala create mode 100644 assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyData.scala create mode 100644 assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala create mode 100644 assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/Base64Util.java create mode 100644 assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/CryptoUtil.scala create mode 100644 assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JWTTokenType.scala create mode 100644 assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JsonUtil.scala create mode 100644 assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JwtUtil.scala diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala index 16e47ccde..e30bba3a9 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala @@ -45,4 +45,35 @@ object AssessmentConstants { val PRE_CONDITION: String = "preCondition" val SOURCE: String = "source" val PRE_CONDITION_VAR : String = "var" + val ASSESSMENTS = "assessments" + val QUESTION_SET_TOKEN = "questionSetToken" + val QUESTION_LIST = "questionList" + val QUESTION_LIST_EDITOR_URL = "question.list.search.editor.url" + val QUESTIONS = "questions" + val CORRECT_RESPONSE = "correctResponse" + val EVENTS = "events" + val EDATA = "edata" + val ITEM = "item" + val ID = "id" + val RESPONSE1 = "response1" + val CARDINALITY = "cardinality" + val MAX_SCORE = "maxScore" + val MULTIPLE = "multiple" + val EDITOR_STATE = "editorState" + val RESPONSE_DECLARATION = "responseDeclaration" + val PASS = "pass" + val YES = "Yes" + val NO = "No" + val RESVALUES = "resvalues" + val PARAMS = "params" + val SCORE = "score" + val VALUE = "value" + val MAPPING = "mapping" + val RESPONSE = "response" + val OUTCOMES = "outcomes" + val OPTIONS = "options" + val EVAL: String = "eval" + val SERVER: String = "server" + val FLOWER_BRACKETS: String = "{}" + } diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala index 935c774a2..7f2e9eab0 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala @@ -1,5 +1,6 @@ package org.sunbird.v5.actors +import com.fasterxml.jackson.databind.ObjectMapper import org.apache.commons.lang3.StringUtils import org.sunbird.`object`.importer.{ImportConfig, ImportManager} import org.sunbird.actor.core.BaseActor @@ -9,8 +10,10 @@ import org.sunbird.common.{DateUtils, Platform} import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.nodes.DataNode import org.sunbird.graph.schema.DefinitionNode +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.managers.AssessmentManager.mapper import org.sunbird.managers.CopyManager -import org.sunbird.utils.{AssessmentErrorCodes, RequestUtil} +import org.sunbird.utils.{AssessmentConstants, AssessmentErrorCodes, RequestUtil} import org.sunbird.v5.managers.AssessmentV5Manager import java.util @@ -26,6 +29,7 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA private lazy val importConfig = getImportConfig() private lazy val importMgr = new ImportManager(importConfig) val defaultVersion = Platform.config.getNumber("v5_default_qumlVersion") + private val mapper = new ObjectMapper() override def onReceive(request: Request): Future[Response] = request.getOperation match { case "createQuestion" => AssessmentV5Manager.create(request) @@ -48,6 +52,16 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA val extPropNameList:util.List[String] = DefinitionNode.getExternalProps(request.getContext.get("graph_id").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String], request.getContext.get("schemaName").asInstanceOf[String]).asJava request.getRequest.put("fields", extPropNameList) DataNode.read(request).map(node => { + val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) + val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) + if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE) == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { + val hideEditorResponse = AssessmentV5Manager.hideEditorStateAns(node) + if (StringUtils.isNotEmpty(hideEditorResponse)) + node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorResponse) + val hideCorrectAns = AssessmentV5Manager.hideCorrectResponse(node) + if (StringUtils.isNotEmpty(hideCorrectAns)) + node.getMetadata.put(AssessmentConstants.RESPONSE_DECLARATION, hideCorrectAns) + } if (StringUtils.equalsIgnoreCase(node.getMetadata.get("visibility").asInstanceOf[String], "Private")) throw new ClientException(AssessmentErrorCodes.ERR_ACCESS_DENIED, s"Question visibility is private, hence access denied") ResponseHandler.OK.put("question", AssessmentV5Manager.getQuestionMetadata(node, fields, extPropNameList)) @@ -78,10 +92,23 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA val fields: util.List[String] = JavaConverters.seqAsJavaListConverter(request.get("fields").asInstanceOf[String].split(",").filter(field => StringUtils.isNotBlank(field) && !StringUtils.equalsIgnoreCase(field, "null"))).asJava request.getRequest.put("fields", fields) DataNode.search(request).map(nodeList => { - val questionList = nodeList.map(node => AssessmentV5Manager.getQuestionMetadata(node, fields, List().asJava)).asJava - ResponseHandler.OK.put("questions", questionList).put("count", questionList.size) - }) - } + val questionList = nodeList.map(node => { + val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) + val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) + if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE).equalsIgnoreCase(AssessmentConstants.SERVER) && !StringUtils.equals(request.get("isEditor").asInstanceOf[String], "true")) { + val hideEditorStateAns = AssessmentV5Manager.hideEditorStateAns(node) + if (StringUtils.isNotEmpty(hideEditorStateAns)) + node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorStateAns) + val hideCorrectResponse = AssessmentV5Manager.hideCorrectResponse(node) + if (StringUtils.isNotEmpty(hideCorrectResponse)) + node.getMetadata.put(AssessmentConstants.RESPONSE_DECLARATION, hideCorrectResponse) + } + NodeUtil.serialize(node, fields, node.getObjectType.toLowerCase.replace("Image", ""), request.getContext.get("version").asInstanceOf[String]) + }).asJava + ResponseHandler.OK.put("questions", questionList).put("count", questionList.size) + }) + } + def privateRead(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { val fields: util.List[String] = JavaConverters.seqAsJavaListConverter(request.get("fields").asInstanceOf[String].split(",").filter(field => StringUtils.isNotBlank(field) && !StringUtils.equalsIgnoreCase(field, "null"))).asJava diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetAssessActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetAssessActor.scala new file mode 100644 index 000000000..a6f5cb4b3 --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetAssessActor.scala @@ -0,0 +1,32 @@ +package org.sunbird.v5.actors + +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.managers.AssessmentManager +import org.sunbird.utils.AssessmentConstants +import org.sunbird.v5.managers.AssessmentV5Manager + +import java.util +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class QuestionSetAssessActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + + @throws[Throwable] + override def onReceive(request: Request): Future[Response] = request.getOperation match { + case "assessQuestionSet" => assessment(request) + case _ => ERROR(request.getOperation) + } + + private def assessment(req: Request): Future[Response] = { + val assessments = req.getRequest.getOrDefault(AssessmentConstants.ASSESSMENTS, new util.ArrayList[util.Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]] + val quesDoIds = AssessmentV5Manager.validateAssessRequest(req) + val list: Response = AssessmentV5Manager.questionList(quesDoIds) + AssessmentV5Manager.calculateScore(list, assessments) + Future(ResponseHandler.OK.put(AssessmentConstants.QUESTIONS, req.getRequest)) + } + +} diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala index e548e85f5..6b6d3ba10 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala @@ -1,5 +1,6 @@ package org.sunbird.v5.managers +import com.fasterxml.jackson.databind.ObjectMapper import org.apache.commons.collections4.CollectionUtils import org.apache.commons.lang3.StringUtils import org.sunbird.common.{DateUtils, JsonUtils, Platform} @@ -12,7 +13,7 @@ import org.sunbird.graph.schema.{DefinitionNode, ObjectCategoryDefinition} import org.sunbird.graph.utils.NodeUtil import org.sunbird.managers.HierarchyManager import org.sunbird.telemetry.util.LogTelemetryEventUtil -import org.sunbird.utils.{AssessmentErrorCodes, RequestUtil} +import org.sunbird.utils.{AssessmentConstants, AssessmentErrorCodes, RequestUtil} import java.util import java.util.UUID @@ -21,6 +22,11 @@ import scala.collection.JavaConversions._ import scala.collection.JavaConverters._ import scala.collection.mutable.ListBuffer import scala.concurrent.duration.Duration +import com.mashape.unirest.http.Unirest +import org.apache.http.HttpResponse +import org.sunbird.utils.{AssessmentConstants, JavaJsonUtils, RequestUtil} +import com.fasterxml.jackson.databind.node.{ArrayNode, ObjectNode} +import org.sunbird.common.exception.{ClientException, ErrorCodes, ResourceNotFoundException, ServerException} object AssessmentV5Manager { @@ -28,6 +34,8 @@ object AssessmentV5Manager { val supportedVersions: java.util.List[Number] = Platform.config.getNumberList("v5_supported_qumlVersions") val skipValidation: Boolean = Platform.getBoolean("assessment.skip.validation", false) val validStatus = List("Draft", "Review") + val mapper = new ObjectMapper() + val map = Map("userId" -> "userID", "attemptId" -> "attemptID") def validateAndGetVersion(ver: AnyRef): AnyRef = { if (supportedVersions.contains(ver)) ver else throw new ClientException(AssessmentErrorCodes.ERR_REQUEST_DATA_VALIDATION, s"Platform doesn't support quml version ${ver} | Currently Supported quml version are: ${supportedVersions}") @@ -84,6 +92,16 @@ object AssessmentV5Manager { def getValidateNodeForReject(request: Request, errCode: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { request.put("mode", "edit") DataNode.read(request).map(node => { + val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) + val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) + if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE) == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { + val hideEditorResponse = hideEditorStateAns(node) + if (StringUtils.isNotEmpty(hideEditorResponse)) + node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorResponse) + val hideCorrectAns = hideCorrectResponse(node) + if (StringUtils.isNotEmpty(hideCorrectAns)) + node.getMetadata.put(AssessmentConstants.RESPONSE_DECLARATION, hideCorrectAns) + } if (StringUtils.equalsIgnoreCase(node.getMetadata.getOrDefault("visibility", "").asInstanceOf[String], "Parent")) throw new ClientException(errCode, s"${node.getObjectType.replace("Image", "")} with visibility Parent, can't be sent for reject individually.") if (!StringUtils.equalsIgnoreCase("Review", node.getMetadata.get("status").asInstanceOf[String])) @@ -489,4 +507,147 @@ object AssessmentV5Manager { } } + def validateAssessRequest(req: Request) = { + val body = req.getRequest + val jwt = body.getOrDefault(AssessmentConstants.QUESTION_SET_TOKEN, "").asInstanceOf[String] + val payload = try { + val tuple = HierarchyManager.verifyRS256Token(jwt) + if (tuple._1 == false) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Token Authentication Failed") + tuple._2.get("data").asInstanceOf[String] + } catch { + case e: Exception => throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Token Authentication Failed") + } + val questToken = JavaJsonUtils.deserialize[java.util.Map[String, AnyRef]](payload) + val assessments = body.getOrDefault(AssessmentConstants.ASSESSMENTS, new util.ArrayList[util.Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]] + val courseMetaData = Option(assessments.get(0)).getOrElse(new util.HashMap[String, AnyRef]) + val count = map.filter(key => StringUtils.equals(courseMetaData.get(key._1).asInstanceOf[String], questToken.get(key._2).asInstanceOf[String])).size + if (count != map.size) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Token Authentication Failed") + questToken.get(AssessmentConstants.QUESTION_LIST).asInstanceOf[String].split(",").asInstanceOf[Array[String]] + } + + def questionList(fields: Array[String]): Response = { + val url: String = Platform.getString(AssessmentConstants.QUESTION_LIST_EDITOR_URL, "") + val bdy = "{\"request\":{\"search\":{\"identifier\":" + JavaJsonUtils.serialize(fields) + "}}}" + val httpResponse = post(url, bdy) + if (200 != httpResponse.status) throw new ServerException("ERR_QUESTION_LIST_API_COMM", "Error communicating to question list api") + JsonUtils.deserialize(httpResponse.body, classOf[Response]) + } + + def calculateScore(privateList: Response, assessments: util.List[util.Map[String, AnyRef]]): Unit = { + val answerMaps: (Map[String, AnyRef], Map[String, AnyRef]) = getListMap(privateList.getResult, AssessmentConstants.QUESTIONS) + .map { que => + ((que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.RESPONSE_DECLARATION)), + (que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.EDITOR_STATE))) + }.unzip match { + case (map1, map2) => (map1.toMap, map2.toMap) + } + val answerMap = answerMaps._1 + val editorStateMap = answerMaps._2 + assessments.foreach { k => + getListMap(k, AssessmentConstants.EVENTS).toList.foreach { event => + val edata = getMap(event, AssessmentConstants.EDATA) + val item = getMap(edata, AssessmentConstants.ITEM) + val identifier = item.getOrDefault(AssessmentConstants.ID, "").asInstanceOf[String] + if (!answerMap.contains(identifier)) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Invalid Request") + val res = getMap(answerMap.get(identifier).asInstanceOf[Some[util.Map[String, AnyRef]]].x, AssessmentConstants.RESPONSE1) + val cardinality = res.getOrDefault(AssessmentConstants.CARDINALITY, "").asInstanceOf[String] + val maxScore = res.getOrDefault(AssessmentConstants.MAX_SCORE, 0.asInstanceOf[Integer]).asInstanceOf[Integer] + cardinality match { + case AssessmentConstants.MULTIPLE => populateMultiCardinality(res, edata, maxScore) + case _ => populateSingleCardinality(res, edata, maxScore) + } + populateParams(item, editorStateMap) + } + } + } + + private def getListMap(arg: util.Map[String, AnyRef], param: String) = { + arg.getOrDefault(param, new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]] + } + + private def getMap(arg: util.Map[String, AnyRef], param: String) = { + arg.getOrDefault(param, new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + } + + def hideEditorStateAns(node: Node): String = { + // Modify editorState + Option(node.getMetadata.get("editorState")) match { + case Some(jsonStr: String) => + val jsonNode = mapper.readTree(jsonStr) + //if (jsonNode != null && jsonNode.has("question")) { + //val questionNode = jsonNode.get("question") + if (jsonNode != null && jsonNode.has("options")) { + val optionsNode = jsonNode.get("options").asInstanceOf[ArrayNode] + val iterator = optionsNode.elements() + while (iterator.hasNext) { + val optionNode = iterator.next().asInstanceOf[ObjectNode] + optionNode.remove("answer") + } + //} + } + mapper.writeValueAsString(jsonNode) + case _ => "" + } + } + + def hideCorrectResponse(node: Node): String = { + val responseDeclaration = Option(node.getMetadata.get("responseDeclaration")) match { + case Some(jsonStr: String) => jsonStr + case _ => "" + } + val jsonNode = mapper.readTree(responseDeclaration) + if (null != jsonNode && jsonNode.has("response1")) { + val responseNode = jsonNode.get("response1").asInstanceOf[ObjectNode] + responseNode.remove("correctResponse") + mapper.writeValueAsString(jsonNode) + } + else + "" + } + + private def populateParams(item: util.Map[String, AnyRef], editorState: Map[String, AnyRef]) = { + item.put(AssessmentConstants.PARAMS, editorState.get(item.get(AssessmentConstants.ID)).asInstanceOf[util.Map[String, AnyRef]].get(AssessmentConstants.OPTIONS)) + } + + private def post(url: String, requestBody: String, headers: Map[String, String] = Map[String, String]("Content-Type" -> "application/json")): HTTPResponse = { + val res = Unirest.post(url).headers(headers.asJava).body(requestBody).asString() + HTTPResponse(res.getStatus, res.getBody) + } + + private case class HTTPResponse(status: Int, body: String) extends Serializable + + private def populateSingleCardinality(res: util.Map[String, AnyRef], edata: util.Map[String, AnyRef], maxScore: Integer): Unit = { + val correctValue = getMap(res, AssessmentConstants.CORRECT_RESPONSE).getOrDefault(AssessmentConstants.VALUE, new util.ArrayList[Integer]).asInstanceOf[String] + val usrResponse = getListMap(edata, AssessmentConstants.RESVALUES).get(0).getOrDefault(AssessmentConstants.VALUE, "").toString + StringUtils.equals(usrResponse, correctValue) match { + case true => { + edata.put(AssessmentConstants.SCORE, maxScore) + edata.put(AssessmentConstants.PASS, AssessmentConstants.YES) + } + case _ => { + edata.put(AssessmentConstants.SCORE, 0.asInstanceOf[Integer]) + edata.put(AssessmentConstants.PASS, AssessmentConstants.NO) + } + } + } + + private def populateMultiCardinality(res: util.Map[String, AnyRef], edata: util.Map[String, AnyRef], maxScore: Integer) = { + val correctValue = getMap(res, AssessmentConstants.CORRECT_RESPONSE).getOrDefault(AssessmentConstants.VALUE, new util.ArrayList[Integer]).asInstanceOf[util.ArrayList[Integer]].flatMap(k => List(k)).sorted + val usrResponse = edata.getOrDefault(AssessmentConstants.RESVALUES, new util.ArrayList[util.ArrayList[util.Map[String, AnyRef]]]()) + .asInstanceOf[util.ArrayList[util.ArrayList[util.Map[String, AnyRef]]]] + .flatMap(_.flatMap(res => List(res.getOrDefault(AssessmentConstants.VALUE, -1.asInstanceOf[Integer]).asInstanceOf[Integer]))).sorted + correctValue.equals(usrResponse) match { + case true => edata.put(AssessmentConstants.SCORE, maxScore) + case _ => { + var ttlScr = 0.0d + getListMap(res, AssessmentConstants.MAPPING).foreach(k => if (usrResponse.contains(k.getOrDefault(AssessmentConstants.RESPONSE, -1.asInstanceOf[Integer]).asInstanceOf[Integer])) + ttlScr += getMap(k, AssessmentConstants.OUTCOMES).get(AssessmentConstants.SCORE).asInstanceOf[Double]) + edata.put(AssessmentConstants.SCORE, ttlScr.asInstanceOf[AnyRef]) + if (ttlScr > 0) edata.put(AssessmentConstants.PASS, AssessmentConstants.YES) else edata.put(AssessmentConstants.PASS, AssessmentConstants.NO) + } + } + } } diff --git a/assessment-api/assessment-service/app/controllers/v5/QuestionController.scala b/assessment-api/assessment-service/app/controllers/v5/QuestionController.scala index c74fc070f..03f65e706 100644 --- a/assessment-api/assessment-service/app/controllers/v5/QuestionController.scala +++ b/assessment-api/assessment-service/app/controllers/v5/QuestionController.scala @@ -25,14 +25,8 @@ class QuestionController @Inject()(@Named(ActorNames.QUESTION_V5_ACTOR) question getResult(ApiId.CREATE_QUESTION, questionActor, questionRequest) } - def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => - val headers = commonHeaders() - val question = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] - question.putAll(headers) - question.putAll(Map("identifier" -> identifier, "fields" -> fields.getOrElse(""), "mode" -> mode.getOrElse("read")).asJava) - val questionRequest = getRequest(question, headers, QuestionOperations.readQuestion.toString) - setRequestContext(questionRequest, defaultVersion, objectType, schemaName) - getResult(ApiId.READ_QUESTION, questionActor, questionRequest) + def read(identifier: String, mode: Option[String], fields: Option[String]) = { + readQuestion(identifier, mode, fields, false) } def privateRead(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => @@ -108,16 +102,8 @@ class QuestionController @Inject()(@Named(ActorNames.QUESTION_V5_ACTOR) question getResult(ApiId.SYSTEM_UPDATE_QUESTION, questionActor, questionRequest) } - def list(fields: Option[String]) = Action.async { implicit request => - val headers = commonHeaders() - val body = requestBody() - val question = body.getOrDefault("search", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; - question.putAll(headers) - question.put("fields", fields.getOrElse("")) - val questionRequest = getRequest(question, headers, QuestionOperations.listQuestions.toString) - questionRequest.put("identifiers", questionRequest.get("identifier")) - setRequestContext(questionRequest, defaultVersion, objectType, schemaName) - getResult(ApiId.LIST_QUESTIONS, questionActor, questionRequest) + def list(fields: Option[String]) = { + fetchQuestions(fields, false) } def reject(identifier: String) = Action.async { implicit request => @@ -141,4 +127,36 @@ class QuestionController @Inject()(@Named(ActorNames.QUESTION_V5_ACTOR) question setRequestContext(questionRequest, defaultVersion, objectType, schemaName) getResult(ApiId.COPY_QUESTION, questionActor, questionRequest) } + + def editorList(fields: Option[String]) = { + fetchQuestions(fields, true) + } + + def editorRead(identifier: String, mode: Option[String], fields: Option[String]) = { + readQuestion(identifier, mode, fields, true) + } + + private def readQuestion(identifier: String, mode: Option[String], fields: Option[String], exclusive: Boolean) = Action.async { implicit request => + val headers = commonHeaders() + val question = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + question.putAll(headers) + question.putAll(Map("identifier" -> identifier, "fields" -> fields.getOrElse(""), "mode" -> mode.getOrElse("read")).asJava) + if (exclusive) question.put("isEditor", "true") + val questionRequest = getRequest(question, headers, QuestionOperations.readQuestion.toString) + setRequestContext(questionRequest, defaultVersion, objectType, schemaName) + getResult(ApiId.READ_QUESTION, questionActor, questionRequest) + } + + private def fetchQuestions(fields: Option[String], exclusive: Boolean) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val question = body.getOrDefault("search", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + question.putAll(headers) + question.put("fields", fields.getOrElse("")) + if (exclusive) question.put("isEditor", "true") + val questionRequest = getRequest(question, headers, QuestionOperations.listQuestions.toString) + questionRequest.put("identifiers", questionRequest.get("identifier")) + setRequestContext(questionRequest, defaultVersion, objectType, schemaName) + getResult(ApiId.LIST_QUESTIONS, questionActor, questionRequest) + } } diff --git a/assessment-api/assessment-service/app/controllers/v5/QuestionSetAssessController.scala b/assessment-api/assessment-service/app/controllers/v5/QuestionSetAssessController.scala new file mode 100644 index 000000000..b62703b60 --- /dev/null +++ b/assessment-api/assessment-service/app/controllers/v5/QuestionSetAssessController.scala @@ -0,0 +1,19 @@ +package controllers.v5 + +import akka.actor.{ActorRef, ActorSystem} +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId, QuestionSetOperations} + +import javax.inject.{Inject, Named} +import scala.concurrent.ExecutionContext + +class QuestionSetAssessController @Inject()(@Named(ActorNames.QUESTION_SET_ASSESS_ACTOR) questionSetAssessActor: ActorRef, + cc: ControllerComponents, + actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + def assessment() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val questionSetAssessRequest = getRequest(body, headers, QuestionSetOperations.assessQuestionSet.toString) + getResult(ApiId.ASSESS_QUESTION_SET, questionSetAssessActor, questionSetAssessRequest) + } +} diff --git a/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala b/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala index 8b0fca6be..d5fa78b3a 100644 --- a/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala +++ b/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala @@ -118,14 +118,18 @@ class QuestionSetController @Inject()(@Named(ActorNames.QUESTION_SET_V5_ACTOR) q getResult(ApiId.UPDATE_HIERARCHY, questionSetActor, questionSetRequest) } - def getHierarchy(identifier: String, mode: Option[String]) = Action.async { implicit request => - val headers = commonHeaders() - val questionSet = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] - questionSet.putAll(headers) - questionSet.putAll(Map("rootId" -> identifier, "mode" -> mode.getOrElse("")).asJava) - val readRequest = getRequest(questionSet, headers, "getHierarchy") - setRequestContext(readRequest, defaultVersion, objectType, schemaName) - getResult(ApiId.GET_HIERARCHY, questionSetActor, readRequest) +// def getHierarchy(identifier: String, mode: Option[String]) = Action.async { implicit request => +// val headers = commonHeaders() +// val questionSet = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] +// questionSet.putAll(headers) +// questionSet.putAll(Map("rootId" -> identifier, "mode" -> mode.getOrElse("")).asJava) +// val readRequest = getRequest(questionSet, headers, "getHierarchy") +// setRequestContext(readRequest, defaultVersion, objectType, schemaName) +// getResult(ApiId.GET_HIERARCHY, questionSetActor, readRequest) +// } + + def getHierarchy(identifier: String, mode: Option[String]) = { + fetchHierarchy(identifier, mode) } def reject(identifier: String) = Action.async { implicit request => @@ -169,4 +173,18 @@ class QuestionSetController @Inject()(@Named(ActorNames.QUESTION_SET_V5_ACTOR) q setRequestContext(questionSetRequest, defaultVersion, objectType, schemaName) getResult(ApiId.COPY_QUESTION_SET, questionSetActor, questionSetRequest) } + + def getHierarchyRead(identifier: String, mode: Option[String]) = { + fetchHierarchy(identifier, mode, "true") + } + def fetchHierarchy(identifier: String, mode: Option[String], evaluable: String = "false") = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val questionSet = body.getOrDefault("questionset", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + questionSet.putAll(headers) + questionSet.putAll(Map("rootId" -> identifier, "mode" -> mode.getOrElse(""), "serverEvaluable" -> evaluable).asJava) + val readRequest = getRequest(questionSet, headers, "getHierarchy") + setRequestContext(readRequest, defaultVersion, objectType, schemaName) + getResult(ApiId.GET_HIERARCHY, questionSetActor, readRequest) + } } diff --git a/assessment-api/assessment-service/app/utils/ActorNames.scala b/assessment-api/assessment-service/app/utils/ActorNames.scala index 526ec1b19..239363249 100644 --- a/assessment-api/assessment-service/app/utils/ActorNames.scala +++ b/assessment-api/assessment-service/app/utils/ActorNames.scala @@ -8,5 +8,6 @@ object ActorNames { final val QUESTION_SET_ACTOR = "questionSetActor" final val QUESTION_V5_ACTOR = "questionV5Actor" final val QUESTION_SET_V5_ACTOR = "questionSetV5Actor" + final val QUESTION_SET_ASSESS_ACTOR="questionSetAssessActor" } diff --git a/assessment-api/assessment-service/app/utils/ApiId.scala b/assessment-api/assessment-service/app/utils/ApiId.scala index 062340338..3486cd75d 100644 --- a/assessment-api/assessment-service/app/utils/ApiId.scala +++ b/assessment-api/assessment-service/app/utils/ApiId.scala @@ -42,4 +42,5 @@ object ApiId { val IMPORT_QUESTION_SET = "api.questionset.import" val SYSTEM_UPDATE_QUESTION_SET = "api.questionset.system.update" val COPY_QUESTION_SET = "api.questionset.copy" + val ASSESS_QUESTION_SET="api.questionset.assess" } diff --git a/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala b/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala index 43f0266b1..97bde8b5c 100644 --- a/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala +++ b/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala @@ -3,5 +3,5 @@ package utils object QuestionSetOperations extends Enumeration { val createQuestionSet, readQuestionSet, readPrivateQuestionSet, updateQuestionSet, reviewQuestionSet, publishQuestionSet, retireQuestionSet, addQuestion, removeQuestion, updateHierarchyQuestion, readHierarchyQuestion, - rejectQuestionSet, importQuestionSet, systemUpdateQuestionSet, copyQuestionSet = Value + rejectQuestionSet, importQuestionSet, systemUpdateQuestionSet, copyQuestionSet, assessQuestionSet = Value } diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index 8c3416b3b..4b5460494 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -101,6 +101,12 @@ akka { nr-of-instances = 5 dispatcher = actors-dispatcher } + /questionSetAssessActor + { + router = smallest-mailbox-pool + nr-of-instances = 5 + dispatcher = actors-dispatcher + } } } } @@ -445,4 +451,7 @@ assessment.copy.props_to_remove=["downloadUrl", "artifactUrl", "variants", "concepts", "keywords", "reservedDialcodes", "dialcodeRequired", "leafNodes", "sYS_INTERNAL_LAST_UPDATED_ON", "prevStatus", "lastPublishedBy", "streamingUrl"] v5_supported_qumlVersions=[1.1] -v5_default_qumlVersion=1.1 \ No newline at end of file +v5_default_qumlVersion=1.1 + +api.jwt.keyprefix=device +api.jwt.keycount=1 \ No newline at end of file diff --git a/assessment-api/assessment-service/conf/routes b/assessment-api/assessment-service/conf/routes index 1abc5c2b0..b817a6e2f 100644 --- a/assessment-api/assessment-service/conf/routes +++ b/assessment-api/assessment-service/conf/routes @@ -72,4 +72,10 @@ GET /questionset/v5/hierarchy/:identifier controllers.v5.QuestionSetC POST /questionset/v5/reject/:identifier controllers.v5.QuestionSetController.reject(identifier:String) POST /questionset/v5/import controllers.v5.QuestionSetController.importQuestionSet() PATCH /questionset/v5/system/update/:identifier controllers.v5.QuestionSetController.systemUpdate(identifier:String) -POST /questionset/v5/copy/:identifier controllers.v5.QuestionSetController.copy(identifier:String, mode:Option[String], type:String?="deep") \ No newline at end of file +POST /questionset/v5/copy/:identifier controllers.v5.QuestionSetController.copy(identifier:String, mode:Option[String], type:String?="deep") +POST /questionset/v5/hierarchy/:identifier controllers.v5.QuestionSetController.getHierarchyRead(identifier:String, mode:Option[String]) +POST /question/v5/editor/list controllers.v5.QuestionController.editorList(fields:Option[String]) +GET /question/v5/editor/read/:identifier controllers.v5.QuestionController.editorRead(identifier:String, mode:Option[String], fields:Option[String]) + +# QuestionValidate API's +POST /questionset/v5/assestment/validate controllers.v5.QuestionSetAssessController.assessment \ No newline at end of file diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala index f55881a1c..4b59b77f3 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala @@ -21,16 +21,20 @@ import com.mashape.unirest.http.Unirest import org.apache.commons.collections4.{CollectionUtils, MapUtils} import org.sunbird.graph.OntologyEngineContext import org.sunbird.telemetry.logger.TelemetryManager -import org.sunbird.utils.{HierarchyConstants, HierarchyErrorCodes} +import org.sunbird.utils.{HierarchyConstants, HierarchyErrorCodes, JwtUtils} + +import scala.collection.mutable object HierarchyManager { val schemaName: String = "questionset" val imgSuffix: String = ".img" val hierarchyPrefix: String = "qs_hierarchy_" - val statusList = List("Live", "Unlisted", "Flagged") + val statusList = List("Live", "Unlisted", "Flagged", "Draft") val ASSESSMENT_OBJECT_TYPES = List("Question", "QuestionSet") + val keyManager = new KeyManager(Platform.getString("api.jwt.basepath","./keys/"), Platform.getString("api.jwt.keyprefix","device"), Platform.getInteger("keys.count",1)) + val keyTobeRemoved = { if(Platform.config.hasPath("content.hierarchy.removed_props_for_leafNodes")) Platform.config.getStringList("content.hierarchy.removed_props_for_leafNodes") @@ -250,9 +254,38 @@ object HierarchyManager { hierarchyFuture.map(result => { if (!result.isEmpty) { val bookmarkId = request.get("bookmarkId").asInstanceOf[String] - val rootHierarchy = result.get("questionSet").asInstanceOf[util.Map[String, AnyRef]] + val rootHierarchy = result.get(HierarchyConstants.QUESTIONSET).asInstanceOf[util.Map[String, AnyRef]] if (StringUtils.isEmpty(bookmarkId)) { - ResponseHandler.OK.put("questionSet", rootHierarchy) + if (request.get(HierarchyConstants.SERVEREVALUABLE).asInstanceOf[String].equalsIgnoreCase(HierarchyConstants.TRUE)) { + val mutableRootHierarchy = mutable.Map[String, AnyRef](rootHierarchy.asScala.toSeq: _*) + val childrenList = mutableRootHierarchy + .getOrElse(HierarchyConstants.CHILDREN, new util.ArrayList[java.util.Map[String, AnyRef]]) + .asInstanceOf[util.ArrayList[java.util.Map[String, AnyRef]]] + val updatedChildrenList = childrenList.asScala.map(child => { + val maxQuestions = Option(child.get(HierarchyConstants.MAXQUESTIONS)).map(_.asInstanceOf[Int]).getOrElse(0) + val shuffle = Option(child.get(HierarchyConstants.SHUFFLE)).map(_.asInstanceOf[Boolean]).getOrElse(false) + val randomizedChild = if (shuffle) shuffleQuestions(child) else child + val limitedChild = limitQuestions(randomizedChild, maxQuestions) + + limitedChild + }).asJava + val serverEvaluable = updatedChildrenList.get(0).getOrDefault(HierarchyConstants.EVAL, new util.LinkedHashMap()).asInstanceOf[java.util.LinkedHashMap[String, String]] + if (serverEvaluable.get(HierarchyConstants.MODE) != null && serverEvaluable.get(HierarchyConstants.MODE).equalsIgnoreCase(HierarchyConstants.SERVER)) { + request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.SERVER) + } else { + request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.CLIENT) + } + val nestedChildrenIdentifiers = getNestedChildrenIdentifiers(updatedChildrenList) + val mergedMap: util.Map[String, String] = createMergedMap(request, nestedChildrenIdentifiers) + val userMapJson = JsonUtils.serialize(mergedMap) + val jwtToken = generateJwtToken(userMapJson) + mutableRootHierarchy.put(HierarchyConstants.IDENTIFIER, request.get(HierarchyConstants.ROOTID)) + mutableRootHierarchy.put(HierarchyConstants.QUESTIONSETTOKEN, jwtToken) + mutableRootHierarchy.put(HierarchyConstants.CHILDREN, updatedChildrenList) + ResponseHandler.OK.put(HierarchyConstants.QUESTIONSET, mutableRootHierarchy.asJava) + } else { + ResponseHandler.OK.put(HierarchyConstants.QUESTIONSET, rootHierarchy) + } } else { val children = rootHierarchy.getOrElse("children", new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.List[util.Map[String, AnyRef]]] val bookmarkHierarchy = filterBookmarkHierarchy(children, bookmarkId) @@ -520,7 +553,7 @@ object HierarchyManager { val hierarchy = fetchHierarchy(request, request.getRequest.get("rootId").asInstanceOf[String]) hierarchy.map(hierarchy => { if (!hierarchy.isEmpty) { - if (StringUtils.isNotEmpty(hierarchy.getOrDefault("status", "").asInstanceOf[String]) && statusList.contains(hierarchy.getOrDefault("status", "").asInstanceOf[String])) { + if (StringUtils.isNotEmpty(hierarchy.getOrDefault("status", "Draft").asInstanceOf[String]) && statusList.contains(hierarchy.getOrDefault("status", "Draft").asInstanceOf[String])) { val hierarchyMap = mapAsJavaMap(hierarchy) rootHierarchy.put("questionSet", hierarchyMap) RedisCache.set(hierarchyPrefix + request.get("rootId"), JsonUtils.serialize(hierarchyMap)) @@ -756,5 +789,67 @@ object HierarchyManager { updatedBranchingLogic } + def shuffleQuestions(child: util.Map[String, AnyRef]): util.Map[String, AnyRef] = { + val questions = child.getOrDefault(HierarchyConstants.CHILDREN, new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.List[util.Map[String, AnyRef]]] + util.Collections.shuffle(questions) + child + } + + def limitQuestions(child: util.Map[String, AnyRef], maxQuestions: Int): util.Map[String, AnyRef] = { + if (maxQuestions > 0) { + val questions = child.getOrDefault(HierarchyConstants.CHILDREN, new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.List[util.Map[String, AnyRef]]] + val limitedQuestions = questions.subList(0, Math.min(maxQuestions, questions.size())) + child.put(HierarchyConstants.CHILDREN, limitedQuestions) + } + child + } + + + private def getNestedChildrenIdentifiers(childrenList: util.List[java.util.Map[String, AnyRef]]): String = { + val javaChildrenList: java.util.List[java.util.Map[String, AnyRef]] = childrenList.map(map => mapAsJavaMap(map)).asJava + javaChildrenList.asScala.flatMap { child => + val nestedChildren = child + .getOrDefault(HierarchyConstants.CHILDREN, new util.ArrayList[java.util.Map[String, AnyRef]]) + .asInstanceOf[util.List[java.util.Map[String, AnyRef]]] + .asScala + .toList + .asInstanceOf[Seq[java.util.Map[String, AnyRef]]] + val javaNestedChildren = JavaConverters.seqAsJavaListConverter(nestedChildren).asJava + javaNestedChildren.asScala.map(_.get(HierarchyConstants.IDENTIFIER).asInstanceOf[String]) + }.mkString(",") + } + + private def createMergedMap(request: Request, nestedChildrenIdentifiers: String): util.Map[String, String] = { + val questionMap: util.HashMap[String, String] = new util.HashMap[String, String]() + val userMap: util.Map[String, String] = new util.HashMap[String, String]() + val mergedMap: util.Map[String, String] = new util.HashMap[String, String]() + + questionMap.put(HierarchyConstants.QUESTIONLIST, nestedChildrenIdentifiers) + userMap.put(HierarchyConstants.CONTENTID, request.get(HierarchyConstants.ROOTID).asInstanceOf[String]) + userMap.put(HierarchyConstants.COLLECTIONID, request.get(HierarchyConstants.COLLECTIONID).asInstanceOf[String]) + userMap.put(HierarchyConstants.USERID, request.get(HierarchyConstants.USERID).asInstanceOf[String]) + userMap.put(HierarchyConstants.ATTEMPTID, request.get(HierarchyConstants.ATTEMPTID).asInstanceOf[String]) + userMap.put(HierarchyConstants.EVAL_MODE, request.get(HierarchyConstants.EVAL_MODE).asInstanceOf[String]) + mergedMap.putAll(userMap) + mergedMap.putAll(questionMap) + + mergedMap + } + + private def generateJwtToken(userMapJson: String): String = { + val headerOptions: java.util.Map[String, String] = new java.util.HashMap[String, String]() + val keyData = keyManager.getRandomKey() + val id = keyData.getKeyId + val privateKey = keyData.getPrivateKey + headerOptions.put("type", "jwt") + headerOptions.put("alg", "RS256") + headerOptions.put("keyId", id) + JwtUtils.createRS256Token(userMapJson, privateKey, headerOptions) + } + + def verifyRS256Token(token: String) = { + JwtUtils.verifyRS256Token(token, keyManager) + } + } diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyData.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyData.scala new file mode 100644 index 000000000..4c238d5f4 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyData.scala @@ -0,0 +1,25 @@ +package org.sunbird.managers + +import java.security.PrivateKey +import java.security.PublicKey + +class KeyData(private var keyId: String, private var privateKey: PrivateKey, private var publicKey: PublicKey) { + + def getKeyId: String = keyId + + def setKeyId(keyId: String): Unit = { + this.keyId = keyId + } + + def getPrivateKey: PrivateKey = privateKey + + def setPrivateKey(privateKey: PrivateKey): Unit = { + this.privateKey = privateKey + } + + def getPublicKey: PublicKey = publicKey + + def setPublicKey(publicKey: PublicKey): Unit = { + this.publicKey = publicKey + } +} \ No newline at end of file diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala new file mode 100644 index 000000000..76df935ff --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala @@ -0,0 +1,114 @@ +package org.sunbird.managers +import org.slf4j.LoggerFactory +import org.sunbird.common.Platform +import org.sunbird.utils.Base64Util + +import java.io.FileInputStream +import java.nio.charset.StandardCharsets +import java.security.{KeyFactory, PrivateKey, PublicKey} +import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} +import scala.collection.mutable.HashMap +import java.util.Base64 +import java.nio.file.{Files, Paths} + +class KeyManager(private val basePath: String, private val keyPrefix: String, private val keyCount: Int) { + private val log = LoggerFactory.getLogger(this.getClass) + + private val keyMap: HashMap[String, KeyData] = new HashMap[String, KeyData]() + + private val loadHardcodedKeys = Platform.getBoolean("useHardcodedKeys",true); + + init() + + + private def init(): Unit = { + loadKeys() + } + + private def loadKeys(): Unit = { + for (i <- 0 until keyCount) { + val keyId = keyPrefix + i + log.info("Private key loaded - " + basePath + keyId) + keyMap.put(keyId, new KeyData(keyId, getPrivateKey(basePath + keyId ), loadPublicKey(basePath + keyId + Platform.getString("public.key.suffix",".pub")))) + } + } + + private def loadPublicKey(path: String): PublicKey = { + try { + if (!loadHardcodedKeys) { + val in = new FileInputStream(path) + val keyBytes = new Array[Byte](in.available()) + in.read(keyBytes) + in.close() + + val publicKey = new String(keyBytes, StandardCharsets.UTF_8) + .replaceAll("(-+BEGIN RSA PUBLIC KEY-+\\r?\\n|-+END RSA PUBLIC KEY-+\\r?\\n?)", "") + .replaceAll("(-+BEGIN PUBLIC KEY-+\\r?\\n|-+END PUBLIC KEY-+\\r?\\n?)", "") + // .replaceAll("-", "") + .replaceAll("\\s", "") + + + val publicBytes: Array[Byte] = Base64.getMimeDecoder.decode(publicKey) + val keySpec: X509EncodedKeySpec = new X509EncodedKeySpec(publicBytes) + val keyFactory: KeyFactory = KeyFactory.getInstance("RSA") + log.info("Public key loaded from filesystem - " + path) + keyFactory.generatePublic(keySpec) + } else { + val publicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAypzjIoeWTbPJRYNlwhzv\nJ+6q9vXk9HJHvubsMJhdpXX77eEQojtgAkrk3vv5edyhJHs/XY69OJMu4o8qHyhY\nSsSiw0TIuPPIQ3+moZB+yY6MKY7xYiHbKp9xeB1XsFt38H+HtOGX32Q5bL/4CvDS\nHUq7bKoG5wg5dyPkMwQRU/F4T3z9fSnKuRNjsb4OkyyYglJ6tn7uWp+RjPzXXLnB\nnu2S8R6Enw2DPjtQlJmtI941UsONuHPdj7srb4t+2p7jtROhMARDeT3X1DtbqIdK\nNrMu/+Q9APhHSQ5jUgk2nttPFjH8d31pDrcnFjKL7pQytQZeAYIVUB4MQZLnSYVB\nLwIDAQAB\n-----END PUBLIC KEY-----" + .replaceAll("(-+BEGIN RSA PUBLIC KEY-+\\r?\\n|-+END RSA PUBLIC KEY-+\\r?\\n?)", "") + .replaceAll("(-+BEGIN PUBLIC KEY-+\\r?\\n|-+END PUBLIC KEY-+\\r?\\n?)", "") + // .replaceAll("-", "") + .replaceAll("\\s", "") + + val publicBytes = Base64.getDecoder.decode(publicKey) + val keySpec = new X509EncodedKeySpec(publicBytes) + val keyFactory = KeyFactory.getInstance("RSA") + keyFactory.generatePublic(keySpec) + } + } catch { + case e: Exception => + throw new Exception("failed to load public key", e) + } + } + + def getRandomKey(): KeyData = { + val keyId = keyPrefix + (Math.random() * keyCount).toInt + keyMap.getOrElse(keyId, null) + } + + private def getPrivateKey(path: String): PrivateKey = { + try { + if (!loadHardcodedKeys) { + val privateKeyContent: String = new String(Files.readAllBytes(Paths.get(path))) + val privateKeyPEM: String = privateKeyContent + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s", "") + val keyBytesDecoded: Array[Byte] = Base64.getDecoder.decode(privateKeyPEM) + + val spec: PKCS8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytesDecoded) + val keyFactory: KeyFactory = KeyFactory.getInstance("RSA") + log.info("loading private key from filesystem", path) + keyFactory.generatePrivate(spec) + + } else { + var privateKey = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKnOMih5ZNs8lF\ng2XCHO8n7qr29eT0cke+5uwwmF2ldfvt4RCiO2ACSuTe+/l53KEkez9djr04ky7i\njyofKFhKxKLDRMi488hDf6ahkH7JjowpjvFiIdsqn3F4HVewW3fwf4e04ZffZDls\nv/gK8NIdSrtsqgbnCDl3I+QzBBFT8XhPfP19Kcq5E2Oxvg6TLJiCUnq2fu5an5GM\n/NdcucGe7ZLxHoSfDYM+O1CUma0j3jVSw424c92Puytvi37anuO1E6EwBEN5PdfU\nO1uoh0o2sy7/5D0A+EdJDmNSCTae208WMfx3fWkOtycWMovulDK1Bl4BghVQHgxB\nkudJhUEvAgMBAAECggEAB3Bz1u05vUTU84q/CwqkuoeE9HtgoQJFlLW5RcS0Arxb\nXmEaPlkSwRLLgE2p9WnLhHtLMmq1LOVOkX5mZaCsGT0XqTbJ1FMMu9m6Hj2GQjoY\nw5MRiHDFmAHxslJvFhuA3GFtjXX10+IQr7Seui9PouHleGuhXdmlKBtqKHgr3YEA\nxAkPoQl1Co/yafQ911RH13PM005UVVpCaD/2xwLnJPqrNxM3YktWNQKBAunDTdxo\nQ8JeCgTKNdWSJ03nBJ2u9wi6EGaZJfVG4uEF5B85lhEfo3O0kTh2B/3qdSl8Aknm\ngcOsoN1D8ivgld/IZebJ89NKgwSbiCP035cTYtS4jQKBgQDWsbb3016HE6AxUwGl\n/pmhGV4uWx0YjwgM2T30SXICNtQw7JWs9CXjoxD+ee3mYcB70Ag3+3g/CC82w/Lr\nz1sYNEx4HodvuIqo6XYYE5dg0QXkMehW+dxqx+VtCcwC0w7nGar9SfMtIwr8Wg/6\nqH5jcKxPaB2LMFtR/yCqBmbWowKBgQDxmCHT3RBE+VvmhAeM6i6EwLV1JS6+ZFf6\nOjDC3SRN2cMU/8wFEpZ12tDTTJnxrCKgWoZKUwdRm9upmGiMelq5M9mhJRPZeGrr\nlBs9I/vj2vJbu8QLCYVUC+JY8r3ezlnLp2Xr9dCebnECbgzQcAj+rq5tzedLhUbZ\nPsgC6HGwBQKBgDXjkauPEJETKgh3b1h9GY7IUU2NbTY24Kxo8xYYQVew734ARGmP\nNtt2mNNnQ4GqU6hARW/X3QzlPwSeFqF+AL2IkxEriI9QYO2Y/B16/Wo9zR7EMC90\ntBDRcBL4fI7Q71KurK67GyDfROimqpAeLutC4t1jotbHIoToZwiGZtXFAoGAHc/P\nBMy3kDtQ+s35/Ip9OQZqncz7yqSpMohxseoF69FeQD4cV9fmVx6sPBasvGSoVS82\neP9r3MclwPS8mfETNt1OEpN3spMoZm99OPsyvvgqheVSmKYRHMDmqmExysedzwKW\nEhrgJlysd0dLL4FTqtG1Vnlc/DWy+2XC2pECTl0CgYEAtzasBUsXp59IMhTSITKA\nZphiiBSTmEyhs6WptbbgOkAlLbT7YYhgpsCjKqFdLjGBZ+/Z9sICKlowgE8EueJ7\ntM0bnS/28Bgbd/QU1yJmglm3MbXzk9Q5tOE1Y3SNzwkT4kjp4e4AQvkVJt78ds+y\nPBvoTfmeV7/ov7/1OzRzHG0=\n-----END PRIVATE KEY-----" + privateKey = privateKey + .replaceAll("(-+BEGIN RSA PRIVATE KEY-+\\r?\\n|-+END RSA PRIVATE KEY-+\\r?\\n?)", "") + .replaceAll("(-+BEGIN PRIVATE KEY-+\\r?\\n|-+END PRIVATE KEY-+\\r?\\n?)", "") + + val keySpec = new PKCS8EncodedKeySpec(Base64Util.decode(privateKey.getBytes("UTF-8"), Base64Util.DEFAULT)) + val keyFactory = KeyFactory.getInstance("RSA") + keyFactory.generatePrivate(keySpec) + } + } catch { + case e: Exception => + throw new Exception("Failed to retrieve private key", e) + } + } + + def getValueFromKeyMap(keyId: String): KeyData = { + keyMap.getOrElse(keyId, throw new NoSuchElementException(s"KeyData not found for keyId: $keyId")) + } + +} diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/Base64Util.java b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/Base64Util.java new file mode 100644 index 000000000..d7c253014 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/Base64Util.java @@ -0,0 +1,722 @@ +package org.sunbird.utils; + +import java.io.UnsupportedEncodingException; + +public class Base64Util { + + + /** + * Default values for encoder/decoder flags. + */ + public static final int DEFAULT = 0; + + /** + * Encoder flag bit to omit the padding '=' characters at the end + * of the output (if any). + */ + public static final int NO_PADDING = 1; + + /** + * Encoder flag bit to omit all line terminators (i.e., the output + * will be on one long line). + */ + public static final int NO_WRAP = 2; + + /** + * Encoder flag bit to indicate lines should be terminated with a + * CRLF pair instead of just an LF. Has no effect if {@code + * NO_WRAP} is specified as well. + */ + public static final int CRLF = 4; + + /** + * Encoder/decoder flag bit to indicate using the "URL and + * filename safe" variant of Base64 (see RFC 3548 section 4) where + * {@code -} and {@code _} are used in place of {@code +} and + * {@code /}. + */ + public static final int URL_SAFE = 8; + + /** + * Flag to pass to {Base64OutputStream} to indicate that it + * should not close the output stream it is wrapping when it + * itself is closed. + */ + public static final int NO_CLOSE = 16; + + // -------------------------------------------------------- + // shared code + // -------------------------------------------------------- + + private Base64Util() { + } // don't instantiate + + // -------------------------------------------------------- + // decoding + // -------------------------------------------------------- + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + *

+ *

The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param str the input String to decode, which is converted to + * bytes using the default charset + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(String str, int flags) { + return decode(str.getBytes(), flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + *

+ *

The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the input array to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int flags) { + return decode(input, 0, input.length, flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + *

+ *

The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the data to decode + * @param offset the position within the input array at which to start + * @param len the number of bytes of input to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int offset, int len, int flags) { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]); + + if (!decoder.process(input, offset, len, true)) { + throw new IllegalArgumentException("bad base-64"); + } + + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == decoder.output.length) { + return decoder.output; + } + + // Need to shorten the array, so allocate a new one of the + // right size and copy. + byte[] temp = new byte[decoder.op]; + System.arraycopy(decoder.output, 0, temp, 0, decoder.op); + return temp; + } + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int flags) { + try { + return new String(encode(input, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + // -------------------------------------------------------- + // encoding + // -------------------------------------------------------- + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int offset, int len, int flags) { + try { + return new String(encode(input, offset, len, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int flags) { + return encode(input, 0, input.length, flags); + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int offset, int len, int flags) { + Encoder encoder = new Encoder(flags, null); + + // Compute the exact length of the array we will produce. + int output_len = len / 3 * 4; + + // Account for the tail of the data and the padding bytes, if any. + if (encoder.do_padding) { + if (len % 3 > 0) { + output_len += 4; + } + } else { + switch (len % 3) { + case 0: + break; + case 1: + output_len += 2; + break; + case 2: + output_len += 3; + break; + } + } + + // Account for the newlines, if any. + if (encoder.do_newline && len > 0) { + output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) * + (encoder.do_cr ? 2 : 1); + } + + encoder.output = new byte[output_len]; + encoder.process(input, offset, len, true); + + assert encoder.op == output_len; + + return encoder.output; + } + + /* package */ static abstract class Coder { + public byte[] output; + public int op; + + /** + * Encode/decode another block of input data. this.output is + * provided by the caller, and must be big enough to hold all + * the coded data. On exit, this.opwill be set to the length + * of the coded data. + * + * @param finish true if this is the final call to process for + * this object. Will finalize the coder state and + * include any final bytes in the output. + * @return true if the input so far is good; false if some + * error has been detected in the input stream.. + */ + public abstract boolean process(byte[] input, int offset, int len, boolean finish); + + /** + * @return the maximum number of bytes a call to process() + * could produce for the given number of input bytes. This may + * be an overestimate. + */ + public abstract int maxOutputSize(int len); + } + + /* package */ static class Decoder extends Coder { + /** + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ + private static final int DECODE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ + private static final int DECODE_WEBSAFE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Non-data values in the DECODE arrays. + */ + private static final int SKIP = -1; + private static final int EQUALS = -2; + final private int[] alphabet; + /** + * States 0-3 are reading through the next input tuple. + * State 4 is having read one '=' and expecting exactly + * one more. + * State 5 is expecting no more data or padding characters + * in the input. + * State 6 is the error state; an error has been detected + * in the input and no future input can "fix" it. + */ + private int state; // state number (0 to 6) + private int value; + + public Decoder(int flags, byte[] output) { + this.output = output; + + alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; + state = 0; + value = 0; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could decode to. + */ + public int maxOutputSize(int len) { + return len * 3 / 4 + 10; + } + + /** + * Decode another block of input data. + * + * @return true if the state machine is still healthy. false if + * bad base-64 data has been detected in the input stream. + */ + public boolean process(byte[] input, int offset, int len, boolean finish) { + if (this.state == 6) return false; + + int p = offset; + len += offset; + + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + int state = this.state; + int value = this.value; + int op = 0; + final byte[] output = this.output; + final int[] alphabet = this.alphabet; + + while (p < len) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + while (p + 4 <= len && + (value = ((alphabet[input[p] & 0xff] << 18) | + (alphabet[input[p + 1] & 0xff] << 12) | + (alphabet[input[p + 2] & 0xff] << 6) | + (alphabet[input[p + 3] & 0xff]))) >= 0) { + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + p += 4; + } + if (p >= len) break; + } + + // The fast path isn't available -- either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + + int d = alphabet[input[p++] & 0xff]; + + switch (state) { + case 0: + if (d >= 0) { + value = d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 1: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 2: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output[op++] = (byte) (value >> 4); + state = 4; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 3: + if (d >= 0) { + // Emit the output triple and return to state 0. + value = (value << 6) | d; + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + state = 0; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output[op + 1] = (byte) (value >> 2); + output[op] = (byte) (value >> 10); + op += 2; + state = 5; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 4: + if (d == EQUALS) { + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 5: + if (d != SKIP) { + this.state = 6; + return false; + } + break; + } + } + + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state; + this.value = value; + this.op = op; + return true; + } + + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + + switch (state) { + case 0: + // Output length is a multiple of three. Fine. + break; + case 1: + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6; + return false; + case 2: + // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output[op++] = (byte) (value >> 4); + break; + case 3: + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output[op++] = (byte) (value >> 10); + output[op++] = (byte) (value >> 2); + break; + case 4: + // Read one padding '=' when we expected 2. Illegal. + this.state = 6; + return false; + case 5: + // Read all the padding '='s we expected and no more. + // Fine. + break; + } + + this.state = state; + this.op = op; + return true; + } + } + + /* package */ static class Encoder extends Coder { + /** + * Emit a new line every this many output tuples. Corresponds to + * a 76-character line length (the maximum allowable according to + * RFC 2045). + */ + public static final int LINE_GROUPS = 19; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte ENCODE[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', + }; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte ENCODE_WEBSAFE[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', + }; + final public boolean do_padding; + final public boolean do_newline; + final public boolean do_cr; + final private byte[] tail; + final private byte[] alphabet; + /* package */ int tailLen; + private int count; + + public Encoder(int flags, byte[] output) { + this.output = output; + + do_padding = (flags & NO_PADDING) == 0; + do_newline = (flags & NO_WRAP) == 0; + do_cr = (flags & CRLF) != 0; + alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; + + tail = new byte[2]; + tailLen = 0; + + count = do_newline ? LINE_GROUPS : -1; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could encode to. + */ + public int maxOutputSize(int len) { + return len * 8 / 5 + 10; + } + + public boolean process(byte[] input, int offset, int len, boolean finish) { + // Using local variables makes the encoder about 9% faster. + final byte[] alphabet = this.alphabet; + final byte[] output = this.output; + int op = 0; + int count = this.count; + + int p = offset; + len += offset; + int v = -1; + + // First we need to concatenate the tail of the previous call + // with any input bytes available now and see if we can empty + // the tail. + + switch (tailLen) { + case 0: + // There was no tail. + break; + + case 1: + if (p + 2 <= len) { + // A 1-byte tail with at least 2 bytes of + // input available now. + v = ((tail[0] & 0xff) << 16) | + ((input[p++] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + ; + break; + + case 2: + if (p + 1 <= len) { + // A 2-byte tail with at least 1 byte of input. + v = ((tail[0] & 0xff) << 16) | + ((tail[1] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + break; + } + + if (v != -1) { + output[op++] = alphabet[(v >> 18) & 0x3f]; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + // At this point either there is no tail, or there are fewer + // than 3 bytes of input available. + + // The main loop, turning 3 input bytes into 4 output bytes on + // each iteration. + while (p + 3 <= len) { + v = ((input[p] & 0xff) << 16) | + ((input[p + 1] & 0xff) << 8) | + (input[p + 2] & 0xff); + output[op] = alphabet[(v >> 18) & 0x3f]; + output[op + 1] = alphabet[(v >> 12) & 0x3f]; + output[op + 2] = alphabet[(v >> 6) & 0x3f]; + output[op + 3] = alphabet[v & 0x3f]; + p += 3; + op += 4; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + if (finish) { + // Finish up the tail of the input. Note that we need to + // consume any bytes in tail before any bytes + // remaining in input; there should be at most two bytes + // total. + + if (p - tailLen == len - 1) { + int t = 0; + v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; + tailLen -= t; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (p - tailLen == len - 2) { + int t = 0; + v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | + (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); + tailLen -= t; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (do_newline && op > 0 && count != LINE_GROUPS) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + + assert tailLen == 0; + assert p == len; + } else { + // Save the leftovers in tail to be consumed on the next + // call to encodeInternal. + + if (p == len - 1) { + tail[tailLen++] = input[p]; + } else if (p == len - 2) { + tail[tailLen++] = input[p]; + tail[tailLen++] = input[p + 1]; + } + } + + this.op = op; + this.count = count; + + return true; + } + } +} diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/CryptoUtil.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/CryptoUtil.scala new file mode 100644 index 000000000..b74999b61 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/CryptoUtil.scala @@ -0,0 +1,25 @@ +package org.sunbird.utils + +import java.nio.charset.StandardCharsets +import java.security._ +import javax.crypto.spec.SecretKeySpec +import javax.crypto.Mac +import scala.util.{Try, Either, Left, Right} +object CryptoUtil { + private val US_ASCII = StandardCharsets.US_ASCII + def generateRSASign(payLoad: String, key: PrivateKey, algorithm: String): Array[Byte] = { + val sign: Signature = Signature.getInstance(algorithm) + sign.initSign(key) + sign.update(payLoad.getBytes(StandardCharsets.US_ASCII)) + sign.sign() + } + + def verifyRSASign(payLoad: String, signature: Array[Byte], key: PublicKey, algorithm: String): Boolean = { + Try { + val sign = Signature.getInstance(algorithm) + sign.initVerify(key) + sign.update(payLoad.getBytes(StandardCharsets.US_ASCII)) + sign.verify(signature) + }.getOrElse(false) + } +} diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala index 25408886c..d1500cb46 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala @@ -57,4 +57,21 @@ object HierarchyConstants { val SOURCE: String = "source" val PRE_CONDITION: String = "preCondition" val QUESTION_VISIBILITY: List[String] = List("Default", "Parent") + val MAXQUESTIONS: String = "maxQuestions" + val SERVEREVALUABLE: String = "serverEvaluable" + val TRUE: String = "true" + val FALSE: String = "false" + val SERVER: String = "server" + val EVAL: String = "eval" + val EVAL_MODE: String = "eval-mode" + val CLIENT: String = "client" + val CONTENTID: String = "contentID" + val COLLECTIONID: String = "collectionID" + val USERID: String = "userID" + val ATTEMPTID: String = "attemptID" + val QUESTIONLIST: String = "questionList" + val ROOTID: String = "rootId" + val QUESTIONSETTOKEN: String = "questionSetToken" + val QUESTIONSET: String = "questionSet" + val SHUFFLE: String = "shuffle"; } diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JWTTokenType.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JWTTokenType.scala new file mode 100644 index 000000000..b85279860 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JWTTokenType.scala @@ -0,0 +1,19 @@ +package org.sunbird.utils + + +sealed trait JWTokenType { + def algorithmName: String + def tokenType: String +} + +object JWTokenType { + case object HS256 extends JWTokenType { + val algorithmName: String = "HmacSHA256" + val tokenType: String = "HS256" + } + + case object RS256 extends JWTokenType { + val algorithmName: String = "SHA256withRSA" + val tokenType: String = "RS256" + } +} diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JsonUtil.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JsonUtil.scala new file mode 100644 index 000000000..241fffe56 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JsonUtil.scala @@ -0,0 +1,30 @@ +package org.sunbird.utils + +import com.fasterxml.jackson.databind.ObjectMapper + +import java.lang.reflect.Type +import java.util.Map + +object JsonUtil { + private val objectMapper: ObjectMapper = new ObjectMapper() + + def fromJson[C](json: String, classOfC: Class[C]): C = { + objectMapper.readValue(json, classOfC) + } + + def fromJson[T](json: String, `type`: Type): T = { + objectMapper.readValue(json, objectMapper.constructType(`type`)) + } + + def fromJson[T](json: String, classOfT: Class[T], exceptionMessage: String): T = { + objectMapper.readValue(json, classOfT) + } + + def fromMap[C](map: Map[_, _], classOfC: Class[C]): C = { + objectMapper.convertValue(map, classOfC) + } + + def toJson(obj: Any): String = { + objectMapper.writeValueAsString(obj) + } +} \ No newline at end of file diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JwtUtil.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JwtUtil.scala new file mode 100644 index 000000000..a4f88d89a --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JwtUtil.scala @@ -0,0 +1,70 @@ +package org.sunbird.utils + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import org.sunbird.managers.{KeyData, KeyManager} + +import java.nio.charset.StandardCharsets +import java.security.PrivateKey +import java.util +import java.util.{Base64, HashMap, Map} + +object JwtUtils { + + private val SEPARATOR = "." + private val objectMapper: ObjectMapper = new ObjectMapper().registerModule(DefaultScalaModule) + def createRS256Token(key: String, privateKey: PrivateKey, headerOptions: Map[String, String]): String = { + val tokenType = JWTokenType.RS256 + val payLoad = createHeader(tokenType, headerOptions) + SEPARATOR + createClaims(key) + val signature = encodeToBase64Uri(CryptoUtil.generateRSASign(payLoad, privateKey, tokenType.algorithmName)) + payLoad + SEPARATOR + signature + } + + + private def createHeader(tokenType: JWTokenType, headerOptions: Map[String, String]): String = { + val headerData = new HashMap[String, String]() + if (headerOptions != null) + headerData.putAll(headerOptions) + headerData.put("alg", tokenType.tokenType) + encodeToBase64Uri(JsonUtil.toJson(headerData).getBytes) + } + + private def createClaims(subject: String): String = { + val payloadData = new HashMap[String, Any]() + payloadData.put("data", subject) + payloadData.put("iat", System.currentTimeMillis / 1000) + encodeToBase64Uri(JsonUtil.toJson(payloadData).getBytes) + } + + private def encodeToBase64Uri(data: Array[Byte]): String = { + Base64Util.encodeToString(data, 11) + } + + def verifyRS256Token(token: String, keyManager: KeyManager): (Boolean, Map[String, Any])= { + val tokenElements = token.split("\\.") + val header = tokenElements(0) + val header_decode=payload(header) + val body = tokenElements(1) + val signature = tokenElements(2) + val payLoad = header + SEPARATOR + body + var keyData: KeyData = null + var isValid = false + keyData = keyManager.getValueFromKeyMap(header_decode.get("keyId").asInstanceOf[String]) + if (keyData != null) { + isValid = CryptoUtil.verifyRSASign(payLoad, decodeFromBase64(signature), keyData.getPublicKey, "SHA256withRSA") + } + if(isValid) + (isValid,payload(body)) + else + (isValid,new util.HashMap[String,Any]()) + } + + def decodeFromBase64(data: String): Array[Byte] = { + Base64Util.decode(data, 11) + } + + def payload(encodedPayload: String): Map[String, Any] = { + val decodedPayload = new String(Base64.getDecoder.decode(encodedPayload), StandardCharsets.UTF_8) + objectMapper.readValue(decodedPayload, classOf[Map[String, Any]]) + } +} diff --git a/schemas/question/1.0/schema.json b/schemas/question/1.0/schema.json index 4361cf4bd..ca3727b2d 100644 --- a/schemas/question/1.0/schema.json +++ b/schemas/question/1.0/schema.json @@ -601,6 +601,15 @@ }, "migrationVersion": { "type": "number" + }, + "eval": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["server","client"] + } + } } }, "additionalProperties": false diff --git a/schemas/question/1.1/schema.json b/schemas/question/1.1/schema.json index 9e304e419..e161b4ce6 100644 --- a/schemas/question/1.1/schema.json +++ b/schemas/question/1.1/schema.json @@ -721,6 +721,15 @@ "items": { "type": "object" } + }, + "eval": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["server","client"] + } + } } }, "additionalProperties": false diff --git a/schemas/questionset/1.0/schema.json b/schemas/questionset/1.0/schema.json index 4c17e4705..c6b886308 100644 --- a/schemas/questionset/1.0/schema.json +++ b/schemas/questionset/1.0/schema.json @@ -718,6 +718,15 @@ "artifactUrl": { "type": "string", "format": "url" + }, + "eval": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["server","client"] + } + } } }, "additionalProperties": false diff --git a/schemas/questionset/1.1/schema.json b/schemas/questionset/1.1/schema.json index 0c001f573..06ab77322 100644 --- a/schemas/questionset/1.1/schema.json +++ b/schemas/questionset/1.1/schema.json @@ -705,6 +705,15 @@ "artifactUrl": { "type": "string", "format": "url" + }, + "eval": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["server","client"] + } + } } }, "additionalProperties": false From fa88cf6e66dfd23354ef861c9d001ba70d65ad6f Mon Sep 17 00:00:00 2001 From: anil Date: Wed, 2 Aug 2023 16:50:15 +0530 Subject: [PATCH 02/33] client side evaluation changes --- .../main/scala/org/sunbird/managers/HierarchyManager.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala index 4b59b77f3..62268bfba 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala @@ -30,7 +30,7 @@ object HierarchyManager { val schemaName: String = "questionset" val imgSuffix: String = ".img" val hierarchyPrefix: String = "qs_hierarchy_" - val statusList = List("Live", "Unlisted", "Flagged", "Draft") + val statusList = List("Live", "Unlisted", "Flagged") val ASSESSMENT_OBJECT_TYPES = List("Question", "QuestionSet") val keyManager = new KeyManager(Platform.getString("api.jwt.basepath","./keys/"), Platform.getString("api.jwt.keyprefix","device"), Platform.getInteger("keys.count",1)) @@ -553,7 +553,7 @@ object HierarchyManager { val hierarchy = fetchHierarchy(request, request.getRequest.get("rootId").asInstanceOf[String]) hierarchy.map(hierarchy => { if (!hierarchy.isEmpty) { - if (StringUtils.isNotEmpty(hierarchy.getOrDefault("status", "Draft").asInstanceOf[String]) && statusList.contains(hierarchy.getOrDefault("status", "Draft").asInstanceOf[String])) { + if (StringUtils.isNotEmpty(hierarchy.getOrDefault("status", "").asInstanceOf[String]) && statusList.contains(hierarchy.getOrDefault("status", "").asInstanceOf[String])) { val hierarchyMap = mapAsJavaMap(hierarchy) rootHierarchy.put("questionSet", hierarchyMap) RedisCache.set(hierarchyPrefix + request.get("rootId"), JsonUtils.serialize(hierarchyMap)) From 79991ce0fb26cddc79204253d7e9c594efe987ff Mon Sep 17 00:00:00 2001 From: anil Date: Thu, 3 Aug 2023 10:07:29 +0530 Subject: [PATCH 03/33] client side evaluation changes --- .../sunbird/managers/UpdateHierarchyManager.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala index 40860367d..37016a79c 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala @@ -1,8 +1,9 @@ package org.sunbird.managers +import com.fasterxml.jackson.databind.ObjectMapper + import java.util import java.util.concurrent.CompletionException - import org.apache.commons.collections4.{CollectionUtils, MapUtils} import org.apache.commons.lang3.StringUtils import org.sunbird.common.dto.{Request, Response, ResponseHandler} @@ -25,13 +26,22 @@ import scala.concurrent.{ExecutionContext, Future} object UpdateHierarchyManager { val neo4jCreateTypes: java.util.List[String] = Platform.getStringList("neo4j_objecttypes_enabled", List("Question").asJava) - + val mapper: ObjectMapper = new ObjectMapper() @throws[Exception] def updateHierarchy(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { val (nodesModified, hierarchy) = validateRequest(request) val rootId: String = getRootId(nodesModified, hierarchy) request.getContext.put(HierarchyConstants.ROOT_ID, rootId) getValidatedRootNode(rootId, request).map(node => { + val eval = node.getMetadata.getOrDefault("eval", "{}").asInstanceOf[String] + val data = mapper.readValue(eval, classOf[java.util.Map[String, String]]) + var mode = data.get("mode") + nodesModified.foreach { n => + if (!rootId.equals(n._1) && !(mode.equals(n._2.asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].get("metadata") + .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].getOrElse("eval", new util.LinkedHashMap()) + .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].getOrDefault("mode", "client").asInstanceOf[String]))) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "All children of QuestionSet should be the same eval status") + } getExistingHierarchy(request, node).map(existingHierarchy => { val existingChildren = existingHierarchy.getOrElse(HierarchyConstants.CHILDREN, new java.util.ArrayList[java.util.HashMap[String, AnyRef]]()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] val nodes = List(node) From 3a00be65a9fa9a7564e8e775a03bcf8dc630fef8 Mon Sep 17 00:00:00 2001 From: anil Date: Thu, 3 Aug 2023 14:04:34 +0530 Subject: [PATCH 04/33] client side evaluation changes --- assessment-api/assessment-service/conf/application.conf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index 4b5460494..b5b32ef72 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -454,4 +454,7 @@ v5_supported_qumlVersions=[1.1] v5_default_qumlVersion=1.1 api.jwt.keyprefix=device -api.jwt.keycount=1 \ No newline at end of file +api.jwt.keycount=1 +api.jwt.basepath=./keys/ +question.list.search.editor.url="http://localhost:9001/question/v4/editor/list" +useHardcodedKeys=false \ No newline at end of file From 404603ee5baae14a723134e9709135a1a60e056b Mon Sep 17 00:00:00 2001 From: anil Date: Thu, 3 Aug 2023 16:47:00 +0530 Subject: [PATCH 05/33] client side evaluation changes --- .../src/main/scala/org/sunbird/v5/actors/QuestionActor.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala index 7f2e9eab0..3c42cd6a6 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala @@ -11,7 +11,6 @@ import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.nodes.DataNode import org.sunbird.graph.schema.DefinitionNode import org.sunbird.graph.utils.NodeUtil -import org.sunbird.managers.AssessmentManager.mapper import org.sunbird.managers.CopyManager import org.sunbird.utils.{AssessmentConstants, AssessmentErrorCodes, RequestUtil} import org.sunbird.v5.managers.AssessmentV5Manager From 2feb89ea42830c5ea986b30a35924a8e881cb330 Mon Sep 17 00:00:00 2001 From: anil Date: Fri, 4 Aug 2023 08:13:18 +0530 Subject: [PATCH 06/33] client side evaluation changes --- .../assessment-service/app/modules/AssessmentModule.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/assessment-api/assessment-service/app/modules/AssessmentModule.scala b/assessment-api/assessment-service/app/modules/AssessmentModule.scala index 55087d5ea..6d3d85094 100644 --- a/assessment-api/assessment-service/app/modules/AssessmentModule.scala +++ b/assessment-api/assessment-service/app/modules/AssessmentModule.scala @@ -15,6 +15,7 @@ class AssessmentModule extends AbstractModule with AkkaGuiceSupport { bindActor(classOf[QuestionSetActor], ActorNames.QUESTION_SET_ACTOR) bindActor(classOf[org.sunbird.v5.actors.QuestionActor], ActorNames.QUESTION_V5_ACTOR) bindActor(classOf[org.sunbird.v5.actors.QuestionSetActor], ActorNames.QUESTION_SET_V5_ACTOR) + bindActor(classOf[org.sunbird.v5.actors.QuestionSetAssessActor], ActorNames.QUESTION_SET_ASSESS_ACTOR) println("Initialized application actors for assessment-service") } } From cf6cd58174dc96669fbcc8338d6ce4358263b80b Mon Sep 17 00:00:00 2001 From: anil Date: Fri, 4 Aug 2023 11:01:18 +0530 Subject: [PATCH 07/33] client side evaluation changes --- .../src/main/scala/org/sunbird/v5/actors/QuestionActor.scala | 2 +- .../src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala index 3c42cd6a6..7aee69b5a 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala @@ -27,7 +27,7 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA private lazy val importConfig = getImportConfig() private lazy val importMgr = new ImportManager(importConfig) - val defaultVersion = Platform.config.getNumber("v5_default_qumlVersion") + val defaultVersion:String = Platform.config.getNumber("v5_default_qumlVersion").toString private val mapper = new ObjectMapper() override def onReceive(request: Request): Future[Response] = request.getOperation match { diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala index 3911e48a7..1102a5552 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala @@ -29,7 +29,7 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba implicit val ec: ExecutionContext = getContext().dispatcher private lazy val importConfig = getImportConfig() private lazy val importMgr = new ImportManager(importConfig) - val defaultVersion = Platform.config.getNumber("v5_default_qumlVersion") + val defaultVersion:String = Platform.config.getNumber("v5_default_qumlVersion").toString override def onReceive(request: Request): Future[Response] = request.getOperation match { case "createQuestionSet" => AssessmentV5Manager.create(request) From 12f3bd2960ef6d67b4c98204d377d66f8f447e79 Mon Sep 17 00:00:00 2001 From: anil Date: Fri, 11 Aug 2023 12:45:25 +0530 Subject: [PATCH 08/33] added eval property --- schemas/questionset/1.1/schema.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/schemas/questionset/1.1/schema.json b/schemas/questionset/1.1/schema.json index 06ab77322..2ea76d435 100644 --- a/schemas/questionset/1.1/schema.json +++ b/schemas/questionset/1.1/schema.json @@ -650,6 +650,15 @@ "type": "object" } }, + "eval": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["server","client"] + } + } + }, "origin": { "type": "string" }, @@ -705,15 +714,6 @@ "artifactUrl": { "type": "string", "format": "url" - }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - } - } } }, "additionalProperties": false From 75c4d053ba746a6e0038fa23fa39d11517b66ea2 Mon Sep 17 00:00:00 2001 From: anil Date: Mon, 21 Aug 2023 16:53:23 +0530 Subject: [PATCH 09/33] added eval property --- schemas/question/1.0/schema.json | 18 +++++++++--------- schemas/question/1.1/schema.json | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/schemas/question/1.0/schema.json b/schemas/question/1.0/schema.json index ca3727b2d..da6028563 100644 --- a/schemas/question/1.0/schema.json +++ b/schemas/question/1.0/schema.json @@ -569,6 +569,15 @@ "No" ] }, + "eval": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["server","client"] + } + } + }, "evidence": { "type": "object" }, @@ -601,15 +610,6 @@ }, "migrationVersion": { "type": "number" - }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - } - } } }, "additionalProperties": false diff --git a/schemas/question/1.1/schema.json b/schemas/question/1.1/schema.json index e161b4ce6..fecf2ba45 100644 --- a/schemas/question/1.1/schema.json +++ b/schemas/question/1.1/schema.json @@ -676,6 +676,15 @@ "No" ] }, + "eval": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["server","client"] + } + } + }, "allowAnonymousAccess": { "type": "string", "enum": [ @@ -721,15 +730,6 @@ "items": { "type": "object" } - }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - } - } } }, "additionalProperties": false From 3dd609cd75eb9e74674c6ecd7b646fee9b4a33d3 Mon Sep 17 00:00:00 2001 From: anil Date: Sun, 27 Aug 2023 10:55:59 +0530 Subject: [PATCH 10/33] added eval property --- .../sunbird/managers/UpdateHierarchyManager.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala index 37016a79c..92b6e46f3 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala @@ -36,11 +36,14 @@ object UpdateHierarchyManager { val eval = node.getMetadata.getOrDefault("eval", "{}").asInstanceOf[String] val data = mapper.readValue(eval, classOf[java.util.Map[String, String]]) var mode = data.get("mode") - nodesModified.foreach { n => - if (!rootId.equals(n._1) && !(mode.equals(n._2.asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].get("metadata") - .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].getOrElse("eval", new util.LinkedHashMap()) - .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].getOrDefault("mode", "client").asInstanceOf[String]))) - throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "All children of QuestionSet should be the same eval status") + if (nodesModified.get(rootId) != null) { + val updMode = nodesModified.get(rootId).asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] + .get("metadata").asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] + .getOrElse("eval", new util.LinkedHashMap()) + .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].get("mode").asInstanceOf[String] + if (StringUtils.isNotEmpty(mode) && !mode.equals(updMode)) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "QuestionSet eval status cannot be modified") + mode = updMode } getExistingHierarchy(request, node).map(existingHierarchy => { val existingChildren = existingHierarchy.getOrElse(HierarchyConstants.CHILDREN, new java.util.ArrayList[java.util.HashMap[String, AnyRef]]()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] From 16fa8c780d7ac29f7e01c65a40d48b9b01fefe90 Mon Sep 17 00:00:00 2001 From: anil Date: Thu, 7 Sep 2023 13:28:08 +0530 Subject: [PATCH 11/33] added property --- .../assessment-service/conf/application.conf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index b5b32ef72..05260c283 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -358,7 +358,7 @@ schema.base_path="../../schemas/" # Cassandra Configuration cassandra.lp.connection="127.0.0.1:9042" -content.keyspace = "content_store" +content.keyspace = "dev_content_store" # Redis Configuration redis.host="localhost" @@ -408,12 +408,12 @@ kafka { topic.send.enable : true topics.instruction : "sunbirddev.assessment.publish.request" } -objectcategorydefinition.keyspace="local_category_store" +objectcategorydefinition.keyspace="dev_category_store" question { - keyspace = "local_question_store" + keyspace = "dev_question_store" list.limit=20 } -questionset.keyspace="local_hierarchy_store" +questionset.keyspace="dev_hierarchy_store" cassandra { lp { @@ -456,5 +456,5 @@ v5_default_qumlVersion=1.1 api.jwt.keyprefix=device api.jwt.keycount=1 api.jwt.basepath=./keys/ -question.list.search.editor.url="http://localhost:9001/question/v4/editor/list" -useHardcodedKeys=false \ No newline at end of file +question.list.search.editor.url="http://localhost:9000/question/v5/editor/list" +useHardcodedKeys=true \ No newline at end of file From fd08f18e83af98187df81ac12b823a595c8166bf Mon Sep 17 00:00:00 2001 From: anil Date: Thu, 7 Sep 2023 13:31:02 +0530 Subject: [PATCH 12/33] added property --- .../assessment-service/conf/application.conf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index 05260c283..09310471d 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -358,7 +358,7 @@ schema.base_path="../../schemas/" # Cassandra Configuration cassandra.lp.connection="127.0.0.1:9042" -content.keyspace = "dev_content_store" +content.keyspace = "content_store" # Redis Configuration redis.host="localhost" @@ -408,12 +408,12 @@ kafka { topic.send.enable : true topics.instruction : "sunbirddev.assessment.publish.request" } -objectcategorydefinition.keyspace="dev_category_store" +objectcategorydefinition.keyspace="local_category_store" question { - keyspace = "dev_question_store" + keyspace = "local_question_store" list.limit=20 } -questionset.keyspace="dev_hierarchy_store" +questionset.keyspace="local_hierarchy_store" cassandra { lp { @@ -457,4 +457,4 @@ api.jwt.keyprefix=device api.jwt.keycount=1 api.jwt.basepath=./keys/ question.list.search.editor.url="http://localhost:9000/question/v5/editor/list" -useHardcodedKeys=true \ No newline at end of file +useHardcodedKeys=false \ No newline at end of file From 97e8a67ea5a63431c51bd5f36ffb39d446aee0fb Mon Sep 17 00:00:00 2001 From: anil Date: Thu, 7 Sep 2023 16:54:03 +0530 Subject: [PATCH 13/33] changed code --- .../scala/org/sunbird/v5/managers/AssessmentV5Manager.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala index 6b6d3ba10..6e891b5c5 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala @@ -237,6 +237,7 @@ object AssessmentV5Manager { def pushInstructionEvent(identifier: String, node: Node)(implicit oec: OntologyEngineContext): Unit = { val (actor, context, objData, eData) = generateInstructionEventMetadata(identifier.replace(".img", ""), node) val beJobRequestEvent: String = LogTelemetryEventUtil.logInstructionEvent(actor.asJava, context.asJava, objData.asJava, eData) + println("printing beJobRequestEvent", beJobRequestEvent) val topic: String = Platform.getString("kafka.topics.instruction", "sunbirddev.learning.job.request") if (StringUtils.isBlank(beJobRequestEvent)) throw new ClientException("BE_JOB_REQUEST_EXCEPTION", "Event is not generated properly.") oec.kafkaClient.send(beJobRequestEvent, topic) @@ -554,7 +555,7 @@ object AssessmentV5Manager { throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Invalid Request") val res = getMap(answerMap.get(identifier).asInstanceOf[Some[util.Map[String, AnyRef]]].x, AssessmentConstants.RESPONSE1) val cardinality = res.getOrDefault(AssessmentConstants.CARDINALITY, "").asInstanceOf[String] - val maxScore = res.getOrDefault(AssessmentConstants.MAX_SCORE, 0.asInstanceOf[Integer]).asInstanceOf[Integer] + val maxScore = res.getOrDefault(AssessmentConstants.MAX_SCORE, 0).asInstanceOf[Integer] cardinality match { case AssessmentConstants.MULTIPLE => populateMultiCardinality(res, edata, maxScore) case _ => populateSingleCardinality(res, edata, maxScore) @@ -620,7 +621,7 @@ object AssessmentV5Manager { private case class HTTPResponse(status: Int, body: String) extends Serializable private def populateSingleCardinality(res: util.Map[String, AnyRef], edata: util.Map[String, AnyRef], maxScore: Integer): Unit = { - val correctValue = getMap(res, AssessmentConstants.CORRECT_RESPONSE).getOrDefault(AssessmentConstants.VALUE, new util.ArrayList[Integer]).asInstanceOf[String] + val correctValue = getMap(res, AssessmentConstants.CORRECT_RESPONSE).getOrDefault(AssessmentConstants.VALUE, new util.ArrayList[Integer]).toString val usrResponse = getListMap(edata, AssessmentConstants.RESVALUES).get(0).getOrDefault(AssessmentConstants.VALUE, "").toString StringUtils.equals(usrResponse, correctValue) match { case true => { From 3413985baa053a418744a8eceac058b348c521bc Mon Sep 17 00:00:00 2001 From: anil Date: Thu, 7 Sep 2023 16:56:58 +0530 Subject: [PATCH 14/33] changed code --- .../scala/org/sunbird/v5/managers/AssessmentV5Manager.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala index 6e891b5c5..abd0754b9 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala @@ -237,7 +237,6 @@ object AssessmentV5Manager { def pushInstructionEvent(identifier: String, node: Node)(implicit oec: OntologyEngineContext): Unit = { val (actor, context, objData, eData) = generateInstructionEventMetadata(identifier.replace(".img", ""), node) val beJobRequestEvent: String = LogTelemetryEventUtil.logInstructionEvent(actor.asJava, context.asJava, objData.asJava, eData) - println("printing beJobRequestEvent", beJobRequestEvent) val topic: String = Platform.getString("kafka.topics.instruction", "sunbirddev.learning.job.request") if (StringUtils.isBlank(beJobRequestEvent)) throw new ClientException("BE_JOB_REQUEST_EXCEPTION", "Event is not generated properly.") oec.kafkaClient.send(beJobRequestEvent, topic) @@ -555,7 +554,7 @@ object AssessmentV5Manager { throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Invalid Request") val res = getMap(answerMap.get(identifier).asInstanceOf[Some[util.Map[String, AnyRef]]].x, AssessmentConstants.RESPONSE1) val cardinality = res.getOrDefault(AssessmentConstants.CARDINALITY, "").asInstanceOf[String] - val maxScore = res.getOrDefault(AssessmentConstants.MAX_SCORE, 0).asInstanceOf[Integer] + val maxScore = res.getOrDefault(AssessmentConstants.MAX_SCORE, 0.asInstanceOf[Integer]).asInstanceOf[Integer] cardinality match { case AssessmentConstants.MULTIPLE => populateMultiCardinality(res, edata, maxScore) case _ => populateSingleCardinality(res, edata, maxScore) From 7fa30588024cb33799941b5b9102f84df9303337 Mon Sep 17 00:00:00 2001 From: anil Date: Mon, 11 Sep 2023 12:10:30 +0530 Subject: [PATCH 15/33] changed code --- .../src/main/scala/org/sunbird/managers/KeyManager.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala index 76df935ff..a7c0eae00 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala @@ -26,7 +26,7 @@ class KeyManager(private val basePath: String, private val keyPrefix: String, pr } private def loadKeys(): Unit = { - for (i <- 0 until keyCount) { + for (i <- 1 until keyCount) { val keyId = keyPrefix + i log.info("Private key loaded - " + basePath + keyId) keyMap.put(keyId, new KeyData(keyId, getPrivateKey(basePath + keyId ), loadPublicKey(basePath + keyId + Platform.getString("public.key.suffix",".pub")))) From 9f4a4c6cac40fd531ec667666956d839d8b57df1 Mon Sep 17 00:00:00 2001 From: anil Date: Wed, 13 Sep 2023 17:44:15 +0530 Subject: [PATCH 16/33] modify calculate score function --- .../v5/managers/AssessmentV5Manager.scala | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala index abd0754b9..e0cca9e1f 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala @@ -536,15 +536,29 @@ object AssessmentV5Manager { } def calculateScore(privateList: Response, assessments: util.List[util.Map[String, AnyRef]]): Unit = { - val answerMaps: (Map[String, AnyRef], Map[String, AnyRef]) = getListMap(privateList.getResult, AssessmentConstants.QUESTIONS) - .map { que => - ((que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.RESPONSE_DECLARATION)), - (que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.EDITOR_STATE))) - }.unzip match { - case (map1, map2) => (map1.toMap, map2.toMap) +// val answerMaps: (Map[String, AnyRef], Map[String, AnyRef]) = getListMap(privateList.getResult, AssessmentConstants.QUESTIONS) +// .map { que => +// ((que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.RESPONSE_DECLARATION)), +// (que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.EDITOR_STATE))) +// ) +// }.unzip match { +// case (map1, map2) => (map1.toMap, map2.toMap) +// } +val answerMaps: (Map[String, AnyRef], Map[String, AnyRef], Map[String, AnyRef]) = { + val listOfMaps = getListMap(privateList.getResult, AssessmentConstants.QUESTIONS) + .map { que => + ( + que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.RESPONSE_DECLARATION), + que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.EDITOR_STATE), + que.get(AssessmentConstants.IDENTIFIER).toString -> que.get(AssessmentConstants.MAX_SCORE) + ) } + val (map1, map2, map3) = (listOfMaps.map(_._1).toMap, listOfMaps.map(_._2).toMap, listOfMaps.map(_._3).toMap) + (map1, map2, map3) +} val answerMap = answerMaps._1 val editorStateMap = answerMaps._2 + val maxScoreMap = answerMaps._3 assessments.foreach { k => getListMap(k, AssessmentConstants.EVENTS).toList.foreach { event => val edata = getMap(event, AssessmentConstants.EDATA) @@ -554,7 +568,8 @@ object AssessmentV5Manager { throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Invalid Request") val res = getMap(answerMap.get(identifier).asInstanceOf[Some[util.Map[String, AnyRef]]].x, AssessmentConstants.RESPONSE1) val cardinality = res.getOrDefault(AssessmentConstants.CARDINALITY, "").asInstanceOf[String] - val maxScore = res.getOrDefault(AssessmentConstants.MAX_SCORE, 0.asInstanceOf[Integer]).asInstanceOf[Integer] + // val maxScore = res.getOrDefault(AssessmentConstants.MAX_SCORE, 0.asInstanceOf[Integer]).asInstanceOf[Integer] + val maxScore = maxScoreMap.get(identifier).asInstanceOf[Integer] cardinality match { case AssessmentConstants.MULTIPLE => populateMultiCardinality(res, edata, maxScore) case _ => populateSingleCardinality(res, edata, maxScore) From 6f15257c634f6432a2d75daf00dfe554ddf97b6a Mon Sep 17 00:00:00 2001 From: anil Date: Wed, 13 Sep 2023 18:25:37 +0530 Subject: [PATCH 17/33] updated property --- .../assessment-service/conf/application.conf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index 09310471d..05260c283 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -358,7 +358,7 @@ schema.base_path="../../schemas/" # Cassandra Configuration cassandra.lp.connection="127.0.0.1:9042" -content.keyspace = "content_store" +content.keyspace = "dev_content_store" # Redis Configuration redis.host="localhost" @@ -408,12 +408,12 @@ kafka { topic.send.enable : true topics.instruction : "sunbirddev.assessment.publish.request" } -objectcategorydefinition.keyspace="local_category_store" +objectcategorydefinition.keyspace="dev_category_store" question { - keyspace = "local_question_store" + keyspace = "dev_question_store" list.limit=20 } -questionset.keyspace="local_hierarchy_store" +questionset.keyspace="dev_hierarchy_store" cassandra { lp { @@ -457,4 +457,4 @@ api.jwt.keyprefix=device api.jwt.keycount=1 api.jwt.basepath=./keys/ question.list.search.editor.url="http://localhost:9000/question/v5/editor/list" -useHardcodedKeys=false \ No newline at end of file +useHardcodedKeys=true \ No newline at end of file From 50fc39970506ec20007fc632b3fe95a0d9dc2735 Mon Sep 17 00:00:00 2001 From: anil Date: Wed, 13 Sep 2023 19:00:53 +0530 Subject: [PATCH 18/33] updated calculatescore function --- .../scala/org/sunbird/v5/managers/AssessmentV5Manager.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala index e0cca9e1f..9abfa1c0c 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala @@ -569,7 +569,8 @@ val answerMaps: (Map[String, AnyRef], Map[String, AnyRef], Map[String, AnyRef]) val res = getMap(answerMap.get(identifier).asInstanceOf[Some[util.Map[String, AnyRef]]].x, AssessmentConstants.RESPONSE1) val cardinality = res.getOrDefault(AssessmentConstants.CARDINALITY, "").asInstanceOf[String] // val maxScore = res.getOrDefault(AssessmentConstants.MAX_SCORE, 0.asInstanceOf[Integer]).asInstanceOf[Integer] - val maxScore = maxScoreMap.get(identifier).asInstanceOf[Integer] + val maxScoreOption = maxScoreMap.get(identifier) + val maxScore = maxScoreOption.getOrElse(0).asInstanceOf[Integer] cardinality match { case AssessmentConstants.MULTIPLE => populateMultiCardinality(res, edata, maxScore) case _ => populateSingleCardinality(res, edata, maxScore) From d453e6217c4f681772d8756b7bff05ac20d69dcb Mon Sep 17 00:00:00 2001 From: anil Date: Mon, 25 Sep 2023 12:34:36 +0530 Subject: [PATCH 19/33] worked on code review comments --- .../org/sunbird/v5/actors/QuestionActor.scala | 58 +++++++++++++------ .../sunbird/v5/actors/QuestionSetActor.scala | 48 +++++++++++++-- .../assessment-service/conf/application.conf | 4 +- schemas/question/1.0/schema.json | 9 --- schemas/question/1.1/schema.json | 3 +- schemas/questionset/1.0/schema.json | 9 --- schemas/questionset/1.1/schema.json | 6 +- 7 files changed, 92 insertions(+), 45 deletions(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala index 7aee69b5a..10b945b33 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala @@ -5,7 +5,7 @@ import org.apache.commons.lang3.StringUtils import org.sunbird.`object`.importer.{ImportConfig, ImportManager} import org.sunbird.actor.core.BaseActor import org.sunbird.common.dto.{Request, Response, ResponseHandler} -import org.sunbird.common.exception.ClientException +import org.sunbird.common.exception.{ClientException, ServerException} import org.sunbird.common.{DateUtils, Platform} import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.nodes.DataNode @@ -20,6 +20,9 @@ import javax.inject.Inject import scala.collection.JavaConverters import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} +import scala.collection.JavaConverters._ +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { @@ -90,23 +93,44 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA RequestUtil.validateListRequest(request) val fields: util.List[String] = JavaConverters.seqAsJavaListConverter(request.get("fields").asInstanceOf[String].split(",").filter(field => StringUtils.isNotBlank(field) && !StringUtils.equalsIgnoreCase(field, "null"))).asJava request.getRequest.put("fields", fields) - DataNode.search(request).map(nodeList => { - val questionList = nodeList.map(node => { - val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) - val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) - if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE).equalsIgnoreCase(AssessmentConstants.SERVER) && !StringUtils.equals(request.get("isEditor").asInstanceOf[String], "true")) { - val hideEditorStateAns = AssessmentV5Manager.hideEditorStateAns(node) - if (StringUtils.isNotEmpty(hideEditorStateAns)) - node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorStateAns) - val hideCorrectResponse = AssessmentV5Manager.hideCorrectResponse(node) - if (StringUtils.isNotEmpty(hideCorrectResponse)) - node.getMetadata.put(AssessmentConstants.RESPONSE_DECLARATION, hideCorrectResponse) - } - NodeUtil.serialize(node, fields, node.getObjectType.toLowerCase.replace("Image", ""), request.getContext.get("version").asInstanceOf[String]) - }).asJava - ResponseHandler.OK.put("questions", questionList).put("count", questionList.size) + val extPropNameList: util.List[String] = DefinitionNode.getExternalProps(request.getContext.get("graph_id").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String], request.getContext.get("schemaName").asInstanceOf[String]).asJava + request.getRequest.put("extPropNameList", extPropNameList) + + DataNode.search(request).flatMap(nodeList => { + // Use map to process each node and return a Future[util.Map[String, AnyRef]] + val processedNodes: List[Future[util.Map[String, AnyRef]]] = nodeList.map(node => { + val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) + val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) + if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE).equalsIgnoreCase(AssessmentConstants.SERVER) && !StringUtils.equals(request.get("isEditor").asInstanceOf[String], "true")) { + val hideEditorStateAns = AssessmentV5Manager.hideEditorStateAns(node) + if (StringUtils.isNotEmpty(hideEditorStateAns)) + node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorStateAns) + val hideCorrectResponse = AssessmentV5Manager.hideCorrectResponse(node) + if (StringUtils.isNotEmpty(hideCorrectResponse)) + node.getMetadata.put(AssessmentConstants.RESPONSE_DECLARATION, hideCorrectResponse) + } + + // Process each node and return a Future[util.Map[String, AnyRef]] + val result = NodeUtil.serialize(node, fields, node.getObjectType.toLowerCase.replace("Image", ""), request.getContext.get("version").asInstanceOf[String]) + val questionMetadata = AssessmentV5Manager.getQuestionMetadata(node, fields, extPropNameList) + val responseMap: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() + responseMap.put("question", questionMetadata) + Future.successful(responseMap) }) - } + + // Use Future.sequence to collect the results into a List[util.Map[String, AnyRef]] + val collectedResponses: Future[List[util.Map[String, AnyRef]]] = Future.sequence(processedNodes) + + collectedResponses.map { responses => + val collectedResponsesJava = new java.util.ArrayList[util.Map[String, AnyRef]](responses.asJava) + ResponseHandler.OK.put("questions", collectedResponsesJava).put("count", responses.size) + }.recover { + case _ => // Handle the error case here + val errorMessage = "Failed to retrieve questions." + throw new ServerException("ERR_QUESTION_","" + errorMessage) + } + }) + } def privateRead(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala index 1102a5552..4fa2f7883 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala @@ -17,12 +17,16 @@ import org.sunbird.graph.schema.{DefinitionNode, ObjectCategoryDefinition} import org.sunbird.graph.utils.NodeUtil import org.sunbird.managers.HierarchyManager.hierarchyPrefix import org.sunbird.managers.{CopyManager, HierarchyManager, UpdateHierarchyManager} -import org.sunbird.utils.{AssessmentErrorCodes, RequestUtil} +import org.sunbird.utils.{AssessmentErrorCodes, HierarchyConstants, RequestUtil} import org.sunbird.v5.managers.AssessmentV5Manager import scala.collection.JavaConverters import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import com.fasterxml.jackson.databind.JsonNode +import scala.collection.mutable class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { @@ -75,14 +79,46 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba HierarchyManager.getHierarchy(request).map(resp => { if (StringUtils.equalsIgnoreCase(resp.getResponseCode.toString, "OK")) { val hierarchyMap = resp.getResult.get("questionSet").asInstanceOf[util.Map[String, AnyRef]] - val schemaVersion = hierarchyMap.getOrDefault("schemaVersion", "1.0").asInstanceOf[String] - val updateHierarchy = if (StringUtils.equalsIgnoreCase("1.0", schemaVersion)) AssessmentV5Manager.getTransformedHierarchy(hierarchyMap) else { - hierarchyMap + val schemaVersion = hierarchyMap.getOrDefault("schemaVersion", "1.1").asInstanceOf[String] + val updateHierarchy = if (StringUtils.equalsIgnoreCase(schemaVersion, "1.0")) { + AssessmentV5Manager.getTransformedHierarchy(hierarchyMap).asInstanceOf[mutable.Map[String, AnyRef]] + } else { + mutable.Map[String, AnyRef](hierarchyMap.asScala.toSeq: _*) + } + val mode = request.getOrDefault("mode", "").asInstanceOf[String] + val serverEvaluable = request.getOrDefault(HierarchyConstants.SERVEREVALUABLE, HierarchyConstants.FALSE).asInstanceOf[String] + if (!mode.equals("edit") && serverEvaluable.equalsIgnoreCase(HierarchyConstants.TRUE)) { + val childrenList = updateHierarchy.get(HierarchyConstants.CHILDREN).getOrElse(new util.ArrayList[java.util.Map[String, AnyRef]]()) + .asInstanceOf[util.ArrayList[java.util.Map[String, AnyRef]]] + val updatedChildrenList = childrenList.asScala.map(child => { + val maxQuestions = Option(child.get(HierarchyConstants.MAXQUESTIONS)).map(_.asInstanceOf[Int]).getOrElse(0) + val shuffle = Option(child.get(HierarchyConstants.SHUFFLE)).map(_.asInstanceOf[Boolean]).getOrElse(false) + val randomizedChild = if (shuffle) HierarchyManager.shuffleQuestions(child) else child + val limitedChild = HierarchyManager.limitQuestions(randomizedChild, maxQuestions) + + limitedChild + }).asJava + val serverEvaluable = updatedChildrenList.get(0).getOrDefault(HierarchyConstants.EVAL, new util.LinkedHashMap()).asInstanceOf[java.util.LinkedHashMap[String, String]] + if (serverEvaluable.get(HierarchyConstants.MODE) != null && serverEvaluable.get(HierarchyConstants.MODE).equalsIgnoreCase(HierarchyConstants.SERVER)) { + request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.SERVER) + } else { + request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.CLIENT) + } + val nestedChildrenIdentifiers = HierarchyManager.getNestedChildrenIdentifiers(updatedChildrenList) + val mergedMap: util.Map[String, String] = HierarchyManager.createMergedMap(request, nestedChildrenIdentifiers) + val userMapJson = JsonUtils.serialize(mergedMap) + val jwtToken = HierarchyManager.generateJwtToken(userMapJson) + updateHierarchy.put(HierarchyConstants.QUESTIONSETTOKEN, jwtToken) + updateHierarchy.put(HierarchyConstants.IDENTIFIER, request.get("contentID")) + updateHierarchy.put(HierarchyConstants.CHILDREN, updatedChildrenList) + resp.getResult.put("questionset", updateHierarchy.asJava) } resp.getResult.remove("questionSet") - resp.put("questionset", updateHierarchy) resp - } else resp + } else { + resp.getResult.remove("questionSet") + resp + } }) } diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index 05260c283..16f8f34ab 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -454,7 +454,7 @@ v5_supported_qumlVersions=[1.1] v5_default_qumlVersion=1.1 api.jwt.keyprefix=device -api.jwt.keycount=1 -api.jwt.basepath=./keys/ +api.jwt.keycount=3 +api.jwt.basepath=../../keys/ question.list.search.editor.url="http://localhost:9000/question/v5/editor/list" useHardcodedKeys=true \ No newline at end of file diff --git a/schemas/question/1.0/schema.json b/schemas/question/1.0/schema.json index da6028563..4361cf4bd 100644 --- a/schemas/question/1.0/schema.json +++ b/schemas/question/1.0/schema.json @@ -569,15 +569,6 @@ "No" ] }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - } - } - }, "evidence": { "type": "object" }, diff --git a/schemas/question/1.1/schema.json b/schemas/question/1.1/schema.json index fecf2ba45..dd0fe010c 100644 --- a/schemas/question/1.1/schema.json +++ b/schemas/question/1.1/schema.json @@ -682,7 +682,8 @@ "mode": { "type": "string", "enum": ["server","client"] - } + }, + "additionalProperties": false } }, "allowAnonymousAccess": { diff --git a/schemas/questionset/1.0/schema.json b/schemas/questionset/1.0/schema.json index c6b886308..4c17e4705 100644 --- a/schemas/questionset/1.0/schema.json +++ b/schemas/questionset/1.0/schema.json @@ -718,15 +718,6 @@ "artifactUrl": { "type": "string", "format": "url" - }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - } - } } }, "additionalProperties": false diff --git a/schemas/questionset/1.1/schema.json b/schemas/questionset/1.1/schema.json index 2ea76d435..52a67c240 100644 --- a/schemas/questionset/1.1/schema.json +++ b/schemas/questionset/1.1/schema.json @@ -656,7 +656,8 @@ "mode": { "type": "string", "enum": ["server","client"] - } + }, + "additionalProperties": false } }, "origin": { @@ -714,6 +715,9 @@ "artifactUrl": { "type": "string", "format": "url" + }, + "questionSetToken": { + "type": "string" } }, "additionalProperties": false From 3c81f492ff354cec5865046b9a8d2597f925edf5 Mon Sep 17 00:00:00 2001 From: anil Date: Mon, 25 Sep 2023 12:35:38 +0530 Subject: [PATCH 20/33] worked on code review comments --- .../sunbird/managers/HierarchyManager.scala | 39 +++---------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala index 62268bfba..526e46b7d 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala @@ -33,7 +33,7 @@ object HierarchyManager { val statusList = List("Live", "Unlisted", "Flagged") val ASSESSMENT_OBJECT_TYPES = List("Question", "QuestionSet") - val keyManager = new KeyManager(Platform.getString("api.jwt.basepath","./keys/"), Platform.getString("api.jwt.keyprefix","device"), Platform.getInteger("keys.count",1)) + val keyManager = new KeyManager(Platform.getString("api.jwt.basepath","./keys/"), Platform.getString("api.jwt.keyprefix","device"), Platform.getInteger("api.jwt.keycount",1)) val keyTobeRemoved = { if(Platform.config.hasPath("content.hierarchy.removed_props_for_leafNodes")) @@ -256,36 +256,7 @@ object HierarchyManager { val bookmarkId = request.get("bookmarkId").asInstanceOf[String] val rootHierarchy = result.get(HierarchyConstants.QUESTIONSET).asInstanceOf[util.Map[String, AnyRef]] if (StringUtils.isEmpty(bookmarkId)) { - if (request.get(HierarchyConstants.SERVEREVALUABLE).asInstanceOf[String].equalsIgnoreCase(HierarchyConstants.TRUE)) { - val mutableRootHierarchy = mutable.Map[String, AnyRef](rootHierarchy.asScala.toSeq: _*) - val childrenList = mutableRootHierarchy - .getOrElse(HierarchyConstants.CHILDREN, new util.ArrayList[java.util.Map[String, AnyRef]]) - .asInstanceOf[util.ArrayList[java.util.Map[String, AnyRef]]] - val updatedChildrenList = childrenList.asScala.map(child => { - val maxQuestions = Option(child.get(HierarchyConstants.MAXQUESTIONS)).map(_.asInstanceOf[Int]).getOrElse(0) - val shuffle = Option(child.get(HierarchyConstants.SHUFFLE)).map(_.asInstanceOf[Boolean]).getOrElse(false) - val randomizedChild = if (shuffle) shuffleQuestions(child) else child - val limitedChild = limitQuestions(randomizedChild, maxQuestions) - - limitedChild - }).asJava - val serverEvaluable = updatedChildrenList.get(0).getOrDefault(HierarchyConstants.EVAL, new util.LinkedHashMap()).asInstanceOf[java.util.LinkedHashMap[String, String]] - if (serverEvaluable.get(HierarchyConstants.MODE) != null && serverEvaluable.get(HierarchyConstants.MODE).equalsIgnoreCase(HierarchyConstants.SERVER)) { - request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.SERVER) - } else { - request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.CLIENT) - } - val nestedChildrenIdentifiers = getNestedChildrenIdentifiers(updatedChildrenList) - val mergedMap: util.Map[String, String] = createMergedMap(request, nestedChildrenIdentifiers) - val userMapJson = JsonUtils.serialize(mergedMap) - val jwtToken = generateJwtToken(userMapJson) - mutableRootHierarchy.put(HierarchyConstants.IDENTIFIER, request.get(HierarchyConstants.ROOTID)) - mutableRootHierarchy.put(HierarchyConstants.QUESTIONSETTOKEN, jwtToken) - mutableRootHierarchy.put(HierarchyConstants.CHILDREN, updatedChildrenList) - ResponseHandler.OK.put(HierarchyConstants.QUESTIONSET, mutableRootHierarchy.asJava) - } else { - ResponseHandler.OK.put(HierarchyConstants.QUESTIONSET, rootHierarchy) - } + ResponseHandler.OK.put(HierarchyConstants.QUESTIONSET, rootHierarchy) } else { val children = rootHierarchy.getOrElse("children", new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.List[util.Map[String, AnyRef]]] val bookmarkHierarchy = filterBookmarkHierarchy(children, bookmarkId) @@ -805,7 +776,7 @@ object HierarchyManager { } - private def getNestedChildrenIdentifiers(childrenList: util.List[java.util.Map[String, AnyRef]]): String = { + def getNestedChildrenIdentifiers(childrenList: util.List[java.util.Map[String, AnyRef]]): String = { val javaChildrenList: java.util.List[java.util.Map[String, AnyRef]] = childrenList.map(map => mapAsJavaMap(map)).asJava javaChildrenList.asScala.flatMap { child => val nestedChildren = child @@ -819,7 +790,7 @@ object HierarchyManager { }.mkString(",") } - private def createMergedMap(request: Request, nestedChildrenIdentifiers: String): util.Map[String, String] = { + def createMergedMap(request: Request, nestedChildrenIdentifiers: String): util.Map[String, String] = { val questionMap: util.HashMap[String, String] = new util.HashMap[String, String]() val userMap: util.Map[String, String] = new util.HashMap[String, String]() val mergedMap: util.Map[String, String] = new util.HashMap[String, String]() @@ -836,7 +807,7 @@ object HierarchyManager { mergedMap } - private def generateJwtToken(userMapJson: String): String = { + def generateJwtToken(userMapJson: String): String = { val headerOptions: java.util.Map[String, String] = new java.util.HashMap[String, String]() val keyData = keyManager.getRandomKey() val id = keyData.getKeyId From c09bea738e65ed87336c843d531a9ddf852d1b21 Mon Sep 17 00:00:00 2001 From: anil Date: Fri, 3 Nov 2023 18:36:47 +0530 Subject: [PATCH 21/33] updated code --- .../src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala index 4d2b40997..a1c768c7c 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala @@ -115,7 +115,6 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba updateHierarchy.put(HierarchyConstants.CHILDREN, updatedChildrenList) resp.getResult.put("questionset", updateHierarchy.asJava) } - resp.getResult.remove("questionSet") resp } else { resp.getResult.remove("questionSet") From e8f335b9a8ebd4b2567426cf56cf44c1e2e44730 Mon Sep 17 00:00:00 2001 From: anil Date: Mon, 6 Nov 2023 11:34:01 +0530 Subject: [PATCH 22/33] removed unwanted imports --- .../src/main/scala/org/sunbird/v5/actors/QuestionActor.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala index 10b945b33..40b64eb2b 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala @@ -20,9 +20,6 @@ import javax.inject.Inject import scala.collection.JavaConverters import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} -import scala.collection.JavaConverters._ -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { From 3de66f84ece725d708e16fb857551f33ba41fd21 Mon Sep 17 00:00:00 2001 From: anil Date: Mon, 6 Nov 2023 11:46:01 +0530 Subject: [PATCH 23/33] removed unwanted imports --- .../v5/QuestionSetController.scala | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala b/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala index 30bd827fd..aba1bd078 100644 --- a/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala +++ b/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala @@ -188,28 +188,29 @@ class QuestionSetController @Inject()(@Named(ActorNames.QUESTION_SET_V5_ACTOR) q val readRequest = getRequest(questionSet, headers, "getHierarchy") setRequestContext(readRequest, defaultVersion, objectType, schemaName) getResult(ApiId.GET_HIERARCHY, questionSetActor, readRequest) - - def updateComment() = Action.async { implicit request => - val headers = commonHeaders() - val body = requestBody() - val commentList = body.getOrElse("comments", new java.util.ArrayList[java.util.Map[String, Object]]()).asInstanceOf[java.util.ArrayList[java.util.Map[String, Object]]].asScala.toList - val filteredComments = new java.util.ArrayList[java.util.Map[String, Object]](commentList.groupBy(_.getOrElse("identifier", "")).values.map(_.last).toList.asJava) - val questionSet = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] - questionSet.putAll(headers) - questionSet.put("comments", filteredComments) - val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.updateCommentQuestionSet.toString) - setRequestContext(questionSetRequest, defaultVersion, objectType, schemaName) - getResult(ApiId.UPDATE_COMMENT_QUESTION_SET, questionSetActor, questionSetRequest) } - def readComment(identifier: String) = Action.async { implicit request => - val headers = commonHeaders() - val questionSet = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] - questionSet.putAll(headers) - questionSet.putAll(Map("identifier" -> identifier, "fields" -> "", "mode" -> "read").asJava) - val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.readCommentQuestionSet.toString) - setRequestContext(questionSetRequest, defaultVersion, objectType, schemaName) - getResult(ApiId.READ_COMMENT_QUESTION_SET, questionSetActor, questionSetRequest) - - } + def updateComment() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val commentList = body.getOrElse("comments", new java.util.ArrayList[java.util.Map[String, Object]]()).asInstanceOf[java.util.ArrayList[java.util.Map[String, Object]]].asScala.toList + val filteredComments = new java.util.ArrayList[java.util.Map[String, Object]](commentList.groupBy(_.getOrElse("identifier", "")).values.map(_.last).toList.asJava) + val questionSet = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + questionSet.putAll(headers) + questionSet.put("comments", filteredComments) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.updateCommentQuestionSet.toString) + setRequestContext(questionSetRequest, defaultVersion, objectType, schemaName) + getResult(ApiId.UPDATE_COMMENT_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def readComment(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val questionSet = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + questionSet.putAll(headers) + questionSet.putAll(Map("identifier" -> identifier, "fields" -> "", "mode" -> "read").asJava) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.readCommentQuestionSet.toString) + setRequestContext(questionSetRequest, defaultVersion, objectType, schemaName) + getResult(ApiId.READ_COMMENT_QUESTION_SET, questionSetActor, questionSetRequest) + + } } From 8a4cb2b46e8c87483fdc3289ea64350747a55314 Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Mon, 18 Dec 2023 13:53:42 +0530 Subject: [PATCH 24/33] eval change to evalMode --- .../org/sunbird/utils/AssessmentContants.scala | 2 +- .../org/sunbird/v5/actors/QuestionActor.scala | 12 ++++++------ .../org/sunbird/v5/actors/QuestionSetActor.scala | 4 ++-- .../sunbird/v5/managers/AssessmentV5Manager.scala | 6 +++--- .../sunbird/managers/UpdateHierarchyManager.scala | 14 ++++++-------- .../org/sunbird/utils/HierarchyConstants.scala | 2 +- schemas/question/1.1/schema.json | 15 ++++++--------- schemas/questionset/1.1/schema.json | 15 ++++++--------- 8 files changed, 31 insertions(+), 39 deletions(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala index e30bba3a9..af637268e 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala @@ -72,7 +72,7 @@ object AssessmentConstants { val RESPONSE = "response" val OUTCOMES = "outcomes" val OPTIONS = "options" - val EVAL: String = "eval" + val EVAL: String = "evalMode" val SERVER: String = "server" val FLOWER_BRACKETS: String = "{}" diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala index 10b945b33..f47e4686a 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala @@ -54,9 +54,9 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA val extPropNameList:util.List[String] = DefinitionNode.getExternalProps(request.getContext.get("graph_id").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String], request.getContext.get("schemaName").asInstanceOf[String]).asJava request.getRequest.put("fields", extPropNameList) DataNode.read(request).map(node => { - val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) - val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) - if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE) == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { + val serverEvaluable = node.getMetadata.get(AssessmentConstants.EVAL) + val data = serverEvaluable + if (data != null && data == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { val hideEditorResponse = AssessmentV5Manager.hideEditorStateAns(node) if (StringUtils.isNotEmpty(hideEditorResponse)) node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorResponse) @@ -99,9 +99,9 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA DataNode.search(request).flatMap(nodeList => { // Use map to process each node and return a Future[util.Map[String, AnyRef]] val processedNodes: List[Future[util.Map[String, AnyRef]]] = nodeList.map(node => { - val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) - val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) - if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE).equalsIgnoreCase(AssessmentConstants.SERVER) && !StringUtils.equals(request.get("isEditor").asInstanceOf[String], "true")) { + val serverEvaluable = node.getMetadata.get(AssessmentConstants.EVAL) + val data = serverEvaluable + if (data != null && data == AssessmentConstants.SERVER && !StringUtils.equals(request.get("isEditor").asInstanceOf[String], "true")) { val hideEditorStateAns = AssessmentV5Manager.hideEditorStateAns(node) if (StringUtils.isNotEmpty(hideEditorStateAns)) node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorStateAns) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala index 4fa2f7883..7f19bea61 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala @@ -98,8 +98,8 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba limitedChild }).asJava - val serverEvaluable = updatedChildrenList.get(0).getOrDefault(HierarchyConstants.EVAL, new util.LinkedHashMap()).asInstanceOf[java.util.LinkedHashMap[String, String]] - if (serverEvaluable.get(HierarchyConstants.MODE) != null && serverEvaluable.get(HierarchyConstants.MODE).equalsIgnoreCase(HierarchyConstants.SERVER)) { + val serverEvaluable = updatedChildrenList.get(0).get(HierarchyConstants.EVAL) + if (serverEvaluable != null && serverEvaluable == HierarchyConstants.SERVER) { request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.SERVER) } else { request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.CLIENT) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala index 9abfa1c0c..a8658aaad 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala @@ -92,9 +92,9 @@ object AssessmentV5Manager { def getValidateNodeForReject(request: Request, errCode: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { request.put("mode", "edit") DataNode.read(request).map(node => { - val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) - val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) - if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE) == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { + val serverEvaluable = node.getMetadata.get(AssessmentConstants.EVAL) + val data = serverEvaluable + if (data != null && data == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { val hideEditorResponse = hideEditorStateAns(node) if (StringUtils.isNotEmpty(hideEditorResponse)) node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorResponse) diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala index 92b6e46f3..d3955cfe5 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala @@ -33,16 +33,14 @@ object UpdateHierarchyManager { val rootId: String = getRootId(nodesModified, hierarchy) request.getContext.put(HierarchyConstants.ROOT_ID, rootId) getValidatedRootNode(rootId, request).map(node => { - val eval = node.getMetadata.getOrDefault("eval", "{}").asInstanceOf[String] - val data = mapper.readValue(eval, classOf[java.util.Map[String, String]]) - var mode = data.get("mode") + var mode = node.getMetadata.get(HierarchyConstants.EVAL).asInstanceOf[String] if (nodesModified.get(rootId) != null) { - val updMode = nodesModified.get(rootId).asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] - .get("metadata").asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] - .getOrElse("eval", new util.LinkedHashMap()) - .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].get("mode").asInstanceOf[String] + val updMode = nodesModified.get(rootId) + .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] + .get(HierarchyConstants.METADATA).asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] + .get(HierarchyConstants.EVAL).asInstanceOf[String] if (StringUtils.isNotEmpty(mode) && !mode.equals(updMode)) - throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "QuestionSet eval status cannot be modified") + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "QuestionSet evaluation mode status cannot be modified") mode = updMode } getExistingHierarchy(request, node).map(existingHierarchy => { diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala index d1500cb46..a65e62a13 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala @@ -62,7 +62,7 @@ object HierarchyConstants { val TRUE: String = "true" val FALSE: String = "false" val SERVER: String = "server" - val EVAL: String = "eval" + val EVAL: String = "evalMode" val EVAL_MODE: String = "eval-mode" val CLIENT: String = "client" val CONTENTID: String = "contentID" diff --git a/schemas/question/1.1/schema.json b/schemas/question/1.1/schema.json index dd0fe010c..88c885a22 100644 --- a/schemas/question/1.1/schema.json +++ b/schemas/question/1.1/schema.json @@ -676,15 +676,12 @@ "No" ] }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - }, - "additionalProperties": false - } + "evalMode": { + "type": "string", + "enum": [ + "server", + "client" + ] }, "allowAnonymousAccess": { "type": "string", diff --git a/schemas/questionset/1.1/schema.json b/schemas/questionset/1.1/schema.json index 52a67c240..29e2e792a 100644 --- a/schemas/questionset/1.1/schema.json +++ b/schemas/questionset/1.1/schema.json @@ -650,15 +650,12 @@ "type": "object" } }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - }, - "additionalProperties": false - } + "evalMode": { + "type": "string", + "enum": [ + "server", + "client" + ] }, "origin": { "type": "string" From 66e34f185fce7a0755784f9748ef8103d040e4ec Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Tue, 19 Dec 2023 12:06:32 +0530 Subject: [PATCH 25/33] eval changes to evalMode and object to string --- .../main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala index a8658aaad..8969122cb 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala @@ -188,6 +188,7 @@ object AssessmentV5Manager { def updateHierarchy(hierarchy: util.Map[String, AnyRef], status: String, rootUserId: String): (java.util.Map[String, AnyRef], java.util.List[String]) = { val keys = List("identifier", "children").asJava hierarchy.keySet().retainAll(keys) + val children = hierarchy.getOrDefault("children", new util.ArrayList[java.util.Map[String, AnyRef]]).asInstanceOf[util.List[java.util.Map[String, AnyRef]]] val childrenToUpdate: List[String] = updateChildrenRecursive(children, status, List(), rootUserId) (hierarchy, childrenToUpdate.asJava) From a363e2f087e0770588051782d2ac1589a6bb7e45 Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Tue, 19 Dec 2023 12:20:09 +0530 Subject: [PATCH 26/33] eval mode changes --- assessment-api/qs-hierarchy-manager/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assessment-api/qs-hierarchy-manager/pom.xml b/assessment-api/qs-hierarchy-manager/pom.xml index e4de07f22..8431814a0 100644 --- a/assessment-api/qs-hierarchy-manager/pom.xml +++ b/assessment-api/qs-hierarchy-manager/pom.xml @@ -14,7 +14,7 @@ org.sunbird - graph-engine_2.11 + graph-engine_2.12 1.0-SNAPSHOT jar From 3db5b31146e7d7081192e91508bd46013c7f56ee Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Tue, 19 Dec 2023 12:32:31 +0530 Subject: [PATCH 27/33] eval mode changes and graph engine version to 2.12 --- assessment-api/assessment-actors/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assessment-api/assessment-actors/pom.xml b/assessment-api/assessment-actors/pom.xml index 77f689950..f7548b3be 100644 --- a/assessment-api/assessment-actors/pom.xml +++ b/assessment-api/assessment-actors/pom.xml @@ -29,7 +29,7 @@ org.sunbird - graph-engine_2.11 + graph-engine_2.12 1.0-SNAPSHOT jar From 7612aafeb33fbecc859df217b5f09408f24572e8 Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Tue, 19 Dec 2023 17:17:51 +0530 Subject: [PATCH 28/33] pom file scala version changes --- assessment-api/pom.xml | 2 +- pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assessment-api/pom.xml b/assessment-api/pom.xml index ce2499ce3..6bbccbef6 100644 --- a/assessment-api/pom.xml +++ b/assessment-api/pom.xml @@ -20,7 +20,7 @@ UTF-8 UTF-8 - 2.11 + 2.12 diff --git a/pom.xml b/pom.xml index b95bc1029..3226c97e5 100644 --- a/pom.xml +++ b/pom.xml @@ -12,8 +12,8 @@ UTF-8 1.4.1 - 2.11 - 2.11.12 + 2.12 + 2.12.11 3.0.8 2.9.8 From fe7995a0c8e50d6a7641e049e0597112c76df22c Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Wed, 20 Dec 2023 23:07:15 +0530 Subject: [PATCH 29/33] PR changes --- .../org/sunbird/utils/AssessmentContants.scala | 2 +- .../org/sunbird/v5/actors/QuestionActor.scala | 12 ++++++------ .../org/sunbird/v5/actors/QuestionSetActor.scala | 4 ++-- .../sunbird/v5/managers/AssessmentV5Manager.scala | 6 +++--- .../sunbird/managers/UpdateHierarchyManager.scala | 12 +++++------- .../org/sunbird/utils/HierarchyConstants.scala | 2 +- schemas/question/1.1/schema.json | 15 ++++++--------- schemas/questionset/1.1/schema.json | 15 ++++++--------- 8 files changed, 30 insertions(+), 38 deletions(-) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala index e30bba3a9..af637268e 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/AssessmentContants.scala @@ -72,7 +72,7 @@ object AssessmentConstants { val RESPONSE = "response" val OUTCOMES = "outcomes" val OPTIONS = "options" - val EVAL: String = "eval" + val EVAL: String = "evalMode" val SERVER: String = "server" val FLOWER_BRACKETS: String = "{}" diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala index 40b64eb2b..b5d491c3e 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionActor.scala @@ -51,9 +51,9 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA val extPropNameList:util.List[String] = DefinitionNode.getExternalProps(request.getContext.get("graph_id").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String], request.getContext.get("schemaName").asInstanceOf[String]).asJava request.getRequest.put("fields", extPropNameList) DataNode.read(request).map(node => { - val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) - val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) - if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE) == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { + val serverEvaluable = node.getMetadata.get(AssessmentConstants.EVAL) + val data = serverEvaluable + if (data != null && data == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { val hideEditorResponse = AssessmentV5Manager.hideEditorStateAns(node) if (StringUtils.isNotEmpty(hideEditorResponse)) node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorResponse) @@ -96,9 +96,9 @@ class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseA DataNode.search(request).flatMap(nodeList => { // Use map to process each node and return a Future[util.Map[String, AnyRef]] val processedNodes: List[Future[util.Map[String, AnyRef]]] = nodeList.map(node => { - val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) - val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) - if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE).equalsIgnoreCase(AssessmentConstants.SERVER) && !StringUtils.equals(request.get("isEditor").asInstanceOf[String], "true")) { + val serverEvaluable = node.getMetadata.get(AssessmentConstants.EVAL) + val data = serverEvaluable + if (data != null && data == AssessmentConstants.SERVER && !StringUtils.equals(request.get("isEditor").asInstanceOf[String], "true")) { val hideEditorStateAns = AssessmentV5Manager.hideEditorStateAns(node) if (StringUtils.isNotEmpty(hideEditorStateAns)) node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorStateAns) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala index a1c768c7c..9644b1d56 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala @@ -100,8 +100,8 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba limitedChild }).asJava - val serverEvaluable = updatedChildrenList.get(0).getOrDefault(HierarchyConstants.EVAL, new util.LinkedHashMap()).asInstanceOf[java.util.LinkedHashMap[String, String]] - if (serverEvaluable.get(HierarchyConstants.MODE) != null && serverEvaluable.get(HierarchyConstants.MODE).equalsIgnoreCase(HierarchyConstants.SERVER)) { + val serverEvaluable = updatedChildrenList.get(0).get(HierarchyConstants.EVAL) + if (serverEvaluable != null && serverEvaluable == HierarchyConstants.SERVER) { request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.SERVER) } else { request.put(HierarchyConstants.EVAL_MODE, HierarchyConstants.CLIENT) diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala index 56b28aed6..42a9d7de6 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/managers/AssessmentV5Manager.scala @@ -93,9 +93,9 @@ object AssessmentV5Manager { def getValidateNodeForReject(request: Request, errCode: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { request.put("mode", "edit") DataNode.read(request).map(node => { - val serverEvaluable = node.getMetadata.getOrDefault(AssessmentConstants.EVAL, AssessmentConstants.FLOWER_BRACKETS) - val data = mapper.readValue(serverEvaluable.asInstanceOf[String], classOf[java.util.Map[String, String]]) - if (data.get(AssessmentConstants.MODE) != null && data.get(AssessmentConstants.MODE) == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { + val serverEvaluable = node.getMetadata.get(AssessmentConstants.EVAL) + val data = serverEvaluable + if (data != null && data == AssessmentConstants.SERVER && !StringUtils.equals(request.getOrDefault("isEditor", "").asInstanceOf[String], "true")) { val hideEditorResponse = hideEditorStateAns(node) if (StringUtils.isNotEmpty(hideEditorResponse)) node.getMetadata.put(AssessmentConstants.EDITOR_STATE, hideEditorResponse) diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala index 107119ba4..38bda89de 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala @@ -33,14 +33,12 @@ object UpdateHierarchyManager { val rootId: String = getRootId(nodesModified, hierarchy) request.getContext.put(HierarchyConstants.ROOT_ID, rootId) getValidatedRootNode(rootId, request).map(node => { - val eval = node.getMetadata.getOrDefault("eval", "{}").asInstanceOf[String] - val data = mapper.readValue(eval, classOf[java.util.Map[String, String]]) - var mode = data.get("mode") + var mode = node.getMetadata.get(HierarchyConstants.EVAL).asInstanceOf[String] if (nodesModified.get(rootId) != null) { - val updMode = nodesModified.get(rootId).asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] - .get("metadata").asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] - .getOrElse("eval", new util.LinkedHashMap()) - .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]].get("mode").asInstanceOf[String] + val updMode = nodesModified.get(rootId) + .asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] + .get(HierarchyConstants.METADATA).asInstanceOf[java.util.LinkedHashMap[String, AnyRef]] + .get(HierarchyConstants.EVAL).asInstanceOf[String] if (StringUtils.isNotEmpty(mode) && !mode.equals(updMode)) throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "QuestionSet eval status cannot be modified") mode = updMode diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala index d1500cb46..a65e62a13 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala @@ -62,7 +62,7 @@ object HierarchyConstants { val TRUE: String = "true" val FALSE: String = "false" val SERVER: String = "server" - val EVAL: String = "eval" + val EVAL: String = "evalMode" val EVAL_MODE: String = "eval-mode" val CLIENT: String = "client" val CONTENTID: String = "contentID" diff --git a/schemas/question/1.1/schema.json b/schemas/question/1.1/schema.json index dd0fe010c..88c885a22 100644 --- a/schemas/question/1.1/schema.json +++ b/schemas/question/1.1/schema.json @@ -676,15 +676,12 @@ "No" ] }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - }, - "additionalProperties": false - } + "evalMode": { + "type": "string", + "enum": [ + "server", + "client" + ] }, "allowAnonymousAccess": { "type": "string", diff --git a/schemas/questionset/1.1/schema.json b/schemas/questionset/1.1/schema.json index 52a67c240..29e2e792a 100644 --- a/schemas/questionset/1.1/schema.json +++ b/schemas/questionset/1.1/schema.json @@ -650,15 +650,12 @@ "type": "object" } }, - "eval": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["server","client"] - }, - "additionalProperties": false - } + "evalMode": { + "type": "string", + "enum": [ + "server", + "client" + ] }, "origin": { "type": "string" From 7a94c1c7cde778b3fc00134298f693d4ff5a54d3 Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Tue, 26 Dec 2023 11:59:02 +0530 Subject: [PATCH 30/33] assessment actors and controllers removed and added to questionSet --- .../sunbird/v5/actors/QuestionSetActor.scala | 11 ++++++- .../v5/actors/QuestionSetAssessActor.scala | 32 ------------------- .../v5/QuestionSetAssessController.scala | 19 ----------- .../v5/QuestionSetController.scala | 6 ++++ .../app/utils/ActorNames.scala | 2 -- assessment-api/assessment-service/conf/routes | 2 +- 6 files changed, 17 insertions(+), 55 deletions(-) delete mode 100644 assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetAssessActor.scala delete mode 100644 assessment-api/assessment-service/app/controllers/v5/QuestionSetAssessController.scala diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala index 9644b1d56..3b34e99e3 100644 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetActor.scala @@ -17,7 +17,7 @@ import org.sunbird.graph.schema.{DefinitionNode, ObjectCategoryDefinition} import org.sunbird.graph.utils.NodeUtil import org.sunbird.managers.HierarchyManager.hierarchyPrefix import org.sunbird.managers.{CopyManager, HierarchyManager, UpdateHierarchyManager} -import org.sunbird.utils.{AssessmentErrorCodes, HierarchyConstants, RequestUtil} +import org.sunbird.utils.{AssessmentConstants, AssessmentErrorCodes, HierarchyConstants, RequestUtil} import org.sunbird.v5.managers.AssessmentV5Manager import scala.collection.JavaConverters @@ -26,6 +26,7 @@ import scala.concurrent.{ExecutionContext, Future} import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.fasterxml.jackson.databind.JsonNode + import scala.collection.mutable class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { @@ -53,6 +54,7 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba case "copyQuestionSet" => copy(request) case "updateCommentQuestionSet" => updateComment(request) case "readCommentQuestionSet" => AssessmentV5Manager.readComment(request, "comments") + case "assessQuestionSet" => assessment(request) case _ => ERROR(request.getOperation) } @@ -307,5 +309,12 @@ class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends Ba } } } + private def assessment(req: Request): Future[Response] = { + val assessments = req.getRequest.getOrDefault(AssessmentConstants.ASSESSMENTS, new util.ArrayList[util.Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]] + val quesDoIds = AssessmentV5Manager.validateAssessRequest(req) + val list: Response = AssessmentV5Manager.questionList(quesDoIds) + AssessmentV5Manager.calculateScore(list, assessments) + Future(ResponseHandler.OK.put(AssessmentConstants.QUESTIONS, req.getRequest)) + } } diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetAssessActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetAssessActor.scala deleted file mode 100644 index a6f5cb4b3..000000000 --- a/assessment-api/assessment-actors/src/main/scala/org/sunbird/v5/actors/QuestionSetAssessActor.scala +++ /dev/null @@ -1,32 +0,0 @@ -package org.sunbird.v5.actors - -import org.sunbird.actor.core.BaseActor -import org.sunbird.common.dto.{Request, Response, ResponseHandler} -import org.sunbird.graph.OntologyEngineContext -import org.sunbird.managers.AssessmentManager -import org.sunbird.utils.AssessmentConstants -import org.sunbird.v5.managers.AssessmentV5Manager - -import java.util -import javax.inject.Inject -import scala.concurrent.{ExecutionContext, Future} - -class QuestionSetAssessActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { - - implicit val ec: ExecutionContext = getContext().dispatcher - - @throws[Throwable] - override def onReceive(request: Request): Future[Response] = request.getOperation match { - case "assessQuestionSet" => assessment(request) - case _ => ERROR(request.getOperation) - } - - private def assessment(req: Request): Future[Response] = { - val assessments = req.getRequest.getOrDefault(AssessmentConstants.ASSESSMENTS, new util.ArrayList[util.Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]] - val quesDoIds = AssessmentV5Manager.validateAssessRequest(req) - val list: Response = AssessmentV5Manager.questionList(quesDoIds) - AssessmentV5Manager.calculateScore(list, assessments) - Future(ResponseHandler.OK.put(AssessmentConstants.QUESTIONS, req.getRequest)) - } - -} diff --git a/assessment-api/assessment-service/app/controllers/v5/QuestionSetAssessController.scala b/assessment-api/assessment-service/app/controllers/v5/QuestionSetAssessController.scala deleted file mode 100644 index b62703b60..000000000 --- a/assessment-api/assessment-service/app/controllers/v5/QuestionSetAssessController.scala +++ /dev/null @@ -1,19 +0,0 @@ -package controllers.v5 - -import akka.actor.{ActorRef, ActorSystem} -import play.api.mvc.ControllerComponents -import utils.{ActorNames, ApiId, QuestionSetOperations} - -import javax.inject.{Inject, Named} -import scala.concurrent.ExecutionContext - -class QuestionSetAssessController @Inject()(@Named(ActorNames.QUESTION_SET_ASSESS_ACTOR) questionSetAssessActor: ActorRef, - cc: ControllerComponents, - actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { - def assessment() = Action.async { implicit request => - val headers = commonHeaders() - val body = requestBody() - val questionSetAssessRequest = getRequest(body, headers, QuestionSetOperations.assessQuestionSet.toString) - getResult(ApiId.ASSESS_QUESTION_SET, questionSetAssessActor, questionSetAssessRequest) - } -} diff --git a/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala b/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala index aba1bd078..7ac04030c 100644 --- a/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala +++ b/assessment-api/assessment-service/app/controllers/v5/QuestionSetController.scala @@ -213,4 +213,10 @@ class QuestionSetController @Inject()(@Named(ActorNames.QUESTION_SET_V5_ACTOR) q getResult(ApiId.READ_COMMENT_QUESTION_SET, questionSetActor, questionSetRequest) } + def assessment() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val questionSetAssessRequest = getRequest(body, headers, QuestionSetOperations.assessQuestionSet.toString) + getResult(ApiId.ASSESS_QUESTION_SET, questionSetActor, questionSetAssessRequest) + } } diff --git a/assessment-api/assessment-service/app/utils/ActorNames.scala b/assessment-api/assessment-service/app/utils/ActorNames.scala index 239363249..73206702c 100644 --- a/assessment-api/assessment-service/app/utils/ActorNames.scala +++ b/assessment-api/assessment-service/app/utils/ActorNames.scala @@ -8,6 +8,4 @@ object ActorNames { final val QUESTION_SET_ACTOR = "questionSetActor" final val QUESTION_V5_ACTOR = "questionV5Actor" final val QUESTION_SET_V5_ACTOR = "questionSetV5Actor" - final val QUESTION_SET_ASSESS_ACTOR="questionSetAssessActor" - } diff --git a/assessment-api/assessment-service/conf/routes b/assessment-api/assessment-service/conf/routes index 0ea7bde7c..ddce2941c 100644 --- a/assessment-api/assessment-service/conf/routes +++ b/assessment-api/assessment-service/conf/routes @@ -81,7 +81,7 @@ POST /question/v5/editor/list controllers.v5.QuestionCont GET /question/v5/editor/read/:identifier controllers.v5.QuestionController.editorRead(identifier:String, mode:Option[String], fields:Option[String]) # QuestionValidate API's -POST /questionset/v5/assestment/validate controllers.v5.QuestionSetAssessController.assessment +POST /questionset/v5/assestment/validate controllers.v5.QuestionSetController.assessment PATCH /questionset/v5/comment/update controllers.v5.QuestionSetController.updateComment GET /questionset/v5/comment/read/:identifier controllers.v5.QuestionSetController.readComment(identifier:String) From 62766487321249dac27943a7ba9e91718fb6dbe0 Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Wed, 27 Dec 2023 16:06:18 +0530 Subject: [PATCH 31/33] assessment actor and controller removed --- .../assessment-service/app/modules/AssessmentModule.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/assessment-api/assessment-service/app/modules/AssessmentModule.scala b/assessment-api/assessment-service/app/modules/AssessmentModule.scala index 6d3d85094..55087d5ea 100644 --- a/assessment-api/assessment-service/app/modules/AssessmentModule.scala +++ b/assessment-api/assessment-service/app/modules/AssessmentModule.scala @@ -15,7 +15,6 @@ class AssessmentModule extends AbstractModule with AkkaGuiceSupport { bindActor(classOf[QuestionSetActor], ActorNames.QUESTION_SET_ACTOR) bindActor(classOf[org.sunbird.v5.actors.QuestionActor], ActorNames.QUESTION_V5_ACTOR) bindActor(classOf[org.sunbird.v5.actors.QuestionSetActor], ActorNames.QUESTION_SET_V5_ACTOR) - bindActor(classOf[org.sunbird.v5.actors.QuestionSetAssessActor], ActorNames.QUESTION_SET_ASSESS_ACTOR) println("Initialized application actors for assessment-service") } } From d6f9821b0e750c4223ac6fe897cee1eaf78a09f8 Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Tue, 2 Apr 2024 12:55:07 +0530 Subject: [PATCH 32/33] moved harcode values to properties --- .../assessment-service/conf/application.conf | 4 +- .../org/sunbird/managers/KeyManager.scala | 10 +- .../scala/org/sunbird/utils/Base64Util.java | 722 ------------------ .../scala/org/sunbird/utils/JwtUtil.scala | 15 +- 4 files changed, 16 insertions(+), 735 deletions(-) delete mode 100644 assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/Base64Util.java diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index 16f8f34ab..85110e1cd 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -457,4 +457,6 @@ api.jwt.keyprefix=device api.jwt.keycount=3 api.jwt.basepath=../../keys/ question.list.search.editor.url="http://localhost:9000/question/v5/editor/list" -useHardcodedKeys=true \ No newline at end of file +useHardcodedKeys=true +api.jwt.publickey="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAypzjIoeWTbPJRYNlwhzv\nJ+6q9vXk9HJHvubsMJhdpXX77eEQojtgAkrk3vv5edyhJHs/XY69OJMu4o8qHyhY\nSsSiw0TIuPPIQ3+moZB+yY6MKY7xYiHbKp9xeB1XsFt38H+HtOGX32Q5bL/4CvDS\nHUq7bKoG5wg5dyPkMwQRU/F4T3z9fSnKuRNjsb4OkyyYglJ6tn7uWp+RjPzXXLnB\nnu2S8R6Enw2DPjtQlJmtI941UsONuHPdj7srb4t+2p7jtROhMARDeT3X1DtbqIdK\nNrMu/+Q9APhHSQ5jUgk2nttPFjH8d31pDrcnFjKL7pQytQZeAYIVUB4MQZLnSYVB\nLwIDAQAB\n-----END PUBLIC KEY-----" +api.jwt.privatekey="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKnOMih5ZNs8lF\ng2XCHO8n7qr29eT0cke+5uwwmF2ldfvt4RCiO2ACSuTe+/l53KEkez9djr04ky7i\njyofKFhKxKLDRMi488hDf6ahkH7JjowpjvFiIdsqn3F4HVewW3fwf4e04ZffZDls\nv/gK8NIdSrtsqgbnCDl3I+QzBBFT8XhPfP19Kcq5E2Oxvg6TLJiCUnq2fu5an5GM\n/NdcucGe7ZLxHoSfDYM+O1CUma0j3jVSw424c92Puytvi37anuO1E6EwBEN5PdfU\nO1uoh0o2sy7/5D0A+EdJDmNSCTae208WMfx3fWkOtycWMovulDK1Bl4BghVQHgxB\nkudJhUEvAgMBAAECggEAB3Bz1u05vUTU84q/CwqkuoeE9HtgoQJFlLW5RcS0Arxb\nXmEaPlkSwRLLgE2p9WnLhHtLMmq1LOVOkX5mZaCsGT0XqTbJ1FMMu9m6Hj2GQjoY\nw5MRiHDFmAHxslJvFhuA3GFtjXX10+IQr7Seui9PouHleGuhXdmlKBtqKHgr3YEA\nxAkPoQl1Co/yafQ911RH13PM005UVVpCaD/2xwLnJPqrNxM3YktWNQKBAunDTdxo\nQ8JeCgTKNdWSJ03nBJ2u9wi6EGaZJfVG4uEF5B85lhEfo3O0kTh2B/3qdSl8Aknm\ngcOsoN1D8ivgld/IZebJ89NKgwSbiCP035cTYtS4jQKBgQDWsbb3016HE6AxUwGl\n/pmhGV4uWx0YjwgM2T30SXICNtQw7JWs9CXjoxD+ee3mYcB70Ag3+3g/CC82w/Lr\nz1sYNEx4HodvuIqo6XYYE5dg0QXkMehW+dxqx+VtCcwC0w7nGar9SfMtIwr8Wg/6\nqH5jcKxPaB2LMFtR/yCqBmbWowKBgQDxmCHT3RBE+VvmhAeM6i6EwLV1JS6+ZFf6\nOjDC3SRN2cMU/8wFEpZ12tDTTJnxrCKgWoZKUwdRm9upmGiMelq5M9mhJRPZeGrr\nlBs9I/vj2vJbu8QLCYVUC+JY8r3ezlnLp2Xr9dCebnECbgzQcAj+rq5tzedLhUbZ\nPsgC6HGwBQKBgDXjkauPEJETKgh3b1h9GY7IUU2NbTY24Kxo8xYYQVew734ARGmP\nNtt2mNNnQ4GqU6hARW/X3QzlPwSeFqF+AL2IkxEriI9QYO2Y/B16/Wo9zR7EMC90\ntBDRcBL4fI7Q71KurK67GyDfROimqpAeLutC4t1jotbHIoToZwiGZtXFAoGAHc/P\nBMy3kDtQ+s35/Ip9OQZqncz7yqSpMohxseoF69FeQD4cV9fmVx6sPBasvGSoVS82\neP9r3MclwPS8mfETNt1OEpN3spMoZm99OPsyvvgqheVSmKYRHMDmqmExysedzwKW\nEhrgJlysd0dLL4FTqtG1Vnlc/DWy+2XC2pECTl0CgYEAtzasBUsXp59IMhTSITKA\nZphiiBSTmEyhs6WptbbgOkAlLbT7YYhgpsCjKqFdLjGBZ+/Z9sICKlowgE8EueJ7\ntM0bnS/28Bgbd/QU1yJmglm3MbXzk9Q5tOE1Y3SNzwkT4kjp4e4AQvkVJt78ds+y\nPBvoTfmeV7/ov7/1OzRzHG0=\n-----END PRIVATE KEY-----" \ No newline at end of file diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala index a7c0eae00..9a730f67d 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/KeyManager.scala @@ -1,7 +1,6 @@ package org.sunbird.managers import org.slf4j.LoggerFactory import org.sunbird.common.Platform -import org.sunbird.utils.Base64Util import java.io.FileInputStream import java.nio.charset.StandardCharsets @@ -54,7 +53,7 @@ class KeyManager(private val basePath: String, private val keyPrefix: String, pr log.info("Public key loaded from filesystem - " + path) keyFactory.generatePublic(keySpec) } else { - val publicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAypzjIoeWTbPJRYNlwhzv\nJ+6q9vXk9HJHvubsMJhdpXX77eEQojtgAkrk3vv5edyhJHs/XY69OJMu4o8qHyhY\nSsSiw0TIuPPIQ3+moZB+yY6MKY7xYiHbKp9xeB1XsFt38H+HtOGX32Q5bL/4CvDS\nHUq7bKoG5wg5dyPkMwQRU/F4T3z9fSnKuRNjsb4OkyyYglJ6tn7uWp+RjPzXXLnB\nnu2S8R6Enw2DPjtQlJmtI941UsONuHPdj7srb4t+2p7jtROhMARDeT3X1DtbqIdK\nNrMu/+Q9APhHSQ5jUgk2nttPFjH8d31pDrcnFjKL7pQytQZeAYIVUB4MQZLnSYVB\nLwIDAQAB\n-----END PUBLIC KEY-----" + val publicKey = Platform.getString("api.jwt.publickey","") .replaceAll("(-+BEGIN RSA PUBLIC KEY-+\\r?\\n|-+END RSA PUBLIC KEY-+\\r?\\n?)", "") .replaceAll("(-+BEGIN PUBLIC KEY-+\\r?\\n|-+END PUBLIC KEY-+\\r?\\n?)", "") // .replaceAll("-", "") @@ -92,12 +91,13 @@ class KeyManager(private val basePath: String, private val keyPrefix: String, pr keyFactory.generatePrivate(spec) } else { - var privateKey = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKnOMih5ZNs8lF\ng2XCHO8n7qr29eT0cke+5uwwmF2ldfvt4RCiO2ACSuTe+/l53KEkez9djr04ky7i\njyofKFhKxKLDRMi488hDf6ahkH7JjowpjvFiIdsqn3F4HVewW3fwf4e04ZffZDls\nv/gK8NIdSrtsqgbnCDl3I+QzBBFT8XhPfP19Kcq5E2Oxvg6TLJiCUnq2fu5an5GM\n/NdcucGe7ZLxHoSfDYM+O1CUma0j3jVSw424c92Puytvi37anuO1E6EwBEN5PdfU\nO1uoh0o2sy7/5D0A+EdJDmNSCTae208WMfx3fWkOtycWMovulDK1Bl4BghVQHgxB\nkudJhUEvAgMBAAECggEAB3Bz1u05vUTU84q/CwqkuoeE9HtgoQJFlLW5RcS0Arxb\nXmEaPlkSwRLLgE2p9WnLhHtLMmq1LOVOkX5mZaCsGT0XqTbJ1FMMu9m6Hj2GQjoY\nw5MRiHDFmAHxslJvFhuA3GFtjXX10+IQr7Seui9PouHleGuhXdmlKBtqKHgr3YEA\nxAkPoQl1Co/yafQ911RH13PM005UVVpCaD/2xwLnJPqrNxM3YktWNQKBAunDTdxo\nQ8JeCgTKNdWSJ03nBJ2u9wi6EGaZJfVG4uEF5B85lhEfo3O0kTh2B/3qdSl8Aknm\ngcOsoN1D8ivgld/IZebJ89NKgwSbiCP035cTYtS4jQKBgQDWsbb3016HE6AxUwGl\n/pmhGV4uWx0YjwgM2T30SXICNtQw7JWs9CXjoxD+ee3mYcB70Ag3+3g/CC82w/Lr\nz1sYNEx4HodvuIqo6XYYE5dg0QXkMehW+dxqx+VtCcwC0w7nGar9SfMtIwr8Wg/6\nqH5jcKxPaB2LMFtR/yCqBmbWowKBgQDxmCHT3RBE+VvmhAeM6i6EwLV1JS6+ZFf6\nOjDC3SRN2cMU/8wFEpZ12tDTTJnxrCKgWoZKUwdRm9upmGiMelq5M9mhJRPZeGrr\nlBs9I/vj2vJbu8QLCYVUC+JY8r3ezlnLp2Xr9dCebnECbgzQcAj+rq5tzedLhUbZ\nPsgC6HGwBQKBgDXjkauPEJETKgh3b1h9GY7IUU2NbTY24Kxo8xYYQVew734ARGmP\nNtt2mNNnQ4GqU6hARW/X3QzlPwSeFqF+AL2IkxEriI9QYO2Y/B16/Wo9zR7EMC90\ntBDRcBL4fI7Q71KurK67GyDfROimqpAeLutC4t1jotbHIoToZwiGZtXFAoGAHc/P\nBMy3kDtQ+s35/Ip9OQZqncz7yqSpMohxseoF69FeQD4cV9fmVx6sPBasvGSoVS82\neP9r3MclwPS8mfETNt1OEpN3spMoZm99OPsyvvgqheVSmKYRHMDmqmExysedzwKW\nEhrgJlysd0dLL4FTqtG1Vnlc/DWy+2XC2pECTl0CgYEAtzasBUsXp59IMhTSITKA\nZphiiBSTmEyhs6WptbbgOkAlLbT7YYhgpsCjKqFdLjGBZ+/Z9sICKlowgE8EueJ7\ntM0bnS/28Bgbd/QU1yJmglm3MbXzk9Q5tOE1Y3SNzwkT4kjp4e4AQvkVJt78ds+y\nPBvoTfmeV7/ov7/1OzRzHG0=\n-----END PRIVATE KEY-----" + var privateKey = Platform.getString("api.jwt.privatekey", "") privateKey = privateKey .replaceAll("(-+BEGIN RSA PRIVATE KEY-+\\r?\\n|-+END RSA PRIVATE KEY-+\\r?\\n?)", "") .replaceAll("(-+BEGIN PRIVATE KEY-+\\r?\\n|-+END PRIVATE KEY-+\\r?\\n?)", "") - - val keySpec = new PKCS8EncodedKeySpec(Base64Util.decode(privateKey.getBytes("UTF-8"), Base64Util.DEFAULT)) + .replaceAll("\\s", "") + val privateBytes = Base64.getDecoder.decode(privateKey) + val keySpec = new PKCS8EncodedKeySpec(privateBytes) val keyFactory = KeyFactory.getInstance("RSA") keyFactory.generatePrivate(keySpec) } diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/Base64Util.java b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/Base64Util.java deleted file mode 100644 index d7c253014..000000000 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/Base64Util.java +++ /dev/null @@ -1,722 +0,0 @@ -package org.sunbird.utils; - -import java.io.UnsupportedEncodingException; - -public class Base64Util { - - - /** - * Default values for encoder/decoder flags. - */ - public static final int DEFAULT = 0; - - /** - * Encoder flag bit to omit the padding '=' characters at the end - * of the output (if any). - */ - public static final int NO_PADDING = 1; - - /** - * Encoder flag bit to omit all line terminators (i.e., the output - * will be on one long line). - */ - public static final int NO_WRAP = 2; - - /** - * Encoder flag bit to indicate lines should be terminated with a - * CRLF pair instead of just an LF. Has no effect if {@code - * NO_WRAP} is specified as well. - */ - public static final int CRLF = 4; - - /** - * Encoder/decoder flag bit to indicate using the "URL and - * filename safe" variant of Base64 (see RFC 3548 section 4) where - * {@code -} and {@code _} are used in place of {@code +} and - * {@code /}. - */ - public static final int URL_SAFE = 8; - - /** - * Flag to pass to {Base64OutputStream} to indicate that it - * should not close the output stream it is wrapping when it - * itself is closed. - */ - public static final int NO_CLOSE = 16; - - // -------------------------------------------------------- - // shared code - // -------------------------------------------------------- - - private Base64Util() { - } // don't instantiate - - // -------------------------------------------------------- - // decoding - // -------------------------------------------------------- - - /** - * Decode the Base64-encoded data in input and return the data in - * a new byte array. - *

- *

The padding '=' characters at the end are considered optional, but - * if any are present, there must be the correct number of them. - * - * @param str the input String to decode, which is converted to - * bytes using the default charset - * @param flags controls certain features of the decoded output. - * Pass {@code DEFAULT} to decode standard Base64. - * @throws IllegalArgumentException if the input contains - * incorrect padding - */ - public static byte[] decode(String str, int flags) { - return decode(str.getBytes(), flags); - } - - /** - * Decode the Base64-encoded data in input and return the data in - * a new byte array. - *

- *

The padding '=' characters at the end are considered optional, but - * if any are present, there must be the correct number of them. - * - * @param input the input array to decode - * @param flags controls certain features of the decoded output. - * Pass {@code DEFAULT} to decode standard Base64. - * @throws IllegalArgumentException if the input contains - * incorrect padding - */ - public static byte[] decode(byte[] input, int flags) { - return decode(input, 0, input.length, flags); - } - - /** - * Decode the Base64-encoded data in input and return the data in - * a new byte array. - *

- *

The padding '=' characters at the end are considered optional, but - * if any are present, there must be the correct number of them. - * - * @param input the data to decode - * @param offset the position within the input array at which to start - * @param len the number of bytes of input to decode - * @param flags controls certain features of the decoded output. - * Pass {@code DEFAULT} to decode standard Base64. - * @throws IllegalArgumentException if the input contains - * incorrect padding - */ - public static byte[] decode(byte[] input, int offset, int len, int flags) { - // Allocate space for the most data the input could represent. - // (It could contain less if it contains whitespace, etc.) - Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]); - - if (!decoder.process(input, offset, len, true)) { - throw new IllegalArgumentException("bad base-64"); - } - - // Maybe we got lucky and allocated exactly enough output space. - if (decoder.op == decoder.output.length) { - return decoder.output; - } - - // Need to shorten the array, so allocate a new one of the - // right size and copy. - byte[] temp = new byte[decoder.op]; - System.arraycopy(decoder.output, 0, temp, 0, decoder.op); - return temp; - } - - /** - * Base64-encode the given data and return a newly allocated - * String with the result. - * - * @param input the data to encode - * @param flags controls certain features of the encoded output. - * Passing {@code DEFAULT} results in output that - * adheres to RFC 2045. - */ - public static String encodeToString(byte[] input, int flags) { - try { - return new String(encode(input, flags), "US-ASCII"); - } catch (UnsupportedEncodingException e) { - // US-ASCII is guaranteed to be available. - throw new AssertionError(e); - } - } - - // -------------------------------------------------------- - // encoding - // -------------------------------------------------------- - - /** - * Base64-encode the given data and return a newly allocated - * String with the result. - * - * @param input the data to encode - * @param offset the position within the input array at which to - * start - * @param len the number of bytes of input to encode - * @param flags controls certain features of the encoded output. - * Passing {@code DEFAULT} results in output that - * adheres to RFC 2045. - */ - public static String encodeToString(byte[] input, int offset, int len, int flags) { - try { - return new String(encode(input, offset, len, flags), "US-ASCII"); - } catch (UnsupportedEncodingException e) { - // US-ASCII is guaranteed to be available. - throw new AssertionError(e); - } - } - - /** - * Base64-encode the given data and return a newly allocated - * byte[] with the result. - * - * @param input the data to encode - * @param flags controls certain features of the encoded output. - * Passing {@code DEFAULT} results in output that - * adheres to RFC 2045. - */ - public static byte[] encode(byte[] input, int flags) { - return encode(input, 0, input.length, flags); - } - - /** - * Base64-encode the given data and return a newly allocated - * byte[] with the result. - * - * @param input the data to encode - * @param offset the position within the input array at which to - * start - * @param len the number of bytes of input to encode - * @param flags controls certain features of the encoded output. - * Passing {@code DEFAULT} results in output that - * adheres to RFC 2045. - */ - public static byte[] encode(byte[] input, int offset, int len, int flags) { - Encoder encoder = new Encoder(flags, null); - - // Compute the exact length of the array we will produce. - int output_len = len / 3 * 4; - - // Account for the tail of the data and the padding bytes, if any. - if (encoder.do_padding) { - if (len % 3 > 0) { - output_len += 4; - } - } else { - switch (len % 3) { - case 0: - break; - case 1: - output_len += 2; - break; - case 2: - output_len += 3; - break; - } - } - - // Account for the newlines, if any. - if (encoder.do_newline && len > 0) { - output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) * - (encoder.do_cr ? 2 : 1); - } - - encoder.output = new byte[output_len]; - encoder.process(input, offset, len, true); - - assert encoder.op == output_len; - - return encoder.output; - } - - /* package */ static abstract class Coder { - public byte[] output; - public int op; - - /** - * Encode/decode another block of input data. this.output is - * provided by the caller, and must be big enough to hold all - * the coded data. On exit, this.opwill be set to the length - * of the coded data. - * - * @param finish true if this is the final call to process for - * this object. Will finalize the coder state and - * include any final bytes in the output. - * @return true if the input so far is good; false if some - * error has been detected in the input stream.. - */ - public abstract boolean process(byte[] input, int offset, int len, boolean finish); - - /** - * @return the maximum number of bytes a call to process() - * could produce for the given number of input bytes. This may - * be an overestimate. - */ - public abstract int maxOutputSize(int len); - } - - /* package */ static class Decoder extends Coder { - /** - * Lookup table for turning bytes into their position in the - * Base64 alphabet. - */ - private static final int DECODE[] = { - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, - -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, - 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - }; - - /** - * Decode lookup table for the "web safe" variant (RFC 3548 - * sec. 4) where - and _ replace + and /. - */ - private static final int DECODE_WEBSAFE[] = { - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, - -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, - 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - }; - - /** - * Non-data values in the DECODE arrays. - */ - private static final int SKIP = -1; - private static final int EQUALS = -2; - final private int[] alphabet; - /** - * States 0-3 are reading through the next input tuple. - * State 4 is having read one '=' and expecting exactly - * one more. - * State 5 is expecting no more data or padding characters - * in the input. - * State 6 is the error state; an error has been detected - * in the input and no future input can "fix" it. - */ - private int state; // state number (0 to 6) - private int value; - - public Decoder(int flags, byte[] output) { - this.output = output; - - alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; - state = 0; - value = 0; - } - - /** - * @return an overestimate for the number of bytes {@code - * len} bytes could decode to. - */ - public int maxOutputSize(int len) { - return len * 3 / 4 + 10; - } - - /** - * Decode another block of input data. - * - * @return true if the state machine is still healthy. false if - * bad base-64 data has been detected in the input stream. - */ - public boolean process(byte[] input, int offset, int len, boolean finish) { - if (this.state == 6) return false; - - int p = offset; - len += offset; - - // Using local variables makes the decoder about 12% - // faster than if we manipulate the member variables in - // the loop. (Even alphabet makes a measurable - // difference, which is somewhat surprising to me since - // the member variable is final.) - int state = this.state; - int value = this.value; - int op = 0; - final byte[] output = this.output; - final int[] alphabet = this.alphabet; - - while (p < len) { - // Try the fast path: we're starting a new tuple and the - // next four bytes of the input stream are all data - // bytes. This corresponds to going through states - // 0-1-2-3-0. We expect to use this method for most of - // the data. - // - // If any of the next four bytes of input are non-data - // (whitespace, etc.), value will end up negative. (All - // the non-data values in decode are small negative - // numbers, so shifting any of them up and or'ing them - // together will result in a value with its top bit set.) - // - // You can remove this whole block and the output should - // be the same, just slower. - if (state == 0) { - while (p + 4 <= len && - (value = ((alphabet[input[p] & 0xff] << 18) | - (alphabet[input[p + 1] & 0xff] << 12) | - (alphabet[input[p + 2] & 0xff] << 6) | - (alphabet[input[p + 3] & 0xff]))) >= 0) { - output[op + 2] = (byte) value; - output[op + 1] = (byte) (value >> 8); - output[op] = (byte) (value >> 16); - op += 3; - p += 4; - } - if (p >= len) break; - } - - // The fast path isn't available -- either we've read a - // partial tuple, or the next four input bytes aren't all - // data, or whatever. Fall back to the slower state - // machine implementation. - - int d = alphabet[input[p++] & 0xff]; - - switch (state) { - case 0: - if (d >= 0) { - value = d; - ++state; - } else if (d != SKIP) { - this.state = 6; - return false; - } - break; - - case 1: - if (d >= 0) { - value = (value << 6) | d; - ++state; - } else if (d != SKIP) { - this.state = 6; - return false; - } - break; - - case 2: - if (d >= 0) { - value = (value << 6) | d; - ++state; - } else if (d == EQUALS) { - // Emit the last (partial) output tuple; - // expect exactly one more padding character. - output[op++] = (byte) (value >> 4); - state = 4; - } else if (d != SKIP) { - this.state = 6; - return false; - } - break; - - case 3: - if (d >= 0) { - // Emit the output triple and return to state 0. - value = (value << 6) | d; - output[op + 2] = (byte) value; - output[op + 1] = (byte) (value >> 8); - output[op] = (byte) (value >> 16); - op += 3; - state = 0; - } else if (d == EQUALS) { - // Emit the last (partial) output tuple; - // expect no further data or padding characters. - output[op + 1] = (byte) (value >> 2); - output[op] = (byte) (value >> 10); - op += 2; - state = 5; - } else if (d != SKIP) { - this.state = 6; - return false; - } - break; - - case 4: - if (d == EQUALS) { - ++state; - } else if (d != SKIP) { - this.state = 6; - return false; - } - break; - - case 5: - if (d != SKIP) { - this.state = 6; - return false; - } - break; - } - } - - if (!finish) { - // We're out of input, but a future call could provide - // more. - this.state = state; - this.value = value; - this.op = op; - return true; - } - - // Done reading input. Now figure out where we are left in - // the state machine and finish up. - - switch (state) { - case 0: - // Output length is a multiple of three. Fine. - break; - case 1: - // Read one extra input byte, which isn't enough to - // make another output byte. Illegal. - this.state = 6; - return false; - case 2: - // Read two extra input bytes, enough to emit 1 more - // output byte. Fine. - output[op++] = (byte) (value >> 4); - break; - case 3: - // Read three extra input bytes, enough to emit 2 more - // output bytes. Fine. - output[op++] = (byte) (value >> 10); - output[op++] = (byte) (value >> 2); - break; - case 4: - // Read one padding '=' when we expected 2. Illegal. - this.state = 6; - return false; - case 5: - // Read all the padding '='s we expected and no more. - // Fine. - break; - } - - this.state = state; - this.op = op; - return true; - } - } - - /* package */ static class Encoder extends Coder { - /** - * Emit a new line every this many output tuples. Corresponds to - * a 76-character line length (the maximum allowable according to - * RFC 2045). - */ - public static final int LINE_GROUPS = 19; - - /** - * Lookup table for turning Base64 alphabet positions (6 bits) - * into output bytes. - */ - private static final byte ENCODE[] = { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', - 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', - }; - - /** - * Lookup table for turning Base64 alphabet positions (6 bits) - * into output bytes. - */ - private static final byte ENCODE_WEBSAFE[] = { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', - 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', - }; - final public boolean do_padding; - final public boolean do_newline; - final public boolean do_cr; - final private byte[] tail; - final private byte[] alphabet; - /* package */ int tailLen; - private int count; - - public Encoder(int flags, byte[] output) { - this.output = output; - - do_padding = (flags & NO_PADDING) == 0; - do_newline = (flags & NO_WRAP) == 0; - do_cr = (flags & CRLF) != 0; - alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; - - tail = new byte[2]; - tailLen = 0; - - count = do_newline ? LINE_GROUPS : -1; - } - - /** - * @return an overestimate for the number of bytes {@code - * len} bytes could encode to. - */ - public int maxOutputSize(int len) { - return len * 8 / 5 + 10; - } - - public boolean process(byte[] input, int offset, int len, boolean finish) { - // Using local variables makes the encoder about 9% faster. - final byte[] alphabet = this.alphabet; - final byte[] output = this.output; - int op = 0; - int count = this.count; - - int p = offset; - len += offset; - int v = -1; - - // First we need to concatenate the tail of the previous call - // with any input bytes available now and see if we can empty - // the tail. - - switch (tailLen) { - case 0: - // There was no tail. - break; - - case 1: - if (p + 2 <= len) { - // A 1-byte tail with at least 2 bytes of - // input available now. - v = ((tail[0] & 0xff) << 16) | - ((input[p++] & 0xff) << 8) | - (input[p++] & 0xff); - tailLen = 0; - } - ; - break; - - case 2: - if (p + 1 <= len) { - // A 2-byte tail with at least 1 byte of input. - v = ((tail[0] & 0xff) << 16) | - ((tail[1] & 0xff) << 8) | - (input[p++] & 0xff); - tailLen = 0; - } - break; - } - - if (v != -1) { - output[op++] = alphabet[(v >> 18) & 0x3f]; - output[op++] = alphabet[(v >> 12) & 0x3f]; - output[op++] = alphabet[(v >> 6) & 0x3f]; - output[op++] = alphabet[v & 0x3f]; - if (--count == 0) { - if (do_cr) output[op++] = '\r'; - output[op++] = '\n'; - count = LINE_GROUPS; - } - } - - // At this point either there is no tail, or there are fewer - // than 3 bytes of input available. - - // The main loop, turning 3 input bytes into 4 output bytes on - // each iteration. - while (p + 3 <= len) { - v = ((input[p] & 0xff) << 16) | - ((input[p + 1] & 0xff) << 8) | - (input[p + 2] & 0xff); - output[op] = alphabet[(v >> 18) & 0x3f]; - output[op + 1] = alphabet[(v >> 12) & 0x3f]; - output[op + 2] = alphabet[(v >> 6) & 0x3f]; - output[op + 3] = alphabet[v & 0x3f]; - p += 3; - op += 4; - if (--count == 0) { - if (do_cr) output[op++] = '\r'; - output[op++] = '\n'; - count = LINE_GROUPS; - } - } - - if (finish) { - // Finish up the tail of the input. Note that we need to - // consume any bytes in tail before any bytes - // remaining in input; there should be at most two bytes - // total. - - if (p - tailLen == len - 1) { - int t = 0; - v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; - tailLen -= t; - output[op++] = alphabet[(v >> 6) & 0x3f]; - output[op++] = alphabet[v & 0x3f]; - if (do_padding) { - output[op++] = '='; - output[op++] = '='; - } - if (do_newline) { - if (do_cr) output[op++] = '\r'; - output[op++] = '\n'; - } - } else if (p - tailLen == len - 2) { - int t = 0; - v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | - (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); - tailLen -= t; - output[op++] = alphabet[(v >> 12) & 0x3f]; - output[op++] = alphabet[(v >> 6) & 0x3f]; - output[op++] = alphabet[v & 0x3f]; - if (do_padding) { - output[op++] = '='; - } - if (do_newline) { - if (do_cr) output[op++] = '\r'; - output[op++] = '\n'; - } - } else if (do_newline && op > 0 && count != LINE_GROUPS) { - if (do_cr) output[op++] = '\r'; - output[op++] = '\n'; - } - - assert tailLen == 0; - assert p == len; - } else { - // Save the leftovers in tail to be consumed on the next - // call to encodeInternal. - - if (p == len - 1) { - tail[tailLen++] = input[p]; - } else if (p == len - 2) { - tail[tailLen++] = input[p]; - tail[tailLen++] = input[p + 1]; - } - } - - this.op = op; - this.count = count; - - return true; - } - } -} diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JwtUtil.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JwtUtil.scala index a4f88d89a..2868eb29c 100644 --- a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JwtUtil.scala +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JwtUtil.scala @@ -35,11 +35,12 @@ object JwtUtils { payloadData.put("iat", System.currentTimeMillis / 1000) encodeToBase64Uri(JsonUtil.toJson(payloadData).getBytes) } - private def encodeToBase64Uri(data: Array[Byte]): String = { - Base64Util.encodeToString(data, 11) + Base64.getUrlEncoder.encodeToString(data) + } + def decodeFromBase64(data: String): Array[Byte] = { + Base64.getDecoder.decode(data) } - def verifyRS256Token(token: String, keyManager: KeyManager): (Boolean, Map[String, Any])= { val tokenElements = token.split("\\.") val header = tokenElements(0) @@ -58,10 +59,10 @@ object JwtUtils { else (isValid,new util.HashMap[String,Any]()) } - - def decodeFromBase64(data: String): Array[Byte] = { - Base64Util.decode(data, 11) - } +// +// def decodeFromBase64(data: String): Array[Byte] = { +// Base64Util.decode(data, 11) +// } def payload(encodedPayload: String): Map[String, Any] = { val decodedPayload = new String(Base64.getDecoder.decode(encodedPayload), StandardCharsets.UTF_8) From a03a0ebd9276c09c5e0158582bcbd12ce0408645 Mon Sep 17 00:00:00 2001 From: Dilavar Davood Date: Tue, 2 Apr 2024 13:02:23 +0530 Subject: [PATCH 33/33] removed hardcode api keys --- assessment-api/assessment-service/conf/application.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf index 85110e1cd..4ef2acbe1 100644 --- a/assessment-api/assessment-service/conf/application.conf +++ b/assessment-api/assessment-service/conf/application.conf @@ -458,5 +458,5 @@ api.jwt.keycount=3 api.jwt.basepath=../../keys/ question.list.search.editor.url="http://localhost:9000/question/v5/editor/list" useHardcodedKeys=true -api.jwt.publickey="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAypzjIoeWTbPJRYNlwhzv\nJ+6q9vXk9HJHvubsMJhdpXX77eEQojtgAkrk3vv5edyhJHs/XY69OJMu4o8qHyhY\nSsSiw0TIuPPIQ3+moZB+yY6MKY7xYiHbKp9xeB1XsFt38H+HtOGX32Q5bL/4CvDS\nHUq7bKoG5wg5dyPkMwQRU/F4T3z9fSnKuRNjsb4OkyyYglJ6tn7uWp+RjPzXXLnB\nnu2S8R6Enw2DPjtQlJmtI941UsONuHPdj7srb4t+2p7jtROhMARDeT3X1DtbqIdK\nNrMu/+Q9APhHSQ5jUgk2nttPFjH8d31pDrcnFjKL7pQytQZeAYIVUB4MQZLnSYVB\nLwIDAQAB\n-----END PUBLIC KEY-----" -api.jwt.privatekey="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKnOMih5ZNs8lF\ng2XCHO8n7qr29eT0cke+5uwwmF2ldfvt4RCiO2ACSuTe+/l53KEkez9djr04ky7i\njyofKFhKxKLDRMi488hDf6ahkH7JjowpjvFiIdsqn3F4HVewW3fwf4e04ZffZDls\nv/gK8NIdSrtsqgbnCDl3I+QzBBFT8XhPfP19Kcq5E2Oxvg6TLJiCUnq2fu5an5GM\n/NdcucGe7ZLxHoSfDYM+O1CUma0j3jVSw424c92Puytvi37anuO1E6EwBEN5PdfU\nO1uoh0o2sy7/5D0A+EdJDmNSCTae208WMfx3fWkOtycWMovulDK1Bl4BghVQHgxB\nkudJhUEvAgMBAAECggEAB3Bz1u05vUTU84q/CwqkuoeE9HtgoQJFlLW5RcS0Arxb\nXmEaPlkSwRLLgE2p9WnLhHtLMmq1LOVOkX5mZaCsGT0XqTbJ1FMMu9m6Hj2GQjoY\nw5MRiHDFmAHxslJvFhuA3GFtjXX10+IQr7Seui9PouHleGuhXdmlKBtqKHgr3YEA\nxAkPoQl1Co/yafQ911RH13PM005UVVpCaD/2xwLnJPqrNxM3YktWNQKBAunDTdxo\nQ8JeCgTKNdWSJ03nBJ2u9wi6EGaZJfVG4uEF5B85lhEfo3O0kTh2B/3qdSl8Aknm\ngcOsoN1D8ivgld/IZebJ89NKgwSbiCP035cTYtS4jQKBgQDWsbb3016HE6AxUwGl\n/pmhGV4uWx0YjwgM2T30SXICNtQw7JWs9CXjoxD+ee3mYcB70Ag3+3g/CC82w/Lr\nz1sYNEx4HodvuIqo6XYYE5dg0QXkMehW+dxqx+VtCcwC0w7nGar9SfMtIwr8Wg/6\nqH5jcKxPaB2LMFtR/yCqBmbWowKBgQDxmCHT3RBE+VvmhAeM6i6EwLV1JS6+ZFf6\nOjDC3SRN2cMU/8wFEpZ12tDTTJnxrCKgWoZKUwdRm9upmGiMelq5M9mhJRPZeGrr\nlBs9I/vj2vJbu8QLCYVUC+JY8r3ezlnLp2Xr9dCebnECbgzQcAj+rq5tzedLhUbZ\nPsgC6HGwBQKBgDXjkauPEJETKgh3b1h9GY7IUU2NbTY24Kxo8xYYQVew734ARGmP\nNtt2mNNnQ4GqU6hARW/X3QzlPwSeFqF+AL2IkxEriI9QYO2Y/B16/Wo9zR7EMC90\ntBDRcBL4fI7Q71KurK67GyDfROimqpAeLutC4t1jotbHIoToZwiGZtXFAoGAHc/P\nBMy3kDtQ+s35/Ip9OQZqncz7yqSpMohxseoF69FeQD4cV9fmVx6sPBasvGSoVS82\neP9r3MclwPS8mfETNt1OEpN3spMoZm99OPsyvvgqheVSmKYRHMDmqmExysedzwKW\nEhrgJlysd0dLL4FTqtG1Vnlc/DWy+2XC2pECTl0CgYEAtzasBUsXp59IMhTSITKA\nZphiiBSTmEyhs6WptbbgOkAlLbT7YYhgpsCjKqFdLjGBZ+/Z9sICKlowgE8EueJ7\ntM0bnS/28Bgbd/QU1yJmglm3MbXzk9Q5tOE1Y3SNzwkT4kjp4e4AQvkVJt78ds+y\nPBvoTfmeV7/ov7/1OzRzHG0=\n-----END PRIVATE KEY-----" \ No newline at end of file +api.jwt.publickey="" +api.jwt.privatekey="" \ No newline at end of file