diff --git a/.circleci/config.yml b/.circleci/config.yml index a4ff9a685..2ef79658b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,17 +1,44 @@ version: 2.1 +executorType: machine jobs: - kp-build: - machine: true + build: + docker: + - image: circleci/openjdk:14-jdk-buster-node-browsers-legacy steps: - checkout - restore_cache: - key: kp-dependency-cache-{{ checksum "pom.xml" }} + key: kp-dependency-build-cache-{{ checksum "pom.xml" }} - run: - name: Setup VM and Build + name: Run build + command: | + mvn clean install -DskipTests + - save_cache: + paths: + - ~/.m2 + key: kp-dependency-build-cache-{{ checksum "pom.xml" }} + + unit-tests: + docker: + - image: circleci/openjdk:14-jdk-buster-node-browsers-legacy + - image: circleci/redis:latest + parallelism: 1 + steps: + - checkout + - restore_cache: + key: kp-dependency-test-cache-{{ checksum "pom.xml" }} + - run: + name: Setup environment and run tests command: bash vmsetup.sh + - save_cache: + paths: + - ~/.m2 + key: kp-dependency-test-cache-{{ checksum "pom.xml" }} workflows: version: 2.1 - workflow: - jobs: - - kp-build + build-then-test: + jobs: + - build + - unit-tests: + requires: + - build diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f1826648e..920b32a49 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,7 +18,7 @@ Please describe the tests that you ran to verify your changes in the below check **Test Configuration**: * Software versions: Java 11, scala-2.11, play-2.7.2 -* Hardware versions: +* Hardware versions: 2 CPU/ 4GB RAM ### Checklist: @@ -30,3 +30,4 @@ Please describe the tests that you ran to verify your changes in the below check - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules + diff --git a/.github/pull_request_template.md.yaml b/.github/pull_request_template.md.yaml new file mode 100644 index 000000000..f1826648e --- /dev/null +++ b/.github/pull_request_template.md.yaml @@ -0,0 +1,32 @@ +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +### Type of change + +Please choose appropriate options. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +### How Has This Been Tested? + +Please describe the tests that you ran to verify your changes in the below checkboxes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Ran Test A +- [ ] Ran Test B + +**Test Configuration**: +* Software versions: Java 11, scala-2.11, play-2.7.2 +* Hardware versions: + +### Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b31b7eb74..000000000 --- a/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM openjdk:8-jre-alpine -RUN apk update \ - && apk add unzip \ - && apk add curl \ - && adduser -u 1001 -h /home/sunbird/ -D sunbird \ - && mkdir -p /home/sunbird -RUN chown -R sunbird:sunbird /home/sunbird -USER sunbird -COPY ./learning-api/content-service/target/content-service-1.0-SNAPSHOT-dist.zip /home/sunbird/ -RUN unzip /home/sunbird/content-service-1.0-SNAPSHOT-dist.zip -d /home/sunbird/ -RUN rm /home/sunbird/content-service-1.0-SNAPSHOT-dist.zip -COPY --chown=sunbird ./schemas /home/sunbird/content-service-1.0-SNAPSHOT/schemas -WORKDIR /home/sunbird/ -CMD java -cp '/home/sunbird/content-service-1.0-SNAPSHOT/lib/*' -Dconfig.file=/home/sunbird/content-service-1.0-SNAPSHOT/config/application.conf play.core.server.ProdServerStart /home/sunbird/content-service-1.0-SNAPSHOT diff --git a/assessment-api/assessment-actors/pom.xml b/assessment-api/assessment-actors/pom.xml new file mode 100644 index 000000000..d56dd0a67 --- /dev/null +++ b/assessment-api/assessment-actors/pom.xml @@ -0,0 +1,123 @@ + + + + assessment-api + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + assessment-actors + + + + org.scala-lang + scala-library + ${scala.version} + + + javax.inject + javax.inject + 1 + + + org.sunbird + actor-core + 1.0-SNAPSHOT + + + org.sunbird + graph-engine_2.11 + 1.0-SNAPSHOT + jar + + + org.sunbird + qs-hierarchy-manager + 1.0-SNAPSHOT + + + org.sunbird + import-manager + 1.0-SNAPSHOT + jar + + + org.scalatest + scalatest_${scala.maj.version} + ${scalatest.version} + test + + + org.scalamock + scalamock_${scala.maj.version} + 4.4.0 + test + + + com.typesafe.akka + akka-testkit_${scala.maj.version} + 2.5.22 + test + + + + + src/main/scala + src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + ${scala.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + + test + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + + + \ No newline at end of file diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/HealthActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/HealthActor.scala new file mode 100644 index 000000000..3072bb928 --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/HealthActor.scala @@ -0,0 +1,20 @@ +package org.sunbird.actors + +import javax.inject.Inject +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.health.HealthCheckManager + +import scala.concurrent.{ExecutionContext, Future} + + +class HealthActor @Inject() (implicit oec: OntologyEngineContext) extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + + @throws[Throwable] + override def onReceive(request: Request): Future[Response] = { + HealthCheckManager.checkAllSystemHealth() + } +} diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/ItemSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/ItemSetActor.scala new file mode 100644 index 000000000..524a0604c --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/ItemSetActor.scala @@ -0,0 +1,98 @@ +package org.sunbird.actors + +import java.util + +import javax.inject.Inject +import org.apache.commons.collections4.CollectionUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Relation +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.parseq.Task + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters.seqAsJavaListConverter +import scala.concurrent.{ExecutionContext, Future} + +class ItemSetActor @Inject() (implicit oec: OntologyEngineContext) extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = request.getOperation match { + case "createItemSet" => create(request) + case "readItemSet" => read(request) + case "updateItemSet" => update(request) + case "reviewItemSet" => review(request) + case "retireItemSet" => retire(request) + case _ => ERROR(request.getOperation) + } + + + def create(request: Request): Future[Response] = DataNode.create(request).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier) + }) + + def read(request: Request): Future[Response] = { + val fields = request.getRequest.getOrDefault("fields", "").asInstanceOf[String] + .split(",").filter((field: String) => StringUtils.isNotBlank(field) && !StringUtils.equalsIgnoreCase(field, "null")).toList.asJava + request.getRequest.put("fields", fields) + DataNode.read(request).map(node => { + val metadata = NodeUtil.serialize(node, fields, request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + metadata.remove("versionKey") + ResponseHandler.OK.put("itemset", metadata) + }) + } + + def update(request: Request): Future[Response] = DataNode.update(request).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier) + }) + + def review(request: Request): Future[Response] = { + val identifier = request.getContext.get("identifier").asInstanceOf[String] + var flag = false + val readReq = new Request(); + val reqContext = request.getContext + readReq.setContext(reqContext) + readReq.put("identifier", identifier) + readReq.put("fields", new util.ArrayList[String]) + val updateReq = new Request() + updateReq.setContext(reqContext) + DataNode.read(readReq).map(node => { + if (CollectionUtils.isNotEmpty(node.getOutRelations)) { + //process relations with AssessmentItem + val itemRels: util.List[Relation] = node.getOutRelations.filter((rel: Relation) => StringUtils.equalsAnyIgnoreCase("AssessmentItem", rel.getEndNodeObjectType)).filterNot((reln: Relation) => StringUtils.equalsAnyIgnoreCase("Retired", reln.getEndNodeMetadata.get("status").toString)) + val draftRelIds: List[String] = itemRels.filter((rel: Relation) => StringUtils.equalsAnyIgnoreCase("Draft", rel.getEndNodeMetadata.get("status").toString)).map(rel => rel.getEndNodeId).toList + if (CollectionUtils.isNotEmpty(draftRelIds)) { + updateReq.put("identifiers", draftRelIds.asJava) + updateReq.put("metadata", new util.HashMap[String, AnyRef]() {{put("status", "Review")}}) + flag = true + } + val newRels: util.List[util.HashMap[String, AnyRef]] = itemRels.sortBy((rel: Relation) => rel.getMetadata.get("IL_SEQUENCE_INDEX").asInstanceOf[Long])(Ordering.Long).map(rel => { + new util.HashMap[String, AnyRef]() {{put("identifier", rel.getEndNodeId);}}}).toList + request.put("items", newRels); + } + request.put("status", "Review") + val func = flag match { + case true => DataNode.bulkUpdate(updateReq).map(f => ResponseHandler.OK()) + case false => Future(ResponseHandler.OK()) + } + val futureList = Task.parallel[Response](func, + DataNode.update(request).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier) + })) + futureList + }).flatMap(f => f).map(f => f.get(1)) + } + + def retire(request: Request): Future[Response] = { + request.put("status", "Retired") + DataNode.update(request).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier) + }) + } + + +} diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionActor.scala new file mode 100644 index 000000000..478060d2e --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionActor.scala @@ -0,0 +1,115 @@ +package org.sunbird.actors + +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.{DateUtils, Platform} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.nodes.DataNode +import org.sunbird.managers.AssessmentManager +import org.sunbird.utils.RequestUtil +import java.util + +import javax.inject.Inject +import org.apache.commons.lang3.StringUtils +import org.sunbird.graph.utils.NodeUtil + +import scala.collection.JavaConverters +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +class QuestionActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + + private lazy val importConfig = getImportConfig() + private lazy val importMgr = new ImportManager(importConfig) + + override def onReceive(request: Request): Future[Response] = request.getOperation match { + case "createQuestion" => AssessmentManager.create(request, "ERR_QUESTION_CREATE") + case "readQuestion" => AssessmentManager.read(request, "question") + case "updateQuestion" => update(request) + case "reviewQuestion" => review(request) + case "publishQuestion" => publish(request) + case "retireQuestion" => retire(request) + case "importQuestion" => importQuestion(request) + case "systemUpdateQuestion" => systemUpdate(request) + case "listQuestions" => listQuestions(request) + case _ => ERROR(request.getOperation) + } + + def update(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + request.getRequest.put("identifier", request.getContext.get("identifier")) + AssessmentManager.getValidatedNodeForUpdate(request, "ERR_QUESTION_UPDATE").flatMap(_ => AssessmentManager.updateNode(request)) + } + + def review(request: Request): Future[Response] = { + request.getRequest.put("identifier", request.getContext.get("identifier")) + AssessmentManager.getValidatedNodeForReview(request, "ERR_QUESTION_REVIEW").flatMap(node => { + val updateRequest = new Request(request) + updateRequest.getContext.put("identifier", request.get("identifier")) + updateRequest.putAll(Map("versionKey" -> node.getMetadata.get("versionKey"), "prevState" -> "Draft", "status" -> "Review", "lastStatusChangedOn" -> DateUtils.formatCurrentDate).asJava) + AssessmentManager.updateNode(updateRequest) + }) + } + + def publish(request: Request): Future[Response] = { + request.getRequest.put("identifier", request.getContext.get("identifier")) + AssessmentManager.getValidatedNodeForPublish(request, "ERR_QUESTION_PUBLISH").map(node => { + AssessmentManager.pushInstructionEvent(node.getIdentifier, node) + ResponseHandler.OK.putAll(Map[String, AnyRef]("identifier" -> node.getIdentifier.replace(".img", ""), "message" -> "Question is successfully sent for Publish").asJava) + }) + } + + def retire(request: Request): Future[Response] = { + request.getRequest.put("identifier", request.getContext.get("identifier")) + AssessmentManager.getValidatedNodeForRetire(request, "ERR_QUESTION_RETIRE").flatMap(node => { + val updateRequest = new Request(request) + updateRequest.put("identifiers", java.util.Arrays.asList(request.get("identifier").asInstanceOf[String], request.get("identifier").asInstanceOf[String] + ".img")) + val updateMetadata: util.Map[String, AnyRef] = Map[String, AnyRef]("status" -> "Retired", "lastStatusChangedOn" -> DateUtils.formatCurrentDate).asJava + updateRequest.put("metadata", updateMetadata) + DataNode.bulkUpdate(updateRequest).map(_ => { + ResponseHandler.OK.putAll(Map("identifier" -> node.getIdentifier.replace(".img", ""), "versionKey" -> node.getMetadata.get("versionKey")).asJava) + }) + }) + } + + def importQuestion(request: Request): Future[Response] = importMgr.importObject(request) + + def getImportConfig(): ImportConfig = { + val requiredProps = Platform.getStringList("import.required_props.question", java.util.Arrays.asList("name", "code", "mimeType", "framework")).asScala.toList + val validStages = Platform.getStringList("import.valid_stages.question", java.util.Arrays.asList("create", "upload", "review", "publish")).asScala.toList + val propsToRemove = Platform.getStringList("import.remove_props.question", java.util.Arrays.asList()).asScala.toList + val topicName = Platform.config.getString("import.output_topic_name") + val reqLimit = Platform.getInteger("import.request_size_limit", 200) + ImportConfig(topicName, reqLimit, requiredProps, validStages, propsToRemove) + } + + def systemUpdate(request: Request): Future[Response] = { + val identifier = request.getContext.get("identifier").asInstanceOf[String] + RequestUtil.validateRequest(request) + val readReq = new Request(request) + val identifiers = new util.ArrayList[String](){{ + add(identifier) + if (!identifier.endsWith(".img")) + add(identifier.concat(".img")) + }} + readReq.put("identifiers", identifiers) + DataNode.list(readReq).flatMap(response => { + DataNode.systemUpdate(request, response,"", None) + }).map(node => ResponseHandler.OK.put("identifier", identifier).put("status", "success")) + } + + def listQuestions(request: Request): Future[Response] = { + 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 => { + 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) + }) + } +} diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionSetActor.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionSetActor.scala new file mode 100644 index 000000000..b48d8e64e --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/actors/QuestionSetActor.scala @@ -0,0 +1,155 @@ +package org.sunbird.actors + +import java.util + +import javax.inject.Inject +import org.apache.commons.collections4.CollectionUtils +import org.sunbird.`object`.importer.{ImportConfig, ImportManager} +import org.sunbird.actor.core.BaseActor +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.{DateUtils, Platform} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.dac.model.Node +import org.sunbird.managers.HierarchyManager.hierarchyPrefix +import org.sunbird.managers.{AssessmentManager, HierarchyManager, UpdateHierarchyManager} +import org.sunbird.utils.RequestUtil + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +class QuestionSetActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + private lazy val importConfig = getImportConfig() + private lazy val importMgr = new ImportManager(importConfig) + + override def onReceive(request: Request): Future[Response] = request.getOperation match { + case "createQuestionSet" => AssessmentManager.create(request, "ERR_QUESTION_SET_CREATE") + case "readQuestionSet" => AssessmentManager.read(request, "questionset") + case "updateQuestionSet" => update(request) + case "reviewQuestionSet" => review(request) + case "publishQuestionSet" => publish(request) + case "retireQuestionSet" => retire(request) + case "addQuestion" => HierarchyManager.addLeafNodesToHierarchy(request) + case "removeQuestion" => HierarchyManager.removeLeafNodesFromHierarchy(request) + case "updateHierarchy" => UpdateHierarchyManager.updateHierarchy(request) + case "getHierarchy" => HierarchyManager.getHierarchy(request) + case "rejectQuestionSet" => reject(request) + case "importQuestionSet" => importQuestionSet(request) + case "systemUpdateQuestionSet" => systemUpdate(request) + case _ => ERROR(request.getOperation) + } + + def update(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + request.getRequest.put("identifier", request.getContext.get("identifier")) + AssessmentManager.getValidatedNodeForUpdate(request, "ERR_QUESTION_SET_UPDATE").flatMap(_ => AssessmentManager.updateNode(request)) + } + + def review(request: Request): Future[Response] = { + request.getRequest.put("identifier", request.getContext.get("identifier")) + request.getRequest.put("mode", "edit") + AssessmentManager.getValidatedNodeForReview(request, "ERR_QUESTION_SET_REVIEW").flatMap(node => { + AssessmentManager.getQuestionSetHierarchy(request, node).flatMap(hierarchyString => { + AssessmentManager.validateQuestionSetHierarchy(hierarchyString.asInstanceOf[String]) + val (updatedHierarchy, nodeIds) = AssessmentManager.updateHierarchy(hierarchyString.asInstanceOf[String], "Review") + val updateReq = new Request(request) + val date = DateUtils.formatCurrentDate + updateReq.putAll(Map("identifiers" -> nodeIds, "metadata" -> Map("status" -> "Review", "prevState" -> node.getMetadata.get("status"), "lastStatusChangedOn" -> date, "lastUpdatedOn" -> date).asJava).asJava) + updateHierarchyNodes(updateReq, node, Map("status" -> "Review", "hierarchy" -> updatedHierarchy), nodeIds) + }) + }) + } + + def publish(request: Request): Future[Response] = { + request.getRequest.put("identifier", request.getContext.get("identifier")) + AssessmentManager.getValidatedNodeForPublish(request, "ERR_QUESTION_SET_PUBLISH").flatMap(node => { + AssessmentManager.getQuestionSetHierarchy(request, node).map(hierarchyString => { + AssessmentManager.validateQuestionSetHierarchy(hierarchyString.asInstanceOf[String]) + AssessmentManager.pushInstructionEvent(node.getIdentifier, node) + ResponseHandler.OK.putAll(Map[String, AnyRef]("identifier" -> node.getIdentifier.replace(".img", ""), "message" -> "Question is successfully sent for Publish").asJava) + }) + }) + } + + def retire(request: Request): Future[Response] = { + request.getRequest.put("identifier", request.getContext.get("identifier")) + AssessmentManager.getValidatedNodeForRetire(request, "ERR_QUESTION_SET_RETIRE").flatMap(node => { + val updateRequest = new Request(request) + updateRequest.put("identifiers", java.util.Arrays.asList(request.get("identifier").asInstanceOf[String], request.get("identifier").asInstanceOf[String] + ".img")) + val updateMetadata: util.Map[String, AnyRef] = Map("prevState" -> node.getMetadata.get("status"), "status" -> "Retired", "lastStatusChangedOn" -> DateUtils.formatCurrentDate, "lastUpdatedOn" -> DateUtils.formatCurrentDate).asJava + updateRequest.put("metadata", updateMetadata) + DataNode.bulkUpdate(updateRequest).map(_ => { + ResponseHandler.OK.putAll(Map("identifier" -> node.getIdentifier.replace(".img", ""), "versionKey" -> node.getMetadata.get("versionKey")).asJava) + }) + }) + } + + def reject(request: Request): Future[Response] = { + request.getRequest.put("identifier", request.getContext.get("identifier")) + request.getRequest.put("mode", "edit") + AssessmentManager.getValidateNodeForReject(request, "ERR_QUESTION_SET_REJECT").flatMap(node => { + AssessmentManager.getQuestionSetHierarchy(request, node).flatMap(hierarchyString => { + AssessmentManager.validateQuestionSetHierarchy(hierarchyString.asInstanceOf[String]) + val (updatedHierarchy, nodeIds) = AssessmentManager.updateHierarchy(hierarchyString.asInstanceOf[String], "Draft") + val updateReq = new Request(request) + val date = DateUtils.formatCurrentDate + updateReq.putAll(Map("identifiers" -> nodeIds, "metadata" -> Map("status" -> "Draft", "prevState" -> node.getMetadata.get("status"), "lastStatusChangedOn" -> date, "lastUpdatedOn" -> date).asJava).asJava) + updateHierarchyNodes(updateReq, node, Map("status" -> "Draft", "hierarchy" -> updatedHierarchy), nodeIds) + }) + }) + } + + def updateHierarchyNodes(request: Request, node: Node, metadata: Map[String, AnyRef], nodeIds: util.List[String]): Future[Response] = { + if (CollectionUtils.isNotEmpty(nodeIds)) { + DataNode.bulkUpdate(request).flatMap(_ => { + updateNode(request, node, metadata) + }) + } else { + updateNode(request, node, metadata) + } + } + + def updateNode(request: Request, node: Node, metadata: Map[String, AnyRef]): Future[Response] = { + val updateRequest = new Request(request) + val date = DateUtils.formatCurrentDate + val fMeta: Map[String, AnyRef] = Map("versionKey" -> node.getMetadata.get("versionKey"), "prevState" -> node.getMetadata.get("status"), "lastStatusChangedOn" -> date, "lastUpdatedOn" -> date) ++ metadata + updateRequest.getContext.put("identifier", request.getContext.get("identifier")) + updateRequest.putAll(fMeta.asJava) + DataNode.update(updateRequest).map(_ => { + ResponseHandler.OK.putAll(Map("identifier" -> node.getIdentifier.replace(".img", ""), "versionKey" -> node.getMetadata.get("versionKey")).asJava) + }) + } + + def importQuestionSet(request: Request): Future[Response] = importMgr.importObject(request) + + def getImportConfig(): ImportConfig = { + val requiredProps = Platform.getStringList("import.required_props.questionset", java.util.Arrays.asList("name", "code", "mimeType", "framework")).asScala.toList + val validStages = Platform.getStringList("import.valid_stages.questionset", java.util.Arrays.asList("create", "upload", "review", "publish")).asScala.toList + val propsToRemove = Platform.getStringList("import.remove_props.questionset", java.util.Arrays.asList()).asScala.toList + val topicName = Platform.config.getString("import.output_topic_name") + val reqLimit = Platform.getInteger("import.request_size_limit", 200) + ImportConfig(topicName, reqLimit, requiredProps, validStages, propsToRemove) + } + + def systemUpdate(request: Request): Future[Response] = { + val identifier = request.getContext.get("identifier").asInstanceOf[String] + RequestUtil.validateRequest(request) + if(Platform.getBoolean("questionset.cache.enable", false)) + RedisCache.delete(hierarchyPrefix + identifier) + + val readReq = new Request(request) + val identifiers = new util.ArrayList[String](){{ + add(identifier) + if (!identifier.endsWith(".img")) + add(identifier.concat(".img")) + }} + readReq.put("identifiers", identifiers) + DataNode.list(readReq).flatMap(response => { + DataNode.systemUpdate(request, response,"questionSet", Some(HierarchyManager.getHierarchy)) + }).map(node => ResponseHandler.OK.put("identifier", identifier).put("status", "success")) + } + +} diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/managers/AssessmentManager.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/managers/AssessmentManager.scala new file mode 100644 index 000000000..97386a152 --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/managers/AssessmentManager.scala @@ -0,0 +1,200 @@ +package org.sunbird.managers + +import java.util + +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.{DateUtils, JsonUtils, Platform} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResourceNotFoundException, ServerException} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.{Node, Relation} +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.telemetry.logger.TelemetryManager +import org.sunbird.telemetry.util.LogTelemetryEventUtil + +import scala.concurrent.{ExecutionContext, Future} +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters +import scala.collection.JavaConverters._ + +object AssessmentManager { + + val skipValidation: Boolean = Platform.getBoolean("assessment.skip.validation", true) + + def create(request: Request, errCode: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val visibility: String = request.getRequest.getOrDefault("visibility", "").asInstanceOf[String] + if (StringUtils.isNotBlank(visibility) && StringUtils.equalsIgnoreCase(visibility, "Parent")) + throw new ClientException(errCode, "Visibility cannot be Parent!") + DataNode.create(request).map(node => { + val response = ResponseHandler.OK + response.putAll(Map("identifier" -> node.getIdentifier, "versionKey" -> node.getMetadata.get("versionKey")).asJava) + response + }) + } + + def read(request: Request, resName: String)(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 + request.getRequest.put("fields", fields) + DataNode.read(request).map(node => { + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, fields, node.getObjectType.toLowerCase.replace("Image", ""), request.getContext.get("version").asInstanceOf[String]) + metadata.put("identifier", node.getIdentifier.replace(".img", "")) + ResponseHandler.OK.put(resName, metadata) + }) + } + + def updateNode(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + DataNode.update(request).map(node => { + ResponseHandler.OK.putAll(Map("identifier" -> node.getIdentifier.replace(".img", ""), "versionKey" -> node.getMetadata.get("versionKey")).asJava) + }) + } + + def getValidatedNodeForUpdate(request: Request, errCode: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + DataNode.read(request).map(node => { + if (StringUtils.equalsIgnoreCase(node.getMetadata.getOrDefault("visibility", "").asInstanceOf[String], "Parent")) + throw new ClientException(errCode, node.getMetadata.getOrDefault("objectType", "").asInstanceOf[String].replace("Image", "") + " with visibility Parent, can't be updated individually.") + node + }) + } + + def getValidatedNodeForReview(request: Request, errCode: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + request.put("mode", "edit") + DataNode.read(request).map(node => { + 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 review individually.") + if (!StringUtils.equalsAnyIgnoreCase(node.getMetadata.getOrDefault("status", "").asInstanceOf[String], "Draft")) + throw new ClientException(errCode, s"${node.getObjectType.replace("Image", "")} with status other than Draft can't be sent for review.") + node + }) + } + + def getValidatedNodeForPublish(request: Request, errCode: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + request.put("mode", "edit") + DataNode.read(request).map(node => { + 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 publish individually.") + if (StringUtils.equalsAnyIgnoreCase(node.getMetadata.getOrDefault("status", "").asInstanceOf[String], "Processing")) + throw new ClientException(errCode, s"${node.getObjectType.replace("Image", "")} having Processing status can't be sent for publish.") + node + }) + } + + def getValidatedNodeForRetire(request: Request, errCode: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + DataNode.read(request).map(node => { + if (StringUtils.equalsIgnoreCase("Retired", node.getMetadata.get("status").asInstanceOf[String])) + throw new ClientException(errCode, s"${node.getObjectType.replace("Image", "")} with identifier : ${node.getIdentifier} is already Retired.") + node + }) + } + + def getValidateNodeForReject(request: Request, errCode: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + request.put("mode", "edit") + DataNode.read(request).map(node => { + 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])) + throw new ClientException(errCode, s"${node.getObjectType.replace("Image", "")} is not in 'Review' state for identifier: " + node.getIdentifier) + node + }) + } + + def getValidatedQuestionSet(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + request.put("mode", "edit") + DataNode.read(request).map(node => { + if (!StringUtils.equalsIgnoreCase("QuestionSet", node.getObjectType)) + throw new ClientException("ERR_QUESTION_SET_ADD", "Node with Identifier " + node.getIdentifier + " is not a Question Set") + node + }) + } + + def validateQuestionSetHierarchy(hierarchyString: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Unit = { + if (!skipValidation) { + val hierarchy = if (!hierarchyString.asInstanceOf[String].isEmpty) { + JsonUtils.deserialize(hierarchyString.asInstanceOf[String], classOf[java.util.Map[String, AnyRef]]) + } else + new java.util.HashMap[String, AnyRef]() + val children = hierarchy.getOrDefault("children", new util.ArrayList[java.util.Map[String, AnyRef]]).asInstanceOf[util.List[java.util.Map[String, AnyRef]]] + validateChildrenRecursive(children) + } + } + + def getQuestionSetHierarchy(request: Request, rootNode: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Any] = { + oec.graphService.readExternalProps(request, List("hierarchy")).flatMap(response => { + if (ResponseHandler.checkError(response) && ResponseHandler.isResponseNotFoundError(response)) { + if (StringUtils.equalsIgnoreCase("Live", rootNode.getMetadata.get("status").asInstanceOf[String])) + throw new ServerException("ERR_QUESTION_SET_REVIEW", "No hierarchy is present in cassandra for identifier:" + rootNode.getIdentifier) + request.put("identifier", if (!rootNode.getIdentifier.endsWith(".img")) rootNode.getIdentifier + ".img" else rootNode.getIdentifier) + oec.graphService.readExternalProps(request, List("hierarchy")).map(resp => { + resp.getResult.toMap.getOrElse("hierarchy", "{}").asInstanceOf[String] + }) recover { case e: ResourceNotFoundException => TelemetryManager.log("No hierarchy is present in cassandra for identifier:" + request.get("identifier")) } + } else Future(response.getResult.toMap.getOrElse("hierarchy", "{}").asInstanceOf[String]) + }) + } + + private def validateChildrenRecursive(children: util.List[util.Map[String, AnyRef]]): Unit = { + children.toList.foreach(content => { + if (!StringUtils.equalsAnyIgnoreCase(content.getOrDefault("visibility", "").asInstanceOf[String], "Parent") + && !StringUtils.equalsIgnoreCase(content.getOrDefault("status", "").asInstanceOf[String], "Live")) + throw new ClientException("ERR_QUESTION_SET", "Content with identifier: " + content.get("identifier") + "is not Live. Please Publish it.") + validateChildrenRecursive(content.getOrDefault("children", new util.ArrayList[Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]]) + }) + } + + def getChildIdsFromRelation(node: Node): (List[String], List[String]) = { + val outRelations: List[Relation] = if (node.getOutRelations != null) node.getOutRelations.asScala.toList else List[Relation]() + val visibilityIdMap: Map[String, List[String]] = outRelations + .groupBy(_.getEndNodeMetadata.get("visibility").asInstanceOf[String]) + .mapValues(_.map(_.getEndNodeId).toList) + (visibilityIdMap.getOrDefault("Default", List()), visibilityIdMap.getOrDefault("Parent", List())) + } + + def updateHierarchy(hierarchyString: String, status: String): (java.util.Map[String, AnyRef], java.util.List[String]) = { + val hierarchy = if (!hierarchyString.asInstanceOf[String].isEmpty) { + JsonUtils.deserialize(hierarchyString.asInstanceOf[String], classOf[java.util.Map[String, AnyRef]]) + } else + new java.util.HashMap[String, AnyRef]() + val children = hierarchy.getOrDefault("children", new util.ArrayList[java.util.Map[String, AnyRef]]).asInstanceOf[util.List[java.util.Map[String, AnyRef]]] + hierarchy.put("status", status) + val childrenToUpdate: List[String] = updateChildrenRecursive(children, status, List()) + (hierarchy, childrenToUpdate.asJava) + } + + private def updateChildrenRecursive(children: util.List[util.Map[String, AnyRef]], status: String, idList: List[String]): List[String] = { + children.toList.flatMap(content => { + val updatedIdList: List[String] = + if (StringUtils.equalsAnyIgnoreCase(content.getOrDefault("visibility", "").asInstanceOf[String], "Parent")) { + content.put("lastStatusChangedOn", DateUtils.formatCurrentDate) + content.put("status", status) + content.put("prevState", "Draft") + content.put("lastUpdatedOn", DateUtils.formatCurrentDate) + content.get("identifier").asInstanceOf[String] :: idList + } else idList + val list = updateChildrenRecursive(content.getOrDefault("children", new util.ArrayList[Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]], status, updatedIdList) + list ++ updatedIdList + }) + } + + @throws[Exception] + 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) + 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) + } + + def generateInstructionEventMetadata(identifier: String, node: Node): (Map[String, AnyRef], Map[String, AnyRef], Map[String, AnyRef], util.Map[String, AnyRef]) = { + val metadata: util.Map[String, AnyRef] = node.getMetadata + val publishType = if (StringUtils.equalsIgnoreCase(metadata.getOrDefault("status", "").asInstanceOf[String], "Unlisted")) "unlisted" else "public" + val eventMetadata = Map("identifier" -> identifier, "mimeType" -> metadata.getOrDefault("mimeType", ""), "objectType" -> node.getObjectType.replace("Image", ""), "pkgVersion" -> metadata.getOrDefault("pkgVersion", 0.asInstanceOf[AnyRef]), "lastPublishedBy" -> metadata.getOrDefault("lastPublishedBy", "")) + val actor = Map("id" -> s"${node.getObjectType.toLowerCase().replace("image", "")}-publish", "type" -> "System".asInstanceOf[AnyRef]) + val context = Map("channel" -> metadata.getOrDefault("channel", ""), "pdata" -> Map("id" -> "org.sunbird.platform", "ver" -> "1.0").asJava, "env" -> Platform.getString("cloud_storage.env", "dev")) + val objData = Map("id" -> identifier, "ver" -> metadata.getOrDefault("versionKey", "")) + val eData: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef] {{ + put("action", "publish") + put("publish_type", publishType) + put("metadata", eventMetadata.asJava) + }} + (actor, context, objData, eData) + } +} diff --git a/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/RequestUtil.scala b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/RequestUtil.scala new file mode 100644 index 000000000..4246a74c8 --- /dev/null +++ b/assessment-api/assessment-actors/src/main/scala/org/sunbird/utils/RequestUtil.scala @@ -0,0 +1,43 @@ +package org.sunbird.utils + +import org.sunbird.common.Platform +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.{ClientException, ErrorCodes} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.schema.DefinitionNode + +import scala.concurrent.ExecutionContext +import scala.collection.JavaConversions._ + +object RequestUtil { + + private val SYSTEM_UPDATE_ALLOWED_CONTENT_STATUS = List("Live", "Unlisted") + val questionListLimit = if (Platform.config.hasPath("question.list.limit")) Platform.config.getInt("question.list.limit") else 20 + + def restrictProperties(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Unit = { + val graphId = request.getContext.getOrDefault("graph_id","").asInstanceOf[String] + val version = request.getContext.getOrDefault("version","").asInstanceOf[String] + val objectType = request.getContext.getOrDefault("objectType", "").asInstanceOf[String] + val schemaName = request.getContext.getOrDefault("schemaName","").asInstanceOf[String] + val operation = request.getOperation.toLowerCase.replace(objectType.toLowerCase, "") + val restrictedProps =DefinitionNode.getRestrictedProperties(graphId, version, operation, schemaName) + if (restrictedProps.exists(prop => request.getRequest.containsKey(prop))) throw new ClientException("ERROR_RESTRICTED_PROP", "Properties in list " + restrictedProps.mkString("[", ", ", "]") + " are not allowed in request") + } + + def validateRequest(request: Request): Unit = { + if (request.getRequest.isEmpty) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), s"Request Body cannot be Empty.") + + if (request.get("status") != null && SYSTEM_UPDATE_ALLOWED_CONTENT_STATUS.contains(request.get("status").asInstanceOf[String])) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), s"Cannot update content status to : ${SYSTEM_UPDATE_ALLOWED_CONTENT_STATUS.mkString("[", ", ", "]")}.") + + } + + def validateListRequest(request: Request): Unit = { + if (request.get("identifiers") == null || request.get("identifiers").asInstanceOf[java.util.List[String]].isEmpty) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Required field identifier is missing or empty.") + + if (request.get("identifiers").asInstanceOf[java.util.List[String]].length > questionListLimit) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Request contains more than the permissible limit of identifier: 20.") + } +} diff --git a/assessment-api/assessment-actors/src/test/resources/application.conf b/assessment-api/assessment-actors/src/test/resources/application.conf new file mode 100644 index 000000000..f873cbaf6 --- /dev/null +++ b/assessment-api/assessment-actors/src/test/resources/application.conf @@ -0,0 +1,416 @@ +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +# ~~~~~ +# Play uses HOCON as its configuration file format. HOCON has a number +# of advantages over other config formats, but there are two things that +# can be used when modifying settings. +# +# You can include other configuration files in this main application.conf file: +#include "extra-config.conf" +# +# You can declare variables and substitute for them: +#mykey = ${some.value} +# +# And if an environment variable exists when there is no other substitution, then +# HOCON will fall back to substituting environment variable: +#mykey = ${JAVA_HOME} + +## Akka +# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration +# https://www.playframework.com/documentation/latest/JavaAkka#Configuration +# ~~~~~ +# Play uses Akka internally and exposes Akka Streams and actors in Websockets and +# other streaming HTTP responses. +akka { + # "akka.log-config-on-start" is extraordinarly useful because it log the complete + # configuration at INFO level, including defaults and overrides, so it s worth + # putting at the very top. + # + # Put the following in your conf/logback.xml file: + # + # + # + # And then uncomment this line to debug the configuration. + # + #log-config-on-start = true +} + +## Secret key +# http://www.playframework.com/documentation/latest/ApplicationSecret +# ~~~~~ +# The secret key is used to sign Play's session cookie. +# This must be changed for production, but we don't recommend you change it in this file. +play.http.secret.key = a-long-secret-to-calm-the-rage-of-the-entropy-gods + +## Modules +# https://www.playframework.com/documentation/latest/Modules +# ~~~~~ +# Control which modules are loaded when Play starts. Note that modules are +# the replacement for "GlobalSettings", which are deprecated in 2.5.x. +# Please see https://www.playframework.com/documentation/latest/GlobalSettings +# for more information. +# +# You can also extend Play functionality by using one of the publically available +# Play modules: https://playframework.com/documentation/latest/ModuleDirectory +play.modules { + # By default, Play will load any class called Module that is defined + # in the root package (the "app" directory), or you can define them + # explicitly below. + # If there are any built-in modules that you want to enable, you can list them here. + #enabled += my.application.Module + + # If there are any built-in modules that you want to disable, you can list them here. + #disabled += "" +} + +## IDE +# https://www.playframework.com/documentation/latest/IDE +# ~~~~~ +# Depending on your IDE, you can add a hyperlink for errors that will jump you +# directly to the code location in the IDE in dev mode. The following line makes +# use of the IntelliJ IDEA REST interface: +#play.editor="http://localhost:63342/api/file/?file=%s&line=%s" + +## Internationalisation +# https://www.playframework.com/documentation/latest/JavaI18N +# https://www.playframework.com/documentation/latest/ScalaI18N +# ~~~~~ +# Play comes with its own i18n settings, which allow the user's preferred language +# to map through to internal messages, or allow the language to be stored in a cookie. +play.i18n { + # The application languages + langs = [ "en" ] + + # Whether the language cookie should be secure or not + #langCookieSecure = true + + # Whether the HTTP only attribute of the cookie should be set to true + #langCookieHttpOnly = true +} + +## Play HTTP settings +# ~~~~~ +play.http { + ## Router + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # Define the Router object to use for this application. + # This router will be looked up first when the application is starting up, + # so make sure this is the entry point. + # Furthermore, it's assumed your route file is named properly. + # So for an application router like `my.application.Router`, + # you may need to define a router file `conf/my.application.routes`. + # Default to Routes in the root package (aka "apps" folder) (and conf/routes) + #router = my.application.Router + + ## Action Creator + # https://www.playframework.com/documentation/latest/JavaActionCreator + # ~~~~~ + #actionCreator = null + + ## ErrorHandler + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # If null, will attempt to load a class called ErrorHandler in the root package, + #errorHandler = null + + ## Session & Flash + # https://www.playframework.com/documentation/latest/JavaSessionFlash + # https://www.playframework.com/documentation/latest/ScalaSessionFlash + # ~~~~~ + session { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + + # Sets the max-age field of the cookie to 5 minutes. + # NOTE: this only sets when the browser will discard the cookie. Play will consider any + # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, + # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. + #maxAge = 300 + + # Sets the domain on the session cookie. + #domain = "example.com" + } + + flash { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + } +} + +## Netty Provider +# https://www.playframework.com/documentation/latest/SettingsNetty +# ~~~~~ +play.server.netty { + # Whether the Netty wire should be logged + log.wire = true + + # If you run Play on Linux, you can use Netty's native socket transport + # for higher performance with less garbage. + transport = "native" +} + +## WS (HTTP Client) +# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS +# ~~~~~ +# The HTTP client primarily used for REST APIs. The default client can be +# configured directly, but you can also create different client instances +# with customized settings. You must enable this by adding to build.sbt: +# +# libraryDependencies += ws // or javaWs if using java +# +play.ws { + # Sets HTTP requests not to follow 302 requests + #followRedirects = false + + # Sets the maximum number of open HTTP connections for the client. + #ahc.maxConnectionsTotal = 50 + + ## WS SSL + # https://www.playframework.com/documentation/latest/WsSSL + # ~~~~~ + ssl { + # Configuring HTTPS with Play WS does not require programming. You can + # set up both trustManager and keyManager for mutual authentication, and + # turn on JSSE debugging in development with a reload. + #debug.handshake = true + #trustManager = { + # stores = [ + # { type = "JKS", path = "exampletrust.jks" } + # ] + #} + } +} + +## Cache +# https://www.playframework.com/documentation/latest/JavaCache +# https://www.playframework.com/documentation/latest/ScalaCache +# ~~~~~ +# Play comes with an integrated cache API that can reduce the operational +# overhead of repeated requests. You must enable this by adding to build.sbt: +# +# libraryDependencies += cache +# +play.cache { + # If you want to bind several caches, you can bind the individually + #bindCaches = ["db-cache", "user-cache", "session-cache"] +} + +## Filter Configuration +# https://www.playframework.com/documentation/latest/Filters +# ~~~~~ +# There are a number of built-in filters that can be enabled and configured +# to give Play greater security. +# +play.filters { + + # Enabled filters are run automatically against Play. + # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. + enabled = [] + + # Disabled filters remove elements from the enabled list. + # disabled += filters.CSRFFilter + + + ## CORS filter configuration + # https://www.playframework.com/documentation/latest/CorsFilter + # ~~~~~ + # CORS is a protocol that allows web applications to make requests from the browser + # across different domains. + # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has + # dependencies on CORS settings. + cors { + # Filter paths by a whitelist of path prefixes + #pathPrefixes = ["/some/path", ...] + + # The allowed origins. If null, all origins are allowed. + #allowedOrigins = ["http://www.example.com"] + + # The allowed HTTP methods. If null, all methods are allowed + #allowedHttpMethods = ["GET", "POST"] + } + + ## Security headers filter configuration + # https://www.playframework.com/documentation/latest/SecurityHeaders + # ~~~~~ + # Defines security headers that prevent XSS attacks. + # If enabled, then all options are set to the below configuration by default: + headers { + # The X-Frame-Options header. If null, the header is not set. + #frameOptions = "DENY" + + # The X-XSS-Protection header. If null, the header is not set. + #xssProtection = "1; mode=block" + + # The X-Content-Type-Options header. If null, the header is not set. + #contentTypeOptions = "nosniff" + + # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. + #permittedCrossDomainPolicies = "master-only" + + # The Content-Security-Policy header. If null, the header is not set. + #contentSecurityPolicy = "default-src 'self'" + } + + ## Allowed hosts filter configuration + # https://www.playframework.com/documentation/latest/AllowedHostsFilter + # ~~~~~ + # Play provides a filter that lets you configure which hosts can access your application. + # This is useful to prevent cache poisoning attacks. + hosts { + # Allow requests to example.com, its subdomains, and localhost:9000. + #allowed = [".example.com", "localhost:9000"] + } +} + +# Learning-Service Configuration +content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit"] + +# Cassandra Configuration +content.keyspace.name=content_store +content.keyspace.table=content_data +#TODO: Add Configuration for assessment. e.g: question_data +orchestrator.keyspace.name=script_store +orchestrator.keyspace.table=script_data +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" + +# Redis Configuration +redis.host=localhost +redis.port=6379 +redis.maxConnections=128 + +#Condition to enable publish locally +content.publish_task.enabled=true + +#directory location where store unzip file +dist.directory=/data/tmp/dist/ +output.zipfile=/data/tmp/story.zip +source.folder=/data/tmp/temp2/ +save.directory=/data/tmp/temp/ + +# Content 2 vec analytics URL +CONTENT_TO_VEC_URL="http://172.31.27.233:9000/content-to-vec" + +# FOR CONTENT WORKFLOW PIPELINE (CWP) + +#--Content Workflow Pipeline Mode +OPERATION_MODE=TEST + +#--Maximum Content Package File Size Limit in Bytes (50 MB) +MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=52428800 + +#--Maximum Asset File Size Limit in Bytes (20 MB) +MAX_ASSET_FILE_SIZE_LIMIT=20971520 + +#--No of Retry While File Download Fails +RETRY_ASSET_DOWNLOAD_COUNT=1 + +#Google-vision-API +google.vision.tagging.enabled = false + +#Orchestrator env properties +env="https://dev.ekstep.in/api/learning" + +#Current environment +cloud_storage.env=dev + + +#Folder configuration +cloud_storage.content.folder=content +cloud_storage.asset.folder=assets +cloud_storage.artefact.folder=artifact +cloud_storage.bundle.folder=bundle +cloud_storage.media.folder=media +cloud_storage.ecar.folder=ecar_files + +# Media download configuration +content.media.base.url="https://dev.open-sunbird.org" +plugin.media.base.url="https://dev.open-sunbird.org" + +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + + +framework.max_term_creation_limit=200 + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + + +#Top N Config for Search Telemetry +telemetry_env=dev +telemetry.search.topn=5 + +installation.id=ekstep + + +channel.default="in.ekstep" + + +# Language-Code Configuration +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] + + +framework.categories_cached=["subject", "medium", "gradeLevel", "board"] +framework.cache.ttl=86400 +framework.cache.read=false + + +# Max size(width/height) of thumbnail in pixels +max.thumbnail.size.pixels=150 + +schema.base_path="../../schemas/" + +assessment.skip.validation=true +uestion.keyspace="dev_question_store" +questionset.keyspace="dev_hierarchy_store" +cassandra { + lp { + connection: "127.0.0.1:9042,127.0.0.1:9042,127.0.0.1:9042" + } + lpa { + connection: "127.0.0.1:9042" + } +} +questionset.keyspace = "dev_hierarchy_store" + +import { + request_size_limit = 200 + output_topic_name = "local.auto.creation.job.request" + required_props { + question = ["name", "code", "mimeType", "framework", "channel"] + questionset = ["name", "code", "mimeType", "framework", "channel"] + } + remove_props { + question = [] + questionseet = [] + } +} + + + diff --git a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/BaseSpec.scala b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/BaseSpec.scala new file mode 100644 index 000000000..bb11e5b5b --- /dev/null +++ b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/BaseSpec.scala @@ -0,0 +1,45 @@ +package org.sunbird.actors + +import java.util +import java.util.concurrent.TimeUnit + +import akka.actor.{ActorSystem, Props} +import akka.testkit.TestKit +import org.scalatest.{FlatSpec, Matchers} +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Node + +import scala.concurrent.duration.FiniteDuration + +class BaseSpec extends FlatSpec with Matchers { + val system = ActorSystem.create("system") + + def testUnknownOperation(props: Props, request: Request)(implicit oec: OntologyEngineContext) = { + request.setOperation("unknown") + val response = callActor(request, props) + assert("failed".equals(response.getParams.getStatus)) + } + + def callActor(request: Request, props: Props): Response = { + val probe = new TestKit(system) + val actorRef = system.actorOf(props) + actorRef.tell(request, probe.testActor) + probe.expectMsgType[Response](FiniteDuration.apply(100, TimeUnit.SECONDS)) + } + + def getNode(objectType: String, metadata: Option[util.Map[String, AnyRef]]): Node = { + val node = new Node("domain", "DATA_NODE", objectType) + node.setGraphId("domain") + val nodeMetadata = metadata.getOrElse(new util.HashMap[String, AnyRef]() {{ + put("name", "Sunbird Node") + put("code", "sunbird-node") + put("status", "Draft") + }}) + node.setNodeType("DATA_NODE") + node.setMetadata(nodeMetadata) + node.setObjectType(objectType) + node.setIdentifier("test_id") + node + } +} diff --git a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionActorTest.scala b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionActorTest.scala new file mode 100644 index 000000000..be238503d --- /dev/null +++ b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionActorTest.scala @@ -0,0 +1,233 @@ +package org.sunbird.actors +import java.util + +import akka.actor.Props +import org.scalamock.scalatest.MockFactory +import org.sunbird.common.HttpUtil +import org.sunbird.common.dto.ResponseHandler +import org.sunbird.common.dto.{Property, Request, Response} +import org.sunbird.graph.dac.model.{Node, SearchCriteria} +import org.sunbird.graph.utils.ScalaJsonUtils +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.kafka.client.KafkaClient + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class QuestionActorTest extends BaseSpec with MockFactory { + + "questionActor" should "return failed response for 'unknown' operation" in { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new QuestionActor()), getQuestionRequest()) + } + + it should "return success response for 'createQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Question", None) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(node)) + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())).anyNumberOfTimes() + val request = getQuestionRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("channel"-> "in.ekstep","name" -> "New Content", "code" -> "1234", "mimeType"-> "application/vnd.sunbird.question", "primaryCategory" -> "Multiple Choice Question", "visibility" -> "Default"))) + request.setOperation("createQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'readQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + val node = getNode("Question", None) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + val request = getQuestionRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "fields" -> ""))) + request.setOperation("readQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'updateQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Question", None) + node.getMetadata.putAll(Map("versionKey" -> "1234", "primaryCategory" -> "Multiple Choice Question", "name" -> "Updated New Content", "code" -> "1234", "mimeType"-> "application/vnd.sunbird.question").asJava) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("1234")))) + val request = getQuestionRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map( "versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("updateQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'reviewQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Question", None) + node.getMetadata.putAll(Map("versionKey" -> "1234", "primaryCategory" -> "Multiple Choice Question", "name" -> "Updated New Content", "code" -> "1234", "mimeType"-> "application/vnd.sunbird.question").asJava) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("1234")))) + val request = getQuestionRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map( "versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("reviewQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'retireQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Question", None) + node.getMetadata.putAll(Map("versionKey" -> "1234", "primaryCategory" -> "Practice Question Set", "name" -> "Updated New Content", "code" -> "1234", "mimeType"-> "application/vnd.sunbird.question").asJava) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.updateNodes(_: String, _: util.List[String], _: util.HashMap[String, AnyRef])).expects(*, *, *).returns(Future(new util.HashMap[String, Node])) + val request = getQuestionRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map( "versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("retireQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'publishQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + val kfClient = mock[KafkaClient] + (oec.kafkaClient _).expects().returns(kfClient).anyNumberOfTimes() + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Question", None) + node.getMetadata.putAll(Map("versionKey" -> "1234", "primaryCategory" -> "Practice Question Set", "name" -> "Updated New Content", "code" -> "1234", "mimeType"-> "application/vnd.sunbird.question").asJava) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (kfClient.send(_:String, _:String)).expects(*,*).once() + val request = getQuestionRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map( "versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("publishQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "send events to kafka topic" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val kfClient = mock[KafkaClient] + val hUtil = mock[HttpUtil] + (oec.httpUtil _).expects().returns(hUtil) + val resp :Response = ResponseHandler.OK() + resp.put("question", new util.HashMap[String, AnyRef](){{ + put("framework", "NCF") + put("channel", "test") + }}) + (hUtil.get(_: String, _: String, _: util.Map[String, String])).expects(*, *, *).returns(resp) + (oec.kafkaClient _).expects().returns(kfClient) + (kfClient.send(_: String, _: String)).expects(*, *).returns(None) + val request = getQuestionRequest() + request.getRequest.put("question", new util.HashMap[String, AnyRef](){{ + put("source", "https://dock.sunbirded.org/api/question/v1/read/do_11307822356267827219477") + put("metadata", new util.HashMap[String, AnyRef](){{ + put("name", "Test Question") + put("description", "Test Question") + put("mimeType", "application/vnd.sunbird.question") + put("code", "test.ques.1") + put("primaryCategory", "Learning Resource") + }}) + }}) + request.setOperation("importQuestion") + request.setObjectType("Question") + val response = callActor(request, Props(new QuestionActor())) + assert(response.get("processId") != null) + } + + it should "return success response for 'systemUpdateQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Question", None) + node.getMetadata.putAll(Map("versionKey" -> "1234", "primaryCategory" -> "Multiple Choice Question", "name" -> "Updated New Content", "code" -> "1234", "mimeType" -> "application/vnd.sunbird.question").asJava) + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(List(node))).once() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("1234")))) + val request = getQuestionRequest() + request.getContext.put("identifier", "test_id") + request.putAll(mapAsJavaMap(Map("versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("systemUpdateQuestion") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'listQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Question", None) + node.getMetadata.putAll(Map("versionKey" -> "1234", "primaryCategory" -> "Multiple Choice Question", "name" -> "Updated New Content", "code" -> "1234", "mimeType" -> "application/vnd.sunbird.question").asJava) + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())).anyNumberOfTimes() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(List(node))).once() + val request = getQuestionRequest() + request.put("identifiers", util.Arrays.asList( "test_id")) + request.put("identifier", util.Arrays.asList( "test_id")) + request.put("fields", "") + request.setOperation("listQuestions") + val response = callActor(request, Props(new QuestionActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "throw exception for 'listQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getQuestionRequest() + request.put("identifier", null) + request.put("fields", "") + request.setOperation("listQuestions") + val response = callActor(request, Props(new QuestionActor())) + assert(response.getResponseCode.code == 400) + } + + it should "throw client exception for 'listQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getQuestionRequest() + request.put("identifiers", util.Arrays.asList( "test_id_1","test_id_2","test_id_3","test_id_4","test_id_5","test_id_6","test_id_7","test_id_8","test_id_9","test_id_10","test_id_11","test_id_12","test_id_13","test_id_14","test_id_15","test_id_16","test_id_17","test_id_18","test_id_19","test_id_20","test_id_21")) + request.setOperation("listQuestions") + request.put("fields", "") + val response = callActor(request, Props(new QuestionActor())) + assert(response.getResponseCode.code == 400) + } + + private def getQuestionRequest(): Request = { + val request = new Request() + request.setContext(new java.util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Question") + put("schemaName", "question") + } + }) + request.setObjectType("Question") + request + } + + def getDefinitionNode(): Node = { + val node = new Node() + node.setIdentifier("obj-cat:practice-question-set_question_all") + node.setNodeType("DATA_NODE") + node.setObjectType("ObjectCategoryDefinition") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap( + ScalaJsonUtils.deserialize[Map[String,AnyRef]]("{\n \"objectCategoryDefinition\": {\n \"name\": \"Learning Resource\",\n \"description\": \"Content Playlist\",\n \"categoryId\": \"obj-cat:practice_question_set\",\n \"targetObjectType\": \"Content\",\n \"objectMetadata\": {\n \"config\": {},\n \"schema\": {\n \"required\": [\n \"author\",\n \"copyright\",\n \"license\",\n \"audience\"\n ],\n \"properties\": {\n \"audience\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Student\",\n \"Teacher\"\n ]\n },\n \"default\": [\n \"Student\"\n ]\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"enum\": [\n \"application/pdf\"\n ]\n }\n }\n }\n }\n }\n }"))) + node + } +} \ No newline at end of file diff --git a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionSetActorTest.scala b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionSetActorTest.scala new file mode 100644 index 000000000..578b1af51 --- /dev/null +++ b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/QuestionSetActorTest.scala @@ -0,0 +1,584 @@ +package org.sunbird.actors + +import java.util + +import akka.actor.Props +import org.scalamock.scalatest.MockFactory +import org.sunbird.common.HttpUtil +import org.sunbird.common.dto.{Property, Request, Response, ResponseHandler} +import org.sunbird.graph.dac.model.{Node, Relation, SearchCriteria} +import org.sunbird.graph.nodes.DataNode.getRelationMap +import org.sunbird.graph.utils.ScalaJsonUtils +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.kafka.client.KafkaClient +import org.sunbird.utils.JavaJsonUtils + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class QuestionSetActorTest extends BaseSpec with MockFactory { + + "questionSetActor" should "return failed response for 'unknown' operation" in { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new QuestionSetActor()), getQuestionSetRequest()) + } + + it should "return success response for 'createQuestionSet'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(node)) + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())).anyNumberOfTimes() + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + request.setOperation("createQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'readQuestionSet'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", Some(new util.HashMap[String, AnyRef]() { + { + put("name", "QuestionSet") + put("description", "Updated question Set") + } + })) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "fields" -> ""))) + request.setOperation("readQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'updateQuestionSet'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "description" -> "Updated description", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("updateQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'reviewQuestionSet'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "versionKey" -> "1234", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(getCassandraHierarchy())).anyNumberOfTimes + (graphDB.updateExternalProps(_: Request)).expects(*).returns(Future(new Response())).anyNumberOfTimes + (graphDB.updateNodes(_:String, _:util.List[String], _: util.Map[String, AnyRef])).expects(*, *, *).returns(Future(Map[String, Node]().asJava)).anyNumberOfTimes + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("reviewQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'retireQuestionSet" + + "'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.updateNodes(_: String, _: util.List[String], _: util.HashMap[String, AnyRef])).expects(*, *, *).returns(Future(new util.HashMap[String, Node])) + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("retireQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'publishQuestionSet" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + val kfClient = mock[KafkaClient] + (oec.kafkaClient _).expects().returns(kfClient).anyNumberOfTimes() + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(getCassandraHierarchy())).anyNumberOfTimes + (kfClient.send(_: String, _: String)).expects(*, *).once() + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("publishQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'addQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + node.setIdentifier("do_1234") + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "versionKey" -> "1234", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(List(node).asJava)).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(getCassandraHierarchy())).anyNumberOfTimes + (graphDB.saveExternalProps(_: Request)).expects(*).returns(Future(new Response())).anyNumberOfTimes + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll((Map("children" -> List("do_749").asJava.asInstanceOf[AnyRef], "rootId" -> "do1234")).asJava) + request.setOperation("addQuestion") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'removeQuestion'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + node.setIdentifier("do_1234") + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "versionKey" -> "1234", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(List(node).asJava)).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(getCassandraHierarchy())).anyNumberOfTimes + (graphDB.saveExternalProps(_: Request)).expects(*).returns(Future(new Response())).anyNumberOfTimes + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll((Map("children" -> List("do_914").asJava.asInstanceOf[AnyRef], "rootId" -> "do1234")).asJava) + request.setOperation("removeQuestion") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return error response for 'updateHierarchyQuestionSet'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getInvalidUpdateHierarchyReq() + request.getContext.put("rootId", "do_123") + request.setOperation("updateHierarchy") + val response = callActor(request, Props(new QuestionSetActor())) + assert("failed".equals(response.getParams.getStatus)) + } + + + it should "return success response for 'updateHierarchyQuestionSet'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val rootNode = getRootNode() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(rootNode)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(rootNode)).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(getEmptyCassandraHierarchy())).anyNumberOfTimes + (graphDB.updateExternalProps(_: Request)).expects(*).returns(Future(new Response())).anyNumberOfTimes + val request = getUpdateHierarchyReq() + request.getContext.put("rootId", "do_1234") + request.setOperation("updateHierarchy") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + + it should "return success response for 'rejectQuestionSet'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "status" -> "Review", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "versionKey" -> "1234", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(getCassandraHierarchy())).anyNumberOfTimes + (graphDB.updateExternalProps(_: Request)).expects(*).returns(Future(new Response())).anyNumberOfTimes + (graphDB.updateNodes(_:String, _:util.List[String], _: util.Map[String, AnyRef])).expects(*, *, *).returns(Future(Map[String, Node]().asJava)).anyNumberOfTimes + val request = getQuestionSetRequest() + request.getContext.put("identifier", "do1234") + request.putAll(mapAsJavaMap(Map("versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("rejectQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "send events to kafka topic" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val kfClient = mock[KafkaClient] + val hUtil = mock[HttpUtil] + (oec.httpUtil _).expects().returns(hUtil) + val resp :Response = ResponseHandler.OK() + resp.put("questionset", new util.HashMap[String, AnyRef](){{ + put("framework", "NCF") + put("channel", "test") + }}) + (hUtil.get(_: String, _: String, _: util.Map[String, String])).expects(*, *, *).returns(resp) + (oec.kafkaClient _).expects().returns(kfClient) + (kfClient.send(_: String, _: String)).expects(*, *).returns(None) + val request = getQuestionSetRequest() + request.getRequest.put("questionset", new util.HashMap[String, AnyRef](){{ + put("source", "https://dock.sunbirded.org/api/questionset/v1/read/do_11307822356267827219477") + put("metadata", new util.HashMap[String, AnyRef](){{ + put("name", "Test QuestionSet") + put("description", "Test QuestionSet") + put("mimeType", "application/vnd.sunbird.questionset") + put("code", "test.ques.1") + put("primaryCategory", "Learning Resource") + }}) + }}) + request.setOperation("importQuestionSet") + request.setObjectType("QuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert(response.get("processId") != null) + } + + it should "return success response for 'systemUpdateQuestionSet'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + node.setIdentifier("test_id") + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "description" -> "Updated description", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "status" -> "Draft", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(List(node))).once() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(getCassandraHierarchy())).anyNumberOfTimes + val request = getQuestionSetRequest() + request.getContext.put("identifier", "test_id") + request.putAll(mapAsJavaMap(Map("versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("systemUpdateQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'systemUpdateQuestionSet' with image Node" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("QuestionSet", None) + val imageNode = getNode("QuestionSet", None) + node.setIdentifier("test_id") + imageNode.setIdentifier("test_id.img") + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "description" -> "Updated description", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "status" -> "Live", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + imageNode.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "description" -> "Updated description", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "status" -> "Draft", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).atLeastOnce() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(List(node, imageNode))).once() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(getCassandraHierarchy())).anyNumberOfTimes + (graphDB.updateExternalProps(_: Request)).expects(*).returns(Future(new Response())).anyNumberOfTimes + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)).anyNumberOfTimes() + val request = getQuestionSetRequest() + request.getContext.put("identifier", "test_id") + request.putAll(mapAsJavaMap(Map("versionKey" -> "1234", "description" -> "updated desc"))) + request.setOperation("systemUpdateQuestionSet") + val response = callActor(request, Props(new QuestionSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + private def getQuestionSetRequest(): Request = { + val request = new Request() + request.setContext(new java.util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "QuestionSet") + put("schemaName", "questionset") + } + }) + request.setObjectType("QuestionSet") + request + } + + + private def getRelationNode(): Node = { + val node = new Node() + node.setGraphId("domain") + node.setIdentifier("do_749") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_749") + put("mimeType", "application/vnd.sunbird.question") + put("visibility", "Default") + put("status", "Draft") + put("primaryCategory", "Practice Question Set") + } + }) + node.setObjectType("Question") + node.setNodeType("DATA_NODE") + node + } + + private def getRelationNode_1(): Node = { + val node = new Node() + node.setGraphId("domain") + node.setIdentifier("do_914") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_914") + put("visibility", "Default") + put("mimeType", "application/vnd.sunbird.question") + put("status", "Draft") + put("primaryCategory", "Practice Question Set") + } + }) + node.setObjectType("Question") + node.setNodeType("DATA_NODE") + node + } + + def getDefinitionNode(): Node = { + val node = new Node() + node.setIdentifier("obj-cat:practice-question-set_question-set_all") + node.setNodeType("DATA_NODE") + node.setObjectType("ObjectCategoryDefinition") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap( + ScalaJsonUtils.deserialize[Map[String, AnyRef]]("{\n \"objectCategoryDefinition\": {\n \"name\": \"Learning Resource\",\n \"description\": \"Content Playlist\",\n \"categoryId\": \"obj-cat:practice_question_set\",\n \"targetObjectType\": \"Content\",\n \"objectMetadata\": {\n \"config\": {},\n \"schema\": {\n \"required\": [\n \"author\",\n \"copyright\",\n \"license\",\n \"audience\"\n ],\n \"properties\": {\n \"audience\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Student\",\n \"Teacher\"\n ]\n },\n \"default\": [\n \"Student\"\n ]\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"enum\": [\n \"application/pdf\"\n ]\n }\n }\n }\n }\n }\n }"))) + node + } + + def getCassandraHierarchy(): Response = { + val hierarchyString: String = """{"status":"Live","children":[{"parent":"do_113165166851596288123","totalQuestions":0,"code":"QS_V_Parent_Old","allowSkip":"No","description":"QS-2_parent","language":["English"],"mimeType":"application/vnd.sunbird.questionset","showHints":"No","createdOn":"2020-12-04T15:31:45.948+0530","objectType":"QuestionSet","primaryCategory":"Practice Question Set","lastUpdatedOn":"2020-12-04T15:31:45.947+0530","showSolutions":"No","identifier":"do_11316516745992601613","lastStatusChangedOn":"2020-12-04T15:31:45.948+0530","requiresSubmit":"No","visibility":"Parent","maxQuestions":0,"index":1,"setType":"materialised","languageCode":["en"],"version":1,"versionKey":"1607076105948","showFeedback":"No","depth":1,"name":"QS_V_Parent_2","navigationMode":"non-linear","shuffle":true,"status":"Draft"},{"parent":"do_113165166851596288123","totalQuestions":0,"code":"QS_V_Parent_New","allowSkip":"No","description":"QS-1_parent","language":["English"],"mimeType":"application/vnd.sunbird.questionset","showHints":"No","createdOn":"2020-12-04T15:31:45.872+0530","objectType":"QuestionSet","primaryCategory":"Practice Question Set","children":[{"parent":"do_11316516745922969611","identifier":"do_11316399038283776016","lastStatusChangedOn":"2020-12-02T23:36:59.783+0530","code":"question.code","visibility":"Default","index":1,"language":["English"],"mimeType":"application/vnd.sunbird.question","languageCode":["en"],"createdOn":"2020-12-02T23:36:59.783+0530","version":1,"objectType":"Question","versionKey":"1606932419783","depth":2,"primaryCategory":"Practice Question Set","name":"question_1","lastUpdatedOn":"2020-12-02T23:36:59.783+0530","status":"Draft"}],"lastUpdatedOn":"2020-12-04T15:31:45.861+0530","showSolutions":"No","identifier":"do_11316516745922969611","lastStatusChangedOn":"2020-12-04T15:31:45.876+0530","requiresSubmit":"No","visibility":"Parent","maxQuestions":0,"index":2,"setType":"materialised","languageCode":["en"],"version":1,"versionKey":"1607076105872","showFeedback":"No","depth":1,"name":"QS_V_Parent_1","navigationMode":"non-linear","shuffle":true,"status":"Draft"},{"identifier":"do_11315445058114355211","parent":"do_113165166851596288123","lastStatusChangedOn":"2020-11-19T12:08:13.854+0530","code":"finemanfine","visibility":"Default","index":4,"language":["English"],"mimeType":"application/vnd.sunbird.question","languageCode":["en"],"createdOn":"2020-11-19T12:08:13.854+0530","version":1,"objectType":"Question","versionKey":"1605767893854","depth":1,"name":"question_1","lastUpdatedOn":"2020-11-19T12:08:13.854+0530","contentType":"Resource","status":"Draft"},{"identifier":"do_11315319237189632011","parent":"do_113165166851596288123","lastStatusChangedOn":"2020-11-17T17:28:23.277+0530","code":"finemanfine","visibility":"Default","index":3,"language":["English"],"mimeType":"application/vnd.sunbird.question","languageCode":["en"],"createdOn":"2020-11-17T17:28:23.277+0530","version":1,"objectType":"Question","versionKey":"1605614303277","depth":1,"name":"question_1","lastUpdatedOn":"2020-11-17T17:28:23.277+0530","contentType":"Resource","status":"Draft"}],"identifier":"do_113165166851596288123"}""" + val response = new Response + response.put("hierarchy", hierarchyString) + } + + def getEmptyCassandraHierarchy(): Response = { + val response = new Response + response.put("hierarchy", "{}") + } + + def getInvalidUpdateHierarchyReq() = { + val nodesModified = "{\n \"do_1234\": {\n \"metadata\": {\n \"code\": \"updated_code_of_root\"\n },\n \"root\": true,\n \"isNew\": false\n },\n \"QS_V_Parent_New\": {\n \"metadata\": {\n \"code\": \"QS_V_Parent\",\n \"name\": \"QS_V_Parent_1\",\n \"description\": \"QS-1_parent\",\n \"mimeType\": \"application/vnd.sunbird.questionset\",\n \"visibility\": \"Parent\",\n \"primaryCategory\": \"Practice Question Set\"\n },\n \"root\": false,\n \"objectType\": \"QuestionSet\",\n \"isNew\": true\n },\n \"QS_V_Parent_Old\": {\n \"metadata\": {\n \"code\": \"QS_V_Parent\",\n \"name\": \"QS_V_Parent_2\",\n \"description\": \"QS-2_parent\",\n \"mimeType\": \"application/vnd.sunbird.questionset\",\n \"visibility\": \"Parent\",\n \"primaryCategory\": \"Practice Question Set\"\n },\n \"root\": false,\n \"objectType\": \"QuestionSet\",\n \"isNew\": true\n },\n \"do_113178560758022144113\": {\n \"metadata\": {\n \"code\": \"Q_NEW_PARENT\",\n \"name\": \"Q_NEW_PARENT\",\n \"description\": \"Q_NEW_PARENT\",\n \"mimeType\": \"application/vnd.sunbird.question\",\n \"visibility\": \"Parent\",\n \"primaryCategory\": \"Practice Question Set\"\n },\n \"root\": false,\n \"objectType\": \"Question\",\n \"isNew\": true\n }\n }" + val hierarchy = "{\n \"do_1234\": {\n \"children\": [\n \"QS_V_Parent_Old\",\n \"QS_V_Parent_New\"\n ],\n \"root\": true\n },\n \"QS_V_Parent_Old\": {\n \"children\": [],\n \"root\": false\n },\n \"QS_V_Parent_New\": {\n \"children\": [\n \"do_113178560758022144113\"\n ],\n \"root\": false\n },\n \"do_113178560758022144113\": {\n\n }\n }" + val request = getQuestionSetRequest() + request.put("nodesModified", JavaJsonUtils.deserialize[java.util.Map[String, AnyRef]](nodesModified)) + request.put("hierarchy", JavaJsonUtils.deserialize[java.util.Map[String, AnyRef]](hierarchy)) + request + } + + def getUpdateHierarchyReq() = { + val nodesModified = + """ + |{ + | "UUID": { + | "metadata": { + | "mimeType": "application/vnd.sunbird.questionset", + | "name": "Subjective", + | "primaryCategory": "Practice Question Set", + | "code": "questionset" + | }, + | "objectType": "QuestionSet", + | "root": false, + | "isNew": true + | } + | } + """.stripMargin + val hierarchy = + """ + |{ + | "do_1234": { + | "children": [ + | "UUID" + | ], + | "root": true + | } + | } + """.stripMargin + val request = getQuestionSetRequest() + request.put("nodesModified", JavaJsonUtils.deserialize[java.util.Map[String, AnyRef]](nodesModified)) + request.put("hierarchy", JavaJsonUtils.deserialize[java.util.Map[String, AnyRef]](hierarchy)) + request + } + + def getRootNode(): Node = { + val node = getNode("QuestionSet", None) + node.setIdentifier("do_1234") + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "navigationMode" -> "linear", + "allowSkip" -> "Yes", + "requiresSubmit" -> "No", + "shuffle" -> true.asInstanceOf[AnyRef], + "showFeedback" -> "Yes", + "showSolutions" -> "Yes", + "showHints" -> "Yes", + "summaryType" -> "Complete", + "versionKey" -> "1234", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set", + "channel" -> "in.ekstep" + ))) + node + } + + def getQuestionSetNode(identifier:String): Node = { + val node = getNode("QuestionSet", None) + node.setIdentifier(identifier) + node.getMetadata.putAll(mapAsJavaMap(Map("name" -> "question_1", + "visibility" -> "Default", + "code" -> "finemanfine", + "versionKey" -> "1234", + "mimeType" -> "application/vnd.sunbird.questionset", + "primaryCategory" -> "Practice Question Set"))) + node + } +} \ No newline at end of file diff --git a/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/TestItemSetActor.scala b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/TestItemSetActor.scala new file mode 100644 index 000000000..3c4602feb --- /dev/null +++ b/assessment-api/assessment-actors/src/test/scala/org/sunbird/actors/TestItemSetActor.scala @@ -0,0 +1,132 @@ +package org.sunbird.actors + +import java.util + +import akka.actor.Props +import org.scalamock.scalatest.MockFactory +import org.sunbird.common.dto.Request +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.{GraphService, OntologyEngineContext} + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class TestItemSetActor extends BaseSpec with MockFactory { + + "ItemSetActor" should "return failed response for 'unknown' operation" in { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new ItemSetActor()), getItemSetRequest()) + } + + it should "create a itemSetNode and store it in neo4j" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(getValidNode())) + val request = getItemSetRequest() + request.setRequest(mapAsJavaMap(Map("name" -> "test-itemset", "code" -> "1234"))) + request.setOperation("createItemSet") + val response = callActor(request, Props(new ItemSetActor())) + assert(response.get("identifier") != null) + assert(response.get("identifier").equals("1234")) + } + + it should "return success response for updateItemSet" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + val request = getItemSetRequest() + request.getContext.put("identifier", "1234") + request.put("name", "test") + request.put("code", "1234") + request.putAll(mapAsJavaMap(Map("description" -> "test desc"))) + request.setOperation("updateItemSet") + val response = callActor(request, Props(new ItemSetActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.get("identifier").equals("1234")) + } + + + it should "return success response for readItemSet" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(1) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + val request = getItemSetRequest() + request.getContext.put("identifier", "1234") + request.putAll(mapAsJavaMap(Map("fields" -> ""))) + request.setOperation("readItemSet") + val response = callActor(request, Props(new ItemSetActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.get("itemset").asInstanceOf[util.Map[String, AnyRef]].get("identifier").equals("1234")) + } + + it should "return success response for reviewItemSet" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(3) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + val request = getItemSetRequest() + request.getContext.put("identifier","do_1234") + request.put("name", "test") + request.put("code", "1234") + request.setOperation("reviewItemSet") + val response = callActor(request, Props(new ItemSetActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.get("identifier").equals("1234")) + } + + it should "return success response for retireItemSet" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + val request = getItemSetRequest() + request.getContext.put("identifier","do_1234") + request.put("name", "test") + request.put("code", "1234") + request.setOperation("retireItemSet") + val response = callActor(request, Props(new ItemSetActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.get("identifier").equals("1234")) + } + + private def getItemSetRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "2.0") + put("objectType", "ItemSet") + put("schemaName", "itemset") + } + }) + request.setObjectType("ItemSet") + request + } + + private def getValidNode(): Node = { + val node = new Node() + node.setIdentifier("1234") + node.setNodeType("DATA_NODE") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "1234") + put("objectType", "ItemSet") + put("name", "test-itemset") + } + }) + node.setObjectType("ItemSet") + node + } + +} diff --git a/assessment-api/assessment-service/app/controllers/BaseController.scala b/assessment-api/assessment-service/app/controllers/BaseController.scala new file mode 100644 index 000000000..a3b47974b --- /dev/null +++ b/assessment-api/assessment-service/app/controllers/BaseController.scala @@ -0,0 +1,73 @@ +package controllers + +import java.util.UUID + +import akka.actor.ActorRef +import akka.pattern.Patterns +import org.sunbird.common.DateUtils +import org.sunbird.common.dto.{Response, ResponseHandler} +import org.sunbird.common.exception.ResponseCode +import play.api.mvc._ +import utils.JavaJsonUtils + +import collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +abstract class BaseController(protected val cc: ControllerComponents)(implicit exec: ExecutionContext) extends AbstractController(cc) { + + def requestBody()(implicit request: Request[AnyContent]) = { + val body = request.body.asJson.getOrElse("{}").toString + JavaJsonUtils.deserialize[java.util.Map[String, Object]](body).getOrDefault("request", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + } + + def commonHeaders()(implicit request: Request[AnyContent]): java.util.Map[String, Object] = { + val customHeaders = Map("x-channel-id" -> "channel", "X-Consumer-ID" -> "consumerId", "X-App-Id" -> "appId") + customHeaders.map(ch => { + val value = request.headers.get(ch._1) + if (value.isDefined && !value.isEmpty) { + collection.mutable.HashMap[String, Object](ch._2 -> value.get).asJava + } else { + collection.mutable.HashMap[String, Object]().asJava + } + }).reduce((a, b) => { + a.putAll(b) + return a + }) + } + + def getRequest(input: java.util.Map[String, AnyRef], context: java.util.Map[String, AnyRef], operation: String): org.sunbird.common.dto.Request = { + new org.sunbird.common.dto.Request(context, input, operation, null); + } + + def getResult(apiId: String, actor: ActorRef, request: org.sunbird.common.dto.Request) : Future[Result] = { + val future = Patterns.ask(actor, request, 30000) recoverWith {case e: Exception => Future(ResponseHandler.getErrorResponse(e))} + future.map(f => { + val result = f.asInstanceOf[Response] + result.setId(apiId) + setResponseEnvelope(result) + val response = JavaJsonUtils.serialize(result); + result.getResponseCode match { + case ResponseCode.OK => Ok(response).as("application/json") + case ResponseCode.CLIENT_ERROR => BadRequest(response).as("application/json") + case ResponseCode.RESOURCE_NOT_FOUND => NotFound(response).as("application/json") + case _ => play.api.mvc.Results.InternalServerError(response).as("application/json") + } + }) + } + + def setResponseEnvelope(response: Response) = { + response.setTs(DateUtils.formatCurrentDate("yyyy-MM-dd'T'HH:mm:ss'Z'XXX")) + response.getParams.setResmsgid(UUID.randomUUID().toString) + } + + def setRequestContext(request:org.sunbird.common.dto.Request, version: String, objectType: String, schemaName: String): Unit = { + var contextMap: java.util.Map[String, AnyRef] = new java.util.HashMap[String, AnyRef](){{ + put("graph_id", "domain") + put("version" , version) + put("objectType" , objectType) + put("schemaName", schemaName) + }}; + request.setObjectType(objectType); + request.setContext(contextMap) + } +} diff --git a/assessment-api/assessment-service/app/controllers/HealthController.scala b/assessment-api/assessment-service/app/controllers/HealthController.scala new file mode 100644 index 000000000..d278b7681 --- /dev/null +++ b/assessment-api/assessment-service/app/controllers/HealthController.scala @@ -0,0 +1,31 @@ +package controllers + +import akka.actor.{ActorRef, ActorSystem} +import handlers.SignalHandler +import javax.inject._ +import org.sunbird.common.JsonUtils +import org.sunbird.common.dto.ResponseHandler +import play.api.mvc._ +import utils.{ActorNames, ApiId} + +import scala.concurrent.{ExecutionContext, Future} + +class HealthController @Inject()(@Named(ActorNames.HEALTH_ACTOR) healthActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem, signalHandler: SignalHandler)(implicit exec: ExecutionContext) extends BaseController(cc) { + + def health() = Action.async { implicit request => + if (signalHandler.isShuttingDown) { + Future { ServiceUnavailable } + } else { + getResult(ApiId.APPLICATION_HEALTH, healthActor, new org.sunbird.common.dto.Request()) + } + } + + def serviceHealth() = Action.async { implicit request => + if (signalHandler.isShuttingDown) + Future { ServiceUnavailable } + else { + val response = ResponseHandler.OK().setId(ApiId.APPLICATION_SERVICE_HEALTH).put("healthy", true) + Future { Ok(JsonUtils.serialize(response)).as("application/json") } + } + } +} diff --git a/assessment-api/assessment-service/app/controllers/v3/ItemSetController.scala b/assessment-api/assessment-service/app/controllers/v3/ItemSetController.scala new file mode 100644 index 000000000..4786424a7 --- /dev/null +++ b/assessment-api/assessment-service/app/controllers/v3/ItemSetController.scala @@ -0,0 +1,71 @@ +package controllers.v3 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import controllers.BaseController +import javax.inject.{Inject, Named} +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId, ItemSetOperations} + +import scala.collection.JavaConverters._ +import scala.concurrent.ExecutionContext + +@Singleton +class ItemSetController @Inject()(@Named(ActorNames.ITEM_SET_ACTOR) itemSetActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + + val objectType = "ItemSet" + val schemaName: String = "itemset" + val version = "2.0" + + def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val itemset = body.getOrDefault("itemset", new java.util.HashMap()).asInstanceOf[java.util.Map[String, AnyRef]] + itemset.putAll(headers) + val itemSetRequest = getRequest(itemset, headers, ItemSetOperations.createItemSet.toString) + setRequestContext(itemSetRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_ITEM_SET, itemSetActor, itemSetRequest) + } + + def read(identifier: String, fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val itemset = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + itemset.putAll(headers) + itemset.putAll(Map("identifier" -> identifier, "fields" -> fields.getOrElse("")).asJava) + val itemSetRequest = getRequest(itemset, headers, ItemSetOperations.readItemSet.toString) + setRequestContext(itemSetRequest, version, objectType, schemaName) + getResult(ApiId.READ_ITEM_SET, itemSetActor, itemSetRequest) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val itemset = body.getOrDefault("itemset", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + itemset.putAll(headers) + val itemSetRequest = getRequest(itemset, headers, ItemSetOperations.updateItemSet.toString) + setRequestContext(itemSetRequest, version, objectType, schemaName) + itemSetRequest.getContext.put("identifier", identifier); + getResult(ApiId.UPDATE_ITEM_SET, itemSetActor, itemSetRequest) + } + + def review(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val itemset = body.getOrDefault("itemset", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + itemset.putAll(headers) + val itemSetRequest = getRequest(itemset, headers, ItemSetOperations.reviewItemSet.toString) + setRequestContext(itemSetRequest, version, objectType, schemaName) + itemSetRequest.getContext.put("identifier", identifier); + getResult(ApiId.REVIEW_ITEM_SET, itemSetActor, itemSetRequest) + } + + def retire(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val itemset = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + itemset.putAll(headers) + val itemSetRequest = getRequest(itemset, headers, ItemSetOperations.retireItemSet.toString) + setRequestContext(itemSetRequest, version, objectType, schemaName) + itemSetRequest.getContext.put("identifier", identifier) + getResult(ApiId.RETIRE_ITEM_SET, itemSetActor, itemSetRequest) + } +} diff --git a/assessment-api/assessment-service/app/controllers/v4/QuestionController.scala b/assessment-api/assessment-service/app/controllers/v4/QuestionController.scala new file mode 100644 index 000000000..b01eb4f2e --- /dev/null +++ b/assessment-api/assessment-service/app/controllers/v4/QuestionController.scala @@ -0,0 +1,112 @@ +package controllers.v4 + +import akka.actor.{ActorRef, ActorSystem} +import controllers.BaseController +import javax.inject.{Inject, Named} +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId, QuestionOperations} + +import scala.collection.JavaConverters._ +import scala.concurrent.ExecutionContext + +class QuestionController @Inject()(@Named(ActorNames.QUESTION_ACTOR) questionActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + + val objectType = "Question" + val schemaName: String = "question" + val version = "1.0" + + def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val question = body.getOrDefault("question", new java.util.HashMap()).asInstanceOf[java.util.Map[String, AnyRef]] + question.putAll(headers) + val questionRequest = getRequest(question, headers, QuestionOperations.createQuestion.toString) + setRequestContext(questionRequest, version, objectType, schemaName) + 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, version, objectType, schemaName) + getResult(ApiId.READ_QUESTION, questionActor, questionRequest) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val question = body.getOrDefault("question", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + question.putAll(headers) + val questionRequest = getRequest(question, headers, QuestionOperations.updateQuestion.toString) + setRequestContext(questionRequest, version, objectType, schemaName) + questionRequest.getContext.put("identifier", identifier) + getResult(ApiId.UPDATE_QUESTION, questionActor, questionRequest) + } + + def review(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val question = body.getOrDefault("question", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + question.putAll(headers) + val questionRequest = getRequest(question, headers, QuestionOperations.reviewQuestion.toString) + setRequestContext(questionRequest, version, objectType, schemaName) + questionRequest.getContext.put("identifier", identifier) + getResult(ApiId.REVIEW_QUESTION, questionActor, questionRequest) + } + + def publish(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val question = body.getOrDefault("question", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + question.putAll(headers) + val questionRequest = getRequest(question, headers, QuestionOperations.publishQuestion.toString) + setRequestContext(questionRequest, version, objectType, schemaName) + questionRequest.getContext.put("identifier", identifier) + getResult(ApiId.PUBLISH_QUESTION, questionActor, questionRequest) + } + + def retire(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val question = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + question.putAll(headers) + val questionRequest = getRequest(question, headers, QuestionOperations.retireQuestion.toString) + setRequestContext(questionRequest, version, objectType, schemaName) + questionRequest.getContext.put("identifier", identifier) + getResult(ApiId.RETIRE_QUESTION, questionActor, questionRequest) + } + + def importQuestion() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val questionRequest = getRequest(body, headers, QuestionOperations.importQuestion.toString) + setRequestContext(questionRequest, version, objectType, schemaName) + getResult(ApiId.IMPORT_QUESTION, questionActor, questionRequest) + } + + def systemUpdate(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + content.putAll(headers) + val questionRequest = getRequest(content, headers, QuestionOperations.systemUpdateQuestion.toString) + setRequestContext(questionRequest, version, objectType, schemaName) + questionRequest.getContext.put("identifier", identifier); + 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, version, objectType, schemaName) + getResult(ApiId.LIST_QUESTIONS, questionActor, questionRequest) + } +} diff --git a/assessment-api/assessment-service/app/controllers/v4/QuestionSetController.scala b/assessment-api/assessment-service/app/controllers/v4/QuestionSetController.scala new file mode 100644 index 000000000..5577615f8 --- /dev/null +++ b/assessment-api/assessment-service/app/controllers/v4/QuestionSetController.scala @@ -0,0 +1,151 @@ +package controllers.v4 + +import akka.actor.{ActorRef, ActorSystem} +import controllers.BaseController +import javax.inject.{Inject, Named} +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId, QuestionSetOperations} + +import scala.collection.JavaConverters._ +import scala.concurrent.ExecutionContext + +class QuestionSetController @Inject()(@Named(ActorNames.QUESTION_SET_ACTOR) questionSetActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + + val objectType = "QuestionSet" + val schemaName: String = "questionset" + val version = "1.0" + + def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val questionSet = body.getOrDefault("questionset", new java.util.HashMap()).asInstanceOf[java.util.Map[String, AnyRef]] + questionSet.putAll(headers) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.createQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def read(identifier: String, mode: Option[String], fields: 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("identifier" -> identifier, "fields" -> fields.getOrElse(""), "mode" -> mode.getOrElse("read")).asJava) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.readQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + getResult(ApiId.READ_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def update(identifier: String) = 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) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.updateQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + questionSetRequest.getContext.put("identifier", identifier) + getResult(ApiId.UPDATE_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def review(identifier: String) = 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) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.reviewQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + questionSetRequest.getContext.put("identifier", identifier) + getResult(ApiId.REVIEW_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def publish(identifier: String) = 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) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.publishQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + questionSetRequest.getContext.put("identifier", identifier) + getResult(ApiId.PUBLISH_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def retire(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val questionSet = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + questionSet.putAll(headers) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.retireQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + questionSetRequest.getContext.put("identifier", identifier) + getResult(ApiId.RETIRE_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def add() = 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) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.addQuestion.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + getResult(ApiId.ADD_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def remove() = 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) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.removeQuestion.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + getResult(ApiId.REMOVE_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def updateHierarchy() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val data = body.getOrDefault("data", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + data.putAll(headers) + val questionSetRequest = getRequest(data, headers, "updateHierarchy") + setRequestContext(questionSetRequest, version, objectType, schemaName) + 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, version, objectType, schemaName) + getResult(ApiId.GET_HIERARCHY, questionSetActor, readRequest) + } + + def reject(identifier: String) = 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) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.rejectQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + questionSetRequest.getContext.put("identifier", identifier) + getResult(ApiId.REJECT_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def importQuestionSet() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val questionSetRequest = getRequest(body, headers, QuestionSetOperations.importQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + getResult(ApiId.IMPORT_QUESTION_SET, questionSetActor, questionSetRequest) + } + + def systemUpdate(identifier: String) = 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) + val questionSetRequest = getRequest(questionSet, headers, QuestionSetOperations.systemUpdateQuestionSet.toString) + setRequestContext(questionSetRequest, version, objectType, schemaName) + questionSetRequest.getContext.put("identifier", identifier); + getResult(ApiId.SYSTEM_UPDATE_QUESTION_SET, questionSetActor, questionSetRequest) + } +} diff --git a/assessment-api/assessment-service/app/filters/AccessLogFilter.scala b/assessment-api/assessment-service/app/filters/AccessLogFilter.scala new file mode 100644 index 000000000..aad9f8419 --- /dev/null +++ b/assessment-api/assessment-service/app/filters/AccessLogFilter.scala @@ -0,0 +1,45 @@ +package filters + +import akka.util.ByteString +import javax.inject.Inject +import org.sunbird.telemetry.util.TelemetryAccessEventUtil +import play.api.Logging +import play.api.libs.streams.Accumulator +import play.api.mvc._ + +import scala.concurrent.ExecutionContext +import scala.collection.JavaConverters._ + +class AccessLogFilter @Inject() (implicit ec: ExecutionContext) extends EssentialFilter with Logging { + + val xHeaderNames = Map("x-session-id" -> "X-Session-ID", "X-Consumer-ID" -> "x-consumer-id", "x-device-id" -> "X-Device-ID", "x-app-id" -> "APP_ID", "x-authenticated-userid" -> "X-Authenticated-Userid", "x-channel-id" -> "X-Channel-Id") + + def apply(nextFilter: EssentialAction) = new EssentialAction { + def apply(requestHeader: RequestHeader) = { + + val startTime = System.currentTimeMillis + + val accumulator: Accumulator[ByteString, Result] = nextFilter(requestHeader) + + accumulator.map { result => + val endTime = System.currentTimeMillis + val requestTime = endTime - startTime + + val path = requestHeader.uri + if(!path.contains("/health")){ + val headers = requestHeader.headers.headers.groupBy(_._1).mapValues(_.map(_._2)) + val appHeaders = headers.filter(header => xHeaderNames.keySet.contains(header._1.toLowerCase)) + .map(entry => (xHeaderNames.get(entry._1.toLowerCase()).get, entry._2.head)) + val otherDetails = Map[String, Any]("StartTime" -> startTime, "env" -> "assessment", + "RemoteAddress" -> requestHeader.remoteAddress, + "ContentLength" -> result.body.contentLength.getOrElse(0), + "Status" -> result.header.status, "Protocol" -> "http", + "path" -> path, + "Method" -> requestHeader.method.toString) + TelemetryAccessEventUtil.writeTelemetryEventLog((otherDetails ++ appHeaders).asInstanceOf[Map[String, AnyRef]].asJava) + } + result.withHeaders("Request-Time" -> requestTime.toString) + } + } + } + } \ No newline at end of file diff --git a/assessment-api/assessment-service/app/handlers/SignalHandler.scala b/assessment-api/assessment-service/app/handlers/SignalHandler.scala new file mode 100644 index 000000000..4cad301c1 --- /dev/null +++ b/assessment-api/assessment-service/app/handlers/SignalHandler.scala @@ -0,0 +1,33 @@ +package handlers + +import java.util.concurrent.TimeUnit + +import akka.actor.ActorSystem +import javax.inject.{Inject, Singleton} +import org.slf4j.LoggerFactory +import play.api.inject.DefaultApplicationLifecycle +import sun.misc.Signal + +import scala.concurrent.duration.Duration + +@Singleton +class SignalHandler @Inject()(implicit actorSystem: ActorSystem, lifecycle: DefaultApplicationLifecycle) { + val LOG = LoggerFactory.getLogger(classOf[SignalHandler]) + val STOP_DELAY = Duration.create(30, TimeUnit.SECONDS) + var isShuttingDown = false + + println("Initializing SignalHandler...") + Signal.handle(new Signal("TERM"), new sun.misc.SignalHandler() { + override def handle(signal: Signal): Unit = { + // $COVERAGE-OFF$ Disabling scoverage as this code is impossible to test + isShuttingDown = true + println("Termination required, swallowing SIGTERM to allow current requests to finish. : " + System.currentTimeMillis()) + actorSystem.scheduler.scheduleOnce(STOP_DELAY)(() => { + println("ApplicationLifecycle stop triggered... : " + System.currentTimeMillis()) + lifecycle.stop() + })(actorSystem.dispatcher) + // $COVERAGE-ON + } + }) +} + diff --git a/assessment-api/assessment-service/app/modules/AssessmentModule.scala b/assessment-api/assessment-service/app/modules/AssessmentModule.scala new file mode 100644 index 000000000..36d3ee2bb --- /dev/null +++ b/assessment-api/assessment-service/app/modules/AssessmentModule.scala @@ -0,0 +1,18 @@ +package modules + +import com.google.inject.AbstractModule +import org.sunbird.actors.{HealthActor, ItemSetActor, QuestionActor, QuestionSetActor} +import play.libs.akka.AkkaGuiceSupport +import utils.ActorNames + +class AssessmentModule extends AbstractModule with AkkaGuiceSupport { + + override def configure() = { + super.configure() + bindActor(classOf[HealthActor], ActorNames.HEALTH_ACTOR) + bindActor(classOf[ItemSetActor], ActorNames.ITEM_SET_ACTOR) + bindActor(classOf[QuestionActor], ActorNames.QUESTION_ACTOR) + bindActor(classOf[QuestionSetActor], ActorNames.QUESTION_SET_ACTOR) + println("Initialized application actors for assessment-service") + } +} diff --git a/assessment-api/assessment-service/app/utils/ActorNames.scala b/assessment-api/assessment-service/app/utils/ActorNames.scala new file mode 100644 index 000000000..16a5cb726 --- /dev/null +++ b/assessment-api/assessment-service/app/utils/ActorNames.scala @@ -0,0 +1,10 @@ +package utils + +object ActorNames { + + final val HEALTH_ACTOR = "healthActor" + final val ITEM_SET_ACTOR = "itemSetActor" + final val QUESTION_ACTOR = "questionActor" + final val QUESTION_SET_ACTOR = "questionSetActor" + +} diff --git a/assessment-api/assessment-service/app/utils/ApiId.scala b/assessment-api/assessment-service/app/utils/ApiId.scala new file mode 100644 index 000000000..af128df12 --- /dev/null +++ b/assessment-api/assessment-service/app/utils/ApiId.scala @@ -0,0 +1,41 @@ +package utils + +object ApiId { + + final val APPLICATION_HEALTH = "api.assessment.health" + final val APPLICATION_SERVICE_HEALTH = "api.assessment.service.health" + + //ItemSet APIs + val CREATE_ITEM_SET = "api.itemset.create" + val READ_ITEM_SET = "api.itemset.read" + val UPDATE_ITEM_SET = "api.itemset.update" + val REVIEW_ITEM_SET = "api.itemset.review" + val RETIRE_ITEM_SET = "api.itemset.retire" + + //Question APIs + val CREATE_QUESTION = "api.question.create" + val READ_QUESTION = "api.question.read" + val UPDATE_QUESTION = "api.question.update" + val REVIEW_QUESTION = "api.question.review" + val PUBLISH_QUESTION = "api.question.publish" + val RETIRE_QUESTION = "api.question.retire" + val IMPORT_QUESTION = "api.question.import" + val SYSTEM_UPDATE_QUESTION = "api.question.system.update" + val LIST_QUESTIONS = "api.questions.list" + + //QuestionSet APIs + val CREATE_QUESTION_SET = "api.questionset.create" + val READ_QUESTION_SET = "api.questionset.read" + val UPDATE_QUESTION_SET = "api.questionset.update" + val REVIEW_QUESTION_SET = "api.questionset.review" + val PUBLISH_QUESTION_SET = "api.questionset.publish" + val RETIRE_QUESTION_SET = "api.questionset.retire" + val ADD_QUESTION_SET = "api.questionset.add" + val REMOVE_QUESTION_SET = "api.questionset.remove" + val UPDATE_HIERARCHY = "api.questionset.hierarchy.update" + val GET_HIERARCHY = "api.questionset.hierarchy.get" + val REJECT_QUESTION_SET = "api.questionset.reject" + val IMPORT_QUESTION_SET = "api.questionset.import" + val SYSTEM_UPDATE_QUESTION_SET = "api.questionset.system.update" + +} diff --git a/assessment-api/assessment-service/app/utils/ItemSetOperations.scala b/assessment-api/assessment-service/app/utils/ItemSetOperations.scala new file mode 100644 index 000000000..4f3fcaa5b --- /dev/null +++ b/assessment-api/assessment-service/app/utils/ItemSetOperations.scala @@ -0,0 +1,5 @@ +package utils + +object ItemSetOperations extends Enumeration { + val createItemSet, readItemSet, updateItemSet, reviewItemSet, retireItemSet = Value +} diff --git a/learning-api/content-service/app/utils/JavaJsonUtils.scala b/assessment-api/assessment-service/app/utils/JavaJsonUtils.scala similarity index 100% rename from learning-api/content-service/app/utils/JavaJsonUtils.scala rename to assessment-api/assessment-service/app/utils/JavaJsonUtils.scala diff --git a/assessment-api/assessment-service/app/utils/QuestionOperations.scala b/assessment-api/assessment-service/app/utils/QuestionOperations.scala new file mode 100644 index 000000000..b23ff8c5c --- /dev/null +++ b/assessment-api/assessment-service/app/utils/QuestionOperations.scala @@ -0,0 +1,5 @@ +package utils + +object QuestionOperations extends Enumeration { + val createQuestion, readQuestion, updateQuestion, reviewQuestion, publishQuestion, retireQuestion, importQuestion, systemUpdateQuestion, listQuestions = Value +} diff --git a/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala b/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala new file mode 100644 index 000000000..08cf26a11 --- /dev/null +++ b/assessment-api/assessment-service/app/utils/QuestionSetOperations.scala @@ -0,0 +1,7 @@ +package utils + +object QuestionSetOperations extends Enumeration { + val createQuestionSet, readQuestionSet, updateQuestionSet, reviewQuestionSet, publishQuestionSet, + retireQuestionSet, addQuestion, removeQuestion, updateHierarchyQuestion, readHierarchyQuestion, + rejectQuestionSet, importQuestionSet, systemUpdateQuestionSet = Value +} diff --git a/assessment-api/assessment-service/conf/application.conf b/assessment-api/assessment-service/conf/application.conf new file mode 100644 index 000000000..417a6265d --- /dev/null +++ b/assessment-api/assessment-service/conf/application.conf @@ -0,0 +1,409 @@ +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +# ~~~~~ +# Play uses HOCON as its configuration file format. HOCON has a number +# of advantages over other config formats, but there are two things that +# can be used when modifying settings. +# +# You can include other configuration files in this main application.conf file: +#include "extra-config.conf" +# +# You can declare variables and substitute for them: +#mykey = ${some.value} +# +# And if an environment variable exists when there is no other substitution, then +# HOCON will fall back to substituting environment variable: +#mykey = ${JAVA_HOME} + +## Akka +# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration +# https://www.playframework.com/documentation/latest/JavaAkka#Configuration +# ~~~~~ +# Play uses Akka internally and exposes Akka Streams and actors in Websockets and +# other streaming HTTP responses. +akka { + # "akka.log-config-on-start" is extraordinarly useful because it log the complete + # configuration at INFO level, including defaults and overrides, so it s worth + # putting at the very top. + # + # Put the following in your conf/logback.xml file: + # + # + # + # And then uncomment this line to debug the configuration. + # + #log-config-on-start = true + default-dispatcher { + # This will be used if you have set "executor = "fork-join-executor"" + fork-join-executor { + # Min number of threads to cap factor-based parallelism number to + parallelism-min = 8 + + # The parallelism factor is used to determine thread pool size using the + # following formula: ceil(available processors * factor). Resulting size + # is then bounded by the parallelism-min and parallelism-max values. + parallelism-factor = 32.0 + + # Max number of threads to cap factor-based parallelism number to + parallelism-max = 64 + + # Setting to "FIFO" to use queue like peeking mode which "poll" or "LIFO" to use stack + # like peeking mode which "pop". + task-peeking-mode = "FIFO" + } + } + actors-dispatcher { + type = "Dispatcher" + executor = "fork-join-executor" + fork-join-executor { + parallelism-min = 8 + parallelism-factor = 32.0 + parallelism-max = 64 + } + # Throughput for default Dispatcher, set to 1 for as fair as possible + throughput = 1 + } + actor { + deployment { + /healthActor + { + router = smallest-mailbox-pool + nr-of-instances = 5 + dispatcher = actors-dispatcher + } + /itemSetActor + { + router = smallest-mailbox-pool + nr-of-instances = 2 + dispatcher = actors-dispatcher + } + } + } +} + +## Secret key +# http://www.playframework.com/documentation/latest/ApplicationSecret +# ~~~~~ +# The secret key is used to sign Play's session cookie. +# This must be changed for production, but we don't recommend you change it in this file. +play.http.secret.key= a-long-secret-to-calm-the-rage-of-the-entropy-gods + +## Modules +# https://www.playframework.com/documentation/latest/Modules +# ~~~~~ +# Control which modules are loaded when Play starts. Note that modules are +# the replacement for "GlobalSettings", which are deprecated in 2.5.x. +# Please see https://www.playframework.com/documentation/latest/GlobalSettings +# for more information. +# +# You can also extend Play functionality by using one of the publically available +# Play modules: https://playframework.com/documentation/latest/ModuleDirectory +play.modules { + # By default, Play will load any class called Module that is defined + # in the root package (the "app" directory), or you can define them + # explicitly below. + # If there are any built-in modules that you want to enable, you can list them here. + #enabled += my.application.Module + + # If there are any built-in modules that you want to disable, you can list them here. + #disabled += "" + enabled += modules.AssessmentModule +} + +## IDE +# https://www.playframework.com/documentation/latest/IDE +# ~~~~~ +# Depending on your IDE, you can add a hyperlink for errors that will jump you +# directly to the code location in the IDE in dev mode. The following line makes +# use of the IntelliJ IDEA REST interface: +#play.editor="http://localhost:63342/api/file/?file=%s&line=%s" + +## Internationalisation +# https://www.playframework.com/documentation/latest/JavaI18N +# https://www.playframework.com/documentation/latest/ScalaI18N +# ~~~~~ +# Play comes with its own i18n settings, which allow the user's preferred language +# to map through to internal messages, or allow the language to be stored in a cookie. +play.i18n { + # The application languages + langs = [ "en" ] + + # Whether the language cookie should be secure or not + #langCookieSecure = true + + # Whether the HTTP only attribute of the cookie should be set to true + #langCookieHttpOnly = true +} + +## Play HTTP settings +# ~~~~~ +play.http { + ## Router + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # Define the Router object to use for this application. + # This router will be looked up first when the application is starting up, + # so make sure this is the entry point. + # Furthermore, it's assumed your route file is named properly. + # So for an application router like `my.application.Router`, + # you may need to define a router file `conf/my.application.routes`. + # Default to Routes in the root package (aka "apps" folder) (and conf/routes) + #router = my.application.Router + + ## Action Creator + # https://www.playframework.com/documentation/latest/JavaActionCreator + # ~~~~~ + #actionCreator = null + + ## ErrorHandler + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # If null, will attempt to load a class called ErrorHandler in the root package, + #errorHandler = null + + ## Session & Flash + # https://www.playframework.com/documentation/latest/JavaSessionFlash + # https://www.playframework.com/documentation/latest/ScalaSessionFlash + # ~~~~~ + session { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + + # Sets the max-age field of the cookie to 5 minutes. + # NOTE: this only sets when the browser will discard the cookie. Play will consider any + # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, + # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. + #maxAge = 300 + + # Sets the domain on the session cookie. + #domain = "example.com" + } + + flash { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + } +} + +play.server.http.idleTimeout = 60s +play.http.parser.maxDiskBuffer = 10MB +parsers.anyContent.maxLength = 10MB + +## Netty Provider +# https://www.playframework.com/documentation/latest/SettingsNetty +# ~~~~~ +play.server.netty { + # Whether the Netty wire should be logged + log.wire = true + + # If you run Play on Linux, you can use Netty's native socket transport + # for higher performance with less garbage. + transport = "native" +} + +## WS (HTTP Client) +# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS +# ~~~~~ +# The HTTP client primarily used for REST APIs. The default client can be +# configured directly, but you can also create different client instances +# with customized settings. You must enable this by adding to build.sbt: +# +# libraryDependencies += ws // or javaWs if using java +# +play.ws { + # Sets HTTP requests not to follow 302 requests + #followRedirects = false + + # Sets the maximum number of open HTTP connections for the client. + #ahc.maxConnectionsTotal = 50 + + ## WS SSL + # https://www.playframework.com/documentation/latest/WsSSL + # ~~~~~ + ssl { + # Configuring HTTPS with Play WS does not require programming. You can + # set up both trustManager and keyManager for mutual authentication, and + # turn on JSSE debugging in development with a reload. + #debug.handshake = true + #trustManager = { + # stores = [ + # { type = "JKS", path = "exampletrust.jks" } + # ] + #} + } +} + +## Cache +# https://www.playframework.com/documentation/latest/JavaCache +# https://www.playframework.com/documentation/latest/ScalaCache +# ~~~~~ +# Play comes with an integrated cache API that can reduce the operational +# overhead of repeated requests. You must enable this by adding to build.sbt: +# +# libraryDependencies += cache +# +play.cache { + # If you want to bind several caches, you can bind the individually + #bindCaches = ["db-cache", "user-cache", "session-cache"] +} + +## Filter Configuration +# https://www.playframework.com/documentation/latest/Filters +# ~~~~~ +# There are a number of built-in filters that can be enabled and configured +# to give Play greater security. +# +play.filters { + + # Enabled filters are run automatically against Play. + # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. + enabled = [filters.AccessLogFilter] + + # Disabled filters remove elements from the enabled list. + # disabled += filters.CSRFFilter + + + ## CORS filter configuration + # https://www.playframework.com/documentation/latest/CorsFilter + # ~~~~~ + # CORS is a protocol that allows web applications to make requests from the browser + # across different domains. + # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has + # dependencies on CORS settings. + cors { + # Filter paths by a whitelist of path prefixes + #pathPrefixes = ["/some/path", ...] + + # The allowed origins. If null, all origins are allowed. + #allowedOrigins = ["http://www.example.com"] + + # The allowed HTTP methods. If null, all methods are allowed + #allowedHttpMethods = ["GET", "POST"] + } + + ## Security headers filter configuration + # https://www.playframework.com/documentation/latest/SecurityHeaders + # ~~~~~ + # Defines security headers that prevent XSS attacks. + # If enabled, then all options are set to the below configuration by default: + headers { + # The X-Frame-Options header. If null, the header is not set. + #frameOptions = "DENY" + + # The X-XSS-Protection header. If null, the header is not set. + #xssProtection = "1; mode=block" + + # The X-Content-Type-Options header. If null, the header is not set. + #contentTypeOptions = "nosniff" + + # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. + #permittedCrossDomainPolicies = "master-only" + + # The Content-Security-Policy header. If null, the header is not set. + #contentSecurityPolicy = "default-src 'self'" + } + + ## Allowed hosts filter configuration + # https://www.playframework.com/documentation/latest/AllowedHostsFilter + # ~~~~~ + # Play provides a filter that lets you configure which hosts can access your application. + # This is useful to prevent cache poisoning attacks. + hosts { + # Allow requests to example.com, its subdomains, and localhost:9000. + #allowed = [".example.com", "localhost:9000"] + } +} + +play.http.parser.maxMemoryBuffer = 50MB +akka.http.parsing.max-content-length = 50MB +schema.base_path="../../schemas/" + +# Cassandra Configuration +cassandra.lp.connection="127.0.0.1:9042" +content.keyspace = "content_store" + +# Redis Configuration +redis.host="localhost" +redis.port=6379 +redis.maxConnections=128 + +# Graph Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + +#Top N Config for Search Telemetry +telemetry_env=dev + +installation.id=ekstep + + +languageCode { + assamese : "as" + bengali : "bn" + english : "en" + gujarati : "gu" + hindi : "hi" + kannada : "ka" + marathi : "mr" + odia : "or" + tamil : "ta" + telugu : "te" +} + +kafka { + urls : "localhost:9092" + topic.send.enable : true + topics.instruction : "sunbirddev.assessment.publish.request" +} +objectcategorydefinition.keyspace="category_store" +question { + keyspace = "question_store" + list.limit=20 +} +questionset.keyspace="hierarchy_store" + +cassandra { + lp { + connection: "localhost:9042" + } + lpa { + connection: "localhost:9042" + } +} +neo4j_objecttypes_enabled=["Question"] + +import { + request_size_limit = 200 + output_topic_name = "local.auto.creation.job.request" + required_props { + question = ["name", "code", "mimeType", "framework", "channel"] + questionseet = ["name", "code", "mimeType", "framework", "channel"] + } + remove_props { + question = [] + questionseet = [] + } +} \ No newline at end of file diff --git a/learning-api/content-service/conf/logback.xml b/assessment-api/assessment-service/conf/logback.xml similarity index 100% rename from learning-api/content-service/conf/logback.xml rename to assessment-api/assessment-service/conf/logback.xml diff --git a/assessment-api/assessment-service/conf/routes b/assessment-api/assessment-service/conf/routes new file mode 100644 index 000000000..e1fd74252 --- /dev/null +++ b/assessment-api/assessment-service/conf/routes @@ -0,0 +1,38 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ +GET /health controllers.HealthController.health +GET /service/health controllers.HealthController.serviceHealth + +# ItemSet API's +POST /itemset/v3/create controllers.v3.ItemSetController.create +GET /itemset/v3/read/:identifier controllers.v3.ItemSetController.read(identifier:String, fields:Option[String]) +PATCH /itemset/v3/update/:identifier controllers.v3.ItemSetController.update(identifier:String) +POST /itemset/v3/review/:identifier controllers.v3.ItemSetController.review(identifier:String) +DELETE /itemset/v3/retire/:identifier controllers.v3.ItemSetController.retire(identifier:String) + +# Question API's +POST /question/v4/create controllers.v4.QuestionController.create +GET /question/v4/read/:identifier controllers.v4.QuestionController.read(identifier:String, mode:Option[String], fields:Option[String]) +PATCH /question/v4/update/:identifier controllers.v4.QuestionController.update(identifier:String) +POST /question/v4/review/:identifier controllers.v4.QuestionController.review(identifier:String) +POST /question/v4/publish/:identifier controllers.v4.QuestionController.publish(identifier:String) +DELETE /question/v4/retire/:identifier controllers.v4.QuestionController.retire(identifier:String) +POST /question/v4/import controllers.v4.QuestionController.importQuestion() +PATCH /question/v4/system/update/:identifier controllers.v4.QuestionController.systemUpdate(identifier:String) +POST /question/v4/list controllers.v4.QuestionController.list(fields:Option[String]) + +# QuestionSet API's +POST /questionset/v4/create controllers.v4.QuestionSetController.create +GET /questionset/v4/read/:identifier controllers.v4.QuestionSetController.read(identifier:String, mode:Option[String], fields:Option[String]) +PATCH /questionset/v4/update/:identifier controllers.v4.QuestionSetController.update(identifier:String) +POST /questionset/v4/review/:identifier controllers.v4.QuestionSetController.review(identifier:String) +POST /questionset/v4/publish/:identifier controllers.v4.QuestionSetController.publish(identifier:String) +DELETE /questionset/v4/retire/:identifier controllers.v4.QuestionSetController.retire(identifier:String) +PATCH /questionset/v4/add controllers.v4.QuestionSetController.add +DELETE /questionset/v4/remove controllers.v4.QuestionSetController.remove +PATCH /questionset/v4/hierarchy/update controllers.v4.QuestionSetController.updateHierarchy +GET /questionset/v4/hierarchy/:identifier controllers.v4.QuestionSetController.getHierarchy(identifier:String, mode:Option[String]) +POST /questionset/v4/reject/:identifier controllers.v4.QuestionSetController.reject(identifier:String) +POST /questionset/v4/import controllers.v4.QuestionSetController.importQuestionSet() +PATCH /questionset/v4/system/update/:identifier controllers.v4.QuestionSetController.systemUpdate(identifier:String) \ No newline at end of file diff --git a/assessment-api/assessment-service/pom.xml b/assessment-api/assessment-service/pom.xml new file mode 100644 index 000000000..620262701 --- /dev/null +++ b/assessment-api/assessment-service/pom.xml @@ -0,0 +1,144 @@ + + + + assessment-api + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + assessment-service + play2 + + + + scalaz-bintray + Scalaz Bintray - releases + https://dl.bintray.com/scalaz/releases/ + + false + + + + + + typesafe-releases-plugins + https://repo.typesafe.com/typesafe/releases/ + + false + + + + + 2.7.2 + 1.0.0-rc5 + 1.0.0 + + + + + com.typesafe.play + play_${scala.major.version} + ${play2.version} + + + com.typesafe.play + play-guice_${scala.major.version} + ${play2.version} + + + com.typesafe.play + filters-helpers_${scala.major.version} + ${play2.version} + + + com.typesafe.play + play-logback_${scala.major.version} + ${play2.version} + runtime + + + com.typesafe.play + play-netty-server_${scala.major.version} + ${play2.version} + runtime + + + org.scala-lang + scala-library + ${scala.version} + + + org.sunbird + assessment-actors + 1.0-SNAPSHOT + jar + + + org.scalatest + scalatest_${scala.maj.version} + 3.1.2 + test + + + com.typesafe.play + play-specs2_${scala.maj.version} + ${play2.version} + test + + + org.joda + joda-convert + 2.2.1 + + + + ${basedir}/app + ${basedir}/test + + + ${basedir}/conf + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + + **/*Spec.java + **/*Test.java + + + + + com.google.code.play2-maven-plugin + play2-maven-plugin + ${play2.plugin.version} + true + + + com.google.code.sbt-compiler-maven-plugin + sbt-compiler-maven-plugin + ${sbt-compiler.plugin.version} + + -feature -deprecation -Xfatal-warnings + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + .*RoutesPrefix.*;.*Routes.*;.*javascript.* + + + + + + \ No newline at end of file diff --git a/assessment-api/assessment-service/test/controllers/base/BaseSpec.scala b/assessment-api/assessment-service/test/controllers/base/BaseSpec.scala new file mode 100644 index 000000000..883693c86 --- /dev/null +++ b/assessment-api/assessment-service/test/controllers/base/BaseSpec.scala @@ -0,0 +1,38 @@ +package controllers.base + +import com.typesafe.config.ConfigFactory +import modules.TestModule +import org.specs2.mutable.Specification +import play.api.inject.guice.GuiceApplicationBuilder +import play.api.libs.json.Json +import play.api.mvc.Result +import play.api.test.Helpers.{POST, contentAsString, contentType, defaultAwaitTimeout, route, status, _} +import play.api.test.{FakeHeaders, FakeRequest} + +import scala.concurrent.Future + +class BaseSpec extends Specification { + implicit val app = new GuiceApplicationBuilder() + .disable(classOf[modules.AssessmentModule]) + .bindings(new TestModule) + .build + implicit val config = ConfigFactory.load() + + def post(apiURL: String, request: String, h: FakeHeaders = FakeHeaders(Seq())) + : Future[Result] = { + val headers = h.add(("content-type", "application/json")) + route(app, FakeRequest(POST, apiURL, headers, Json.toJson(Json.parse(request)))).get + } + + def isOK(response: Future[Result]) { + status(response) must equalTo(OK) + contentType(response) must beSome.which(_ == "application/json") + contentAsString(response) must contain(""""status":"successful"""") + } + + def hasClientError(response: Future[Result]) { + status(response) must equalTo(BAD_REQUEST) + contentType(response) must beSome.which(_ == "application/json") + contentAsString(response) must contain(""""err":"CLIENT_ERROR","status":"failed"""") + } +} diff --git a/assessment-api/assessment-service/test/controllers/v3/HealthControllerSpec.scala b/assessment-api/assessment-service/test/controllers/v3/HealthControllerSpec.scala new file mode 100644 index 000000000..49f351a62 --- /dev/null +++ b/assessment-api/assessment-service/test/controllers/v3/HealthControllerSpec.scala @@ -0,0 +1,18 @@ +package controllers.v3 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.{FakeRequest, Helpers} +import play.api.test.Helpers.{OK, status} + +@RunWith(classOf[JUnitRunner]) +class HealthControllerSpec extends BaseSpec { + + "return api health status report - successful response" in { + val controller = app.injector.instanceOf[controllers.HealthController] + val result = controller.health()(FakeRequest()) + isOK(result) + status(result)(Helpers.defaultAwaitTimeout) must equalTo(OK) + } +} diff --git a/assessment-api/assessment-service/test/controllers/v3/ItemSetControllerSpec.scala b/assessment-api/assessment-service/test/controllers/v3/ItemSetControllerSpec.scala new file mode 100644 index 000000000..c0a7cd546 --- /dev/null +++ b/assessment-api/assessment-service/test/controllers/v3/ItemSetControllerSpec.scala @@ -0,0 +1,43 @@ +package controllers.v3 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, defaultAwaitTimeout, status} + +@RunWith(classOf[JUnitRunner]) +class ItemSetControllerSpec extends BaseSpec { + + val controller = app.injector.instanceOf[controllers.v3.ItemSetController] + + "create should create an itemset successfully for given valid request" in { + val result = controller.create()(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "read should return an itemset successfully for given valid identifier" in { + val result = controller.read("do_123", None)(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "update should update the itemset successfully for given valid identifier" in { + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "review should update the itemset status to Review successfully for given valid identifier" in { + val result = controller.review("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "retire should update the itemset status to Retired successfully for given valid identifier" in { + val result = controller.retire("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } +} diff --git a/assessment-api/assessment-service/test/controllers/v4/QuestionControllerSpec.scala b/assessment-api/assessment-service/test/controllers/v4/QuestionControllerSpec.scala new file mode 100644 index 000000000..94e8d4329 --- /dev/null +++ b/assessment-api/assessment-service/test/controllers/v4/QuestionControllerSpec.scala @@ -0,0 +1,67 @@ +package controllers.v4 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, defaultAwaitTimeout, status} + +@RunWith(classOf[JUnitRunner]) +class QuestionControllerSpec extends BaseSpec { + + val controller = app.injector.instanceOf[controllers.v4.QuestionController] + + "create should create an question successfully for given valid request" in { + val result = controller.create()(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "read should return an question successfully for given valid identifier" in { + val result = controller.read("do_123", None, None)(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "update should update the question successfully for given valid identifier" in { + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "review should update the question status to Review successfully for given valid identifier" in { + val result = controller.review("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "publish should update the question status to Live successfully for given valid identifier" in { + val result = controller.publish("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "retire should update the question status to Retired successfully for given valid identifier" in { + val result = controller.retire("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "import should create a question successfully for given valid request" in { + val result = controller.importQuestion()(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "systemUpdate should update an question successfully for given valid request" in { + val result = controller.systemUpdate("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "list should list all the questions for given list of ids in the request" in { + val result = controller.list(None)(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } +} diff --git a/assessment-api/assessment-service/test/controllers/v4/QuestionSetControllerSpec.scala b/assessment-api/assessment-service/test/controllers/v4/QuestionSetControllerSpec.scala new file mode 100644 index 000000000..23d12518a --- /dev/null +++ b/assessment-api/assessment-service/test/controllers/v4/QuestionSetControllerSpec.scala @@ -0,0 +1,93 @@ +package controllers.v4 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, defaultAwaitTimeout, status} + +@RunWith(classOf[JUnitRunner]) +class QuestionSetControllerSpec extends BaseSpec { + + val controller = app.injector.instanceOf[controllers.v4.QuestionSetController] + + "create should create an questionSet successfully for given valid request" in { + val result = controller.create()(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "read should return an questionSet successfully for given valid identifier" in { + val result = controller.read("do_123", None, None)(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "update should update the questionSet successfully for given valid identifier" in { + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "review should update the questionSet status to Review successfully for given valid identifier" in { + val result = controller.review("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "publish should update the questionSet status to Live successfully for given valid identifier" in { + val result = controller.publish("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "retire should update the questionSet status to Retired successfully for given valid identifier" in { + val result = controller.retire("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "update Hierarchy should update hierarchy successfully for given valid identifier" in { + val result = controller.updateHierarchy()(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "read Hierarchy should read Hierarchy successfully for given valid identifier" in { + val result = controller.getHierarchy("do_123", None)(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + + "add question should update the questionSet status to Add question successfully for given valid identifier" in { + val result = controller.add()(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + + "remove question should update the questionSet status to remove question successfully for given valid identifier" in { + val result = controller.remove()(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "review should update the questionSet status to Reject successfully for given valid identifier" in { + val result = controller.reject("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "import should create an questionSet successfully for given valid request" in { + val result = controller.importQuestionSet()(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } + + "systemUpdate should update an questionSet successfully for given valid request" in { + val result = controller.systemUpdate("do_123")(FakeRequest()) + isOK(result) + status(result)(defaultAwaitTimeout) must equalTo(OK) + } +} diff --git a/assessment-api/assessment-service/test/modules/TestModule.scala b/assessment-api/assessment-service/test/modules/TestModule.scala new file mode 100644 index 000000000..8867e7cbc --- /dev/null +++ b/assessment-api/assessment-service/test/modules/TestModule.scala @@ -0,0 +1,28 @@ +package modules + +import com.google.inject.AbstractModule +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import play.libs.akka.AkkaGuiceSupport +import utils.ActorNames + +import scala.concurrent.{ExecutionContext, Future} + +class TestModule extends AbstractModule with AkkaGuiceSupport { + override def configure(): Unit = { + bindActor(classOf[TestActor], ActorNames.HEALTH_ACTOR) + bindActor(classOf[TestActor], ActorNames.ITEM_SET_ACTOR) + bindActor(classOf[TestActor], ActorNames.QUESTION_ACTOR) + bindActor(classOf[TestActor], ActorNames.QUESTION_SET_ACTOR) + println("Test Module is initialized...") + } +} + +class TestActor extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = { + Future(ResponseHandler.OK) + } +} diff --git a/assessment-api/pom.xml b/assessment-api/pom.xml new file mode 100644 index 000000000..b1bb913ed --- /dev/null +++ b/assessment-api/pom.xml @@ -0,0 +1,59 @@ + + + + knowledge-platform + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + assessment-api + pom + assessment-api + + assessment-actors + assessment-service + qs-hierarchy-manager + + + + UTF-8 + UTF-8 + 2.11 + + + + + + + maven-assembly-plugin + 3.3.0 + + + src/assembly/bin.xml + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + + + + org.scoverage + scoverage-maven-plugin + + ${scala.version} + true + true + + + + + + + \ No newline at end of file diff --git a/assessment-api/qs-hierarchy-manager/pom.xml b/assessment-api/qs-hierarchy-manager/pom.xml new file mode 100644 index 000000000..e4de07f22 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/pom.xml @@ -0,0 +1,136 @@ + + + + assessment-api + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + qs-hierarchy-manager + 1.0-SNAPSHOT + + + + org.sunbird + graph-engine_2.11 + 1.0-SNAPSHOT + jar + + + org.sunbird + platform-common + 1.0-SNAPSHOT + + + org.scala-lang + scala-library + ${scala.version} + + + org.scalatest + scalatest_${scala.maj.version} + 3.1.2 + test + + + org.neo4j + neo4j-bolt + 3.5.0 + test + + + org.neo4j + neo4j-graphdb-api + 3.5.0 + test + + + org.neo4j + neo4j + 3.5.0 + test + + + org.cassandraunit + cassandra-unit + 3.11.2.0 + test + + + httpcore + org.apache.httpcomponents + + + httpclient + org.apache.httpcomponents + + + + + com.mashape.unirest + unirest-java + 1.4.9 + + + + + + src/main/scala + src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + ${scala.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + + test + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + + + \ 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 new file mode 100644 index 000000000..6b3c8f8aa --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala @@ -0,0 +1,601 @@ +package org.sunbird.managers + +import java.util +import java.util.concurrent.CompletionException + +import com.fasterxml.jackson.databind.ObjectMapper +import org.apache.commons.lang3.StringUtils +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ErrorCodes, ResourceNotFoundException, ResponseCode, ServerException} +import org.sunbird.common.{JsonUtils, Platform} +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.{NodeUtil, ScalaJsonUtils} + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ +import scala.collection.JavaConverters +import scala.concurrent.{ExecutionContext, Future} +import com.mashape.unirest.http.HttpResponse +import com.mashape.unirest.http.Unirest +import org.apache.commons.collections4.{CollectionUtils, MapUtils} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.utils.{HierarchyConstants} + +object HierarchyManager { + + val schemaName: String = "questionset" + val schemaVersion: String = "1.0" + val imgSuffix: String = ".img" + val hierarchyPrefix: String = "qs_hierarchy_" + val statusList = List("Live", "Unlisted", "Flagged") + val ASSESSMENT_OBJECT_TYPES = List("Question", "QuestionSet") + + val keyTobeRemoved = { + if(Platform.config.hasPath("content.hierarchy.removed_props_for_leafNodes")) + Platform.config.getStringList("content.hierarchy.removed_props_for_leafNodes") + else + java.util.Arrays.asList("collections","children","usedByContent","item_sets","methods","libraries","editorState") + } + + @throws[Exception] + def addLeafNodesToHierarchy(request:Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + validateRequest(request) + val rootNodeFuture = getRootNode(request) + rootNodeFuture.map(rootNode => { + val unitId = request.getRequest.getOrDefault("collectionId", "").asInstanceOf[String] + if (StringUtils.isBlank(unitId)) attachLeafToRootNode(request, rootNode, "add") else { + val rootNodeMap = NodeUtil.serialize(rootNode, java.util.Arrays.asList("childNodes", "originData"), schemaName, schemaVersion) + if(!rootNodeMap.get("childNodes").asInstanceOf[Array[String]].toList.contains(unitId)) { + Future{ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "collectionId " + unitId + " does not exist")} + }else { + val hierarchyFuture = fetchHierarchy(request, rootNode.getIdentifier) + hierarchyFuture.map(hierarchy => { + if(hierarchy.isEmpty){ + Future{ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), "hierarchy is empty")} + } else { + val leafNodesFuture = fetchLeafNodes(request) + leafNodesFuture.map(leafNodes => { + updateRootNode(rootNode, request, "add").map(node => { + val updateResponse = updateHierarchy(unitId, hierarchy, leafNodes, node, request, "add") + updateResponse.map(response => { + if(!ResponseHandler.checkError(response)) { + ResponseHandler.OK + .put("rootId", node.getIdentifier.replaceAll(imgSuffix, "")) + .put(unitId, Map("children" -> request.get("children")).asJava) + }else { + response + } + }) + }).flatMap(f => f) + }).flatMap(f => f) + } + }).flatMap(f => f) + } + } + }).flatMap(f => f) recoverWith {case e: CompletionException => throw e.getCause} + } + + @throws[Exception] + def removeLeafNodesFromHierarchy(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + validateRequest(request) + val rootNodeFuture = getRootNode(request) + rootNodeFuture.map(rootNode => { + val unitId = request.getRequest.getOrDefault("collectionId", "").asInstanceOf[String] + if (StringUtils.isBlank(unitId)) attachLeafToRootNode(request, rootNode, "remove") else { + val rootNodeMap = NodeUtil.serialize(rootNode, java.util.Arrays.asList("childNodes", "originData"), schemaName, schemaVersion) + if(!rootNodeMap.get("childNodes").asInstanceOf[Array[String]].toList.contains(unitId)) { + Future{ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "collectionId " + unitId + " does not exist")} + }else { + val hierarchyFuture = fetchHierarchy(request, rootNode.getIdentifier) + hierarchyFuture.map(hierarchy => { + if(hierarchy.isEmpty){ + Future{ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), "hierarchy is empty")} + } else { + updateRootNode(rootNode, request, "remove").map(node =>{ + val updateResponse = updateHierarchy(unitId, hierarchy, null, node, request, "remove") + updateResponse.map(response => { + if(!ResponseHandler.checkError(response)) { + ResponseHandler.OK.put("rootId", node.getIdentifier.replaceAll(imgSuffix, "")) + } else { + response + } + }) + }).flatMap(f => f) + } + }).flatMap(f => f) + } + } + }).flatMap(f => f) recoverWith {case e: CompletionException => throw e.getCause} + } + + def attachLeafToRootNode(request: Request, rootNode: Node, operation: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + fetchHierarchy(request, rootNode.getIdentifier).map(hierarchy => { + if (hierarchy.isEmpty) { + Future(ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), "hierarchy is empty")) + } else { + fetchLeafNodes(request).map(leafNodes => { + updateRootNode(rootNode, request, operation).map(node => { + updateRootHierarchy(hierarchy, leafNodes, node, request, operation).map(response => { + if (!ResponseHandler.checkError(response)) { + ResponseHandler.OK + .put("rootId", node.getIdentifier.replaceAll(imgSuffix, "")) + .put("children", request.get("children")) + } else response + }) + }).flatMap(f => f) + }).flatMap(f => f) + } + }).flatMap(f => f) + } + + def updateRootHierarchy(hierarchy: java.util.Map[String, AnyRef], leafNodes: List[Node], rootNode: Node, request: Request, operation: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext) = { + val leafNodeIds = request.get("children").asInstanceOf[util.List[String]] + val req = new Request(request) + if ("add".equalsIgnoreCase(operation)) { + val updatedChildren = restructureUnit(hierarchy.get("children").asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]], + convertNodeToMap(leafNodes), leafNodeIds, 1, rootNode.getIdentifier.replace(".img", "")) + val updatedHierarchy = Map("children" -> updatedChildren, "identifier" -> rootNode.getIdentifier.replace(".img", "")).asJava + req.put("hierarchy", ScalaJsonUtils.serialize(updatedHierarchy)) + } + if ("remove".equalsIgnoreCase(operation)) { + val filteredChildren = hierarchy.get("children") + .asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]].asScala + .filter(child => !leafNodeIds.contains(child.get("identifier"))) + filteredChildren.sortBy(_.get("index").asInstanceOf[Integer]) + .zipWithIndex.foreach(zippedChild => zippedChild._1.put("index", (zippedChild._2.asInstanceOf[Integer] + 1).asInstanceOf[Object])) + val updatedHierarchy = Map("children" -> filteredChildren, "identifier" -> rootNode.getIdentifier.replace(".img", "")).asJava + req.put("hierarchy", ScalaJsonUtils.serialize(updatedHierarchy)) + } + req.put("identifier", rootNode.getIdentifier) + oec.graphService.saveExternalProps(req) + } + + @throws[Exception] + def getHierarchy(request : Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val mode = request.get("mode").asInstanceOf[String] + if(StringUtils.isNotEmpty(mode) && mode.equals("edit")) + getUnPublishedHierarchy(request) + else + getPublishedHierarchy(request) + } + + @throws[Exception] + def getUnPublishedHierarchy(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val rootNodeFuture = getRootNode(request) + rootNodeFuture.map(rootNode => { + if (StringUtils.equalsIgnoreCase("Retired", rootNode.getMetadata.getOrDefault("status", "").asInstanceOf[String])) { + Future(ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "rootId " + request.get("rootId") + " does not exist")) + } + val bookmarkId = request.get("bookmarkId").asInstanceOf[String] + var metadata: util.Map[String, AnyRef] = NodeUtil.serialize(rootNode, new util.ArrayList[String](), request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + val hierarchy = fetchHierarchy(request, rootNode.getIdentifier) + hierarchy.map(hierarchy => { + val children = hierarchy.getOrDefault("children", new util.ArrayList[java.util.Map[String, AnyRef]]).asInstanceOf[util.ArrayList[java.util.Map[String, AnyRef]]] + val leafNodeIds = new util.ArrayList[String]() + fetchAllLeafNodes(children, leafNodeIds) + getLatestLeafNodes(leafNodeIds).map(leafNodesMap => { + updateLatestLeafNodes(children, leafNodesMap) + metadata.put("children", children) + metadata.put("identifier", request.get("rootId")) + if(StringUtils.isNotEmpty(bookmarkId)) + metadata = filterBookmarkHierarchy(metadata.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]], bookmarkId) + if (MapUtils.isEmpty(metadata)) { + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "bookmarkId " + bookmarkId + " does not exist") + } else { + ResponseHandler.OK.put("questionSet", metadata) + } + }) + }).flatMap(f => f) + }).flatMap(f => f) recoverWith { case e: ResourceNotFoundException => { + val searchResponse = searchRootIdInElasticSearch(request.get("rootId").asInstanceOf[String]) + searchResponse.map(rootHierarchy => { + if(!rootHierarchy.isEmpty && StringUtils.isNotEmpty(rootHierarchy.asInstanceOf[util.HashMap[String, AnyRef]].get("identifier").asInstanceOf[String])){ + val unPublishedBookmarkHierarchy = getUnpublishedBookmarkHierarchy(request, rootHierarchy.asInstanceOf[util.HashMap[String, AnyRef]].get("identifier").asInstanceOf[String]) + unPublishedBookmarkHierarchy.map(hierarchy => { + if (!hierarchy.isEmpty) { + val children = hierarchy.getOrDefault("children", new util.ArrayList[java.util.Map[String, AnyRef]]).asInstanceOf[util.ArrayList[java.util.Map[String, AnyRef]]] + val leafNodeIds = new util.ArrayList[String]() + fetchAllLeafNodes(children, leafNodeIds) + getLatestLeafNodes(leafNodeIds).map(leafNodesMap => { + updateLatestLeafNodes(children, leafNodesMap) + hierarchy.put("children", children) + }) + ResponseHandler.OK.put("questionSet", hierarchy) + } else + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "rootId " + request.get("rootId") + " does not exist") + }) + } else { + Future(ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "rootId " + request.get("rootId") + " does not exist")) + } + }).flatMap(f => f) + } + } + } + + @throws[Exception] + def getPublishedHierarchy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + val redisHierarchy = if(Platform.getBoolean("questionset.cache.enable", false)) RedisCache.get(hierarchyPrefix + request.get("rootId")) else "" + + val hierarchyFuture = if (StringUtils.isNotEmpty(redisHierarchy)) { + Future(mapAsJavaMap(Map("questionSet" -> JsonUtils.deserialize(redisHierarchy, classOf[java.util.Map[String, AnyRef]])))) + } else getCassandraHierarchy(request) + hierarchyFuture.map(result => { + if (!result.isEmpty) { + val bookmarkId = request.get("bookmarkId").asInstanceOf[String] + val rootHierarchy = result.get("questionSet").asInstanceOf[util.Map[String, AnyRef]] + if (StringUtils.isEmpty(bookmarkId)) { + ResponseHandler.OK.put("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) + if (MapUtils.isEmpty(bookmarkHierarchy)) { + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "bookmarkId " + bookmarkId + " does not exist") + } else { + ResponseHandler.OK.put("questionSet", bookmarkHierarchy) + } + } + } else + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "rootId " + request.get("rootId") + " does not exist") + }) + } + + def validateRequest(request: Request)(implicit ec: ExecutionContext) = { + val rootId = request.get("rootId").asInstanceOf[String] + val children = request.get("children").asInstanceOf[java.util.List[String]] + if (StringUtils.isBlank(rootId)) { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "rootId is mandatory") + } + if (null == children || children.isEmpty) { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "children are mandatory") + } + } + + private def getRootNode(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val req = new Request(request) + req.put("identifier", request.get("rootId").asInstanceOf[String]) + req.put("mode", request.get("mode").asInstanceOf[String]) + req.put("fields",request.get("fields").asInstanceOf[java.util.List[String]]) + DataNode.read(req) + } + + def fetchLeafNodes(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + val leafNodes = request.get("children").asInstanceOf[java.util.List[String]] + val req = new Request(request) + req.put("identifiers", leafNodes) + DataNode.list(req).map(nodes => { + if(nodes.size() != leafNodes.size()) { + val filteredList = leafNodes.toList.filter(id => !nodes.contains(id)) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Children which are not available are: " + filteredList) + } else { + val invalidNodes = nodes.filterNot(node => ASSESSMENT_OBJECT_TYPES.contains(node.getObjectType)) + if (CollectionUtils.isNotEmpty(invalidNodes)) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), s"Children must be of types $ASSESSMENT_OBJECT_TYPES for ids: ${invalidNodes.map(_.getIdentifier)}") + else nodes.toList + } + }) + } + + def convertNodeToMap(leafNodes: List[Node])(implicit oec: OntologyEngineContext, ec: ExecutionContext): java.util.List[java.util.Map[String, AnyRef]] = { + leafNodes.map(node => { + val nodeMap:java.util.Map[String,AnyRef] = NodeUtil.serialize(node, null, node.getObjectType.toLowerCase().replace("image", ""), schemaVersion) + nodeMap.keySet().removeAll(keyTobeRemoved) + nodeMap + }) + } + + def addChildrenToUnit(children: java.util.List[java.util.Map[String,AnyRef]], unitId:String, leafNodes: java.util.List[java.util.Map[String, AnyRef]], leafNodeIds: java.util.List[String]): Unit = { + val childNodes = children.filter(child => ("Parent".equalsIgnoreCase(child.get("visibility").asInstanceOf[String]) && unitId.equalsIgnoreCase(child.get("identifier").asInstanceOf[String]))).toList + if(null != childNodes && !childNodes.isEmpty){ + val child = childNodes.get(0) + val childList = child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]] + val restructuredChildren: java.util.List[java.util.Map[String,AnyRef]] = restructureUnit(childList, leafNodes, leafNodeIds, (child.get("depth").asInstanceOf[Integer] + 1), unitId) + child.put("children", restructuredChildren) + } else { + for(child <- children) { + if(null !=child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) + addChildrenToUnit(child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]], unitId, leafNodes, leafNodeIds) + } + } + } + + def removeChildrenFromUnit(children: java.util.List[java.util.Map[String, AnyRef]], unitId: String, leafNodeIds: java.util.List[String]):Unit = { + val childNodes = children.filter(child => ("Parent".equalsIgnoreCase(child.get("visibility").asInstanceOf[String]) && unitId.equalsIgnoreCase(child.get("identifier").asInstanceOf[String]))).toList + if(null != childNodes && !childNodes.isEmpty){ + val child = childNodes.get(0) + if(null != child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) { + var filteredLeafNodes = child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].filter(existingLeafNode => { + !leafNodeIds.contains(existingLeafNode.get("identifier").asInstanceOf[String]) + }) + var index: Integer = 1 + filteredLeafNodes.toList.sortBy(x => x.get("index").asInstanceOf[Integer]).foreach(node => { + node.put("index", index) + index += 1 + }) + child.put("children", filteredLeafNodes) + } + } else { + for(child <- children) { + if(null !=child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) + removeChildrenFromUnit(child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]], unitId, leafNodeIds) + } + } + } + + def updateRootNode(rootNode: Node, request: Request, operation: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext) = { + val req = new Request(request) + val leafNodes = request.get("children").asInstanceOf[java.util.List[String]] + var childNodes = new java.util.ArrayList[String]() + childNodes.addAll(rootNode.getMetadata.getOrDefault("childNodes", Array[String]()).asInstanceOf[Array[String]].toList) + if(operation.equalsIgnoreCase("add")) + childNodes.addAll(leafNodes) + if(operation.equalsIgnoreCase("remove")) + childNodes.removeAll(leafNodes) + req.put("childNodes", childNodes.distinct.toArray) + req.getContext.put("identifier", rootNode.getIdentifier.replaceAll(imgSuffix, "")) + req.getContext.put("skipValidation", java.lang.Boolean.TRUE) + DataNode.update(req) + } + + def updateHierarchy(unitId: String, hierarchy: java.util.Map[String, AnyRef], leafNodes: List[Node], rootNode: Node, request: Request, operation: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext) = { + val children = hierarchy.get("children").asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] + val leafNodeIds = request.get("children").asInstanceOf[java.util.List[String]] + if("add".equalsIgnoreCase(operation)){ + val leafNodesMap:java.util.List[java.util.Map[String, AnyRef]] = convertNodeToMap(leafNodes) + addChildrenToUnit(children, unitId, leafNodesMap, leafNodeIds) + } + if("remove".equalsIgnoreCase(operation)) { + removeChildrenFromUnit(children,unitId, leafNodeIds) + } + val rootId = rootNode.getIdentifier.replaceAll(imgSuffix, "") + val updatedHierarchy = new java.util.HashMap[String, AnyRef]() + updatedHierarchy.put("identifier", rootId) + updatedHierarchy.put("children", children) + val req = new Request(request) + req.put("hierarchy", ScalaJsonUtils.serialize(updatedHierarchy)) + req.put("identifier", rootNode.getIdentifier) + oec.graphService.saveExternalProps(req) + } + + def restructureUnit(childList: java.util.List[java.util.Map[String, AnyRef]], leafNodes: java.util.List[java.util.Map[String, AnyRef]], leafNodeIds: java.util.List[String], depth: Integer, parent: String): java.util.List[java.util.Map[String, AnyRef]] = { + var maxIndex:Integer = 0 + var leafNodeMap: java.util.Map[String, java.util.Map[String, AnyRef]] = new util.HashMap[String, java.util.Map[String, AnyRef]]() + for(leafNode <- leafNodes){ + leafNodeMap.put(leafNode.get("identifier").asInstanceOf[String], JavaConverters.mapAsJavaMapConverter(leafNode).asJava) + } + var filteredLeafNodes: java.util.List[java.util.Map[String, AnyRef]] = new util.ArrayList[java.util.Map[String, AnyRef]]() + if(null != childList && !childList.isEmpty) { + val childMap:Map[String, java.util.Map[String, AnyRef]] = childList.toList.map(f => f.get("identifier").asInstanceOf[String] -> f).toMap + val existingLeafNodes = childMap.filter(p => leafNodeIds.contains(p._1)) + existingLeafNodes.map(en => { + leafNodeMap.get(en._1).put("index", en._2.get("index").asInstanceOf[Integer]) + }) + filteredLeafNodes = bufferAsJavaList(childList.filter(existingLeafNode => { + !leafNodeIds.contains(existingLeafNode.get("identifier").asInstanceOf[String]) + })) + maxIndex = childMap.values.toList.map(child => child.get("index").asInstanceOf[Integer]).toList.max.asInstanceOf[Integer] + } + leafNodeIds.foreach(id => { + var node = leafNodeMap.getOrDefault(id, new util.HashMap[String, AnyRef]()) + node.put("parent", parent) + node.put("depth", depth) + if( null == node.get("index")) { + val index:Integer = maxIndex + 1 + node.put("index", index) + maxIndex += 1 + } + filteredLeafNodes.add(node) + }) + filteredLeafNodes + } + + def fetchHierarchy(request: Request, identifier: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Map[String, AnyRef]] = { + val req = new Request(request) + req.put("identifier", identifier) + val responseFuture = oec.graphService.readExternalProps(req, List("hierarchy")) + responseFuture.map(response => { + if (!ResponseHandler.checkError(response)) { + val hierarchyString = response.getResult.toMap.getOrDefault("hierarchy", "").asInstanceOf[String] + if (StringUtils.isNotEmpty(hierarchyString)) { + Future(JsonUtils.deserialize(hierarchyString, classOf[java.util.Map[String, AnyRef]]).toMap) + } else + Future(Map[String, AnyRef]()) + } else if (ResponseHandler.checkError(response) && response.getResponseCode.code() == 404 && Platform.config.hasPath("collection.image.migration.enabled") && Platform.config.getBoolean("collection.image.migration.enabled")) { + req.put("identifier", identifier.replaceAll(".img", "") + ".img") + val responseFuture = oec.graphService.readExternalProps(req, List("hierarchy")) + responseFuture.map(response => { + if (!ResponseHandler.checkError(response)) { + val hierarchyString = response.getResult.toMap.getOrDefault("hierarchy", "").asInstanceOf[String] + if (StringUtils.isNotEmpty(hierarchyString)) { + JsonUtils.deserialize(hierarchyString, classOf[java.util.Map[String, AnyRef]]).toMap + } else + Map[String, AnyRef]() + } else if (ResponseHandler.checkError(response) && response.getResponseCode.code() == 404) + Map[String, AnyRef]() + else + throw new ServerException("ERR_WHILE_FETCHING_HIERARCHY_FROM_CASSANDRA", "Error while fetching hierarchy from cassandra") + }) + } else if (ResponseHandler.checkError(response) && response.getResponseCode.code() == 404) + Future(Map[String, AnyRef]()) + else + throw new ServerException("ERR_WHILE_FETCHING_HIERARCHY_FROM_CASSANDRA", "Error while fetching hierarchy from cassandra") + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + def getCassandraHierarchy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[util.Map[String, AnyRef]] = { + val rootHierarchy: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() + 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])) { + val hierarchyMap = mapAsJavaMap(hierarchy) + rootHierarchy.put("questionSet", hierarchyMap) + RedisCache.set(hierarchyPrefix + request.get("rootId"), JsonUtils.serialize(hierarchyMap)) + Future(rootHierarchy) + } else { + Future(new util.HashMap[String, AnyRef]()) + } + } else { + val searchResponse = searchRootIdInElasticSearch(request.get("rootId").asInstanceOf[String]) + searchResponse.map(response => { + if (!response.isEmpty) { + if (StringUtils.isNotEmpty(response.getOrDefault("identifier", "").asInstanceOf[String])) { + val parentHierarchy = fetchHierarchy(request, response.get("identifier").asInstanceOf[String]) + parentHierarchy.map(hierarchy => { + if (!hierarchy.isEmpty) { + if (StringUtils.isNoneEmpty(hierarchy.getOrDefault("status", "").asInstanceOf[String]) && statusList.contains(hierarchy.getOrDefault("status", "").asInstanceOf[String]) && CollectionUtils.isNotEmpty(mapAsJavaMap(hierarchy).get("children").asInstanceOf[util.ArrayList[util.HashMap[String, AnyRef]]])) { + val bookmarkHierarchy = filterBookmarkHierarchy(mapAsJavaMap(hierarchy).get("children").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]], request.get("rootId").asInstanceOf[String]) + if (!bookmarkHierarchy.isEmpty) { + rootHierarchy.put("questionSet", hierarchy) + RedisCache.set(hierarchyPrefix + request.get("rootId"), JsonUtils.serialize(hierarchy)) + rootHierarchy + } else { + new util.HashMap[String, AnyRef]() + } + } else { + new util.HashMap[String, AnyRef]() + } + } else { + new util.HashMap[String, AnyRef]() + } + }) + } else { + Future(new util.HashMap[String, AnyRef]()) + } + } else { + Future(new util.HashMap[String, AnyRef]()) + } + }).flatMap(f => f) + } + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + def searchRootIdInElasticSearch(rootId: String)(implicit ec: ExecutionContext): Future[util.Map[String, AnyRef]] = { + val mapper: ObjectMapper = new ObjectMapper() + val searchRequest: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() { + put("request", new util.HashMap[String, AnyRef]() { + put("filters", new util.HashMap[String, AnyRef]() { + put("status", new util.ArrayList[String]() { + add("Live"); + add("Unlisted") + }) + put("mimeType", "application/vnd.ekstep.content-collection") + put("childNodes", new util.ArrayList[String]() { + add(rootId) + }) + put("visibility", "Default") + }) + put("fields", new util.ArrayList[String]() { + add("identifier") + }) + }) + } + val url: String = if (Platform.config.hasPath("composite.search.url")) Platform.config.getString("composite.search.url") else "https://dev.sunbirded.org/action/composite/v3/search" + val httpResponse: HttpResponse[String] = Unirest.post(url).header("Content-Type", "application/json").body(mapper.writeValueAsString(searchRequest)).asString + if (httpResponse.getStatus == 200) { + val response: Response = JsonUtils.deserialize(httpResponse.getBody, classOf[Response]) + if (response.get("count").asInstanceOf[Integer] > 0 && CollectionUtils.isNotEmpty(response.get("content").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]])) { + Future(response.get("content").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]].get(0)) + } else { + Future(new util.HashMap[String, AnyRef]()) + } + } else { + throw new ServerException("SERVER_ERROR", "Invalid response from search") + } + } + + def filterBookmarkHierarchy(children: util.List[util.Map[String, AnyRef]], bookmarkId: String)(implicit ec: ExecutionContext): util.Map[String, AnyRef] = { + if (CollectionUtils.isNotEmpty(children)) { + val response = children.filter(_.get("identifier") == bookmarkId).toList + if (CollectionUtils.isNotEmpty(response)) { + response.get(0) + } else { + val nextChildren = bufferAsJavaList(children.flatMap(child => { + if (!child.isEmpty && CollectionUtils.isNotEmpty(child.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]])) + child.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + else new util.ArrayList[util.Map[String, AnyRef]] + })) + filterBookmarkHierarchy(nextChildren, bookmarkId) + } + } else { + new util.HashMap[String, AnyRef]() + } + } + + def getUnpublishedBookmarkHierarchy(request: Request, identifier: String)(implicit ec: ExecutionContext, oec:OntologyEngineContext): Future[util.Map[String, AnyRef]] = { + if (StringUtils.isNotEmpty(identifier)) { + val parentHierarchy = fetchHierarchy(request, identifier + imgSuffix) + parentHierarchy.map(hierarchy => { + if (!hierarchy.isEmpty && CollectionUtils.isNotEmpty(mapAsJavaMap(hierarchy).get("children").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]])) { + val bookmarkHierarchy = filterBookmarkHierarchy(mapAsJavaMap(hierarchy).get("children").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]], request.get("rootId").asInstanceOf[String]) + if (!bookmarkHierarchy.isEmpty) { + bookmarkHierarchy + } else { + new util.HashMap[String, AnyRef]() + } + } else { + new util.HashMap[String, AnyRef]() + } + }) + } else { + Future(new util.HashMap[String, AnyRef]()) + } + } + + def updateLatestLeafNodes(children: util.List[util.Map[String, AnyRef]], leafNodeMap: util.Map[String, AnyRef]): List[Any] = { + children.toList.map(content => { + if(StringUtils.equalsIgnoreCase("Default", content.getOrDefault("visibility", "").asInstanceOf[String])) { + val metadata: util.Map[String, AnyRef] = leafNodeMap.getOrDefault(content.get("identifier").asInstanceOf[String], new java.util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + if(HierarchyConstants.RETIRED_STATUS.equalsIgnoreCase(metadata.getOrDefault("status", HierarchyConstants.RETIRED_STATUS).asInstanceOf[String])){ + children.remove(content) + } else { + content.putAll(metadata) + } + } else { + updateLatestLeafNodes(content.getOrDefault("children", new util.ArrayList[Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]], leafNodeMap) + } + }) + } + + def fetchAllLeafNodes(children: util.List[util.Map[String, AnyRef]], leafNodeIds: util.List[String]): List[Any] = { + children.toList.map(content => { + if(StringUtils.equalsIgnoreCase("Default", content.getOrDefault("visibility", "").asInstanceOf[String])) { + leafNodeIds.add(content.get("identifier").asInstanceOf[String]) + leafNodeIds + } else { + fetchAllLeafNodes(content.getOrDefault("children", new util.ArrayList[Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]], leafNodeIds) + } + }) + } + + def getLatestLeafNodes(leafNodeIds : util.List[String])(implicit oec: OntologyEngineContext, ec: ExecutionContext) = { + if(CollectionUtils.isNotEmpty(leafNodeIds)) { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put(HierarchyConstants.GRAPH_ID, HierarchyConstants.TAXONOMY_ID) + } + }) + request.put("identifiers", leafNodeIds) + DataNode.list(request).map(nodes => { + val leafNodeMap: Map[String, AnyRef] = nodes.toList.map(node => (node.getIdentifier, NodeUtil.serialize(node, null, node.getObjectType.toLowerCase.replace("image", ""), HierarchyConstants.SCHEMA_VERSION, true).asInstanceOf[AnyRef])).toMap + val imageNodeIds: util.List[String] = JavaConverters.seqAsJavaListConverter(leafNodeIds.toList.map(id => id + HierarchyConstants.IMAGE_SUFFIX)).asJava + request.put("identifiers", imageNodeIds) + DataNode.list(request).map(imageNodes => { + val imageLeafNodeMap: Map[String, AnyRef] = imageNodes.toList.map(imageNode => { + val identifier = imageNode.getIdentifier.replaceAll(HierarchyConstants.IMAGE_SUFFIX, "") + val metadata = NodeUtil.serialize(imageNode, null, imageNode.getObjectType.toLowerCase.replace("image", ""), HierarchyConstants.SCHEMA_VERSION, true) + metadata.replace("identifier", identifier) + (identifier, metadata.asInstanceOf[AnyRef]) + }).toMap + val updatedMap = leafNodeMap ++ imageLeafNodeMap + JavaConverters.mapAsJavaMapConverter(updatedMap).asJava + }) + }).flatMap(f => f) + } else { + Future{new util.HashMap[String, AnyRef]()} + } + } + +} 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 new file mode 100644 index 000000000..fb480c39f --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala @@ -0,0 +1,513 @@ +package org.sunbird.managers + +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} +import org.sunbird.common.exception.{ClientException, ErrorCodes, ResourceNotFoundException, ServerException} +import org.sunbird.common.{DateUtils, JsonUtils, Platform} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.Identifier +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.external.ExternalPropsManager +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.schema.DefinitionNode +import org.sunbird.graph.utils.{NodeUtil, ScalaJsonUtils} +import org.sunbird.telemetry.logger.TelemetryManager +import org.sunbird.utils.{HierarchyConstants, HierarchyErrorCodes} + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.concurrent.{ExecutionContext, Future} + +object UpdateHierarchyManager { + val neo4jCreateTypes: java.util.List[String] = Platform.getStringList("neo4j_objecttypes_enabled", List("Question").asJava) + + @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 => { + 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) + addChildNodesInNodeList(existingChildren, request, nodes).map(list => (existingHierarchy, list)) + }).flatMap(f => f) + .map(result => { + val nodes = result._2 + TelemetryManager.info("NodeList final size: " + nodes.size) + val idMap: mutable.Map[String, String] = mutable.Map() + idMap += (rootId -> rootId) + updateNodesModifiedInNodeList(nodes, nodesModified, request, idMap).map(modifiedNodeList => { + getChildrenHierarchy(modifiedNodeList, rootId, hierarchy, idMap, result._1).map(children => { + TelemetryManager.log("Children for root id :" + rootId +" :: " + JsonUtils.serialize(children)) + updateHierarchyData(rootId, children, modifiedNodeList, request).map(node => { + val response = ResponseHandler.OK() + response.put(HierarchyConstants.IDENTIFIER, rootId) + idMap.remove(rootId) + response.put(HierarchyConstants.IDENTIFIERS, mapAsJavaMap(idMap)) + if (request.getContext.getOrDefault("shouldImageDelete", false.asInstanceOf[AnyRef]).asInstanceOf[Boolean]) + deleteHierarchy(request) + Future(response) + }).flatMap(f => f) + }).flatMap(f => f) + }).flatMap(f => f) + }) + }).flatMap(f => f).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + private def validateRequest(request: Request)(implicit ec: ExecutionContext): (java.util.HashMap[String, AnyRef], java.util.HashMap[String, AnyRef]) = { + if (!request.getRequest.contains(HierarchyConstants.NODES_MODIFIED) && !request.getRequest.contains(HierarchyConstants.HIERARCHY)) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Hierarchy data is empty") + val nodesModified: java.util.HashMap[String, AnyRef] = request.getRequest.get(HierarchyConstants.NODES_MODIFIED).asInstanceOf[java.util.HashMap[String, AnyRef]] + val hierarchy: java.util.HashMap[String, AnyRef] = request.getRequest.get(HierarchyConstants.HIERARCHY).asInstanceOf[java.util.HashMap[String, AnyRef]] + hierarchy.asScala.keys.foreach(key => { + if (StringUtils.equalsIgnoreCase(nodesModified.getOrDefault(key, new util.HashMap()).asInstanceOf[util.Map[String, AnyRef]] + .getOrDefault(HierarchyConstants.OBJECT_TYPE, "").asInstanceOf[String], "Question")) + throw new ClientException("ERR_QS_UPDATE_HIERARCHY", "Question cannot have children in hierarchy") + }) + (nodesModified, hierarchy) + } + + /** + * Checks if root id is empty, all black or image id + * + * @param nodesModified + * @param hierarchy + * @param ec + * @return + */ + private def getRootId(nodesModified: java.util.HashMap[String, AnyRef], hierarchy: java.util.HashMap[String, AnyRef])(implicit ec: ExecutionContext): String = { + val rootId: String = nodesModified.keySet() + .find(key => nodesModified.get(key).asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.ROOT).asInstanceOf[Boolean]) + .getOrElse(hierarchy.keySet().find(key => hierarchy.get(key).asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.ROOT).asInstanceOf[Boolean]).orNull) + if (StringUtils.isEmpty(rootId) && StringUtils.isAllBlank(rootId) || StringUtils.contains(rootId, HierarchyConstants.IMAGE_SUFFIX)) + throw new ClientException(HierarchyErrorCodes.ERR_INVALID_ROOT_ID, "Please Provide Valid Root Node Identifier") + rootId + } + + private def getValidatedRootNode(identifier: String, request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val req = new Request(request) + req.put(HierarchyConstants.IDENTIFIER, identifier) + req.put(HierarchyConstants.MODE, HierarchyConstants.EDIT_MODE) + DataNode.read(req).map(rootNode => { + val metadata: java.util.Map[String, AnyRef] = NodeUtil.serialize(rootNode, new java.util.ArrayList[String](), request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + if (!StringUtils.equals(metadata.get(HierarchyConstants.MIME_TYPE).asInstanceOf[String], HierarchyConstants.QUESTIONSET_MIME_TYPE)) { + throw new ClientException(HierarchyErrorCodes.ERR_INVALID_ROOT_ID, "Invalid MimeType for Root Node Identifier : " + identifier) + TelemetryManager.error("UpdateHierarchyManager.getValidatedRootNode :: Invalid MimeType for Root node id: " + identifier) + } + if(!StringUtils.equals(metadata.getOrDefault(HierarchyConstants.VISIBILITY, "").asInstanceOf[String], HierarchyConstants.DEFAULT)) { + TelemetryManager.error("UpdateHierarchyManager.getValidatedRootNode :: Invalid Visibility found for Root node id: " + identifier) + throw new ClientException(HierarchyErrorCodes.ERR_INVALID_ROOT_ID, "Invalid Visibility found for Root Node Identifier : " + identifier) + } + rootNode.setObjectType(HierarchyConstants.QUESTIONSET_OBJECT_TYPE) + rootNode.getMetadata.put(HierarchyConstants.OBJECT_TYPE, HierarchyConstants.QUESTIONSET_OBJECT_TYPE) + rootNode + }) + } + + private def getExistingHierarchy(request: Request, rootNode: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[java.util.HashMap[String, AnyRef]] = { + fetchHierarchy(request, rootNode).map(hierarchyString => { + if (null != hierarchyString && !hierarchyString.asInstanceOf[String].isEmpty) { + JsonUtils.deserialize(hierarchyString.asInstanceOf[String], classOf[java.util.HashMap[String, AnyRef]]) + } else new java.util.HashMap[String, AnyRef]() + }) + } + + private def fetchHierarchy(request: Request, rootNode: Node)(implicit ec: ExecutionContext, oec:OntologyEngineContext): Future[Any] = { + val req = new Request(request) + req.put(HierarchyConstants.IDENTIFIER, rootNode.getIdentifier) + oec.graphService.readExternalProps(req, List(HierarchyConstants.HIERARCHY)).map(response => { + if (ResponseHandler.checkError(response) && ResponseHandler.isResponseNotFoundError(response)) { + if (CollectionUtils.containsAny(HierarchyConstants.HIERARCHY_LIVE_STATUS, rootNode.getMetadata.get("status").asInstanceOf[String])) + throw new ServerException(HierarchyErrorCodes.ERR_HIERARCHY_NOT_FOUND, "No hierarchy is present in cassandra for identifier:" + rootNode.getIdentifier) + else { + if (rootNode.getMetadata.containsKey("pkgVersion")) + req.put(HierarchyConstants.IDENTIFIER, rootNode.getIdentifier.replace(HierarchyConstants.IMAGE_SUFFIX, "")) + else { + req.put(HierarchyConstants.IDENTIFIER, if (!rootNode.getIdentifier.endsWith(HierarchyConstants.IMAGE_SUFFIX)) rootNode.getIdentifier + HierarchyConstants.IMAGE_SUFFIX else rootNode.getIdentifier) + } + oec.graphService.readExternalProps(req, List(HierarchyConstants.HIERARCHY)).map(resp => { + resp.getResult.toMap.getOrElse(HierarchyConstants.HIERARCHY, "").asInstanceOf[String] + }) recover { case e: ResourceNotFoundException => TelemetryManager.log("No hierarchy is present in cassandra for identifier:" + rootNode.getIdentifier) } + } + } else Future(response.getResult.toMap.getOrElse(HierarchyConstants.HIERARCHY, "").asInstanceOf[String]) + }).flatMap(f => f) + } + + private def addChildNodesInNodeList(childrenMaps: java.util.List[java.util.Map[String, AnyRef]], request: Request, nodes: scala.collection.immutable.List[Node])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[scala.collection.immutable.List[Node]] = { + if (CollectionUtils.isNotEmpty(childrenMaps)) { + val futures = childrenMaps.map(child => { + addNodeToList(child, request, nodes).map(modifiedList => { + if (!StringUtils.equalsIgnoreCase(HierarchyConstants.DEFAULT, child.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])) { + addChildNodesInNodeList(child.get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]], request, modifiedList) + } else + Future(modifiedList) + }).flatMap(f => f) + }).toList + Future.sequence(futures).map(f => f.flatten.distinct) + } else { + Future(nodes) + } + } + + private def addNodeToList(child: java.util.Map[String, AnyRef], request: Request, nodes: scala.collection.immutable.List[Node])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[scala.collection.immutable.List[Node]] = { + if (StringUtils.isNotEmpty(child.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])) + if (StringUtils.equalsIgnoreCase(HierarchyConstants.DEFAULT, child.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])) { + getQuestionNode(child.getOrDefault(HierarchyConstants.IDENTIFIER, "").asInstanceOf[String], HierarchyConstants.TAXONOMY_ID).map(node => { + node.getMetadata.put(HierarchyConstants.DEPTH, child.get(HierarchyConstants.DEPTH)) + node.getMetadata.put(HierarchyConstants.PARENT, child.get(HierarchyConstants.PARENT)) + node.getMetadata.put(HierarchyConstants.INDEX, child.get(HierarchyConstants.INDEX)) + node.setObjectType(HierarchyConstants.QUESTION_OBJECT_TYPE) + node.getMetadata.put(HierarchyConstants.OBJECT_TYPE, HierarchyConstants.QUESTION_OBJECT_TYPE) + val updatedNodes = node :: nodes + updatedNodes + }) recoverWith { case e: CompletionException => throw e.getCause } + } else { + val childData: java.util.Map[String, AnyRef] = new java.util.HashMap[String, AnyRef] + childData.putAll(child) + childData.remove(HierarchyConstants.CHILDREN) + childData.put(HierarchyConstants.STATUS, "Draft") + val rootNode = getTempNode(nodes, request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String]) + childData.put(HierarchyConstants.CHANNEL, rootNode.getMetadata.get(HierarchyConstants.CHANNEL)) + val node = NodeUtil.deserialize(childData, request.getContext.get(HierarchyConstants.SCHEMA_NAME).asInstanceOf[String], DefinitionNode.getRelationsMap(request)) + node.setObjectType(node.getMetadata.getOrDefault("objectType", "").asInstanceOf[String]) + val updatedNodes = node :: nodes + Future(updatedNodes) + } + else { + Future(nodes) + } + } + + + private def updateNodesModifiedInNodeList(nodeList: List[Node], nodesModified: java.util.HashMap[String, AnyRef], request: Request, idMap: mutable.Map[String, String])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + updateRootNode(request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String], nodeList, nodesModified) + val futures = nodesModified.filter(nodeModified => !StringUtils.startsWith(request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String], nodeModified._1)) + .map(nodeModified => { + val objectType = nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].getOrDefault(HierarchyConstants.OBJECT_TYPE, "").asInstanceOf[String] + if(StringUtils.isBlank(objectType)) + throw new ClientException("ERR_UPDATE_QS_HIERARCHY", s"Object Type is mandatory for creation of node with id: ${nodeModified._1}") + val metadata = nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].getOrDefault(HierarchyConstants.METADATA, new java.util.HashMap()).asInstanceOf[java.util.HashMap[String, AnyRef]] + if(!StringUtils.equalsIgnoreCase(metadata.getOrDefault(HierarchyConstants.VISIBILITY, "Parent").asInstanceOf[String], "Parent")) + throw new ClientException("ERR_UPDATE_QS_HIERARCHY", s"Visibility can be only of type Parent for identifier: ${nodeModified._1}") + metadata.remove(HierarchyConstants.DIALCODES) + metadata.put(HierarchyConstants.STATUS, "Draft") + metadata.put(HierarchyConstants.LAST_UPDATED_ON, DateUtils.formatCurrentDate) + metadata.put(HierarchyConstants.OBJECT_TYPE, objectType) + if (nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].containsKey(HierarchyConstants.IS_NEW) + && nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.IS_NEW).asInstanceOf[Boolean]) { + if (!nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.ROOT).asInstanceOf[Boolean]) + metadata.put(HierarchyConstants.VISIBILITY, HierarchyConstants.PARENT) + if (nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].contains(HierarchyConstants.SET_DEFAULT_VALUE)) + createNewNode(nodeModified._1, idMap, metadata, nodeList, request, nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.SET_DEFAULT_VALUE).asInstanceOf[Boolean]) + else + createNewNode(nodeModified._1, idMap, metadata, nodeList, request) + } else { + updateTempNode(request, nodeModified._1, nodeList, idMap, metadata) + } + }) + if (CollectionUtils.isNotEmpty(futures)) + Future.sequence(futures.toList).map(f => f.flatten) + else Future(nodeList) + } + + private def updateRootNode(rootId: String, nodeList: List[Node], nodesModified: java.util.HashMap[String, AnyRef])(implicit ec: ExecutionContext): Unit = { + if (nodesModified.containsKey(rootId)) { + val metadata = nodesModified.getOrDefault(rootId, new java.util.HashMap()).asInstanceOf[java.util.HashMap[String, AnyRef]].getOrDefault(HierarchyConstants.METADATA, new java.util.HashMap()).asInstanceOf[java.util.HashMap[String, AnyRef]] + updateNodeList(nodeList, rootId, metadata) + nodesModified.remove(rootId) + } + } + + private def createNewNode(nodeId: String, idMap: mutable.Map[String, String], metadata: java.util.HashMap[String, AnyRef], nodeList: List[Node], request: Request, setDefaultValue: Boolean = true)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + val objectType = metadata.getOrDefault("objectType", "").asInstanceOf[String] + metadata.remove("objectType") + val identifier: String = Identifier.getIdentifier(HierarchyConstants.TAXONOMY_ID, Identifier.getUniqueIdFromTimestamp) + idMap += (nodeId -> identifier) + metadata.put(HierarchyConstants.IDENTIFIER, identifier) + metadata.put(HierarchyConstants.CODE, nodeId) + metadata.put(HierarchyConstants.VERSION_KEY, System.currentTimeMillis + "") + metadata.put(HierarchyConstants.CREATED_ON, DateUtils.formatCurrentDate) + metadata.put(HierarchyConstants.LAST_STATUS_CHANGED_ON, DateUtils.formatCurrentDate) + val rootNode = getTempNode(nodeList, request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String]) + metadata.put(HierarchyConstants.CHANNEL, rootNode.getMetadata.get(HierarchyConstants.CHANNEL)) + val createRequest: Request = new Request(request) + createRequest.setRequest(metadata) + if (neo4jCreateTypes.contains(objectType)) { + createRequest.getContext.put(HierarchyConstants.SCHEMA_NAME, "question") + DataNode.create(createRequest).map(node => { + node.setGraphId(HierarchyConstants.TAXONOMY_ID) + node.setNodeType(HierarchyConstants.DATA_NODE) + node.setObjectType(objectType) + val updatedList = node :: nodeList + updatedList.distinct + }) + + } else + DefinitionNode.validate(createRequest, setDefaultValue).map(node => { + node.setGraphId(HierarchyConstants.TAXONOMY_ID) + node.setNodeType(HierarchyConstants.DATA_NODE) + node.setObjectType(objectType) + val updatedList = node :: nodeList + updatedList.distinct + }) + } + + private def updateTempNode(request:Request, nodeId: String, nodeList: List[Node], idMap: mutable.Map[String, String], metadata: java.util.HashMap[String, AnyRef])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + val tempNode: Node = getTempNode(nodeList, nodeId) + if(null == tempNode) + throw new ResourceNotFoundException("ERR_UPDATE_QS_HIERARCHY", s"No node found with id: $nodeId") + else { + val objectType = metadata.getOrDefault("objectType", "").asInstanceOf[String] + metadata.put(HierarchyConstants.IDENTIFIER, tempNode.getIdentifier) + val createRequest: Request = new Request(request) + createRequest.setRequest(metadata) + if (neo4jCreateTypes.contains(objectType)) { + createRequest.getContext.put(HierarchyConstants.IDENTIFIER, tempNode.getIdentifier) + createRequest.getContext.put(HierarchyConstants.SCHEMA_NAME, "question") + createRequest.getContext.put(HierarchyConstants.OBJECT_TYPE, objectType) + DataNode.update(createRequest).map(node => { + idMap += (nodeId -> node.getIdentifier) + updateNodeList(nodeList, node.getIdentifier, node.getMetadata) + nodeList + }) + } else { + if (null != tempNode && StringUtils.isNotBlank(tempNode.getIdentifier)) { + metadata.put(HierarchyConstants.IDENTIFIER, tempNode.getIdentifier) + idMap += (nodeId -> tempNode.getIdentifier) + updateNodeList(nodeList, tempNode.getIdentifier, metadata) + Future(nodeList) + } else throw new ResourceNotFoundException(HierarchyErrorCodes.ERR_CONTENT_NOT_FOUND, "Content not found with identifier: " + nodeId) + } + } + } + + private def validateNodes(nodeList: java.util.List[Node], rootId: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + val nodesToValidate = nodeList.filter(node => (StringUtils.equals(HierarchyConstants.PARENT, node.getMetadata.get(HierarchyConstants.VISIBILITY).asInstanceOf[String]) + && !StringUtils.equalsIgnoreCase("Question", node.getObjectType)) + || StringUtils.equalsAnyIgnoreCase(rootId, node.getIdentifier)).toList + DefinitionNode.updateJsonPropsInNodes(nodeList.toList, HierarchyConstants.TAXONOMY_ID, HierarchyConstants.QUESTIONSET_SCHEMA_NAME, HierarchyConstants.SCHEMA_VERSION) + DefinitionNode.validateContentNodes(nodesToValidate, HierarchyConstants.TAXONOMY_ID, HierarchyConstants.QUESTIONSET_SCHEMA_NAME, HierarchyConstants.SCHEMA_VERSION) + } + + def constructHierarchy(list: List[java.util.Map[String, AnyRef]]): java.util.Map[String, AnyRef] = { + val hierarchy: java.util.Map[String, AnyRef] = list.filter(root => root.get(HierarchyConstants.DEPTH).asInstanceOf[Number].intValue() == 0).head + if (MapUtils.isNotEmpty(hierarchy)) { + val maxDepth = list.map(node => node.get(HierarchyConstants.DEPTH).asInstanceOf[Number].intValue()).max + for (i <- 0 to maxDepth) { + val depth = i + val currentLevelNodes: Map[String, List[java.util.Map[String, Object]]] = list.filter(node => node.get(HierarchyConstants.DEPTH).asInstanceOf[Number].intValue() == depth).groupBy(_.get("identifier").asInstanceOf[String].replaceAll(".img", "")) + val nextLevel: List[java.util.Map[String, AnyRef]] = list.filter(node => node.get(HierarchyConstants.DEPTH).asInstanceOf[Number].intValue() == (depth + 1)) + if (CollectionUtils.isNotEmpty(nextLevel) && MapUtils.isNotEmpty(currentLevelNodes)) { + nextLevel.foreach(e => { + val parentId = e.get("parent").asInstanceOf[String] + currentLevelNodes.getOrDefault(parentId, List[java.util.Map[String, AnyRef]]()).foreach(parent => { + val children = parent.getOrDefault(HierarchyConstants.CHILDREN, new java.util.ArrayList[java.util.Map[String, AnyRef]]()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] + children.add(e) + parent.put(HierarchyConstants.CHILDREN, sortByIndex(children)) + }) + }) + } + } + } + hierarchy + } + + @throws[Exception] + private def getChildrenHierarchy(nodeList: List[Node], rootId: String, hierarchyData: java.util.HashMap[String, AnyRef], idMap: mutable.Map[String, String], existingHierarchy: java.util.Map[String, AnyRef])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[java.util.List[java.util.Map[String, AnyRef]]] = { + val childrenIdentifiersMap: Map[String, Map[String, Int]] = getChildrenIdentifiersMap(hierarchyData, idMap, existingHierarchy) + getPreparedHierarchyData(nodeList, rootId, childrenIdentifiersMap).map(nodeMaps => { + TelemetryManager.info("prepared hierarchy list without filtering: " + nodeMaps.size()) + val filteredNodeMaps = nodeMaps.filter(nodeMap => null != nodeMap.get(HierarchyConstants.DEPTH)).toList + TelemetryManager.info("prepared hierarchy list with filtering: " + filteredNodeMaps.size()) + val hierarchyMap = constructHierarchy(filteredNodeMaps) + if (MapUtils.isNotEmpty(hierarchyMap)) { + hierarchyMap.getOrDefault(HierarchyConstants.CHILDREN, new java.util.ArrayList[java.util.Map[String, AnyRef]]()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] + .filter(child => MapUtils.isNotEmpty(child)) + } + else + new java.util.ArrayList[java.util.Map[String, AnyRef]]() + + }) + } + + private def getChildrenIdentifiersMap(hierarchyData: java.util.Map[String, AnyRef], idMap: mutable.Map[String, String], existingHierarchy: java.util.Map[String, AnyRef]): Map[String, Map[String, Int]] = { + if (MapUtils.isNotEmpty(hierarchyData)) { + hierarchyData.map(entry => idMap.getOrDefault(entry._1, entry._1) -> entry._2.asInstanceOf[java.util.HashMap[String, AnyRef]] + .get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.ArrayList[String]] + .map(id => idMap.getOrDefault(id, id)).zipWithIndex.toMap).toMap + } else { + val tempChildMap: java.util.Map[String, Map[String, Int]] = new java.util.HashMap[String, Map[String, Int]]() + val tempResourceMap: java.util.Map[String, Map[String, Int]] = new java.util.HashMap[String, Map[String, Int]]() + getChildrenIdMapFromExistingHierarchy(existingHierarchy, tempChildMap, tempResourceMap) + tempChildMap.putAll(tempResourceMap) + tempChildMap.toMap + } + } + + private def getChildrenIdMapFromExistingHierarchy(existingHierarchy: java.util.Map[String, AnyRef], tempChildMap: java.util.Map[String, Map[String, Int]], tempResourceMap: java.util.Map[String, Map[String, Int]]): Unit = { + if (existingHierarchy.containsKey(HierarchyConstants.CHILDREN) && CollectionUtils.isNotEmpty(existingHierarchy.get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.ArrayList[java.util.HashMap[String, AnyRef]]])) { + tempChildMap.put(existingHierarchy.get(HierarchyConstants.IDENTIFIER).asInstanceOf[String], existingHierarchy.get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.ArrayList[java.util.HashMap[String, AnyRef]]] + .map(child => child.get(HierarchyConstants.IDENTIFIER).asInstanceOf[String] -> child.get(HierarchyConstants.INDEX).asInstanceOf[Int]).toMap) + existingHierarchy.get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.ArrayList[java.util.HashMap[String, AnyRef]]] + .foreach(child => getChildrenIdMapFromExistingHierarchy(child, tempChildMap, tempResourceMap)) + } else + tempResourceMap.put(existingHierarchy.get(HierarchyConstants.IDENTIFIER).asInstanceOf[String], Map[String, Int]()) + } + + @throws[Exception] + private def getPreparedHierarchyData(nodeList: List[Node], rootId: String, childrenIdentifiersMap: Map[String, Map[String, Int]])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[java.util.List[java.util.Map[String, AnyRef]]] = { + if (MapUtils.isNotEmpty(childrenIdentifiersMap)) { + val updatedNodeList = getTempNode(nodeList, rootId) :: List() + updateHierarchyRelatedData(childrenIdentifiersMap.getOrElse(rootId, Map[String, Int]()), 1, + rootId, nodeList, childrenIdentifiersMap, updatedNodeList).map(finalEnrichedNodeList => { + TelemetryManager.info("Final enriched list size: " + finalEnrichedNodeList.size) + val childNodeIds = finalEnrichedNodeList.map(node => node.getIdentifier).filterNot(id => rootId.equalsIgnoreCase(id)).distinct + TelemetryManager.info("Final enriched ids (childNodes): " + childNodeIds + " :: size: " + childNodeIds.size) + updateNodeList(nodeList, rootId, new java.util.HashMap[String, AnyRef]() { + put(HierarchyConstants.DEPTH, 0.asInstanceOf[AnyRef]) + put(HierarchyConstants.CHILD_NODES, new java.util.ArrayList[String](childNodeIds)) + }) + validateNodes(finalEnrichedNodeList, rootId).map(result => HierarchyManager.convertNodeToMap(finalEnrichedNodeList)) + }).flatMap(f => f) + } else { + updateNodeList(nodeList, rootId, new java.util.HashMap[String, AnyRef]() { + { + put(HierarchyConstants.DEPTH, 0.asInstanceOf[AnyRef]) + } + }) + validateNodes(nodeList, rootId).map(result => HierarchyManager.convertNodeToMap(nodeList)) + } + } + + @throws[Exception] + private def updateHierarchyRelatedData(childrenIds: Map[String, Int], depth: Int, parent: String, nodeList: List[Node], hierarchyStructure: Map[String, Map[String, Int]], enrichedNodeList: scala.collection.immutable.List[Node])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[List[Node]] = { + val futures = childrenIds.map(child => { + val id = child._1 + val index = child._2 + 1 + val tempNode = getTempNode(nodeList, id) + if (null != tempNode && StringUtils.equalsIgnoreCase(HierarchyConstants.PARENT, tempNode.getMetadata.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])) { + populateHierarchyRelatedData(tempNode, depth, index, parent) + val nxtEnrichedNodeList = tempNode :: enrichedNodeList + if (MapUtils.isNotEmpty(hierarchyStructure.getOrDefault(child._1, Map[String, Int]()))) + updateHierarchyRelatedData(hierarchyStructure.getOrDefault(child._1, Map[String, Int]()), + tempNode.getMetadata.get(HierarchyConstants.DEPTH).asInstanceOf[Int] + 1, id, nodeList, hierarchyStructure, nxtEnrichedNodeList) + else + Future(nxtEnrichedNodeList) + } else { + getQuestionNode(id, HierarchyConstants.TAXONOMY_ID).map(node => { + populateHierarchyRelatedData(node, depth, index, parent) + //node.getMetadata.put(HierarchyConstants.VISIBILITY, HierarchyConstants.DEFAULT) + node.setObjectType(HierarchyConstants.QUESTION_OBJECT_TYPE) + node.getMetadata.put(HierarchyConstants.OBJECT_TYPE, HierarchyConstants.QUESTION_OBJECT_TYPE) + val nxtEnrichedNodeList = node :: enrichedNodeList + if (MapUtils.isNotEmpty(hierarchyStructure.getOrDefault(id, Map[String, Int]()))) { + updateHierarchyRelatedData(hierarchyStructure.getOrDefault(id, Map[String, Int]()), node.getMetadata.get(HierarchyConstants.DEPTH).asInstanceOf[Int] + 1, id, nodeList, hierarchyStructure, nxtEnrichedNodeList) + } else + Future(nxtEnrichedNodeList) + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + }) + if (CollectionUtils.isNotEmpty(futures)) { + val listOfFutures = Future.sequence(futures.toList) + listOfFutures.map(f => f.flatten.distinct) + } else + Future(enrichedNodeList) + } + + private def populateHierarchyRelatedData(tempNode: Node, depth: Int, index: Int, parent: String) = { + tempNode.getMetadata.put(HierarchyConstants.DEPTH, depth.asInstanceOf[AnyRef]) + tempNode.getMetadata.put(HierarchyConstants.PARENT, parent.replaceAll(".img", "")) + tempNode.getMetadata.put(HierarchyConstants.INDEX, index.asInstanceOf[AnyRef]) + } + + /** + * This method is to check if all the children of the parent entity are present in the populated map + * + * @param children + * @param populatedChildMap + * @return + */ + def isFullyPopulated(children: List[String], populatedChildMap: mutable.Map[_, _]): Boolean = { + children.forall(child => populatedChildMap.containsKey(child)) + } + + def updateHierarchyData(rootId: String, children: java.util.List[java.util.Map[String, AnyRef]], nodeList: List[Node], request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val node = getTempNode(nodeList, rootId) + val updatedHierarchy = new java.util.HashMap[String, AnyRef]() + updatedHierarchy.put(HierarchyConstants.IDENTIFIER, rootId) + updatedHierarchy.put(HierarchyConstants.CHILDREN, children) + val req = new Request(request) + req.getContext.put(HierarchyConstants.IDENTIFIER, rootId) + val metadata = cleanUpRootData(node) + req.getRequest.putAll(metadata) + req.put(HierarchyConstants.HIERARCHY, ScalaJsonUtils.serialize(updatedHierarchy)) + req.put(HierarchyConstants.IDENTIFIER, rootId) + req.put(HierarchyConstants.CHILDREN, new java.util.ArrayList()) + DataNode.update(req) + } + + private def cleanUpRootData(node: Node)(implicit oec: OntologyEngineContext, ec: ExecutionContext): java.util.Map[String, AnyRef] = { + DefinitionNode.getRestrictedProperties(HierarchyConstants.TAXONOMY_ID, HierarchyConstants.SCHEMA_VERSION, HierarchyConstants.OPERATION_UPDATE_HIERARCHY, HierarchyConstants.QUESTIONSET_SCHEMA_NAME) + .foreach(key => node.getMetadata.remove(key)) + node.getMetadata.remove(HierarchyConstants.STATUS) + node.getMetadata.remove(HierarchyConstants.LAST_UPDATED_ON) + node.getMetadata.remove(HierarchyConstants.LAST_STATUS_CHANGED_ON) + node.getMetadata + } + + /** + * Get the Node with ID provided from List else return Null. + * + * @param nodeList + * @param id + * @return + */ + private def getTempNode(nodeList: List[Node], id: String) = { + nodeList.find(node => StringUtils.startsWith(node.getIdentifier, id)).orNull + } + + private def updateNodeList(nodeList: List[Node], id: String, metadata: java.util.Map[String, AnyRef]): Unit = { + nodeList.foreach(node => { + if(node.getIdentifier.startsWith(id)){ + node.getMetadata.putAll(metadata) + } + }) + } + + def getQuestionNode(identifier: String, graphId: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val request: Request = new Request() + request.setContext(new java.util.HashMap[String, AnyRef]() { + { + put(HierarchyConstants.GRAPH_ID, graphId) + put(HierarchyConstants.VERSION, HierarchyConstants.SCHEMA_VERSION) + put(HierarchyConstants.OBJECT_TYPE, HierarchyConstants.QUESTION_OBJECT_TYPE) + put(HierarchyConstants.SCHEMA_NAME, HierarchyConstants.QUESTION_SCHEMA_NAME) + } + }) + request.setObjectType(HierarchyConstants.QUESTION_OBJECT_TYPE) + request.put(HierarchyConstants.IDENTIFIER, identifier) + request.put(HierarchyConstants.MODE, HierarchyConstants.READ_MODE) + request.put(HierarchyConstants.FIELDS, new java.util.ArrayList[String]()) + DataNode.read(request) + } + + + def sortByIndex(childrenMaps: java.util.List[java.util.Map[String, AnyRef]]): java.util.List[java.util.Map[String, AnyRef]] = { + bufferAsJavaList(childrenMaps.sortBy(_.get("index").asInstanceOf[Int])) + } + + + def deleteHierarchy(request: Request)(implicit ec: ExecutionContext): Future[Response] = { + val req = new Request(request) + val rootId = request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String] + req.put(HierarchyConstants.IDENTIFIERS, if (rootId.contains(HierarchyConstants.IMAGE_SUFFIX)) List(rootId) else List(rootId + HierarchyConstants.IMAGE_SUFFIX)) + ExternalPropsManager.deleteProps(req) + } + +} 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 new file mode 100644 index 000000000..badef1915 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala @@ -0,0 +1,53 @@ +package org.sunbird.utils + +object HierarchyConstants { + val DATA: String = "data" + val DATA_NODE: String = "DATA_NODE" + val NODES_MODIFIED: String = "nodesModified" + val HIERARCHY: String = "hierarchy" + val ROOT: String = "root" + val SET_DEFAULT_VALUE: String = "setDefaultValue" + val VERSION: String = "version" + val IDENTIFIER: String = "identifier" + val DEPTH: String = "depth" + val PARENT: String = "Parent" + val INDEX: String = "index" + val CHILDREN: String = "children" + val VISIBILITY: String = "visibility" + val TAXONOMY_ID: String = "domain" + val METADATA: String = "metadata" + val IS_NEW: String = "isNew" + val DIALCODES: String = "dialcodes" + val QUESTION_OBJECT_TYPE: String = "Question" + val OBJECT_TYPE = "objectType" + val STATUS: String = "status" + val LAST_UPDATED_ON: String = "lastUpdatedOn" + val CODE: String = "code" + val VERSION_KEY: String = "versionKey" + val CREATED_ON: String = "createdOn" + val LAST_STATUS_CHANGED_ON: String = "lastStatusChangedOn" + val CHILD_NODES: String = "childNodes" + val QUESTION_SCHEMA_NAME: String = "question" + val QUESTIONSET_SCHEMA_NAME: String = "questionset" + val SCHEMA_NAME: String = "schemaName" + val SCHEMA_VERSION: String = "1.0" + val CONTENT_ID: String = "identifier" + val IDENTIFIERS: String = "identifiers" + val DEFAULT: String = "Default" + val CHANNEL: String = "channel" + val ROOT_ID: String = "rootId" + val HIERARCHY_LIVE_STATUS: List[String] = List("Live", "Unlisted", "Flagged") + val IMAGE_SUFFIX: String = ".img" + val GRAPH_ID: String = "graph_id" + val MODE: String = "mode" + val EDIT_MODE: String = "edit" + val READ_MODE: String = "read" + val CONCEPTS: String = "concepts" + val FIELDS: String = "fields" + val MIME_TYPE: String = "mimeType" + val RETIRED_STATUS: String = "Retired" + val AUDIENCE: String = "audience" + val QUESTIONSET_MIME_TYPE: String = "application/vnd.sunbird.questionset" + val QUESTIONSET_OBJECT_TYPE: String = "QuestionSet" + val OPERATION_UPDATE_HIERARCHY: String = "updateHierarchy" +} diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyErrorCodes.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyErrorCodes.scala new file mode 100644 index 000000000..7b7e8b9ec --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyErrorCodes.scala @@ -0,0 +1,11 @@ +package org.sunbird.utils + +object HierarchyErrorCodes { + val ERR_INVALID_ROOT_ID: String = "ERR_INVALID_ROOT_ID" + val ERR_CONTENT_NOT_FOUND: String = "ERR_CONTENT_NOT_FOUND" + val ERR_HIERARCHY_NOT_FOUND: String = "ERR_HIERARCHY_NOT_FOUND" + val ERR_HIERARCHY_UPDATE_DENIED: String = "ERR_HIERARCHY_UPDATE_DENIED" + val ERR_ADD_HIERARCHY_DENIED: String = "ERR_ADD_HIERARCHY_DENIED" + val ERR_REMOVE_HIERARCHY_DENIED: String = "ERR_REMOVE_HIERARCHY_DENIED" + +} diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JavaJsonUtils.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JavaJsonUtils.scala new file mode 100644 index 000000000..8f1275ef3 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/JavaJsonUtils.scala @@ -0,0 +1,36 @@ +package org.sunbird.utils + +import java.lang.reflect.{ParameterizedType, Type} + +import com.fasterxml.jackson.core.`type`.TypeReference +import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper} + +object JavaJsonUtils { + + @transient val mapper = new ObjectMapper() + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + @throws(classOf[Exception]) + def serialize(obj: AnyRef): String = { + mapper.writeValueAsString(obj) + } + + @throws(classOf[Exception]) + def deserialize[T: Manifest](value: String): T = mapper.readValue(value, typeReference[T]) + + private[this] def typeReference[T: Manifest] = new TypeReference[T] { + override def getType = typeFromManifest(manifest[T]) + } + + + private[this] def typeFromManifest(m: Manifest[_]): Type = { + if (m.typeArguments.isEmpty) { m.runtimeClass } + // $COVERAGE-OFF$Disabling scoverage as this code is impossible to test + else new ParameterizedType { + def getRawType = m.runtimeClass + def getActualTypeArguments = m.typeArguments.map(typeFromManifest).toArray + def getOwnerType = null + } + // $COVERAGE-ON$ + } +} diff --git a/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/NodeUtil.scala b/assessment-api/qs-hierarchy-manager/src/main/scala/org/sunbird/utils/NodeUtil.scala new file mode 100644 index 000000000..e69de29bb diff --git a/assessment-api/qs-hierarchy-manager/src/test/resources/application.conf b/assessment-api/qs-hierarchy-manager/src/test/resources/application.conf new file mode 100644 index 000000000..046ae1a25 --- /dev/null +++ b/assessment-api/qs-hierarchy-manager/src/test/resources/application.conf @@ -0,0 +1,536 @@ +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +# ~~~~~ +# Play uses HOCON as its configuration file format. HOCON has a number +# of advantages over other config formats, but there are two things that +# can be used when modifying settings. +# +# You can include other configuration files in this main application.conf file: +#include "extra-config.conf" +# +# You can declare variables and substitute for them: +#mykey = ${some.value} +# +# And if an environment variable exists when there is no other substitution, then +# HOCON will fall back to substituting environment variable: +#mykey = ${JAVA_HOME} + +## Akka +# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration +# https://www.playframework.com/documentation/latest/JavaAkka#Configuration +# ~~~~~ +# Play uses Akka internally and exposes Akka Streams and actors in Websockets and +# other streaming HTTP responses. +akka { + # "akka.log-config-on-start" is extraordinarly useful because it log the complete + # configuration at INFO level, including defaults and overrides, so it s worth + # putting at the very top. + # + # Put the following in your conf/logback.xml file: + # + # + # + # And then uncomment this line to debug the configuration. + # + #log-config-on-start = true +} + +## Secret key +# http://www.playframework.com/documentation/latest/ApplicationSecret +# ~~~~~ +# The secret key is used to sign Play's session cookie. +# This must be changed for production, but we don't recommend you change it in this file. +play.http.secret.key = a-long-secret-to-calm-the-rage-of-the-entropy-gods + +## Modules +# https://www.playframework.com/documentation/latest/Modules +# ~~~~~ +# Control which modules are loaded when Play starts. Note that modules are +# the replacement for "GlobalSettings", which are deprecated in 2.5.x. +# Please see https://www.playframework.com/documentation/latest/GlobalSettings +# for more information. +# +# You can also extend Play functionality by using one of the publically available +# Play modules: https://playframework.com/documentation/latest/ModuleDirectory +play.modules { + # By default, Play will load any class called Module that is defined + # in the root package (the "app" directory), or you can define them + # explicitly below. + # If there are any built-in modules that you want to enable, you can list them here. + #enabled += my.application.Module + + # If there are any built-in modules that you want to disable, you can list them here. + #disabled += "" +} + +## IDE +# https://www.playframework.com/documentation/latest/IDE +# ~~~~~ +# Depending on your IDE, you can add a hyperlink for errors that will jump you +# directly to the code location in the IDE in dev mode. The following line makes +# use of the IntelliJ IDEA REST interface: +#play.editor="http://localhost:63342/api/file/?file=%s&line=%s" + +## Internationalisation +# https://www.playframework.com/documentation/latest/JavaI18N +# https://www.playframework.com/documentation/latest/ScalaI18N +# ~~~~~ +# Play comes with its own i18n settings, which allow the user's preferred language +# to map through to internal messages, or allow the language to be stored in a cookie. +play.i18n { + # The application languages + langs = [ "en" ] + + # Whether the language cookie should be secure or not + #langCookieSecure = true + + # Whether the HTTP only attribute of the cookie should be set to true + #langCookieHttpOnly = true +} + +## Play HTTP settings +# ~~~~~ +play.http { + ## Router + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # Define the Router object to use for this application. + # This router will be looked up first when the application is starting up, + # so make sure this is the entry point. + # Furthermore, it's assumed your route file is named properly. + # So for an application router like `my.application.Router`, + # you may need to define a router file `conf/my.application.routes`. + # Default to Routes in the root package (aka "apps" folder) (and conf/routes) + #router = my.application.Router + + ## Action Creator + # https://www.playframework.com/documentation/latest/JavaActionCreator + # ~~~~~ + #actionCreator = null + + ## ErrorHandler + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # If null, will attempt to load a class called ErrorHandler in the root package, + #errorHandler = null + + ## Session & Flash + # https://www.playframework.com/documentation/latest/JavaSessionFlash + # https://www.playframework.com/documentation/latest/ScalaSessionFlash + # ~~~~~ + session { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + + # Sets the max-age field of the cookie to 5 minutes. + # NOTE: this only sets when the browser will discard the cookie. Play will consider any + # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, + # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. + #maxAge = 300 + + # Sets the domain on the session cookie. + #domain = "example.com" + } + + flash { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + } +} + +## Netty Provider +# https://www.playframework.com/documentation/latest/SettingsNetty +# ~~~~~ +play.server.netty { + # Whether the Netty wire should be logged + log.wire = true + + # If you run Play on Linux, you can use Netty's native socket transport + # for higher performance with less garbage. + transport = "native" +} + +## WS (HTTP Client) +# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS +# ~~~~~ +# The HTTP client primarily used for REST APIs. The default client can be +# configured directly, but you can also create different client instances +# with customized settings. You must enable this by adding to build.sbt: +# +# libraryDependencies += ws // or javaWs if using java +# +play.ws { + # Sets HTTP requests not to follow 302 requests + #followRedirects = false + + # Sets the maximum number of open HTTP connections for the client. + #ahc.maxConnectionsTotal = 50 + + ## WS SSL + # https://www.playframework.com/documentation/latest/WsSSL + # ~~~~~ + ssl { + # Configuring HTTPS with Play WS does not require programming. You can + # set up both trustManager and keyManager for mutual authentication, and + # turn on JSSE debugging in development with a reload. + #debug.handshake = true + #trustManager = { + # stores = [ + # { type = "JKS", path = "exampletrust.jks" } + # ] + #} + } +} + +## Cache +# https://www.playframework.com/documentation/latest/JavaCache +# https://www.playframework.com/documentation/latest/ScalaCache +# ~~~~~ +# Play comes with an integrated cache API that can reduce the operational +# overhead of repeated requests. You must enable this by adding to build.sbt: +# +# libraryDependencies += cache +# +play.cache { + # If you want to bind several caches, you can bind the individually + #bindCaches = ["db-cache", "user-cache", "session-cache"] +} + +## Filter Configuration +# https://www.playframework.com/documentation/latest/Filters +# ~~~~~ +# There are a number of built-in filters that can be enabled and configured +# to give Play greater security. +# +play.filters { + + # Enabled filters are run automatically against Play. + # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. + enabled = [] + + # Disabled filters remove elements from the enabled list. + # disabled += filters.CSRFFilter + + + ## CORS filter configuration + # https://www.playframework.com/documentation/latest/CorsFilter + # ~~~~~ + # CORS is a protocol that allows web applications to make requests from the browser + # across different domains. + # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has + # dependencies on CORS settings. + cors { + # Filter paths by a whitelist of path prefixes + #pathPrefixes = ["/some/path", ...] + + # The allowed origins. If null, all origins are allowed. + #allowedOrigins = ["http://www.example.com"] + + # The allowed HTTP methods. If null, all methods are allowed + #allowedHttpMethods = ["GET", "POST"] + } + + ## Security headers filter configuration + # https://www.playframework.com/documentation/latest/SecurityHeaders + # ~~~~~ + # Defines security headers that prevent XSS attacks. + # If enabled, then all options are set to the below configuration by default: + headers { + # The X-Frame-Options header. If null, the header is not set. + #frameOptions = "DENY" + + # The X-XSS-Protection header. If null, the header is not set. + #xssProtection = "1; mode=block" + + # The X-Content-Type-Options header. If null, the header is not set. + #contentTypeOptions = "nosniff" + + # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. + #permittedCrossDomainPolicies = "master-only" + + # The Content-Security-Policy header. If null, the header is not set. + #contentSecurityPolicy = "default-src 'self'" + } + + ## Allowed hosts filter configuration + # https://www.playframework.com/documentation/latest/AllowedHostsFilter + # ~~~~~ + # Play provides a filter that lets you configure which hosts can access your application. + # This is useful to prevent cache poisoning attacks. + hosts { + # Allow requests to example.com, its subdomains, and localhost:9000. + #allowed = [".example.com", "localhost:9000"] + } +} + +# Learning-Service Configuration +content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit"] + +# Cassandra Configuration +content.keyspace.name=content_store +content.keyspace.table=content_data +#TODO: Add Configuration for assessment. e.g: question_data +orchestrator.keyspace.name=script_store +orchestrator.keyspace.table=script_data +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" + +# Redis Configuration +redis.host=localhost +redis.port=6379 +redis.maxConnections=128 + +#Condition to enable publish locally +content.publish_task.enabled=true + +#directory location where store unzip file +dist.directory=/data/tmp/dist/ +output.zipfile=/data/tmp/story.zip +source.folder=/data/tmp/temp2/ +save.directory=/data/tmp/temp/ + +# Content 2 vec analytics URL +CONTENT_TO_VEC_URL="http://172.31.27.233:9000/content-to-vec" + +# FOR CONTENT WORKFLOW PIPELINE (CWP) + +#--Content Workflow Pipeline Mode +OPERATION_MODE=TEST + +#--Maximum Content Package File Size Limit in Bytes (50 MB) +MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=52428800 + +#--Maximum Asset File Size Limit in Bytes (20 MB) +MAX_ASSET_FILE_SIZE_LIMIT=20971520 + +#--No of Retry While File Download Fails +RETRY_ASSET_DOWNLOAD_COUNT=1 + +#Google-vision-API +google.vision.tagging.enabled = false + +#Orchestrator env properties +env="https://dev.ekstep.in/api/learning" + +#Current environment +cloud_storage.env=dev + + +#Folder configuration +cloud_storage.content.folder=content +cloud_storage.asset.folder=assets +cloud_storage.artefact.folder=artifact +cloud_storage.bundle.folder=bundle +cloud_storage.media.folder=media +cloud_storage.ecar.folder=ecar_files + +# Media download configuration +content.media.base.url="https://dev.open-sunbird.org" +plugin.media.base.url="https://dev.open-sunbird.org" + +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + +# Elasticsearch properties +search.es_conn_info="localhost:9200" +search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","domain","subject","description^10","keywords^25","ageGroup^10","filter^10","theme^10","genre^10","objects^25","contentType^100","language^200","teachingMode^25","skills^10","learningObjective^10","curriculum^100","gradeLevel^100","developer^100","attributions^10","owner^50","text","words","releaseNotes"] +search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] +search.batch.size=500 +search.connection.timeout=30 +platform-api-url="http://localhost:8080/language-service" +MAX_ITERATION_COUNT_FOR_SAMZA_JOB=2 + + +# DIAL Code Configuration +dialcode.keyspace.name="dialcode_store" +dialcode.keyspace.table="dial_code" +dialcode.max_count=1000 + +# System Configuration +system.config.keyspace.name="dialcode_store" +system.config.table="system_config" + +#Publisher Configuration +publisher.keyspace.name="dialcode_store" +publisher.keyspace.table="publisher" + +#DIAL Code Generator Configuration +dialcode.strip.chars="0" +dialcode.length=6.0 +dialcode.large.prime_number=1679979167 + +#DIAL Code ElasticSearch Configuration +dialcode.index=true +dialcode.object_type="DialCode" + +framework.max_term_creation_limit=200 + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + +# Kafka configuration details +kafka.topics.instruction="local.learning.job.request" +kafka.urls="localhost:9092" + +#Youtube Standard Licence Validation +learning.content.youtube.validate.license=true +learning.content.youtube.application.name=fetch-youtube-license +youtube.license.regex.pattern=["\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)","^([A-Za-z0-9\\-\\_]*)"] + +#Top N Config for Search Telemetry +telemetry_env=dev +telemetry.search.topn=5 + +installation.id=ekstep + + +channel.default="in.ekstep" + +# DialCode Link API Config +learning.content.link_dialcode_validation=true +dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/search" +dialcode.api.authorization=auth_key + +# Language-Code Configuration +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] + +# Kafka send event to topic enable +kafka.topic.send.enable=false + +learning.valid_license=["creativeCommon"] +learning.service_provider=["youtube"] + +stream.mime.type=video/mp4 +compositesearch.index.name="compositesearch" + +hierarchy.keyspace.name=hierarchy_store +content.hierarchy.table=content_hierarchy +framework.hierarchy.table=framework_hierarchy + +# Kafka topic for definition update event. +kafka.topic.system.command="dev.system.command" + +learning.reserve_dialcode.content_type=["TextBook"] +# restrict.metadata.objectTypes=["Content", "ContentImage", "AssessmentItem", "Channel", "Framework", "Category", "CategoryInstance", "Term"] + +#restrict.metadata.objectTypes="Content,ContentImage" + +publish.collection.fullecar.disable=true + +# Consistency Level for Multi Node Cassandra cluster +cassandra.lp.consistency.level=QUORUM + + + + +content.nested.fields="badgeAssertions,targets,badgeAssociations" + +content.cache.ttl=86400 +content.cache.enable=true +collection.cache.enable=true +content.discard.status=["Draft","FlagDraft"] + +framework.categories_cached=["subject", "medium", "gradeLevel", "board"] +framework.cache.ttl=86400 +framework.cache.read=true + + +# Max size(width/height) of thumbnail in pixels +max.thumbnail.size.pixels=150 + +schema.base_path="../../schemas/" +content.hierarchy.removed_props_for_leafNodes=["collections","children","usedByContent","item_sets","methods","libraries","editorState"] + +collection.keyspace = "hierarchy_store" +content.keyspace = "content_store" + +collection.image.migration.enabled=true + + + +# This is added to handle large artifacts sizes differently +content.artifact.size.for_online=209715200 + +contentTypeToPrimaryCategory { + ClassroomTeachingVideo: "Explanation Content" + ConceptMap: "Learning Resource" + Course: "Course" + CuriosityQuestionSet: "Practice Question Set" + eTextBook: "eTextbook" + ExperientialResource: "Learning Resource" + ExplanationResource: "Explanation Content" + ExplanationVideo: "Explanation Content" + FocusSpot: "Teacher Resource" + LearningOutcomeDefinition: "Teacher Resource" + MarkingSchemeRubric: "Teacher Resource" + PedagogyFlow: "Teacher Resource" + PracticeQuestionSet: "Practice Question Set" + PracticeResource: "Practice Question Set" + SelfAssess: "Course Assessment" + TeachingMethod: "Teacher Resource" + TextBook: "Digital Textbook" + Collection: "Content Playlist" + ExplanationReadingMaterial: "Learning Resource" + LearningActivity: "Learning Resource" + LessonPlan: "Content Playlist" + LessonPlanResource: "Teacher Resource" + PreviousBoardExamPapers: "Learning Resource" + TVLesson: "Explanation Content" + OnboardingResource: "Learning Resource" + ReadingMaterial: "Learning Resource" + Template: "Template" + Asset: "Asset" + Plugin: "Plugin" + LessonPlanUnit: "Lesson Plan Unit" + CourseUnit: "Course Unit" + TextBookUnit: "Textbook Unit" +} + +resourceTypeToPrimaryCategory { + Learn: "Learning Resource" + Read: "Learning Resource" + Practice: "Learning Resource" + Teach: "Teacher Resource" + Test: "Learning Resource" + Experiment: "Learning Resource" + LessonPlan: "Teacher Resource" +} + +mimeTypeToPrimaryCategory { + "application/vnd.ekstep.h5p-archive": ["Learning Resource"] + "application/vnd.ekstep.html-archive": ["Learning Resource"] + "application/vnd.android.package-archive": ["Learning Resource"] + "video/webm": ["Explanation Content"] + "video/x-youtube": ["Explanation Content"] + "video/mp4": ["Explanation Content"] + "application/pdf": ["Learning Resource", "Teacher Resource"] + "application/epub": ["Learning Resource", "Teacher Resource"] + "application/vnd.ekstep.ecml-archive": ["Learning Resource", "Teacher Resource"] + "text/x-url": ["Learnin Resource", "Teacher Resource"] +} + +objectcategorydefinition.keyspace=category_store diff --git a/learning-api/hierarchy-manager/src/test/resources/cassandra-unit.yaml b/assessment-api/qs-hierarchy-manager/src/test/resources/cassandra-unit.yaml similarity index 100% rename from learning-api/hierarchy-manager/src/test/resources/cassandra-unit.yaml rename to assessment-api/qs-hierarchy-manager/src/test/resources/cassandra-unit.yaml diff --git a/learning-api/hierarchy-manager/src/test/resources/logback.xml b/assessment-api/qs-hierarchy-manager/src/test/resources/logback.xml similarity index 100% rename from learning-api/hierarchy-manager/src/test/resources/logback.xml rename to assessment-api/qs-hierarchy-manager/src/test/resources/logback.xml diff --git a/learning-api/hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala b/assessment-api/qs-hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala similarity index 57% rename from learning-api/hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala rename to assessment-api/qs-hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala index 346b3c745..16458c14e 100644 --- a/learning-api/hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala +++ b/assessment-api/qs-hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala @@ -9,18 +9,25 @@ import org.neo4j.graphdb.GraphDatabaseService import org.neo4j.graphdb.factory.GraphDatabaseFactory import org.neo4j.graphdb.factory.GraphDatabaseSettings.Connector.ConnectorType import org.neo4j.kernel.configuration.BoltConnector -import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll, Matchers} +import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll, BeforeAndAfterEach, Matchers} import org.sunbird.cassandra.CassandraConnector import org.sunbird.common.Platform -class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { +class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach{ var graphDb: GraphDatabaseService = null var session: Session = null private val script_1 = "CREATE KEYSPACE IF NOT EXISTS content_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" private val script_2 = "CREATE TABLE IF NOT EXISTS content_store.content_data (content_id text, last_updated_on timestamp,body blob,oldBody blob,stageIcons blob,PRIMARY KEY (content_id));" - + private val script_5 = "CREATE KEYSPACE IF NOT EXISTS category_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_6 = "CREATE TABLE IF NOT EXISTS category_store.category_definition_data (identifier text, objectmetadata map, forms map ,PRIMARY KEY (identifier));" + private val script_7 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_content_all', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_8 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:course_collection_all', {'config': '{}', 'schema': '{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"}},\"default\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"additionalProperties\":false},\"additionalCategories\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"default\":\"Textbook\"}},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}'});" + private val script_9 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:course_content_all',{'config': '{}', 'schema': '{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"}},\"default\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"additionalProperties\":false},\"additionalCategories\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"default\":\"Textbook\"}},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}'});" + private val script_10 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_collection_all', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_11 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_content_in.ekstep', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_12 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_collection_in.ekstep', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" def setUpEmbeddedNeo4j(): Unit = { if(null == graphDb) { @@ -72,13 +79,14 @@ class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { } override def beforeAll(): Unit = { + tearEmbeddedNeo4JSetup() setUpEmbeddedNeo4j() setUpEmbeddedCassandra() - executeCassandraQuery(script_1, script_2) + executeCassandraQuery(script_1, script_2, script_5, script_6, script_7, script_8, script_9, script_10, script_11, script_12) } override def afterAll(): Unit = { - deleteEmbeddedNeo4j(new File(Platform.config.getString("graph.dir"))) + tearEmbeddedNeo4JSetup() if(null != session && !session.isClosed) session.close() EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() @@ -102,6 +110,6 @@ class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { } def createRelationData(): Unit = { - graphDb.execute("UNWIND [{identifier:\"Num:C3:SC2\",code:\"Num:C3:SC2\",keywords:[\"Subconcept\",\"Class 3\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",subject:\"numeracy\",channel:\"in.ekstep\",description:\"Multiplication\",versionKey:\"1484389136575\",gradeLevel:[\"Grade 3\",\"Grade 4\"],IL_FUNC_OBJECT_TYPE:\"Concept\",name:\"Multiplication\",lastUpdatedOn:\"2016-06-15T17:15:45.951+0000\",IL_UNIQUE_ID:\"Num:C3:SC2\",status:\"Live\"}, {code:\"31d521da-61de-4220-9277-21ca7ce8335c\",previewUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",downloadUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",channel:\"in.ekstep\",language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790848197_do_11232724509261824014_2.0_spine.ecar\\\",\\\"size\\\":890.0}}\",mimeType:\"application/pdf\",streamingUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",idealScreenSize:\"normal\",createdOn:\"2017-09-07T13:24:20.720+0000\",contentDisposition:\"inline\",artifactUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",contentEncoding:\"identity\",lastUpdatedOn:\"2017-09-07T13:25:53.595+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2017-09-07T13:27:28.417+0000\",contentType:\"Resource\",lastUpdatedBy:\"Ekstep\",audience:[\"Learner\"],visibility:\"Default\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",consumerId:\"e84015d2-a541-4c07-a53f-e31d4553312b\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",pkgVersion:2,versionKey:\"1504790848417\",license:\"Creative Commons Attribution (CC BY)\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",size:4864851,lastPublishedOn:\"2017-09-07T13:27:27.410+0000\",createdBy:\"390\",compatibilityLevel:4,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Untitled Content\",publisher:\"EkStep\",IL_UNIQUE_ID:\"do_11232724509261824014\",status:\"Live\",resourceType:[\"Study material\"]}] as row CREATE (n:domain) SET n += row") + graphDb.execute("UNWIND [{identifier:\"Num:C3:SC2\",code:\"Num:C3:SC2\",keywords:[\"Subconcept\",\"Class 3\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",subject:\"numeracy\",channel:\"in.ekstep\",description:\"Multiplication\",versionKey:\"1484389136575\",gradeLevel:[\"Grade 3\",\"Grade 4\"],IL_FUNC_OBJECT_TYPE:\"Concept\",name:\"Multiplication\",lastUpdatedOn:\"2016-06-15T17:15:45.951+0000\",IL_UNIQUE_ID:\"Num:C3:SC2\",status:\"Live\"}, {code:\"31d521da-61de-4220-9277-21ca7ce8335c\",previewUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",downloadUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",channel:\"in.ekstep\",language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790848197_do_11232724509261824014_2.0_spine.ecar\\\",\\\"size\\\":890.0}}\",mimeType:\"application/pdf\",streamingUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",idealScreenSize:\"normal\",createdOn:\"2017-09-07T13:24:20.720+0000\",contentDisposition:\"inline\",artifactUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",contentEncoding:\"identity\",lastUpdatedOn:\"2017-09-07T13:25:53.595+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2017-09-07T13:27:28.417+0000\",contentType:\"Resource\",lastUpdatedBy:\"Ekstep\",audience:[\"Student\"],visibility:\"Default\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",consumerId:\"e84015d2-a541-4c07-a53f-e31d4553312b\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",pkgVersion:2,versionKey:\"1504790848417\",license:\"Creative Commons Attribution (CC BY)\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",size:4864851,lastPublishedOn:\"2017-09-07T13:27:27.410+0000\",createdBy:\"390\",compatibilityLevel:4,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Untitled Content\",publisher:\"EkStep\",IL_UNIQUE_ID:\"do_11232724509261824014\",status:\"Live\",resourceType:[\"Study material\"]}] as row CREATE (n:domain) SET n += row") } } diff --git a/build/assessment-service/Dockerfile b/build/assessment-service/Dockerfile new file mode 100644 index 000000000..d3b28b2cd --- /dev/null +++ b/build/assessment-service/Dockerfile @@ -0,0 +1,14 @@ +FROM sunbird/openjdk-java11-alpine:latest +RUN apk update \ + && apk add unzip \ + && apk add curl \ + && adduser -u 1001 -h /home/sunbird/ -D sunbird \ + && mkdir -p /home/sunbird +RUN chown -R sunbird:sunbird /home/sunbird +USER sunbird +COPY ./assessment-api/assessment-service/target/assessment-service-1.0-SNAPSHOT-dist.zip /home/sunbird/ +RUN unzip /home/sunbird/assessment-service-1.0-SNAPSHOT-dist.zip -d /home/sunbird/ +RUN rm /home/sunbird/assessment-service-1.0-SNAPSHOT-dist.zip +COPY --chown=sunbird ./schemas /home/sunbird/assessment-service-1.0-SNAPSHOT/schemas +WORKDIR /home/sunbird/ +CMD java -XX:+PrintFlagsFinal $JAVA_OPTIONS -cp '/home/sunbird/assessment-service-1.0-SNAPSHOT/lib/*' -Dconfig.file=/home/sunbird/assessment-service-1.0-SNAPSHOT/config/application.conf -Dlogger.file=/home/sunbird/assessment-service-1.0-SNAPSHOT/config/logback.xml play.core.server.ProdServerStart /home/sunbird/assessment-service-1.0-SNAPSHOT diff --git a/build/assessment-service/Jenkinsfile b/build/assessment-service/Jenkinsfile new file mode 100644 index 000000000..ebc9f1a63 --- /dev/null +++ b/build/assessment-service/Jenkinsfile @@ -0,0 +1,49 @@ +node('build-slave') { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + withEnv(["JAVA_HOME=${JAVA11_HOME}"]) { + stage('Checkout') { + if (!env.hub_org) { + println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) + error 'Please resolve the errors and rerun..' + } else + println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) + } + + cleanWs() + checkout scm + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "build_tag: " + build_tag + + stage('Build') { + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests=true ' + } + + stage('Package') { + dir('assessment-api') { + sh 'mvn play2:dist -pl assessment-service' + } + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"assessment-service"} ${env.NODE_NAME} ${hub_org}") + } + stage('ArchiveArtifacts') { + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + } + } + } + } + catch (err) { + currentBuild.result = "FAILURE" + throw err + } +} diff --git a/build/assessment-service/auto_build_deploy b/build/assessment-service/auto_build_deploy new file mode 100644 index 000000000..d3332edb1 --- /dev/null +++ b/build/assessment-service/auto_build_deploy @@ -0,0 +1,58 @@ +@Library('deploy-conf') _ +node('build-slave') { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + withEnv(["JAVA_HOME=${JAVA11_HOME}"]) { + stage('Checkout') { + tag_name = env.JOB_NAME.split("/")[-1] + pre_checks() + if (!env.hub_org) { + println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) + error 'Please resolve the errors and rerun..' + } else + println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) + } + cleanWs() + def scmVars = checkout scm + checkout scm: [$class: 'GitSCM', branches: [[name: "refs/tags/$tag_name"]], userRemoteConfigs: [[url: scmVars.GIT_URL]]] + build_tag = tag_name + "_" + env.BUILD_NUMBER + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + artifact_version = tag_name + "_" + commit_hash + echo "build_tag: " + build_tag + + // stage Build + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests=true ' + +// stage Package + dir('assessment-api') { + sh 'mvn play2:dist -pl assessment-service' + } + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"assessment-service"} ${env.NODE_NAME} ${hub_org}") + +// stage ArchiveArtifacts + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + + } + currentBuild.result = "SUCCESS" + slack_notify(currentBuild.result, tag_name) + email_notify() + auto_build_deploy() + } + } + catch (err) { + currentBuild.result = "FAILURE" + slack_notify(currentBuild.result, tag_name) + email_notify() + throw err + } +} diff --git a/build.sh b/build/build.sh similarity index 53% rename from build.sh rename to build/build.sh index a27316911..0e24196e8 100755 --- a/build.sh +++ b/build/build.sh @@ -3,9 +3,9 @@ set -eo pipefail build_tag=$1 -name=content-service -node=$2 -org=$3 +name=$2 +node=$3 +org=$4 -docker build -f ./Dockerfile --label commitHash=$(git rev-parse --short HEAD) -t ${org}/${name}:${build_tag} . +docker build -f build/${name}/Dockerfile --label commitHash=$(git rev-parse --short HEAD) -t ${org}/${name}:${build_tag} . echo {\"image_name\" : \"${name}\", \"image_tag\" : \"${build_tag}\", \"node_name\" : \"$node\"} > metadata.json diff --git a/build/content-service/Dockerfile b/build/content-service/Dockerfile new file mode 100644 index 000000000..2e829d061 --- /dev/null +++ b/build/content-service/Dockerfile @@ -0,0 +1,14 @@ +FROM sunbird/openjdk-java11-alpine:latest +RUN apk update \ + && apk add unzip \ + && apk add curl \ + && adduser -u 1001 -h /home/sunbird/ -D sunbird \ + && mkdir -p /home/sunbird +RUN chown -R sunbird:sunbird /home/sunbird +USER sunbird +COPY ./content-api/content-service/target/content-service-1.0-SNAPSHOT-dist.zip /home/sunbird/ +RUN unzip /home/sunbird/content-service-1.0-SNAPSHOT-dist.zip -d /home/sunbird/ +RUN rm /home/sunbird/content-service-1.0-SNAPSHOT-dist.zip +COPY --chown=sunbird ./schemas /home/sunbird/content-service-1.0-SNAPSHOT/schemas +WORKDIR /home/sunbird/ +CMD java -XX:+PrintFlagsFinal $JAVA_OPTIONS -cp '/home/sunbird/content-service-1.0-SNAPSHOT/lib/*' -Dconfig.file=/home/sunbird/content-service-1.0-SNAPSHOT/config/application.conf -Dlogger.file=/home/sunbird/content-service-1.0-SNAPSHOT/config/logback.xml play.core.server.ProdServerStart /home/sunbird/content-service-1.0-SNAPSHOT diff --git a/build/content-service/Jenkinsfile b/build/content-service/Jenkinsfile new file mode 100644 index 000000000..6909c139c --- /dev/null +++ b/build/content-service/Jenkinsfile @@ -0,0 +1,50 @@ +node('build-slave') { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + withEnv(["JAVA_HOME=${JAVA11_HOME}"]) { + stage('Checkout') { + if (!env.hub_org) { + println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) + error 'Please resolve the errors and rerun..' + } else + println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) + } + + cleanWs() + checkout scm + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "build_tag: " + build_tag + + stage('Build') { + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests=true ' + + } + + stage('Package') { + dir('content-api') { + sh 'mvn play2:dist -pl content-service' + } + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"content-service"} ${env.NODE_NAME} ${hub_org}") + } + stage('ArchiveArtifacts') { + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + } + } + } + } + catch (err) { + currentBuild.result = "FAILURE" + throw err + } +} \ No newline at end of file diff --git a/build/content-service/auto_build_deploy b/build/content-service/auto_build_deploy new file mode 100644 index 000000000..1fa11bf83 --- /dev/null +++ b/build/content-service/auto_build_deploy @@ -0,0 +1,58 @@ +@Library('deploy-conf') _ +node('build-slave') { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + withEnv(["JAVA_HOME=${JAVA11_HOME}"]) { + stage('Checkout') { + tag_name = env.JOB_NAME.split("/")[-1] + pre_checks() + if (!env.hub_org) { + println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) + error 'Please resolve the errors and rerun..' + } else + println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) + } + cleanWs() + def scmVars = checkout scm + checkout scm: [$class: 'GitSCM', branches: [[name: "refs/tags/$tag_name"]], userRemoteConfigs: [[url: scmVars.GIT_URL]]] + build_tag = tag_name + "_" + env.BUILD_NUMBER + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + artifact_version = tag_name + "_" + commit_hash + echo "build_tag: " + build_tag + + // stage Build + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests=true ' + + // stage Package + dir('content-api') { + sh 'mvn play2:dist -pl content-service' + } + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"content-service"} ${env.NODE_NAME} ${hub_org}") + + // stage ArchiveArtifacts + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + + } + currentBuild.result = "SUCCESS" + slack_notify(currentBuild.result, tag_name) + email_notify() + auto_build_deploy() + } + } + catch (err) { + currentBuild.result = "FAILURE" + slack_notify(currentBuild.result, tag_name) + email_notify() + throw err + } +} diff --git a/build/search-service/Dockerfile b/build/search-service/Dockerfile new file mode 100644 index 000000000..be9830e24 --- /dev/null +++ b/build/search-service/Dockerfile @@ -0,0 +1,14 @@ +FROM sunbird/openjdk-java11-alpine:latest +RUN apk update \ + && apk add unzip \ + && apk add curl \ + && adduser -u 1001 -h /home/sunbird/ -D sunbird \ + && mkdir -p /home/sunbird +RUN chown -R sunbird:sunbird /home/sunbird +USER sunbird +COPY ./search-api/search-service/target/search-service-1.0-SNAPSHOT-dist.zip /home/sunbird/ +RUN unzip /home/sunbird/search-service-1.0-SNAPSHOT-dist.zip -d /home/sunbird/ +RUN rm /home/sunbird/search-service-1.0-SNAPSHOT-dist.zip +COPY --chown=sunbird ./schemas /home/sunbird/search-service-1.0-SNAPSHOT/schemas +WORKDIR /home/sunbird/ +CMD java -XX:+PrintFlagsFinal $JAVA_OPTIONS -cp '/home/sunbird/search-service-1.0-SNAPSHOT/lib/*' -Dconfig.file=/home/sunbird/search-service-1.0-SNAPSHOT/config/application.conf -Dlogger.file=/home/sunbird/search-service-1.0-SNAPSHOT/config/logback.xml play.core.server.ProdServerStart /home/sunbird/search-service-1.0-SNAPSHOT diff --git a/build/search-service/Jenkinsfile b/build/search-service/Jenkinsfile new file mode 100644 index 000000000..a565e7da3 --- /dev/null +++ b/build/search-service/Jenkinsfile @@ -0,0 +1,50 @@ +node('build-slave') { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + withEnv(["JAVA_HOME=${JAVA11_HOME}"]) { + stage('Checkout') { + if (!env.hub_org) { + println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) + error 'Please resolve the errors and rerun..' + } else + println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) + } + + cleanWs() + checkout scm + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "build_tag: " + build_tag + + stage('Build') { + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests=true ' + + } + + stage('Package') { + dir('search-api') { + sh 'mvn play2:dist -pl search-service' + } + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"search-service"} ${env.NODE_NAME} ${hub_org}") + } + stage('ArchiveArtifacts') { + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + } + } + } + } + catch (err) { + currentBuild.result = "FAILURE" + throw err + } +} diff --git a/build/search-service/auto_build_deploy b/build/search-service/auto_build_deploy new file mode 100644 index 000000000..61b5bab08 --- /dev/null +++ b/build/search-service/auto_build_deploy @@ -0,0 +1,57 @@ +@Library('deploy-conf') _ +node('build-slave') { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + withEnv(["JAVA_HOME=${JAVA11_HOME}"]) { + stage('Checkout') { + tag_name = env.JOB_NAME.split("/")[-1] + pre_checks() + if (!env.hub_org) { + println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) + error 'Please resolve the errors and rerun..' + } else + println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) + } + cleanWs() + def scmVars = checkout scm + checkout scm: [$class: 'GitSCM', branches: [[name: "refs/tags/$tag_name"]], userRemoteConfigs: [[url: scmVars.GIT_URL]]] + build_tag = tag_name + "_" + env.BUILD_NUMBER + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + artifact_version = tag_name + "_" + commit_hash + echo "build_tag: " + build_tag + + // stage Build + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests=true ' + + // stage Package + dir('search-api') { + sh 'mvn play2:dist -pl search-service' + } + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"search-service"} ${env.NODE_NAME} ${hub_org}") + + // stage('ArchiveArtifacts') + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + } + currentBuild.result = "SUCCESS" + slack_notify(currentBuild.result, tag_name) + email_notify() + auto_build_deploy() + } + } + catch (err) { + currentBuild.result = "FAILURE" + slack_notify(currentBuild.result, tag_name) + email_notify() + throw err + } +} diff --git a/build/taxonomy-service/Dockerfile b/build/taxonomy-service/Dockerfile new file mode 100644 index 000000000..89dae255c --- /dev/null +++ b/build/taxonomy-service/Dockerfile @@ -0,0 +1,14 @@ +FROM sunbird/openjdk-java11-alpine:latest +RUN apk update \ + && apk add unzip \ + && apk add curl \ + && adduser -u 1001 -h /home/sunbird/ -D sunbird \ + && mkdir -p /home/sunbird +RUN chown -R sunbird:sunbird /home/sunbird +USER sunbird +COPY ./taxonomy-api/taxonomy-service/target/taxonomy-service-1.0-SNAPSHOT-dist.zip /home/sunbird/ +RUN unzip /home/sunbird/taxonomy-service-1.0-SNAPSHOT-dist.zip -d /home/sunbird/ +RUN rm /home/sunbird/taxonomy-service-1.0-SNAPSHOT-dist.zip +COPY --chown=sunbird ./schemas /home/sunbird/taxonomy-service-1.0-SNAPSHOT/schemas +WORKDIR /home/sunbird/ +CMD java -XX:+PrintFlagsFinal $JAVA_OPTIONS -cp '/home/sunbird/taxonomy-service-1.0-SNAPSHOT/lib/*' -Dconfig.file=/home/sunbird/taxonomy-service-1.0-SNAPSHOT/config/application.conf -Dlogger.file=/home/sunbird/taxonomy-service-1.0-SNAPSHOT/config/logback.xml play.core.server.ProdServerStart /home/sunbird/taxonomy-service-1.0-SNAPSHOT diff --git a/build/taxonomy-service/Jenkinsfile b/build/taxonomy-service/Jenkinsfile new file mode 100644 index 000000000..453a603f6 --- /dev/null +++ b/build/taxonomy-service/Jenkinsfile @@ -0,0 +1,50 @@ +node('build-slave') { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + withEnv(["JAVA_HOME=${JAVA11_HOME}"]) { + stage('Checkout') { + if (!env.hub_org) { + println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) + error 'Please resolve the errors and rerun..' + } else + println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) + } + + cleanWs() + checkout scm + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() + echo "build_tag: " + build_tag + + stage('Build') { + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests=true ' + + } + + stage('Package') { + dir('taxonomy-api') { + sh 'mvn play2:dist -pl taxonomy-service' + } + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"taxonomy-service"} ${env.NODE_NAME} ${hub_org}") + } + stage('ArchiveArtifacts') { + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + } + } + } + } + catch (err) { + currentBuild.result = "FAILURE" + throw err + } +} diff --git a/build/taxonomy-service/auto_build_deploy b/build/taxonomy-service/auto_build_deploy new file mode 100644 index 000000000..30fea1e4d --- /dev/null +++ b/build/taxonomy-service/auto_build_deploy @@ -0,0 +1,59 @@ +@Library('deploy-conf') _ +node('build-slave') { + try { + String ANSI_GREEN = "\u001B[32m" + String ANSI_NORMAL = "\u001B[0m" + String ANSI_BOLD = "\u001B[1m" + String ANSI_RED = "\u001B[31m" + String ANSI_YELLOW = "\u001B[33m" + + ansiColor('xterm') { + withEnv(["JAVA_HOME=${JAVA11_HOME}"]) { + stage('Checkout') { + tag_name = env.JOB_NAME.split("/")[-1] + pre_checks() + if (!env.hub_org) { + println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) + error 'Please resolve the errors and rerun..' + } else + println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) + } + cleanWs() + def scmVars = checkout scm + checkout scm: [$class: 'GitSCM', branches: [[name: "refs/tags/$tag_name"]], userRemoteConfigs: [[url: scmVars.GIT_URL]]] + build_tag = tag_name + "_" + env.BUILD_NUMBER + commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + artifact_version = tag_name + "_" + commit_hash + echo "build_tag: " + build_tag + echo "build_tag: " + build_tag + + // stage Build + env.NODE_ENV = "build" + print "Environment will be : ${env.NODE_ENV}" + sh 'mvn clean install -DskipTests=true ' + + // stage Package + dir('taxonomy-api') { + sh 'mvn play2:dist -pl taxonomy-service' + } + sh('chmod 777 build/build.sh') + sh("build/build.sh ${build_tag} ${"taxonomy-service"} ${env.NODE_NAME} ${hub_org}") + +// stage ArchiveArtifacts + archiveArtifacts "metadata.json" + currentBuild.description = "${build_tag}" + + } + currentBuild.result = "SUCCESS" + slack_notify(currentBuild.result, tag_name) + email_notify() + auto_build_deploy() + } + } + catch (err) { + currentBuild.result = "FAILURE" + slack_notify(currentBuild.result, tag_name) + email_notify() + throw err + } +} diff --git a/content-api/content-actors/pom.xml b/content-api/content-actors/pom.xml new file mode 100644 index 000000000..a429dc8f9 --- /dev/null +++ b/content-api/content-actors/pom.xml @@ -0,0 +1,133 @@ + + + + content-api + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + content-actors + + + + org.scala-lang + scala-library + ${scala.version} + + + org.sunbird + actor-core + 1.0-SNAPSHOT + + + org.sunbird + kafka-client + 1.0-SNAPSHOT + + + org.sunbird + graph-engine_2.11 + 1.0-SNAPSHOT + jar + + + org.sunbird + mimetype-manager + 1.0-SNAPSHOT + jar + + + org.sunbird + import-manager + 1.0-SNAPSHOT + jar + + + org.scalatest + scalatest_${scala.maj.version} + 3.0.8 + test + + + com.mashape.unirest + unirest-java + 1.4.9 + + + org.sunbird + hierarchy-manager + 1.0-SNAPSHOT + + + org.scalamock + scalamock_${scala.maj.version} + 4.4.0 + test + + + com.typesafe.akka + akka-testkit_${scala.maj.version} + 2.5.22 + test + + + + + src/main/scala + src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + ${scala.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + + test + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + + \ No newline at end of file diff --git a/content-api/content-actors/src/main/scala/org/sunbird/channel/actors/ChannelActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/channel/actors/ChannelActor.scala new file mode 100644 index 000000000..6c1f0358b --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/channel/actors/ChannelActor.scala @@ -0,0 +1,80 @@ +package org.sunbird.channel.actors + +import java.util + +import javax.inject.Inject +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.graph.nodes.DataNode +import org.sunbird.util.RequestUtil +import org.sunbird.channel.managers.ChannelManager +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.common.Platform + +import scala.concurrent.{ExecutionContext, Future} +import org.apache.commons.collections4.CollectionUtils +import org.sunbird.graph.OntologyEngineContext + +class ChannelActor @Inject() (implicit oec: OntologyEngineContext) extends BaseActor { + implicit val ec: ExecutionContext = getContext().dispatcher + + val suggestFrameworks = if(Platform.config.hasPath("channel.fetch.suggested_frameworks")) Platform.config.getBoolean("channel.fetch.suggested_frameworks") else true + + override def onReceive(request: Request): Future[Response] = { + request.getOperation match { + case "createChannel" => create(request) + case "readChannel" => read(request) + case "updateChannel" => update(request) + case "retireChannel" => retire(request) + case _ => ERROR(request.getOperation) + } + + } + + def create(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + if (!request.getRequest.containsKey("code")) + throw new ClientException("ERR_CODE_IS_REQUIRED", "Code is required for creating a channel") + request.getRequest.put("identifier", request.getRequest.get("code").asInstanceOf[String]) + ChannelManager.validateTranslationMap(request) + ChannelManager.validateObjectCategory(request) + DataNode.create(request).map(node => { + ChannelManager.channelLicenseCache(request, node.getIdentifier) + ResponseHandler.OK.put("identifier", node.getIdentifier).put("node_id", node.getIdentifier) + }) + } + + def read(request: Request): Future[Response] = { + DataNode.read(request).map(node => { + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, null, request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + if (suggestFrameworks && CollectionUtils.isEmpty(metadata.getOrDefault("frameworks", new util.ArrayList[AnyRef]()).asInstanceOf[util.List[AnyRef]])) { + val frameworkList = ChannelManager.getAllFrameworkList() + if (!frameworkList.isEmpty) metadata.put("suggested_frameworks", frameworkList) + } + ChannelManager.setPrimaryAndAdditionCategories(metadata) + ResponseHandler.OK.put("channel", metadata) + }) + } + + def update(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + ChannelManager.validateTranslationMap(request) + ChannelManager.validateObjectCategory(request) + request.getRequest.put("status", "Live") + DataNode.update(request).map(node => { + val identifier: String = node.getIdentifier + ChannelManager.channelLicenseCache(request, identifier) + ResponseHandler.OK.put("node_id", identifier).put("identifier", identifier) + }) + } + + def retire(request: Request): Future[Response] = { + request.getRequest.put("status", "Retired") + DataNode.update(request).map(node => { + val identifier: String = node.getIdentifier + ResponseHandler.OK.put("node_id", identifier).put("identifier", identifier) + }) + } + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/channel/managers/ChannelManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/channel/managers/ChannelManager.scala new file mode 100644 index 000000000..ae9d69050 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/channel/managers/ChannelManager.scala @@ -0,0 +1,153 @@ +package org.sunbird.channel.managers + +import java.util +import java.util.Optional +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.util.{ChannelConstants, HttpUtil} +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.exception.{ClientException, ServerException} +import org.sunbird.common.Platform +import com.mashape.unirest.http.HttpResponse +import com.mashape.unirest.http.Unirest +import org.apache.commons.collections4.CollectionUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.JsonUtils + +import scala.collection.JavaConverters._ +import scala.collection.JavaConversions._ +import scala.collection.mutable.ListBuffer + +object ChannelManager { + + val CONTENT_PRIMARY_CATEGORIES: util.List[String] = Platform.getStringList("channel.content.primarycategories", new util.ArrayList[String]()) + val COLLECTION_PRIMARY_CATEGORIES: util.List[String] = Platform.getStringList("channel.collection.primarycategories", new util.ArrayList[String]()) + val ASSET_PRIMARY_CATEGORIES: util.List[String] = Platform.getStringList("channel.asset.primarycategories", new util.ArrayList[String]()) + val CONTENT_ADDITIONAL_CATEGORIES: util.List[String] = Platform.getStringList("channel.content.additionalcategories", new util.ArrayList[String]()) + val COLLECTION_ADDITIONAL_CATEGORIES: util.List[String] = Platform.getStringList("channel.collection.additionalcategories", new util.ArrayList[String]()) + val ASSET_ADDITIONAL_CATEGORIES: util.List[String] = Platform.getStringList("channel.asset.additionalcategories", new util.ArrayList[String]()) + implicit val httpUtil: HttpUtil = new HttpUtil + + def channelLicenseCache(request: Request, identifier: String): Unit = { + if (request.getRequest.containsKey(ChannelConstants.DEFAULT_LICENSE)) + RedisCache.set(ChannelConstants.CHANNEL_LICENSE_CACHE_PREFIX + identifier + ChannelConstants.CHANNEL_LICENSE_CACHE_SUFFIX, request.getRequest.get(ChannelConstants.DEFAULT_LICENSE).asInstanceOf[String], 0) + } + + def getAllFrameworkList(): util.List[util.Map[String, AnyRef]] = { + val url: String = Platform.getString("composite.search.url", "https://dev.sunbirded.org/action/composite/v3/search") + val httpResponse: HttpResponse[String] = Unirest.post(url).header("Content-Type", "application/json").body("""{"request":{"filters":{"objectType":"Framework","status":"Live"},"fields":["name","code","objectType","identifier"]}}""").asString + if (200 != httpResponse.getStatus) + throw new ServerException("ERR_FETCHING_FRAMEWORK", "Error while fetching framework.") + val response: Response = JsonUtils.deserialize(httpResponse.getBody, classOf[Response]) + response.getResult.getOrDefault("Framework", new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.List[util.Map[String, AnyRef]]] + } + + def validateTranslationMap(request: Request) = { + val translations: util.Map[String, AnyRef] = Optional.ofNullable(request.get("translations").asInstanceOf[util.HashMap[String, AnyRef]]).orElse(new util.HashMap[String, AnyRef]()) + if (translations.isEmpty) request.getRequest.remove("translations") + else { + val languageCodes = Platform.getStringList("platform.language.codes", new util.ArrayList[String]()) + if (translations.asScala.exists(entry => !languageCodes.contains(entry._1))) + throw new ClientException("ERR_INVALID_LANGUAGE_CODE", "Please Provide Valid Language Code For translations. Valid Language Codes are : " + languageCodes) + } + } + + def validateObjectCategory(request: Request) = { + if (!util.Collections.disjoint(request.getRequest.keySet(), ChannelConstants.categoryKeyList)) { + val masterCategoriesList: List[String] = getMasterCategoryList() + val errMsg: ListBuffer[String] = ListBuffer() + compareWithMasterCategory(request, masterCategoriesList, errMsg) + if (errMsg.nonEmpty) + throw new ClientException(ChannelConstants.ERR_VALIDATING_PRIMARY_CATEGORY, "Please provide valid : " + errMsg.mkString("[", ",", "]")) + } + } + + def compareWithMasterCategory(request: Request, masterCat: List[String], errMsg: ListBuffer[String]): Unit = { + ChannelConstants.categoryKeyList.map(cat => { + if (request.getRequest.containsKey(cat)) { + val requestedCategoryList: util.List[String] = getRequestedCategoryList(request, cat) + if (!masterCat.containsAll(requestedCategoryList)) + errMsg += cat + } + }) + } + + def getRequestedCategoryList(request: Request, cat: String): util.ArrayList[String] = { + try { + val requestedList = request.getRequest.get(cat).asInstanceOf[util.ArrayList[String]] + if (requestedList.isEmpty) + throw new ClientException(ChannelConstants.ERR_VALIDATING_PRIMARY_CATEGORY, "Empty list not allowed for " + cat) + requestedList + } catch { + case e: ClassCastException => { + throw new ClientException(ChannelConstants.ERR_VALIDATING_PRIMARY_CATEGORY, "Please provide valid list for " + cat) + } + case e: ClientException => { + throw new ClientException(e.getErrCode, e.getMessage) + } + case e: Exception => { + throw new ServerException(ChannelConstants.ERR_VALIDATING_PRIMARY_CATEGORY, e.getMessage) + } + } + } + + def setPrimaryAndAdditionCategories(metadata: util.Map[String, AnyRef]): Unit = { + metadata.putIfAbsent(ChannelConstants.CONTENT_PRIMARY_CATEGORIES, CONTENT_PRIMARY_CATEGORIES) + metadata.putIfAbsent(ChannelConstants.COLLECTION_PRIMARY_CATEGORIES, COLLECTION_PRIMARY_CATEGORIES) + metadata.putIfAbsent(ChannelConstants.ASSET_PRIMARY_CATEGORIES, ASSET_PRIMARY_CATEGORIES) + metadata.putIfAbsent(ChannelConstants.CONTENT_ADDITIONAL_CATEGORIES, CONTENT_ADDITIONAL_CATEGORIES) + metadata.putIfAbsent(ChannelConstants.COLLECTION_ADDITIONAL_CATEGORIES, COLLECTION_ADDITIONAL_CATEGORIES) + metadata.putIfAbsent(ChannelConstants.ASSET_ADDITIONAL_CATEGORIES, ASSET_ADDITIONAL_CATEGORIES) + val primaryCategories = getChannelPrimaryCategories(metadata.get("identifier").asInstanceOf[String]) + metadata.put("primaryCategories", primaryCategories) + val additionalCategories = getAdditionalCategories() + metadata.put("additionalCategories", additionalCategories) + } + + def getAdditionalCategories()(implicit httpUtil: HttpUtil): java.util.List[String] = { + val body = """{"request":{"filters":{"objectType":"ObjectCategory","visibility":["Default"]},"fields":["name","identifier"]}}""" + val url: String = Platform.getString("composite.search.url", "https://dev.sunbirded.org/action/composite/v3/search") + val httpResponse = httpUtil.post(url, body) + if (200 != httpResponse.status) throw new ServerException("ERR_FETCHING_OBJECT_CATEGORY", "Error while fetching object categories for additional category list.") + val response: Response = JsonUtils.deserialize(httpResponse.body, classOf[Response]) + val objectCategoryList: util.List[util.Map[String, AnyRef]] = response.getResult.getOrDefault(ChannelConstants.OBJECT_CATEGORY, new util.ArrayList[util.Map[String, AnyRef]]).asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]] + objectCategoryList.asScala.map(cat => cat.get("name").asInstanceOf[String]).asJava + + } + + def getChannelPrimaryCategories(channel: String)(implicit httpUtil: HttpUtil): java.util.List[java.util.Map[String, AnyRef]] = { + val globalPCRequest = s"""{"request":{"filters":{"objectType":"ObjectCategoryDefinition", "visibility":["Default"]},"not_exists": "channel","fields":["name","identifier","targetObjectType"]}}""" + val globalPrimaryCategories = getPrimaryCategories(globalPCRequest) + val channelPCRequest = s"""{"request":{"filters":{"objectType":"ObjectCategoryDefinition", "visibility":["Default"], "channel": "$channel"},"fields":["name","identifier","targetObjectType"]}}""" + val channelPrimaryCategories = getPrimaryCategories(channelPCRequest) + if (CollectionUtils.isEmpty(channelPrimaryCategories)) + globalPrimaryCategories + else { + val idsToIgnore = channelPrimaryCategories.map(cat => cat.get("identifier").asInstanceOf[String]) + .map(id => id.replace("_"+channel, "_all")) + globalPrimaryCategories.filter(cat => { + !idsToIgnore.contains(cat.get("identifier").asInstanceOf[String]) + }) ++ channelPrimaryCategories + } + } + + private def getPrimaryCategories(body: String)(implicit httpUtil: HttpUtil): java.util.List[java.util.Map[String, AnyRef]] = { + val url: String = Platform.getString("composite.search.url", "https://dev.sunbirded.org/action/composite/v3/search") + val httpResponse = httpUtil.post(url, body) + if (200 != httpResponse.status) throw new ServerException("ERR_FETCHING_OBJECT_CATEGORY_DEFINITION", "Error while fetching primary categories.") + val response: Response = JsonUtils.deserialize(httpResponse.body, classOf[Response]) + val objectCategoryList: util.List[util.Map[String, AnyRef]] = response.getResult.getOrDefault(ChannelConstants.objectCategoryDefinitionKey, new util.ArrayList[util.Map[String, AnyRef]]).asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]] + objectCategoryList.asScala.map(cat => (cat - "objectType").asJava).asJava + } + + def getMasterCategoryList(): List[String] = { + val url: String = Platform.getString("composite.search.url", "https://dev.sunbirded.org/action/composite/v3/search") + val httpResponse: HttpResponse[String] = Unirest.post(url).header("Content-Type", "application/json").body("""{"request":{"filters":{"objectType":"ObjectCategory"},"fields":["name"]}}""").asString + if (200 != httpResponse.getStatus) + throw new ServerException("ERR_FETCHING_OBJECT_CATEGORY", "Error while fetching object category.") + val response: Response = JsonUtils.deserialize(httpResponse.getBody, classOf[Response]) + val objectCategoryList: util.List[util.Map[String, AnyRef]] = response.getResult.getOrDefault(ChannelConstants.OBJECT_CATEGORY, new util.ArrayList[util.Map[String, AnyRef]]).asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]] + if (objectCategoryList.isEmpty) + throw new ClientException("ERR_NO_MASTER_OBJECT_CATEGORY_DEFINED", "Master category object not present") + objectCategoryList.map(a => a.getOrDefault("name", "").asInstanceOf[String]).toList + } +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/AppActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/AppActor.scala new file mode 100644 index 000000000..8bb6e5aea --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/AppActor.scala @@ -0,0 +1,71 @@ +package org.sunbird.content.actors + +import org.apache.commons.lang3.StringUtils +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.Slug +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.util.RequestUtil + +import java.util +import javax.inject.Inject +import scala.collection.JavaConverters +import scala.concurrent.{ExecutionContext, Future} + +/*** + * TODO: rewrite this Actor after merging the Event and EventSet code. + */ +class AppActor @Inject() (implicit oec: OntologyEngineContext) extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = { + request.getOperation match { + case "create" => create(request) + case "read" => read(request) + case "update" => update(request) + case _ => ERROR(request.getOperation) + } + } + + def create(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + setIdentifier(request) + DataNode.create(request, (node: Node) => node).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier) + }) + } + + @throws[Exception] + private def read(request: Request): 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 + request.getRequest.put("fields", fields) + DataNode.read(request).map(node => { + if (NodeUtil.isRetired(node)) ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name, "App not found with identifier: " + node.getIdentifier) + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, fields, request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + ResponseHandler.OK.put("app", metadata) + }) + } + + @throws[Exception] + private def update(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + DataNode.update(request).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier) + }) + } + + private def setIdentifier(request: Request) = { + val osType = request.getRequest.getOrDefault("osType", "").asInstanceOf[String] + val packageId = request.getRequest.getOrDefault("osMetadata", new util.HashMap[String, AnyRef]()) + .asInstanceOf[java.util.Map[String, AnyRef]] + .getOrDefault("packageId", "").asInstanceOf[String] + val identifier = if (StringUtils.isNotBlank(osType) && StringUtils.isNotBlank(packageId)) Slug.makeSlug(s"$osType-$packageId", true) else "" + request.getRequest.put("identifier", identifier) + } + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/AssetActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/AssetActor.scala new file mode 100644 index 000000000..974ea7f53 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/AssetActor.scala @@ -0,0 +1,28 @@ +package org.sunbird.content.actors + +import com.google.inject.Singleton +import javax.inject.Inject +import org.sunbird.actor.core.BaseActor +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.content.util.AssetCopyManager +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.util.RequestUtil + +import scala.concurrent.{ExecutionContext, Future} +@Singleton +class AssetActor @Inject()(implicit oec: OntologyEngineContext, ss: StorageService) extends BaseActor { + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = { + request.getOperation match { + case "copy" => copy(request) + case _ => ERROR(request.getOperation) + } + } + + def copy(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + AssetCopyManager.copy(request) + } +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/CategoryActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/CategoryActor.scala new file mode 100644 index 000000000..5c466bebe --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/CategoryActor.scala @@ -0,0 +1,74 @@ +package org.sunbird.content.actors + +import java.util + +import javax.inject.Inject +import org.apache.commons.lang3.StringUtils +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.Slug +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResponseCode} +import org.sunbird.content.util.CategoryConstants +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.util.RequestUtil + +import scala.collection.JavaConverters +import scala.concurrent.{ExecutionContext, Future} + +class CategoryActor @Inject()(implicit oec: OntologyEngineContext) extends BaseActor { + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = { + request.getOperation match { + case CategoryConstants.CREATE_CATEGORY => create(request) + case CategoryConstants.READ_CATEGORY => read(request) + case CategoryConstants.UPDATE_CATEGORY => update(request) + case CategoryConstants.RETIRE_CATEGORY => retire(request) + case _ => ERROR(request.getOperation) + } + } + + + @throws[Exception] + private def create(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + if (request.getRequest.containsKey("identifier")) throw new ClientException("ERR_NAME_SET_AS_IDENTIFIER", "name will be set as identifier") + if (request.getRequest.containsKey("name")) request.getRequest.put("identifier", "cat-" + Slug.makeSlug(request.getRequest.get("name").asInstanceOf[String])) + DataNode.create(request).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier).put("node_id", node.getIdentifier) + }) + } + + @throws[Exception] + private def read(request: Request): 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 + request.getRequest.put("fields", fields) + DataNode.read(request).map(node => { + if (NodeUtil.isRetired(node)) ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name, "Category not found with identifier: " + node.getIdentifier) + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, fields, request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + ResponseHandler.OK.put("category", metadata) + }) + } + + @throws[Exception] + private def update(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + request.getRequest.put("status", "Live") + DataNode.update(request).map(node => { + ResponseHandler.OK.put("node_id", node.getIdentifier) + .put("identifier", node.getIdentifier) + }) + } + + @throws[Exception] + private def retire(request: Request): Future[Response] = { + request.getRequest.put("status", "Retired") + DataNode.update(request).map(node => { + ResponseHandler.OK.put("node_id", node.getIdentifier) + .put("identifier", node.getIdentifier) + }) + } + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/CollectionActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/CollectionActor.scala new file mode 100644 index 000000000..f16b76855 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/CollectionActor.scala @@ -0,0 +1,27 @@ +package org.sunbird.content.actors + +import javax.inject.Inject +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.managers.{HierarchyManager, UpdateHierarchyManager} +import org.sunbird.utils.HierarchyConstants + +import scala.concurrent.{ExecutionContext, Future} + +class CollectionActor @Inject() (implicit oec: OntologyEngineContext) extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = { + request.getContext.put(HierarchyConstants.SCHEMA_NAME, HierarchyConstants.COLLECTION_SCHEMA_NAME) + request.getOperation match { + case "addHierarchy" => HierarchyManager.addLeafNodesToHierarchy(request) + case "removeHierarchy" => HierarchyManager.removeLeafNodesFromHierarchy(request) + case "updateHierarchy" => UpdateHierarchyManager.updateHierarchy(request) + case "getHierarchy" => HierarchyManager.getHierarchy(request) + case _ => ERROR(request.getOperation) + } + } + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/ContentActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/ContentActor.scala new file mode 100644 index 000000000..4d8892a79 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/ContentActor.scala @@ -0,0 +1,208 @@ +package org.sunbird.content.actors + +import java.util +import java.util.concurrent.CompletionException +import java.io.File + +import org.apache.commons.io.FilenameUtils +import javax.inject.Inject +import org.apache.commons.lang3.StringUtils +import org.sunbird.`object`.importer.{ImportConfig, ImportManager} +import org.sunbird.actor.core.BaseActor +import org.sunbird.cache.impl.RedisCache +import org.sunbird.content.util.{AcceptFlagManager, ContentConstants, CopyManager, DiscardManager, FlagManager, RetireManager} +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.{ContentParams, Platform, Slug} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.ClientException +import org.sunbird.content.dial.DIALManager +import org.sunbird.util.RequestUtil +import org.sunbird.content.upload.mgr.UploadManager +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil + +import scala.collection.JavaConverters +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +class ContentActor @Inject() (implicit oec: OntologyEngineContext, ss: StorageService) extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + private lazy val importConfig = getImportConfig() + private lazy val importMgr = new ImportManager(importConfig) + + override def onReceive(request: Request): Future[Response] = { + request.getOperation match { + case "createContent" => create(request) + case "readContent" => read(request) + case "updateContent" => update(request) + case "uploadContent" => upload(request) + case "retireContent" => retire(request) + case "copy" => copy(request) + case "uploadPreSignedUrl" => uploadPreSignedUrl(request) + case "discardContent" => discard(request) + case "flagContent" => flag(request) + case "acceptFlag" => acceptFlag(request) + case "linkDIALCode" => linkDIALCode(request) + case "importContent" => importContent(request) + case _ => ERROR(request.getOperation) + } + } + + def create(request: Request): Future[Response] = { + populateDefaultersForCreation(request) + RequestUtil.restrictProperties(request) + DataNode.create(request, dataModifier).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier).put("node_id", node.getIdentifier) + .put("versionKey", node.getMetadata.get("versionKey")) + }) + } + + def read(request: Request): Future[Response] = { + val responseSchemaName: String = request.getContext.getOrDefault(ContentConstants.RESPONSE_SCHEMA_NAME, "").asInstanceOf[String] + 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.read(request).map(node => { + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, fields, node.getObjectType.toLowerCase.replace("image", ""), request.getContext.get("version").asInstanceOf[String]) + metadata.put("identifier", node.getIdentifier.replace(".img", "")) + val response: Response = ResponseHandler.OK + if (responseSchemaName.isEmpty) { + response.put("content", metadata) + } + else { + response.put(responseSchemaName, metadata) + } + response + }) + } + + def update(request: Request): Future[Response] = { + populateDefaultersForUpdation(request) + if (StringUtils.isBlank(request.getRequest.getOrDefault("versionKey", "").asInstanceOf[String])) throw new ClientException("ERR_INVALID_REQUEST", "Please Provide Version Key!") + RequestUtil.restrictProperties(request) + DataNode.update(request, dataModifier).map(node => { + val identifier: String = node.getIdentifier.replace(".img", "") + ResponseHandler.OK.put("node_id", identifier).put("identifier", identifier) + .put("versionKey", node.getMetadata.get("versionKey")) + }) + } + + def upload(request: Request): Future[Response] = { + val identifier: String = request.getContext.getOrDefault("identifier", "").asInstanceOf[String] + val readReq = new Request(request) + readReq.put("identifier", identifier) + readReq.put("fields", new util.ArrayList[String]) + DataNode.read(readReq).map(node => { + if (null != node & StringUtils.isNotBlank(node.getObjectType)) + request.getContext.put("schemaName", node.getObjectType.toLowerCase()) + UploadManager.upload(request, node) + }).flatMap(f => f) + } + + def copy(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + CopyManager.copy(request) + } + + def uploadPreSignedUrl(request: Request): Future[Response] = { + val `type`: String = request.get("type").asInstanceOf[String].toLowerCase() + val fileName: String = request.get("fileName").asInstanceOf[String] + val filePath: String = request.getRequest.getOrDefault("filePath","").asInstanceOf[String] + .replaceAll("^/+|/+$", "") + val identifier: String = request.get("identifier").asInstanceOf[String] + validatePreSignedUrlRequest(`type`, fileName, filePath) + DataNode.read(request).map(node => { + val objectKey = if (StringUtils.isEmpty(filePath)) "content" + File.separator + `type` + File.separator + identifier + File.separator + Slug.makeSlug(fileName, true) + else filePath + File.separator + "content" + File.separator + `type` + File.separator + identifier + File.separator + Slug.makeSlug(fileName, true) + val expiry = Platform.config.getString("cloud_storage.upload.url.ttl") + val preSignedURL = ss.getSignedURL(objectKey, Option.apply(expiry.toInt), Option.apply("w")) + ResponseHandler.OK().put("identifier", identifier).put("pre_signed_url", preSignedURL) + .put("url_expiry", expiry) + }) recoverWith { case e: CompletionException => throw e.getCause } + } + + def retire(request: Request): Future[Response] = { + RetireManager.retire(request) + } + def discard(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + DiscardManager.discard(request) + } + + def flag(request: Request): Future[Response] = { + FlagManager.flag(request) + } + + def acceptFlag(request: Request): Future[Response] = { + AcceptFlagManager.acceptFlag(request) + } + + def linkDIALCode(request: Request): Future[Response] = DIALManager.link(request) + + def importContent(request: Request): Future[Response] = importMgr.importObject(request) + + def populateDefaultersForCreation(request: Request) = { + setDefaultsBasedOnMimeType(request, ContentParams.create.name) + setDefaultLicense(request) + } + + private def setDefaultLicense(request: Request): Unit = { + if (StringUtils.isEmpty(request.getRequest.getOrDefault("license", "").asInstanceOf[String])) { + val cacheKey = "channel_" + request.getRequest.getOrDefault("channel", "").asInstanceOf[String] + "_license" + val defaultLicense = RedisCache.get(cacheKey, null, 0) + if (StringUtils.isNotEmpty(defaultLicense)) request.getRequest.put("license", defaultLicense) + else System.out.println("Default License is not available for channel: " + request.getRequest.getOrDefault("channel", "").asInstanceOf[String]) + } + } + + def populateDefaultersForUpdation(request: Request) = { + if (request.getRequest.containsKey(ContentParams.body.name)) request.put(ContentParams.artifactUrl.name, null) + } + + private def setDefaultsBasedOnMimeType(request: Request, operation: String): Unit = { + val mimeType = request.get(ContentParams.mimeType.name).asInstanceOf[String] + if (StringUtils.isNotBlank(mimeType) && operation.equalsIgnoreCase(ContentParams.create.name)) { + if (StringUtils.equalsIgnoreCase("application/vnd.ekstep.plugin-archive", mimeType)) { + val code = request.get(ContentParams.code.name).asInstanceOf[String] + if (null == code || StringUtils.isBlank(code)) throw new ClientException("ERR_PLUGIN_CODE_REQUIRED", "Unique code is mandatory for plugins") + request.put(ContentParams.identifier.name, request.get(ContentParams.code.name)) + } + else request.put(ContentParams.osId.name, "org.ekstep.quiz.app") + if (mimeType.endsWith("archive") || mimeType.endsWith("vnd.ekstep.content-collection") || mimeType.endsWith("epub")) request.put(ContentParams.contentEncoding.name, ContentParams.gzip.name) + else request.put(ContentParams.contentEncoding.name, ContentParams.identity.name) + if (mimeType.endsWith("youtube") || mimeType.endsWith("x-url")) request.put(ContentParams.contentDisposition.name, ContentParams.online.name) + else request.put(ContentParams.contentDisposition.name, ContentParams.inline.name) + } + } + + private def validatePreSignedUrlRequest(`type`: String, fileName: String, filePath: String): Unit = { + if (StringUtils.isEmpty(fileName)) + throw new ClientException("ERR_CONTENT_BLANK_FILE_NAME", "File name is blank") + if (StringUtils.isBlank(FilenameUtils.getBaseName(fileName)) || StringUtils.length(Slug.makeSlug(fileName, true)) > 256) + throw new ClientException("ERR_CONTENT_INVALID_FILE_NAME", "Please Provide Valid File Name.") + if (!preSignedObjTypes.contains(`type`)) + throw new ClientException("ERR_INVALID_PRESIGNED_URL_TYPE", "Invalid pre-signed url type. It should be one of " + StringUtils.join(preSignedObjTypes, ",")) + if(StringUtils.isNotBlank(filePath) && filePath.size > 100) + throw new ClientException("ERR_CONTENT_INVALID_FILE_PATH", "Please provide valid filepath of character length 100 or Less ") + } + + def dataModifier(node: Node): Node = { + if(node.getMetadata.containsKey("trackable") && + node.getMetadata.getOrDefault("trackable", new java.util.HashMap[String, AnyRef]).asInstanceOf[java.util.Map[String, AnyRef]].containsKey("enabled") && + "Yes".equalsIgnoreCase(node.getMetadata.getOrDefault("trackable", new java.util.HashMap[String, AnyRef]).asInstanceOf[java.util.Map[String, AnyRef]].getOrDefault("enabled", "").asInstanceOf[String])) { + node.getMetadata.put("contentType", "Course") + } + node + } + + def getImportConfig(): ImportConfig = { + val requiredProps = Platform.getStringList("import.required_props", java.util.Arrays.asList("name", "code", "mimeType", "contentType", "artifactUrl", "framework")).asScala.toList + val validStages = Platform.getStringList("import.valid_stages", java.util.Arrays.asList("create", "upload", "review", "publish")).asScala.toList + val propsToRemove = Platform.getStringList("import.remove_props", java.util.Arrays.asList("downloadUrl", "variants", "previewUrl", "streamingUrl", "itemSets")).asScala.toList + val topicName = Platform.config.getString("import.output_topic_name") + val reqLimit = Platform.getInteger("import.request_size_limit", 200) + ImportConfig(topicName, reqLimit, requiredProps, validStages, propsToRemove) + } +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/EventActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/EventActor.scala new file mode 100644 index 000000000..cd42072d8 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/EventActor.scala @@ -0,0 +1,82 @@ +package org.sunbird.content.actors + +import org.apache.commons.lang.StringUtils +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResponseCode} +import org.sunbird.content.util.ContentConstants +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.{Node, Relation} +import org.sunbird.graph.nodes.DataNode + +import java.util +import javax.inject.Inject +import scala.collection.JavaConverters.asScalaBufferConverter +import scala.concurrent.Future + +class EventActor @Inject()(implicit oec: OntologyEngineContext, ss: StorageService) extends ContentActor { + + override def onReceive(request: Request): Future[Response] = { + request.getOperation match { + case "createContent" => create(request) + case "readContent" => read(request) + case "updateContent" => update(request) + case "retireContent" => retire(request) + case "discardContent" => discard(request) + case "publishContent" => publish(request) + case _ => ERROR(request.getOperation) + } + } + + override def update(request: Request): Future[Response] = { + verifyStandaloneEventAndApply(super.update, request, Some(node => { + if (!"Draft".equalsIgnoreCase(node.getMetadata.getOrDefault("status", "").toString)) { + throw new ClientException(ContentConstants.ERR_CONTENT_NOT_DRAFT, "Update not allowed! Event status isn't draft") + } + })) + } + + def publish(request: Request): Future[Response] = { + verifyStandaloneEventAndApply(super.update, request, Some(node => { + if (!"Draft".equalsIgnoreCase(node.getMetadata.getOrDefault("status", "").toString)) { + throw new ClientException(ContentConstants.ERR_CONTENT_NOT_DRAFT, "Publish not allowed! Event status isn't draft") + } + val versionKey = node.getMetadata.getOrDefault("versionKey", "").toString + if (StringUtils.isNotBlank(versionKey)) + request.put("versionKey", versionKey) + })) + } + + override def discard(request: Request): Future[Response] = { + verifyStandaloneEventAndApply(super.discard, request) + } + + override def retire(request: Request): Future[Response] = { + verifyStandaloneEventAndApply(super.retire, request) + } + + private def verifyStandaloneEventAndApply(f: Request => Future[Response], request: Request, dataUpdater: Option[Node => Unit] = None): Future[Response] = { + DataNode.read(request).flatMap(node => { + val inRelations = if (node.getInRelations == null) new util.ArrayList[Relation]() else node.getInRelations; + val hasEventSetParent = inRelations.asScala.exists(rel => "EventSet".equalsIgnoreCase(rel.getStartNodeObjectType)) + if (hasEventSetParent) + Future(ResponseHandler.ERROR(ResponseCode.CLIENT_ERROR, ResponseCode.CLIENT_ERROR.name(), "ERROR: Can't modify an Event which is part of an Event Set!")) + else { + if (dataUpdater.isDefined) { + dataUpdater.get.apply(node) + } + f.apply(request) + } + }) + } + + override def dataModifier(node: Node): Node = { + if (node.getMetadata.containsKey("trackable") && + node.getMetadata.getOrDefault("trackable", new java.util.HashMap[String, AnyRef]).asInstanceOf[java.util.Map[String, AnyRef]].containsKey("enabled") && + "Yes".equalsIgnoreCase(node.getMetadata.getOrDefault("trackable", new java.util.HashMap[String, AnyRef]).asInstanceOf[java.util.Map[String, AnyRef]].getOrDefault("enabled", "").asInstanceOf[String])) { + node.getMetadata.put("contentType", "Event") + } + node + } + +} \ No newline at end of file diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/EventSetActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/EventSetActor.scala new file mode 100644 index 000000000..2894cc228 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/EventSetActor.scala @@ -0,0 +1,255 @@ +package org.sunbird.content.actors + +import org.apache.commons.lang3.StringUtils +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResponseCode} +import org.sunbird.content.util.{ContentConstants, DiscardManager, RetireManager} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.enums.SystemProperties +import org.sunbird.graph.dac.model.{Node, Relation} +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.util.RequestUtil +import org.sunbird.utils.HierarchyConstants + +import java.util +import javax.inject.Inject +import scala.collection.JavaConverters._ +import scala.concurrent.Future + +class EventSetActor @Inject()(implicit oec: OntologyEngineContext, ss: StorageService) extends ContentActor { + + override def onReceive(request: Request): Future[Response] = { + request.getContext.put(HierarchyConstants.SCHEMA_NAME, HierarchyConstants.EVENT_SET_SCHEMA_NAME) + request.getOperation match { + case "createContent" => create(request) + case "updateContent" => update(request) + case "publishContent" => publish(request) + case "getHierarchy" => getHierarchy(request) + case "readContent" => read(request) + case "retireContent" => retire(request) + case "discardContent" => discard(request) + case _ => ERROR(request.getOperation) + } + } + + override def create(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + val originalRequestContent = request.getRequest + addChildEvents(request) + .map(nodes => updateRequestWithChildRelations(request, originalRequestContent, nodes)) + .flatMap(req => { + DataNode.create(req).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier).put("node_id", node.getIdentifier) + .put("versionKey", node.getMetadata.get("versionKey")) + }) + }).recoverWith { + case clientException: ClientException => + Future(ResponseHandler.ERROR(ResponseCode.CLIENT_ERROR, ResponseCode.CLIENT_ERROR.name(), clientException.getMessage)) + case e: Exception => + Future(ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), e.getMessage)) + } + } + + override def update(request: Request): Future[Response] = { + if (StringUtils.isBlank(request.getRequest.getOrDefault("versionKey", "").asInstanceOf[String])) throw new ClientException("ERR_INVALID_REQUEST", "Please Provide Version Key!") + RequestUtil.restrictProperties(request) + val originalRequestContent = request.getRequest + DataNode.read(request).flatMap(node => { + if (!"Draft".equalsIgnoreCase(node.getMetadata.getOrDefault("status", "").toString)) { + throw new ClientException(ContentConstants.ERR_CONTENT_NOT_DRAFT, "Update not allowed! EventSet status isn't draft") + } + deleteExistingEvents(node.getOutRelations, request).flatMap(_ => { + addChildEvents(request).map(nodes => { + updateRequestWithChildRelations(request, originalRequestContent, nodes) + }).flatMap(req => + DataNode.update(req).map(node => { + val identifier: String = node.getIdentifier.replace(".img", "") + ResponseHandler.OK.put("node_id", identifier).put("identifier", identifier) + .put("versionKey", node.getMetadata.get("versionKey")) + }) + ) + }) + }).recoverWith { + case clientException: ClientException => + Future(ResponseHandler.ERROR(ResponseCode.CLIENT_ERROR, ResponseCode.CLIENT_ERROR.name(), clientException.getMessage)) + case e: Exception => + Future(ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), e.getMessage)) + } + } + + def publish(request: Request): Future[Response] = { + DataNode.read(request).flatMap(node => { + if (!"Draft".equalsIgnoreCase(node.getMetadata.getOrDefault("status", "").toString)) { + throw new ClientException(ContentConstants.ERR_CONTENT_NOT_DRAFT, "Publish not allowed! EventSet status isn't draft") + } + publishChildEvents(node.getOutRelations, request).flatMap(_ => { + val existingVersionKey = node.getMetadata.getOrDefault("versionKey", "").asInstanceOf[String] + request.put("versionKey", existingVersionKey) + request.getContext.put("identifier", node.getIdentifier) + request.put("status", "Live") + DataNode.update(request).map(updateNode => { + val identifier: String = updateNode.getIdentifier.replace(".img", "") + ResponseHandler.OK.put("node_id", identifier).put("identifier", identifier) + .put("versionKey", updateNode.getMetadata.get("versionKey")) + }) + } + ) + }).recoverWith { + case clientException: ClientException => + Future(ResponseHandler.ERROR(ResponseCode.CLIENT_ERROR, ResponseCode.CLIENT_ERROR.name(), clientException.getMessage)) + case e: Exception => + Future(ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), e.getMessage)) + } + } + + override def retire(request: Request): Future[Response] = { + DataNode.read(request).flatMap(node => { + retireChildEvents(node.getOutRelations, request) + .flatMap(_ => RetireManager.retire(request)) + }).recoverWith { + case clientException: ClientException => + Future(ResponseHandler.ERROR(ResponseCode.CLIENT_ERROR, ResponseCode.CLIENT_ERROR.name(), clientException.getMessage)) + case e: Exception => + Future(ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), e.getMessage)) + } + } + + override def discard(request: Request): Future[Response] = { + DataNode.read(request).flatMap(node => discardChildEvents(node.getOutRelations, request) + .flatMap(_ => DiscardManager.discard(request))) + .recoverWith { + case clientException: ClientException => + Future(ResponseHandler.ERROR(ResponseCode.CLIENT_ERROR, ResponseCode.CLIENT_ERROR.name(), clientException.getMessage)) + case e: Exception => + Future(ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), e.getMessage)) + } + } + + def getHierarchy(request: Request): Future[Response] = { + val fields: util.List[String] = seqAsJavaListConverter(request.get("fields").asInstanceOf[String].split(",").filter(field => StringUtils.isNotBlank(field) && !StringUtils.equalsIgnoreCase(field, "null"))).asJava + request.getRequest.put("fields", fields) + DataNode.read(request).map(node => { + val outRelations = if (node.getOutRelations == null) List[Relation]() else node.getOutRelations.asScala + val childNodes = outRelations.map(relation => relation.getEndNodeId).toList + val children = outRelations.map(relation => relation.getEndNodeMetadata).map(metadata => { + SystemProperties.values().foreach(value => metadata.remove(value.name())) + metadata + }).toList + + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, new util.ArrayList[String](), node.getObjectType.toLowerCase.replace("image", ""), request.getContext.get("version").asInstanceOf[String], true) + metadata.put("identifier", node.getIdentifier.replace(".img", "")) + metadata.put("childNodes", childNodes.asJava) + metadata.put("children", children.asJava) + ResponseHandler.OK.put("eventset", metadata) + }) + } + + private def updateRequestWithChildRelations(request: Request, originalRequestContent: util.Map[String, AnyRef], nodes: List[Node]) = { + request.setRequest(originalRequestContent) + val relations = new util.ArrayList[util.Map[String, String]]() + nodes.foreach(node => { + relations.add(Map("identifier" -> node.getIdentifier).asJava) + }) + request.getRequest.put("collections", relations) + request + } + + private def addChildEvents(request: Request) = { + val newChildEvents = formChildEvents(request) + Future.sequence(newChildEvents.map(childEvent => { + val childRequest = new Request(request) + childRequest.setRequest(childEvent.asJava) + childRequest.getContext.put("schemaName", "event") + childRequest.getContext.put("objectType", "Event") + DataNode.create(childRequest) + })) + } + + private def formChildEvents(contentRequest: Request): List[collection.mutable.Map[String, AnyRef]] = { + val scheduleObject = contentRequest.getRequest.getOrDefault("schedule", new util.HashMap[String, Object]()).asInstanceOf[util.Map[String, Object]] + val schedules = scheduleObject.getOrDefault("value", new util.ArrayList[util.Map[String, String]]()).asInstanceOf[util.List[util.Map[String, String]]].asScala + schedules.map(schedule => { + var event = collection.mutable.Map[String, AnyRef]() ++ contentRequest.getRequest.asScala + event ++= schedule.asScala + event -= "schedule" + event -= "identifier" + event + }).toList + } + + private def deleteExistingEvents(relations: util.List[Relation], request: Request) = { + if (relations != null) + Future.sequence(relations.asScala.filter(rel => "Event".equalsIgnoreCase(rel.getEndNodeObjectType)).map(relation => { + val deleteReq = new Request() + deleteReq.setContext(request.getContext) + val delMap = new util.HashMap[String, AnyRef]() + delMap.put("identifier", relation.getEndNodeId) + deleteReq.setRequest(delMap) + DataNode.deleteNode(deleteReq) + })) + else + Future(List()) + } + + private def publishChildEvents(relations: util.List[Relation], request: Request) = { + if (relations != null) + Future.sequence(relations.asScala.filter(rel => "Event".equalsIgnoreCase(rel.getEndNodeObjectType)).map(relation => { + val updateReq = new Request() + val context = new util.HashMap[String, Object]() + context.putAll(request.getContext) + updateReq.setContext(context) + updateReq.getContext.put("schemaName", "event") + updateReq.getContext.put("objectType", "Event") + updateReq.getContext.put("identifier", relation.getEndNodeId) + val updateMap = new util.HashMap[String, AnyRef]() + updateMap.put("identifier", relation.getEndNodeId) + updateMap.put("status", "Live") + updateReq.setRequest(updateMap) + DataNode.update(updateReq) + })) + else + Future(List()) + } + + private def retireChildEvents(relations: util.List[Relation], request: Request) = { + if (relations != null) + Future.sequence(relations.asScala.filter(rel => "Event".equalsIgnoreCase(rel.getEndNodeObjectType)).map(relation => { + val retireReq = new Request() + val context = new util.HashMap[String, Object]() + context.putAll(request.getContext) + retireReq.setContext(context) + retireReq.getContext.put("schemaName", "event") + retireReq.getContext.put("objectType", "Event") + retireReq.getContext.put("identifier", relation.getEndNodeId) + val updateMap = new util.HashMap[String, AnyRef]() + updateMap.put("identifier", relation.getEndNodeId) + retireReq.setRequest(updateMap) + RetireManager.retire(retireReq) + + })) + else + Future(List()) + } + + private def discardChildEvents(relations: util.List[Relation], request: Request) = { + if (relations != null) + Future.sequence(relations.asScala.filter(rel => "Event".equalsIgnoreCase(rel.getEndNodeObjectType)).map(relation => { + val discardReq = new Request() + val context = new util.HashMap[String, Object]() + context.putAll(request.getContext) + discardReq.setContext(context) + discardReq.getContext.put("schemaName", "event") + discardReq.getContext.put("objectType", "Event") + discardReq.getContext.put("identifier", relation.getEndNodeId) + val updateMap = new util.HashMap[String, AnyRef]() + updateMap.put("identifier", relation.getEndNodeId) + discardReq.setRequest(updateMap) + RetireManager.retire(discardReq) + + })) + else + Future(List()) + } +} \ No newline at end of file diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/HealthActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/HealthActor.scala new file mode 100644 index 000000000..3e98eaa5f --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/HealthActor.scala @@ -0,0 +1,17 @@ +package org.sunbird.content.actors + +import javax.inject.Inject +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.health.HealthCheckManager + +import scala.concurrent.{ExecutionContext, Future} + +class HealthActor @Inject() (implicit oec: OntologyEngineContext) extends BaseActor { + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = { + HealthCheckManager.checkAllSystemHealth() + } +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/actors/LicenseActor.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/LicenseActor.scala new file mode 100644 index 000000000..b637c6704 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/actors/LicenseActor.scala @@ -0,0 +1,72 @@ +package org.sunbird.content.actors + +import java.util + +import javax.inject.Inject +import org.apache.commons.lang3.StringUtils +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.Slug +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResponseCode} +import org.sunbird.content.util.LicenseConstants +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.util.RequestUtil + +import scala.collection.JavaConverters +import scala.concurrent.{ExecutionContext, Future} + +class LicenseActor @Inject() (implicit oec: OntologyEngineContext) extends BaseActor { + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = { + request.getOperation match { + case LicenseConstants.CREATE_LICENSE => create(request) + case LicenseConstants.READ_LICENSE => read(request) + case LicenseConstants.UPDATE_LICENSE => update(request) + case LicenseConstants.RETIRE_LICENSE => retire(request) + case _ => ERROR(request.getOperation) + } + } + + + @throws[Exception] + private def create(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + if (request.getRequest.containsKey("identifier")) throw new ClientException("ERR_NAME_SET_AS_IDENTIFIER", "name will be set as identifier") + if (request.getRequest.containsKey("name")) request.getRequest.put("identifier", Slug.makeSlug(request.getRequest.get("name").asInstanceOf[String])) + DataNode.create(request).map(node => { + ResponseHandler.OK.put("identifier", node.getIdentifier).put("node_id", node.getIdentifier) + }) + } + + @throws[Exception] + private def read(request: Request): 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 + request.getRequest.put("fields", fields) + DataNode.read(request).map(node => { + if (NodeUtil.isRetired(node)) ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name, "License not found with identifier: " + node.getIdentifier) + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, fields, request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + ResponseHandler.OK.put("license", metadata) + }) + } + + @throws[Exception] + private def update(request: Request): Future[Response] = { + RequestUtil.restrictProperties(request) + request.getRequest.put("status", "Live") + DataNode.update(request).map(node => { + ResponseHandler.OK.put("node_id", node.getIdentifier).put("identifier", node.getIdentifier) + }) + } + + @throws[Exception] + private def retire(request: Request): Future[Response] = { + request.getRequest.put("status", "Retired") + DataNode.update(request).map(node => { + ResponseHandler.OK.put("node_id", node.getIdentifier).put("identifier", node.getIdentifier) + }) + } + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALConstants.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALConstants.scala new file mode 100644 index 000000000..be127079b --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALConstants.scala @@ -0,0 +1,20 @@ +package org.sunbird.content.dial + +object DIALConstants { + + val CONTENT: String = "content" + val COLLECTION: String = "collection" + val LINK_TYPE: String = "linkType" + val CHANNEL: String = "channel" + val IDENTIFIER: String = "identifier" + val IDENTIFIERS: String = "identifiers" + val DIALCODE: String = "dialcode" + val DIALCODES: String = "dialcodes" + val COUNT: String = "count" + val REQUEST: String = "request" + val SEARCH: String = "search" + val AUTHORIZATION: String = "Authorization" + val X_CHANNEL_ID: String = "X-Channel-Id" + val VERSION_KEY: String = "versionKey" + +} \ No newline at end of file diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALErrors.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALErrors.scala new file mode 100644 index 000000000..8d70cfc7c --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALErrors.scala @@ -0,0 +1,16 @@ +package org.sunbird.content.dial + +object DIALErrors { + + //Error Codes + val ERR_DIALCODE_LINK_REQUEST: String = "ERR_DIALCODE_LINK_REQUEST" + val ERR_DIALCODE_LINK: String = "ERR_DIALCODE_LINK" + + //Error Messages + val ERR_INVALID_REQ_MSG: String = "Invalid Request! Please Provide Valid Request." + val ERR_REQUIRED_PROPS_MSG: String = "Invalid Request! Please Provide Required Properties In Request." + val ERR_MAX_LIMIT_MSG: String = "Max Limit For Link Content To DIAL Code In A Request Is " + val ERR_DIAL_NOT_FOUND_MSG: String = "DIAL Code Not Found With Id(s): " + val ERR_CONTENT_NOT_FOUND_MSG: String = "Content Not Found With Id(s): " + val ERR_SERVER_ERROR_MSG: String = "Something Went Wrong While Processing Your Request. Please Try Again After Sometime!" +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALManager.scala new file mode 100644 index 000000000..487ac475c --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/dial/DIALManager.scala @@ -0,0 +1,152 @@ +package org.sunbird.content.dial + +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.Platform +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ErrorCodes, ResourceNotFoundException, ResponseCode, ServerException} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import java.util +import scala.collection.immutable.HashMap +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + + +object DIALManager { + + val DIAL_SEARCH_API_URL = Platform.config.getString("dial_service.api.base_url") + "/dialcode/v3/search" + val DIAL_API_AUTH_KEY = "Bearer " + Platform.config.getString("dial_service.api.auth_key") + val PASSPORT_KEY = Platform.config.getString("graph.passport.key.base") + + def link(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val linkType: String = request.getContext.getOrDefault(DIALConstants.LINK_TYPE, DIALConstants.CONTENT).asInstanceOf[String] + val channelId: String = request.getContext.getOrDefault(DIALConstants.CHANNEL, "").asInstanceOf[String] + val objectId: String = request.getContext.getOrDefault(DIALConstants.IDENTIFIER, "").asInstanceOf[String] + val reqList: List[Map[String, List[String]]] = getRequestData(request) + val requestMap: Map[String, List[String]] = validateAndGetRequestMap(channelId, reqList) + linkType match { + case DIALConstants.CONTENT => linkContent(requestMap, request.getContext) + case DIALConstants.COLLECTION => linkCollection(objectId, requestMap, request.getContext) + case _ => throw new ClientException(DIALErrors.ERR_DIALCODE_LINK_REQUEST, DIALErrors.ERR_INVALID_REQ_MSG) + } + } + + def getRequestData(request: Request): List[Map[String, List[String]]] = { + val req = request.getRequest.get(DIALConstants.CONTENT) + req match { + case req: util.List[util.Map[String, AnyRef]] => req.asScala.toList.map(obj => obj.asScala.toMap.map(x => (x._1, getList(x._2)))) + case req: util.Map[String, AnyRef] => List(req.asScala.toMap.map(x => (x._1, getList(x._2)))) + case _ => throw new ClientException(DIALErrors.ERR_DIALCODE_LINK_REQUEST, DIALErrors.ERR_INVALID_REQ_MSG) + } + } + + def getList(obj: AnyRef): List[String] = { + (obj match { + case obj: util.List[String] => obj.asScala.toList.distinct + case obj: String => List(obj).distinct + case _ => List.empty + }).filter((x: String) => StringUtils.isNotBlank(x) && !StringUtils.equals(" ", x)) + } + + def validateAndGetRequestMap(channelId: String, requestList: List[Map[String, List[String]]])(implicit oec:OntologyEngineContext): Map[String, List[String]] = { + var reqMap = HashMap[String, List[String]]() + requestList.foreach(req => { + val contents: List[String] = req.get(DIALConstants.IDENTIFIER).get + val dialcodes: List[String] = req.get(DIALConstants.DIALCODE).get + validateReqStructure(dialcodes, contents) + contents.foreach(id => reqMap += (id -> dialcodes)) + }) + if (Platform.getBoolean("content.link_dialcode.validation", true)) { + val dials = requestList.collect { case m if m.get(DIALConstants.DIALCODE).nonEmpty => m.get(DIALConstants.DIALCODE).get }.flatten + validateDialCodes(channelId, dials) + } + reqMap + } + + def validateReqStructure(dialcodes: List[String], contents: List[String]): Unit = { + if (null == dialcodes || null == contents || contents.isEmpty) + throw new ClientException(DIALErrors.ERR_DIALCODE_LINK_REQUEST, DIALErrors.ERR_REQUIRED_PROPS_MSG) + val maxLimit: Int = Platform.getInteger("content.link_dialcode.max_limit", 10) + if (dialcodes.size >= maxLimit || contents.size >= maxLimit) + throw new ClientException(DIALErrors.ERR_DIALCODE_LINK_REQUEST, DIALErrors.ERR_MAX_LIMIT_MSG + maxLimit) + } + + def validateDialCodes(channelId: String, dialcodes: List[String])(implicit oec: OntologyEngineContext): Boolean = { + if (!dialcodes.isEmpty) { + val reqMap = new util.HashMap[String, AnyRef]() {{ + put(DIALConstants.REQUEST, new util.HashMap[String, AnyRef]() {{ + put(DIALConstants.SEARCH, new util.HashMap[String, AnyRef]() {{ + put(DIALConstants.IDENTIFIER, dialcodes.distinct.asJava) + }}) + }}) + }} + val headerParam = HashMap[String, String](DIALConstants.X_CHANNEL_ID -> channelId, DIALConstants.AUTHORIZATION -> DIAL_API_AUTH_KEY).asJava + val searchResponse = oec.httpUtil.post(DIAL_SEARCH_API_URL, reqMap, headerParam) + if (searchResponse.getResponseCode.toString == "OK") { + val result = searchResponse.getResult + if (dialcodes.distinct.size == result.get(DIALConstants.COUNT).asInstanceOf[Integer]) { + return true + } else { + val dials = result.get(DIALConstants.DIALCODES).asInstanceOf[util.List[util.Map[String, AnyRef]]].asScala.toList.map(obj => obj.asScala.toMap).map(_.getOrElse(DIALConstants.IDENTIFIER, "")).asInstanceOf[List[String]] + throw new ResourceNotFoundException(DIALErrors.ERR_DIALCODE_LINK, DIALErrors.ERR_DIAL_NOT_FOUND_MSG + dialcodes.distinct.diff(dials).asJava) + } + } + else throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, DIALErrors.ERR_SERVER_ERROR_MSG) + } + true + } + + def linkContent(requestMap: Map[String, List[String]], reqContext: util.Map[String, AnyRef])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + validateContents(requestMap, reqContext).map(result => { + val futureList: List[Future[Node]] = requestMap.filter(x => !result.contains(x._1)).map(map => { + val updateReqMap = new util.HashMap[String, AnyRef]() {{ + val dials: util.List[String] = if (!map._2.isEmpty) map._2.asJava else new util.ArrayList[String]() + put(DIALConstants.DIALCODES, dials) + put(DIALConstants.VERSION_KEY, PASSPORT_KEY) + }} + val updateRequest = new Request() + reqContext.put(DIALConstants.IDENTIFIER, map._1) + updateRequest.setContext(reqContext) + updateRequest.putAll(updateReqMap) + DataNode.update(updateRequest) + }).toList + val updatedNodes: Future[List[Node]] = Future.sequence(futureList) + getResponse(requestMap, updatedNodes, result) + }).flatMap(f => f) + } + + //TODO: Complete the implementation + def linkCollection(objectId: String, requestMap: Map[String, List[String]], getContext: util.Map[String, AnyRef])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + Future { + ResponseHandler.OK() + } + } + + def validateContents(requestMap: Map[String, List[String]], reqContext: util.Map[String, AnyRef])(implicit ec: ExecutionContext, oec:OntologyEngineContext): Future[List[String]] = { + val request = new Request() + request.setContext(reqContext) + request.put(DIALConstants.IDENTIFIERS, requestMap.keys.toList.asJava) + DataNode.list(request).map(obj => { + if (null != obj && !obj.isEmpty) { + val identifiers = obj.asScala.collect { case node if null != node => node.getIdentifier }.toList + Future { + requestMap.keys.toList.diff(identifiers) + } + } else throw new ResourceNotFoundException(DIALErrors.ERR_DIALCODE_LINK, DIALErrors.ERR_CONTENT_NOT_FOUND_MSG + requestMap.keySet.asJava) + }).flatMap(f => f) + } + + def getResponse(requestMap: Map[String, List[String]], updatedNodes: Future[List[Node]], invalidIds: List[String])(implicit ec: ExecutionContext): Future[Response] = { + updatedNodes.map(obj => { + val successIds = obj.collect { case node if null != node => node.getIdentifier } + if (requestMap.keySet.size == successIds.size) + ResponseHandler.OK + else if (invalidIds.nonEmpty && successIds.isEmpty) + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, DIALErrors.ERR_DIALCODE_LINK, DIALErrors.ERR_CONTENT_NOT_FOUND_MSG + invalidIds.asJava) + else + ResponseHandler.ERROR(ResponseCode.PARTIAL_SUCCESS, DIALErrors.ERR_DIALCODE_LINK, DIALErrors.ERR_CONTENT_NOT_FOUND_MSG + invalidIds.asJava) + }) + } + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/upload/mgr/UploadManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/upload/mgr/UploadManager.scala new file mode 100644 index 000000000..e6a26dbe3 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/upload/mgr/UploadManager.scala @@ -0,0 +1,115 @@ +package org.sunbird.content.upload.mgr + +import java.io.File +import java.util + +import org.apache.commons.lang3.StringUtils +import org.sunbird.models.UploadParams +import org.sunbird.common.Platform +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResponseCode} +import org.sunbird.content.util.ContentConstants +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import org.sunbird.mimetype.factory.MimeTypeManagerFactory +import org.sunbird.telemetry.util.LogTelemetryEventUtil + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.{ExecutionContext, Future} +import org.sunbird.kafka.client.KafkaClient + +import scala.collection.Map + +object UploadManager { + + private val MEDIA_TYPE_LIST = List("image", "video") + private val kfClient = new KafkaClient + private val CONTENT_ARTIFACT_ONLINE_SIZE: Double = Platform.getDouble("content.artifact.size.for_online", 209715200.asInstanceOf[Double]) + + + def upload(request: Request, node: Node)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val identifier: String = node.getIdentifier + val fileUrl: String = request.getRequest.getOrDefault("fileUrl", "").asInstanceOf[String] + val file = request.getRequest.get("file").asInstanceOf[File] + val reqFilePath: String = request.getRequest.getOrDefault("filePath", "").asInstanceOf[String].replaceAll("^/+|/+$", "") + val filePath = if(StringUtils.isBlank(reqFilePath)) None else Option(reqFilePath) + val mimeType = node.getMetadata().getOrDefault("mimeType", "").asInstanceOf[String] + val mediaType = node.getMetadata.getOrDefault("mediaType", "").asInstanceOf[String] + val mgr = MimeTypeManagerFactory.getManager(node.getObjectType, mimeType) + val params: UploadParams = request.getContext.get("params").asInstanceOf[UploadParams] + val uploadFuture: Future[Map[String, AnyRef]] = if (StringUtils.isNotBlank(fileUrl)) mgr.upload(identifier, node, fileUrl, filePath, params) else mgr.upload(identifier, node, file, filePath, params) + uploadFuture.map(result => { + if(filePath.isDefined) + updateNode(request, node.getIdentifier, mediaType, node.getObjectType, result + (ContentConstants.ARTIFACT_BASE_PATH -> filePath.get)) + else + updateNode(request, node.getIdentifier, mediaType, node.getObjectType, result) + }).flatMap(f => f) + } + + def updateNode(request: Request, identifier: String, mediaType: String, objectType: String, result: Map[String, AnyRef])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val updatedResult = result - "identifier" + val artifactUrl = updatedResult.getOrElse("artifactUrl", "").asInstanceOf[String] + val size: Double = updatedResult.getOrElse("size", 0.asInstanceOf[Double]).asInstanceOf[Double] + if (StringUtils.isNotBlank(artifactUrl)) { + val updateReq = new Request(request) + updateReq.getContext().put("identifier", identifier) + updateReq.getRequest.putAll(mapAsJavaMap(updatedResult)) + if( size > CONTENT_ARTIFACT_ONLINE_SIZE) + updateReq.put("contentDisposition", "online-only") + if (StringUtils.equalsIgnoreCase("Asset", objectType) && MEDIA_TYPE_LIST.contains(mediaType)) + updateReq.put("status", "Processing") + + DataNode.update(updateReq).map(node => { + if (StringUtils.equalsIgnoreCase("Asset", objectType) && MEDIA_TYPE_LIST.contains(mediaType) && null != node) + pushInstructionEvent(identifier, node) + getUploadResponse(node) + }) + } else { + Future { + ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, "ERR_UPLOAD_FILE", "Something Went Wrong While Processing Your Request.") + } + } + } + + def getUploadResponse(node: Node)(implicit ec: ExecutionContext): Response = { + val id = node.getIdentifier.replace(".img", "") + val url = node.getMetadata.get("artifactUrl").asInstanceOf[String] + ResponseHandler.OK.put("node_id", id).put("identifier", id).put("artifactUrl", url) + .put("content_url", url).put("versionKey", node.getMetadata.get("versionKey")) + } + + @throws[Exception] + private def pushInstructionEvent(identifier: String, node: Node): Unit = { + val actor: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef] + val context: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef] + val objectData: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef] + val edata: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef] + generateInstructionEventMetadata(actor, context, objectData, edata, node, identifier) + val beJobRequestEvent: String = LogTelemetryEventUtil.logInstructionEvent(actor, context, objectData, edata) + 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.") + kfClient.send(beJobRequestEvent, topic) + } + + private def generateInstructionEventMetadata(actor: util.Map[String, AnyRef], context: util.Map[String, AnyRef], objectData: util.Map[String, AnyRef], edata: util.Map[String, AnyRef], node: Node, identifier: String): Unit = { + val metadata: util.Map[String, AnyRef] = node.getMetadata + actor.put("id", "Asset Enrichment Samza Job") + actor.put("type", "System") + context.put("channel", metadata.get("channel")) + context.put("pdata", new util.HashMap[String, AnyRef]() {{ + put("id", "org.sunbird.platform") + put("ver", "1.0") + }}) + if (Platform.config.hasPath("cloud_storage.env")) { + val env: String = Platform.getString("cloud_storage.env", "dev") + context.put("env", env) + } + objectData.put("id", identifier) + objectData.put("ver", metadata.get("versionKey")) + edata.put("action", "assetenrichment") + edata.put("status", metadata.get("status")) + edata.put("mediaType", metadata.get("mediaType")) + edata.put("objectType", node.getObjectType) + } +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/AcceptFlagManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/AcceptFlagManager.scala new file mode 100644 index 000000000..1fb570d7c --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/AcceptFlagManager.scala @@ -0,0 +1,93 @@ +package org.sunbird.content.util + +import java.util + +import org.apache.commons.lang.StringUtils +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.{DateUtils, Platform} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.service.common.DACConfigurationConstants +import org.sunbird.graph.utils.ScalaJsonUtils +import org.sunbird.managers.HierarchyManager + +import scala.concurrent.{ExecutionContext, Future} + +object AcceptFlagManager { + + def acceptFlag(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + DataNode.read(request).map(node => { + if (!StringUtils.equals(ContentConstants.FLAGGED, node.getMetadata.getOrDefault(ContentConstants.STATUS, "").asInstanceOf[String])) { + Future(ResponseHandler.ERROR(ResponseCode.CLIENT_ERROR, ContentConstants.ERR_INVALID_CONTENT, "Invalid Flagged Content! Content Can Not Be Accepted.")) + } else { + request.getContext.put(ContentConstants.IDENTIFIER, node.getIdentifier) + createOrUpdateImageNode(request, node).map(imgNode => { + updateOriginalNode(request, node).map(response => { + if (!ResponseHandler.checkError(response)) { + response.put(ContentConstants.NODE_ID, node.getIdentifier) + response.put(ContentConstants.IDENTIFIER, node.getIdentifier) + response.put(ContentConstants.VERSION_KEY, imgNode.getMetadata.get(ContentConstants.VERSION_KEY)) + response + } else { + response + } + }) + }).flatMap(f => f) + } + }).flatMap(f => f) + } + + private def createOrUpdateImageNode(request: Request, node: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val req: Request = new Request(request) + req.put(ContentConstants.STATUS, ContentConstants.FLAG_DRAFT) + req.put(ContentConstants.IDENTIFIER, node.getIdentifier) + DataNode.update(req).map(node => { + node + }) + } + + private def updateOriginalNode(request: Request, node: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + val currentDate = DateUtils.formatCurrentDate + request.put(ContentConstants.STATUS, ContentConstants.RETIRED) + request.put(ContentConstants.LAST_STATUS_CHANGED_ON, currentDate) + request.put(ContentConstants.LAST_UPDATED_ON, currentDate) + request.put(ContentConstants.VERSION_KEY, Platform.config.getString(DACConfigurationConstants.PASSPORT_KEY_BASE_PROPERTY)) + + request.getContext.put(ContentConstants.VERSIONING, ContentConstants.DISABLE) + + if (StringUtils.equals(node.getMetadata.getOrDefault(ContentConstants.MIME_TYPE, "").asInstanceOf[String], ContentConstants.COLLECTION_MIME_TYPE)) { + request.getContext.put(ContentConstants.SCHEMA_NAME, ContentConstants.COLLECTION_SCHEMA_NAME) + request.put(ContentConstants.ROOT_ID, request.get(ContentConstants.IDENTIFIER)) + HierarchyManager.getHierarchy(request).map(hierarchyResponse => { + if (!ResponseHandler.checkError(hierarchyResponse)) { + val updatedHierarchy = hierarchyResponse.get(ContentConstants.CONTENT).asInstanceOf[util.Map[String, AnyRef]] + updatedHierarchy.putAll(new util.HashMap[String, AnyRef]() { + { + put(ContentConstants.STATUS, ContentConstants.RETIRED) + put(ContentConstants.LAST_STATUS_CHANGED_ON, currentDate) + put(ContentConstants.LAST_UPDATED_ON, currentDate) + } + }) + request.put(ContentConstants.HIERARCHY, ScalaJsonUtils.serialize(updatedHierarchy)) + RedisCache.delete(ContentConstants.HIERARCHY_PREFIX + request.get(ContentConstants.IDENTIFIER).asInstanceOf[String]) + updateNode(request) + } else { + Future(hierarchyResponse) + } + }).flatMap(f => f) + } else { + updateNode(request) + } + } + + private def updateNode(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + DataNode.update(request).map(updatedOriginalNode => { + RedisCache.delete(request.get(ContentConstants.IDENTIFIER).asInstanceOf[String]) + ResponseHandler.OK() + }) + } + +} \ No newline at end of file diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/ActorNames.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/ActorNames.scala new file mode 100644 index 000000000..2c1ef8cd4 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/ActorNames.scala @@ -0,0 +1,7 @@ +package org.sunbird.content.util + +object ActorNames { + + final val COLLECTION_ACTOR = "collectionActor" + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/AssetConstants.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/AssetConstants.scala new file mode 100644 index 000000000..0b815d1df --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/AssetConstants.scala @@ -0,0 +1,19 @@ +package org.sunbird.content.util + +object AssetConstants { + val ASSET_COPY_SCHEME: String = "assetCopyScheme" + val STATUS: String = "status" + val ORIGIN: String = "origin" + val IDENTIFIER: String = "identifier" + val ORIGIN_DATA: String = "originData" + val SCHEMA_NAME: String = "schemaName" + val OBJECT_TYPE: String = "objectType" + val VERSION_KEY: String = "versionKey" + val ARTIFACT_URL: String = "artifactUrl" + val MIME_TYPE: String = "mimeType" + val NAME: String = "name" + val SCHEMA_VERSION: String = "1.0" + val ERR_INVALID_REQUEST: String = "ERR_INVALID_REQUEST" + val MEDIA_TYPE: String = "mediaType" + val FILE_URL: String = "fileUrl" +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/AssetCopyManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/AssetCopyManager.scala new file mode 100644 index 000000000..b45e85dc3 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/AssetCopyManager.scala @@ -0,0 +1,89 @@ +package org.sunbird.content.util + +import java.util +import java.util.concurrent.CompletionException + +import org.apache.commons.lang.StringUtils +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.ClientException +import org.sunbird.content.upload.mgr.UploadManager +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.Identifier +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil + +import scala.concurrent.{ExecutionContext, Future} + +object AssetCopyManager { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + implicit val ss: StorageService = new StorageService + + def copy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + request.getContext.put(AssetConstants.ASSET_COPY_SCHEME, request.getRequest.getOrDefault(AssetConstants.ASSET_COPY_SCHEME, "")) + DataNode.read(request).map(node => { + if (!StringUtils.equals(node.getObjectType, "Asset")) + throw new ClientException(AssetConstants.ERR_INVALID_REQUEST, "Only asset can be copied") + val copiedNodeFuture: Future[Node] = copyAsset(node, request) + copiedNodeFuture.map(copiedNode => { + val response = ResponseHandler.OK() + response.put("node_id", new util.HashMap[String, AnyRef]() { + { + put(node.getIdentifier, copiedNode.getIdentifier) + } + }) + response.put(AssetConstants.VERSION_KEY, copiedNode.getMetadata.get(AssetConstants.VERSION_KEY)) + response + }) + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + def copyAsset(node: Node, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val copyCreateReq: Future[Request] = getCopyRequest(node, request) + copyCreateReq.map(req => { + DataNode.create(req).map(copiedNode => { + artifactUpload(node, copiedNode, request) + }).flatMap(f => f) + }).flatMap(f => f) + } + + def getCopyRequest(node: Node, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Request] = { + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, new util.ArrayList(), node.getObjectType.toLowerCase.replace("image", ""), AssetConstants.SCHEMA_VERSION) + val requestMap = request.getRequest + requestMap.remove(AssetConstants.ASSET_COPY_SCHEME).asInstanceOf[String] + metadata.put(AssetConstants.ORIGIN, node.getIdentifier) + metadata.put(AssetConstants.NAME, "Copy_" + metadata.getOrDefault(AssetConstants.NAME, "")) + metadata.put(AssetConstants.STATUS, "Draft") + metadata.put(AssetConstants.IDENTIFIER, Identifier.getIdentifier(request.getContext.get("graph_id").asInstanceOf[String], Identifier.getUniqueIdFromTimestamp)) + metadata.putAll(requestMap.getOrDefault("asset", new util.HashMap()).asInstanceOf[util.Map[String, AnyRef]]) + request.getContext().put(AssetConstants.SCHEMA_NAME, node.getObjectType.toLowerCase.replace("image", "")) + val req = new Request(request) + req.setRequest(metadata) + Future { + req + } + } + + def artifactUpload(node: Node, copiedNode: Node, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val artifactUrl = node.getMetadata.getOrDefault(AssetConstants.ARTIFACT_URL, "").asInstanceOf[String] + if (StringUtils.isNotBlank(artifactUrl)) { + val updatedReq = getUpdateRequest(request, copiedNode) + val responseFuture = UploadManager.upload(updatedReq, copiedNode) + responseFuture.map(result => { + copiedNode.getMetadata.put(AssetConstants.ARTIFACT_URL, result.getResult.getOrDefault(AssetConstants.ARTIFACT_URL, "").asInstanceOf[String]) + }) + } + Future(copiedNode) + } + + def getUpdateRequest(request: Request, copiedNode: Node): Request = { + val req = new Request() + val context = request.getContext + context.put(AssetConstants.IDENTIFIER, copiedNode.getIdentifier) + req.setContext(context) + req.put(AssetConstants.VERSION_KEY, copiedNode.getMetadata.get(AssetConstants.VERSION_KEY)) + req.put(AssetConstants.FILE_URL, copiedNode.getMetadata.get(AssetConstants.ARTIFACT_URL)) + req + } +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/CategoryConstants.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/CategoryConstants.scala new file mode 100644 index 000000000..9b3fa30c6 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/CategoryConstants.scala @@ -0,0 +1,8 @@ +package org.sunbird.content.util + +object CategoryConstants { + val CREATE_CATEGORY: String = "createCategory" + val READ_CATEGORY: String = "readCategory" + val UPDATE_CATEGORY: String = "updateCategory" + val RETIRE_CATEGORY: String = "retireCategory" +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/ContentConstants.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/ContentConstants.scala new file mode 100644 index 000000000..29889d0eb --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/ContentConstants.scala @@ -0,0 +1,59 @@ +package org.sunbird.content.util + +object ContentConstants { + val CHILDREN: String = "children" + val REQUIRED_KEYS: List[String] = List("createdBy", "createdFor", "organisation", "framework") + val CONTENT_TYPE: String = "contentType" + val CONTENT_TYPE_ASSET_CAN_NOT_COPY: String = "CONTENT_TYPE_ASSET_CAN_NOT_COPY" + val STATUS: String = "status" + val ERR_INVALID_REQUEST: String = "ERR_INVALID_REQUEST" + val MIME_TYPE: String = "mimeType" + val CONTENT_SCHEMA_NAME: String = "content" + val SCHEMA_NAME: String = "schemaName" + val RESPONSE_SCHEMA_NAME: String = "responseSchemaName" + val SCHEMA_VERSION: String = "1.0" + val ARTIFACT_URL: String = "artifactUrl" + val IDENTIFIER: String = "identifier" + val MODE: String = "mode" + val COLLECTION_MIME_TYPE: String = "application/vnd.ekstep.content-collection" + val ORIGIN: String = "origin" + val ORIGIN_DATA: String = "originData" + val ERR_INVALID_UPLOAD_FILE_URL: String = "ERR_INVALID_UPLOAD_FILE_URL" + val ROOT_ID: String = "rootId" + val HIERARCHY: String = "hierarchy" + val ROOT: String = "root" + val NODES_MODIFIED: String = "nodesModified" + val VISIBILITY: String = "visibility" + val METADATA: String = "metadata" + val END_NODE_OBJECT_TYPES = List("Content", "ContentImage") + val VERSION_KEY: String = "versionKey" + val H5P_MIME_TYPE = "application/vnd.ekstep.h5p-archive" + val COPY_TYPE: String = "copyType" + val COPY_TYPE_SHALLOW: String = "shallow" + val COPY_TYPE_DEEP: String = "deep" + val CONTENT: String = "content" + val IMAGE_SUFFIX: String = ".img" + val ERR_INVALID_CONTENT_ID: String = "ERR_INVALID_CONTENT_ID" + val EDIT_MODE: String = "edit" + val CONTENT_IMAGE_OBJECT_TYPE = "ContentImage" + val ERR_CONTENT_NOT_DRAFT = "ERR_CONTENT_NOT_DRAFT" + val COLLECTION_SCHEMA_NAME: String = "collection" + val IDENTIFIERS: String = "identifiers" + val PACKAGE_VERSION: String = "pkgVersion" + val CHANNEL: String = "channel" + val ERR_CONTENT_RETIRE: String = "ERR_CONTENT_RETIRE" + val ARTIFACT_BASE_PATH: String = "artifactBasePath" + val FLAGGED: String = "Flagged" + val FLAG_DRAFT: String = "FlagDraft" + val RETIRED: String = "Retired" + val NODE_ID: String = "node_id" + val VERSIONING: String = "versioning" + val DISABLE: String = "disable" + val LAST_STATUS_CHANGED_ON: String = "lastStatusChangedOn" + val HIERARCHY_PREFIX: String = "hierarchy_" + val ERR_INVALID_CONTENT: String = "ERR_INVALID_CONTENT" + val CONTENT_OBJECT_TYPE: String = "Content" + val LAST_UPDATED_ON:String = "lastUpdatedOn" + val VERSION:String = "version" + val COPY_SCHEME:String = "copyScheme" +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/CopyManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/CopyManager.scala new file mode 100644 index 000000000..11f31cf68 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/CopyManager.scala @@ -0,0 +1,304 @@ +package org.sunbird.content.util + + +import java.io.{File, IOException} +import java.net.URL +import java.util +import java.util.UUID +import java.util.concurrent.CompletionException + +import org.apache.commons.collections.CollectionUtils +import org.apache.commons.collections4.MapUtils +import org.apache.commons.io.{FileUtils, FilenameUtils} +import org.apache.commons.lang.StringUtils +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.Platform +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.Identifier +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.schema.DefinitionNode +import org.sunbird.graph.utils.{NodeUtil, ScalaJsonUtils} +import org.sunbird.managers.{HierarchyManager, UpdateHierarchyManager} +import org.sunbird.mimetype.factory.MimeTypeManagerFactory +import org.sunbird.mimetype.mgr.impl.H5PMimeTypeMgrImpl + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + + +object CopyManager { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + private val TEMP_FILE_LOCATION = Platform.getString("content.upload.temp_location", "/tmp/content") + private val metadataNotTobeCopied = Platform.config.getStringList("content.copy.props_to_remove") + private val invalidStatusList: util.List[String] = Platform.getStringList("content.copy.invalid_statusList", new util.ArrayList[String]()) + private val invalidContentTypes: util.List[String] = Platform.getStringList("content.copy.invalid_contentTypes", new util.ArrayList[String]()) + private val originMetadataKeys: util.List[String] = Platform.getStringList("content.copy.origin_data", new util.ArrayList[String]()) + private val internalHierarchyProps = List("identifier", "parent", "index", "depth") + private val restrictedMimeTypesForUpload = List("application/vnd.ekstep.ecml-archive","application/vnd.ekstep.content-collection") + + implicit val ss: StorageService = new StorageService + + private var copySchemeMap: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() + + def copy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + request.getContext.put(ContentConstants.COPY_SCHEME, request.getRequest.getOrDefault(ContentConstants.COPY_SCHEME, "")) + validateRequest(request) + DataNode.read(request).map(node => { + validateExistingNode(node) + copySchemeMap = DefinitionNode.getCopySchemeContentType(request) + val copiedNodeFuture: Future[Node] = node.getMetadata.get(ContentConstants.MIME_TYPE) match { + case ContentConstants.COLLECTION_MIME_TYPE => + node.setInRelations(null) + node.setOutRelations(null) + validateShallowCopyReq(node, request) + copyCollection(node, request) + case _ => + node.setInRelations(null) + copyContent(node, request) + } + copiedNodeFuture.map(copiedNode => { + val response = ResponseHandler.OK() + response.put("node_id", new util.HashMap[String, AnyRef](){{ + put(node.getIdentifier, copiedNode.getIdentifier) + }}) + response.put(ContentConstants.VERSION_KEY, copiedNode.getMetadata.get(ContentConstants.VERSION_KEY)) + response + }) + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + def copyContent(node: Node, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + // cleanUpNodeRelations(node) + val copyCreateReq: Future[Request] = getCopyRequest(node, request) + copyCreateReq.map(req => { + DataNode.create(req).map(copiedNode => { + artifactUpload(node, copiedNode, request) + }).flatMap(f => f) + }).flatMap(f => f) + } + + def copyCollection(originNode: Node, request: Request)(implicit ec:ExecutionContext, oec: OntologyEngineContext):Future[Node] = { + val copyType = request.getRequest.get(ContentConstants.COPY_TYPE).asInstanceOf[String] + copyContent(originNode, request).map(node => { + val req = new Request(request) + req.getContext.put(ContentConstants.SCHEMA_NAME, ContentConstants.COLLECTION_SCHEMA_NAME) + req.getContext.put(ContentConstants.VERSION, ContentConstants.SCHEMA_VERSION) + req.put(ContentConstants.ROOT_ID, request.get(ContentConstants.IDENTIFIER)) + req.put(ContentConstants.MODE, request.get(ContentConstants.MODE)) + HierarchyManager.getHierarchy(req).map(response => { + val originHierarchy = response.getResult.getOrDefault(ContentConstants.CONTENT, new java.util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + copyType match { + case ContentConstants.COPY_TYPE_SHALLOW => updateShallowHierarchy(request, node, originNode, originHierarchy) + case _ => updateHierarchy(request,node, originNode, originHierarchy, copyType) + } + }).flatMap(f=>f) + }).flatMap(f => f) recoverWith {case e: CompletionException => throw e.getCause} + } + + def updateHierarchy(request: Request, node: Node, originNode: Node, originHierarchy: util.Map[String, AnyRef], copyType:String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val updateHierarchyRequest = prepareHierarchyRequest(originHierarchy, originNode, node, copyType, request) + val hierarchyRequest = new Request(request) + hierarchyRequest.putAll(updateHierarchyRequest) + hierarchyRequest.getContext.put(ContentConstants.SCHEMA_NAME, ContentConstants.COLLECTION_SCHEMA_NAME) + hierarchyRequest.getContext.put(ContentConstants.VERSION, ContentConstants.SCHEMA_VERSION) + UpdateHierarchyManager.updateHierarchy(hierarchyRequest).map(response=>node) + } + + def updateShallowHierarchy(request: Request, node: Node, originNode: Node, originHierarchy: util.Map[String, AnyRef])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val childrenHierarchy = originHierarchy.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + val req = new Request(request) + req.getContext.put(ContentConstants.SCHEMA_NAME, ContentConstants.COLLECTION_SCHEMA_NAME) + req.getContext.put(ContentConstants.VERSION, ContentConstants.SCHEMA_VERSION) + req.getContext.put(ContentConstants.IDENTIFIER, node.getIdentifier) + req.put(ContentConstants.HIERARCHY, ScalaJsonUtils.serialize(new java.util.HashMap[String, AnyRef](){{ + put(ContentConstants.IDENTIFIER, node.getIdentifier) + put(ContentConstants.CHILDREN, childrenHierarchy) + }})) + DataNode.update(req).map(node=>node) + } + + def validateExistingNode(node: Node): Unit = { + if (!CollectionUtils.isEmpty(invalidContentTypes) && invalidContentTypes.contains(node.getMetadata.get(ContentConstants.CONTENT_TYPE).asInstanceOf[String])) + throw new ClientException(ContentConstants.CONTENT_TYPE_ASSET_CAN_NOT_COPY, "ContentType " + node.getMetadata.get(ContentConstants.CONTENT_TYPE).asInstanceOf[String] + " can not be copied.") + if (invalidStatusList.contains(node.getMetadata.get(ContentConstants.STATUS).asInstanceOf[String])) + throw new ClientException(ContentConstants.ERR_INVALID_REQUEST, "Cannot Copy content which is in " + node.getMetadata.get(ContentConstants.STATUS).asInstanceOf[String].toLowerCase + " status") + } + + def validateRequest(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Unit = { + val keysNotPresent = ContentConstants.REQUIRED_KEYS.filter(key => emptyCheckFilter(request.getRequest.getOrDefault(key, ""))) + if (keysNotPresent.nonEmpty) + throw new ClientException(ContentConstants.ERR_INVALID_REQUEST, "Please provide valid value for " + keysNotPresent) + if (StringUtils.equalsIgnoreCase(request.getRequest.getOrDefault(ContentConstants.COPY_TYPE, ContentConstants.COPY_TYPE_DEEP).asInstanceOf[String], ContentConstants.COPY_TYPE_SHALLOW) && + StringUtils.isNotBlank(request.getContext.get(ContentConstants.COPY_SCHEME).asInstanceOf[String])) + throw new ClientException(ContentConstants.ERR_INVALID_REQUEST, "Content can not be shallow copied with copy scheme.") + if(StringUtils.isNotBlank(request.getContext.get(ContentConstants.COPY_SCHEME).asInstanceOf[String]) && !DefinitionNode.getAllCopyScheme(request).contains(request.getRequest.getOrDefault(ContentConstants.COPY_SCHEME, "").asInstanceOf[String])) + throw new ClientException(ContentConstants.ERR_INVALID_REQUEST, "Invalid copy scheme, Please provide valid copy scheme") + } + + def emptyCheckFilter(key: AnyRef): Boolean = key match { + case k: String => k.asInstanceOf[String].isEmpty + case k: util.Map[String, AnyRef] => MapUtils.isEmpty(k.asInstanceOf[util.Map[String, AnyRef]]) + case k: util.List[String] => CollectionUtils.isEmpty(k.asInstanceOf[util.List[String]]) + case _ => true + } + + def getCopyRequest(node: Node, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Request] = { + val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, new util.ArrayList(), node.getObjectType.toLowerCase.replace("image", ""), ContentConstants.SCHEMA_VERSION) + val requestMap = request.getRequest + requestMap.remove(ContentConstants.MODE) + requestMap.remove(ContentConstants.COPY_SCHEME).asInstanceOf[String] + val copyType = requestMap.remove(ContentConstants.COPY_TYPE).asInstanceOf[String] + val originData: java.util.Map[String, AnyRef] = getOriginData(metadata, copyType) + cleanUpCopiedData(metadata, copyType) + metadata.putAll(requestMap) + metadata.put(ContentConstants.STATUS, "Draft") + metadata.put(ContentConstants.ORIGIN, node.getIdentifier) + metadata.put(ContentConstants.IDENTIFIER, Identifier.getIdentifier(request.getContext.get("graph_id").asInstanceOf[String], Identifier.getUniqueIdFromTimestamp)) + if (MapUtils.isNotEmpty(originData)) + metadata.put(ContentConstants.ORIGIN_DATA, originData) + request.getContext().put(ContentConstants.SCHEMA_NAME, node.getObjectType.toLowerCase.replace("image", "")) + updateToCopySchemeContentType(request, metadata.get(ContentConstants.CONTENT_TYPE).asInstanceOf[String], metadata) + val req = new Request(request) + req.setRequest(metadata) + if (StringUtils.equalsIgnoreCase("application/vnd.ekstep.ecml-archive", node.getMetadata.get("mimeType").asInstanceOf[String])) { + val readReq = new Request() + readReq.setContext(request.getContext) + readReq.put("identifier", node.getIdentifier) + readReq.put("fields", util.Arrays.asList("body")) + DataNode.read(readReq).map(node => { + if (null != node.getMetadata.get("body")) + req.put("body", node.getMetadata.get("body").asInstanceOf[String]) + req + }) + } else Future {req} + } + + def getOriginData(metadata: util.Map[String, AnyRef], copyType:String): java.util.Map[String, AnyRef] = { + new java.util.HashMap[String, AnyRef](){{ + putAll(originMetadataKeys.asScala.filter(key => metadata.containsKey(key)).map(key => key -> metadata.get(key)).toMap.asJava) + put(ContentConstants.COPY_TYPE, copyType) + }} + } + + def cleanUpCopiedData(metadata: util.Map[String, AnyRef], copyType:String): util.Map[String, AnyRef] = { + if(StringUtils.equalsIgnoreCase(ContentConstants.COPY_TYPE_SHALLOW, copyType)) { + metadata.keySet().removeAll(metadataNotTobeCopied.asScala.toList.filter(str => !str.contains("dial")).asJava) + } else metadata.keySet().removeAll(metadataNotTobeCopied) + metadata + } + + def copyURLToFile(objectId: String, fileUrl: String): File = try { + val file = new File(getBasePath(objectId) + File.separator + getFileNameFromURL(fileUrl)) + FileUtils.copyURLToFile(new URL(fileUrl), file) + file + } catch { + case e: IOException => throw new ClientException("ERR_INVALID_FILE_URL", "Please Provide Valid File Url!") + } + + def getBasePath(objectId: String): String = { + if (!StringUtils.isBlank(objectId)) TEMP_FILE_LOCATION + File.separator + System.currentTimeMillis + "_temp" + File.separator + objectId else "" + } + + // def cleanUpNodeRelations(node: Node): Unit = { + // val relationsToDelete: util.List[Relation] = node.getOutRelations.asScala.filter(relation => ContentConstants.END_NODE_OBJECT_TYPES.contains(relation.getEndNodeObjectType)).toList.asJava + // node.getOutRelations.removeAll(relationsToDelete) + // } + + def getUpdateRequest(request: Request, copiedNode: Node, artifactUrl: String): Request = { + val req = new Request() + val context = request.getContext + context.put(ContentConstants.IDENTIFIER, copiedNode.getIdentifier) + req.setContext(context) + req.put(ContentConstants.VERSION_KEY, copiedNode.getMetadata.get(ContentConstants.VERSION_KEY)) + req.put(ContentConstants.ARTIFACT_URL, artifactUrl) + req + } + + protected def getFileNameFromURL(fileUrl: String): String = if (!FilenameUtils.getExtension(fileUrl).isEmpty) + FilenameUtils.getBaseName(fileUrl) + "_" + System.currentTimeMillis + "." + FilenameUtils.getExtension(fileUrl) else FilenameUtils.getBaseName(fileUrl) + "_" + System.currentTimeMillis + + protected def isInternalUrl(url: String): Boolean = url.contains(ss.getContainerName()) + + def prepareHierarchyRequest(originHierarchy: util.Map[String, AnyRef], originNode: Node, node: Node, copyType: String, request: Request):util.HashMap[String, AnyRef] = { + val children:util.List[util.Map[String, AnyRef]] = originHierarchy.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + if(null != children && !children.isEmpty) { + val nodesModified = new util.HashMap[String, AnyRef]() + val hierarchy = new util.HashMap[String, AnyRef]() + hierarchy.put(node.getIdentifier, new util.HashMap[String, AnyRef](){{ + put(ContentConstants.CHILDREN, new util.ArrayList[String]()) + put(ContentConstants.ROOT, true.asInstanceOf[AnyRef]) + put(ContentConstants.CONTENT_TYPE, node.getMetadata.get(ContentConstants.CONTENT_TYPE)) + }}) + populateHierarchyRequest(children, nodesModified, hierarchy, node.getIdentifier, copyType, request) + new util.HashMap[String, AnyRef](){{ + put(ContentConstants.NODES_MODIFIED, nodesModified) + put(ContentConstants.HIERARCHY, hierarchy) + }} + } else new util.HashMap[String, AnyRef]() + } + + def populateHierarchyRequest(children: util.List[util.Map[String, AnyRef]], nodesModified: util.HashMap[String, AnyRef], hierarchy: util.HashMap[String, AnyRef], parentId: String, copyType: String, request: Request): Unit = { + if (null != children && !children.isEmpty) { + children.asScala.toList.foreach(child => { + val id = if ("Parent".equalsIgnoreCase(child.get(ContentConstants.VISIBILITY).asInstanceOf[String])) { + val identifier = UUID.randomUUID().toString + updateToCopySchemeContentType(request, child.get(ContentConstants.CONTENT_TYPE).asInstanceOf[String], child) + nodesModified.put(identifier, new util.HashMap[String, AnyRef]() {{ + put(ContentConstants.METADATA, cleanUpCopiedData(new util.HashMap[String, AnyRef]() {{ + putAll(child) + put(ContentConstants.CHILDREN, new util.ArrayList()) + internalHierarchyProps.map(key => remove(key)) + }}, copyType)) + put(ContentConstants.ROOT, false.asInstanceOf[AnyRef]) + put("isNew", true.asInstanceOf[AnyRef]) + put("setDefaultValue", false.asInstanceOf[AnyRef]) + }}) + identifier + } else + child.get(ContentConstants.IDENTIFIER).asInstanceOf[String] + hierarchy.put(id, new util.HashMap[String, AnyRef]() {{ + put(ContentConstants.CHILDREN, new util.ArrayList[String]()) + put(ContentConstants.ROOT, false.asInstanceOf[AnyRef]) + put(ContentConstants.CONTENT_TYPE, child.get(ContentConstants.CONTENT_TYPE)) + }}) + hierarchy.get(parentId).asInstanceOf[util.Map[String, AnyRef]].get(ContentConstants.CHILDREN).asInstanceOf[util.List[String]].add(id) + populateHierarchyRequest(child.get(ContentConstants.CHILDREN).asInstanceOf[util.List[util.Map[String, AnyRef]]], nodesModified, hierarchy, id, copyType, request) + }) + } + } + + def artifactUpload(node: Node, copiedNode: Node, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val artifactUrl = node.getMetadata.getOrDefault(ContentConstants.ARTIFACT_URL, "").asInstanceOf[String] + val mimeType = node.getMetadata.get(ContentConstants.MIME_TYPE).asInstanceOf[String] + val contentType = node.getMetadata.get(ContentConstants.CONTENT_TYPE).asInstanceOf[String] + if (StringUtils.isNotBlank(artifactUrl) && !restrictedMimeTypesForUpload.contains(mimeType)) { + val mimeTypeManager = MimeTypeManagerFactory.getManager(contentType, mimeType) + val uploadFuture = if (isInternalUrl(artifactUrl)) { + val file = copyURLToFile(copiedNode.getIdentifier, artifactUrl) + if (mimeTypeManager.isInstanceOf[H5PMimeTypeMgrImpl]) + mimeTypeManager.asInstanceOf[H5PMimeTypeMgrImpl].copyH5P(file, copiedNode) + else + mimeTypeManager.upload(copiedNode.getIdentifier, copiedNode, file, None, UploadParams()) + } else mimeTypeManager.upload(copiedNode.getIdentifier, copiedNode, node.getMetadata.getOrDefault(ContentConstants.ARTIFACT_URL, "").asInstanceOf[String], None, UploadParams()) + uploadFuture.map(uploadData => { + DataNode.update(getUpdateRequest(request, copiedNode, uploadData.getOrElse(ContentConstants.ARTIFACT_URL, "").asInstanceOf[String])) + }).flatMap(f => f) + } else Future(copiedNode) + } + + def validateShallowCopyReq(node: Node, request: Request) = { + val copyType: String = request.getRequest.get("copyType").asInstanceOf[String] + if(StringUtils.equalsIgnoreCase("shallow", copyType) && !StringUtils.equalsIgnoreCase("Live", node.getMetadata.get("status").asInstanceOf[String])) + throw new ClientException(ContentConstants.ERR_INVALID_REQUEST, "Content with status " + node.getMetadata.get(ContentConstants.STATUS).asInstanceOf[String].toLowerCase + " cannot be partially (shallow) copied.") + //TODO: check if need to throw client exception for combination of copyType=shallow and mode=edit + } + + def updateToCopySchemeContentType(request: Request, contentType: String, metadata: util.Map[String, AnyRef]): Unit = { + if (StringUtils.isNotBlank(request.getContext.getOrDefault(ContentConstants.COPY_SCHEME, "").asInstanceOf[String])) + metadata.put(ContentConstants.CONTENT_TYPE, copySchemeMap.getOrDefault(contentType, contentType)) + } +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/DiscardManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/DiscardManager.scala new file mode 100644 index 000000000..12f924a1f --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/DiscardManager.scala @@ -0,0 +1,67 @@ +package org.sunbird.content.util + +import java.util +import java.util.concurrent.CompletionException + +import org.apache.commons.collections4.CollectionUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.{JsonUtils, Platform} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResourceNotFoundException, ServerException} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.external.ExternalPropsManager +import org.sunbird.managers.UpdateHierarchyManager.{fetchHierarchy, shouldImageBeDeleted} +import org.sunbird.telemetry.logger.TelemetryManager +import org.sunbird.utils.{HierarchyConstants, HierarchyErrorCodes} + +import scala.collection.JavaConversions._ +import scala.concurrent.{ExecutionContext, Future} + +object DiscardManager { + private val CONTENT_DISCARD_STATUS = Platform.getStringList("content.discard.status", util.Arrays.asList("Draft", "FlagDraft")) + + @throws[Exception] + def discard(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + validateRequest(request) + getNodeToDiscard(request).flatMap(node => { + request.put(ContentConstants.IDENTIFIER, node.getIdentifier) + if (!CONTENT_DISCARD_STATUS.contains(node.getMetadata.get(ContentConstants.STATUS))) + throw new ClientException(ContentConstants.ERR_CONTENT_NOT_DRAFT, "No changes to discard for content with content id: " + node.getIdentifier + " since content status isn't draft", node.getIdentifier) + val response = if (StringUtils.equalsIgnoreCase(node.getMetadata.getOrDefault(ContentConstants.MIME_TYPE, "").asInstanceOf[String], ContentConstants.COLLECTION_MIME_TYPE)) + discardForCollection(node, request) + else + DataNode.deleteNode(request) + response.map(resp => { + val response = ResponseHandler.OK() + response.put("node_id", node.getIdentifier) + response.put("identifier", node.getIdentifier) + response.getResult.put("message", "Draft version of the content with id : " + node.getIdentifier + " is discarded") + response + }) + })recoverWith { case e: CompletionException => throw e.getCause } + } + + private def getNodeToDiscard(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val imageRequest = new Request(request) + imageRequest.put(ContentConstants.MODE, ContentConstants.EDIT_MODE) + imageRequest.put(ContentConstants.IDENTIFIER, request.get(ContentConstants.IDENTIFIER)) + DataNode.read(imageRequest) + } + + def validateRequest(request: Request): Unit = { + if (StringUtils.isBlank(request.getRequest.getOrDefault(ContentConstants.IDENTIFIER, "").asInstanceOf[String]) + || StringUtils.endsWith(request.getRequest.getOrDefault(ContentConstants.IDENTIFIER, "").asInstanceOf[String], ContentConstants.IMAGE_SUFFIX)) + throw new ClientException(ContentConstants.ERR_INVALID_CONTENT_ID, "Please provide valid content identifier") + } + + + private def discardForCollection(node: Node, request: Request)(implicit executionContext: ExecutionContext, oec: OntologyEngineContext): Future[java.lang.Boolean] = { + request.put(ContentConstants.IDENTIFIERS, if (node.getMetadata.containsKey(ContentConstants.PACKAGE_VERSION)) List(node.getIdentifier) else List(node.getIdentifier, node.getIdentifier + ContentConstants.IMAGE_SUFFIX)) + request.getContext.put(ContentConstants.SCHEMA_NAME, ContentConstants.COLLECTION_SCHEMA_NAME) + oec.graphService.deleteExternalProps(request).map(resp => DataNode.deleteNode(request)).flatMap(f => f) + } + + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/FlagManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/FlagManager.scala new file mode 100644 index 000000000..c42d4e48d --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/FlagManager.scala @@ -0,0 +1,111 @@ +package org.sunbird.content.util + +import java.util +import java.util.concurrent.CompletionException +import org.apache.commons.collections.CollectionUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.{DateUtils, JsonUtils} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResourceNotFoundException} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.external.ExternalPropsManager +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.NodeUtil +import org.sunbird.telemetry.logger.TelemetryManager +import org.sunbird.utils.HierarchyConstants +import scala.concurrent.{ExecutionContext, Future} +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ + +object FlagManager { + private val FLAGGABLE_STATUS: util.List[String] = util.Arrays.asList("Live", "Unlisted", "Flagged") + private val COLLECTION_SCHEMA_NAME = "collection" + private val COLLECTION_CACHE_KEY_PREFIX = "hierarchy_" + + def flag(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + + DataNode.read(request).map(node => { + val flagReasons: util.List[String] = request.get("flagReasons").asInstanceOf[util.ArrayList[String]] + val flaggedBy: String = request.get("flaggedBy").asInstanceOf[String] + val flags: util.List[String] = request.get("flags").asInstanceOf[util.ArrayList[String]] + + val metadata: util.Map[String, Object] = NodeUtil.serialize(node, null, node.getObjectType.toLowerCase.replace("image", ""), request.getContext.get("version").asInstanceOf[String])//node.getMetadata.asInstanceOf[util.HashMap[String, Object]] + val status: String = metadata.get("status").asInstanceOf[String] + val versionKey = node.getMetadata.get("versionKey").asInstanceOf[String] + request.put("identifier", node.getIdentifier) + + if (!FLAGGABLE_STATUS.contains(status)) + throw new ClientException("ERR_CONTENT_NOT_FLAGGABLE", "Unpublished Content " + node.getIdentifier + " cannot be flagged") + + val flaggedByList: util.List[String] = if(StringUtils.isNotBlank(flaggedBy)) util.Arrays.asList(flaggedBy) else new util.ArrayList[String] + if (StringUtils.isNotEmpty(flaggedBy)) + request.put("flaggedBy", addDataIntoList(flaggedByList, metadata, "flaggedBy")) + request.put("lastUpdatedBy", flaggedBy) + request.put("flags", flags) + request.put("status", "Flagged") + request.put("lastFlaggedOn", DateUtils.formatCurrentDate()) + if (CollectionUtils.isNotEmpty(flagReasons)) + request.put("flagReasons", addDataIntoList(flagReasons, metadata, "flagReasons")) + request.getContext.put("versioning", "disable") + request.put("versionKey", versionKey) + updateContentFlag(node, request).map(flaggedNode => { + val response = ResponseHandler.OK + val identifier: String = flaggedNode.getIdentifier + response.put("node_id", identifier) + response.put("identifier", identifier) + response.put("versionKey", flaggedNode.getMetadata.get("versionKey")) + response + }) + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + def updateCollection(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + fetchHierarchy(request).map(hierarchyString => { + if (!hierarchyString.asInstanceOf[String].isEmpty) { + val hierarchyMap = JsonUtils.deserialize(hierarchyString.asInstanceOf[String], classOf[util.HashMap[String, AnyRef]]) + hierarchyMap.put("lastUpdatedBy", request.get("flaggedBy")) + hierarchyMap.put("flaggedBy", request.get("flaggedBy")) + hierarchyMap.put("flags", request.get("flags")) + hierarchyMap.put("status", request.get("status")) + hierarchyMap.put("lastFlaggedOn", request.get("lastFlaggedOn")) + hierarchyMap.put("flagReasons", request.get("flagReasons")) + + request.put(HierarchyConstants.HIERARCHY, JsonUtils.serialize(hierarchyMap)) + } + val updateNode = DataNode.update(request) + updateNode + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + private def fetchHierarchy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Any] = { + oec.graphService.readExternalProps(request, List(HierarchyConstants.HIERARCHY)).map(resp => { + resp.getResult.toMap.getOrElse(HierarchyConstants.HIERARCHY, "").asInstanceOf[String] + }) recover { case e: ResourceNotFoundException => TelemetryManager.log("No hierarchy is present in cassandra for identifier:" + request.get(HierarchyConstants.IDENTIFIER)) } + } + + def updateContentFlag(node: Node, request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] ={ + val mimeType: String = node.getMetadata.get("mimeType").asInstanceOf[String] + RedisCache.delete(node.getIdentifier) + RedisCache.delete(COLLECTION_CACHE_KEY_PREFIX + node.getIdentifier) + if(StringUtils.equalsIgnoreCase(mimeType, HierarchyConstants.COLLECTION_MIME_TYPE)){ + request.getContext().put("schemaName", COLLECTION_SCHEMA_NAME) + updateCollection(request) + }else + DataNode.update(request) + } + + def addDataIntoList(dataList: util.List[String], metadata: util.Map[String, Object], key: String): util.List[String] = { + val existingData = metadata.getOrDefault(key, new util.ArrayList[String])//.asInstanceOf[util.ArrayList[String]] + val existingDataList = {if(existingData.isInstanceOf[Array[String]]) existingData.asInstanceOf[Array[String]].toList.asJava else if (existingData.isInstanceOf[util.List[String]]) existingData.asInstanceOf[util.List[String]] else new util.ArrayList[String]} + val responseDataList = new util.ArrayList[String] + responseDataList.addAll(existingDataList) + if (CollectionUtils.isEmpty(responseDataList)) { + dataList + }else{ + responseDataList.addAll(dataList) + new util.ArrayList[String](responseDataList.toSet) + } + } +} \ No newline at end of file diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/LicenseConstants.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/LicenseConstants.scala new file mode 100644 index 000000000..558026626 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/LicenseConstants.scala @@ -0,0 +1,8 @@ +package org.sunbird.content.util + +object LicenseConstants { + val CREATE_LICENSE: String = "createLicense" + val READ_LICENSE: String = "readLicense" + val UPDATE_LICENSE: String = "updateLicense" + val RETIRE_LICENSE: String = "retireLicense" +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/content/util/RetireManager.scala b/content-api/content-actors/src/main/scala/org/sunbird/content/util/RetireManager.scala new file mode 100644 index 000000000..cbd8e4eba --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/content/util/RetireManager.scala @@ -0,0 +1,111 @@ +package org.sunbird.content.util + +import java.util +import java.util.{Date, UUID} + +import org.apache.commons.collections4.CollectionUtils +import org.apache.commons.lang.StringUtils +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.{DateUtils, JsonUtils, Platform} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResourceNotFoundException, ServerException} +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.external.ExternalPropsManager +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.ScalaJsonUtils +import org.sunbird.kafka.client.KafkaClient +import org.sunbird.parseq.Task +import org.sunbird.telemetry.logger.TelemetryManager +import org.sunbird.utils.HierarchyConstants + +import scala.collection.JavaConversions._ +import scala.collection.mutable.ListBuffer +import scala.concurrent.{ExecutionContext, Future} + +object RetireManager { + val finalStatus: util.List[String] = util.Arrays.asList("Flagged", "Live", "Unlisted") + private val kfClient = new KafkaClient + + def retire(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + validateRequest(request) + getNodeToRetire(request).flatMap(node => { + val updateMetadataMap = Map(ContentConstants.STATUS -> "Retired", HierarchyConstants.LAST_UPDATED_ON -> DateUtils.formatCurrentDate, HierarchyConstants.LAST_STATUS_CHANGED_ON -> DateUtils.formatCurrentDate) + val futureList = Task.parallel[Response]( + handleCollectionToRetire(node, request, updateMetadataMap), + updateNodesToRetire(request, mapAsJavaMap[String,AnyRef](updateMetadataMap))) + futureList.map(f => { + val response = ResponseHandler.OK() + response.put(ContentConstants.IDENTIFIER, request.get(ContentConstants.IDENTIFIER)) + response.put("node_id", request.get(ContentConstants.IDENTIFIER)) + }) + }) + } + + private def getNodeToRetire(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = DataNode.read(request).map(node => { + if (StringUtils.equalsIgnoreCase("Retired", node.getMetadata.get(ContentConstants.STATUS).asInstanceOf[String])) + throw new ClientException(ContentConstants.ERR_CONTENT_RETIRE, "Content with Identifier " + node.getIdentifier + " is already Retired.") + node + }) + + private def validateRequest(request: Request) = { + val contentId: String = request.get(ContentConstants.IDENTIFIER).asInstanceOf[String] + if (StringUtils.isBlank(contentId) || StringUtils.endsWithIgnoreCase(contentId, HierarchyConstants.IMAGE_SUFFIX)) + throw new ClientException(ContentConstants.ERR_INVALID_CONTENT_ID, "Please Provide Valid Content Identifier.") + } + + private def updateNodesToRetire(request: Request, updateMetadataMap: util.Map[String, AnyRef])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + RedisCache.delete(request.get(ContentConstants.IDENTIFIER).asInstanceOf[String]) + val updateReq = new Request(request) + updateReq.put(ContentConstants.IDENTIFIERS, java.util.Arrays.asList(request.get(ContentConstants.IDENTIFIER).asInstanceOf[String], request.get(ContentConstants.IDENTIFIER).asInstanceOf[String] + HierarchyConstants.IMAGE_SUFFIX)) + updateReq.put(ContentConstants.METADATA, updateMetadataMap) + DataNode.bulkUpdate(updateReq).map(node => ResponseHandler.OK()) + } + + + private def handleCollectionToRetire(node: Node, request: Request, updateMetadataMap: Map[String, AnyRef])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + if (StringUtils.equalsIgnoreCase(ContentConstants.COLLECTION_MIME_TYPE, node.getMetadata.get(ContentConstants.MIME_TYPE).asInstanceOf[String]) && finalStatus.contains(node.getMetadata.get(ContentConstants.STATUS))) { + RedisCache.delete("hierarchy_" + node.getIdentifier) + val req = new Request(request) + req.getContext.put(ContentConstants.SCHEMA_NAME, ContentConstants.COLLECTION_SCHEMA_NAME) + req.put(ContentConstants.IDENTIFIER, request.get(ContentConstants.IDENTIFIER)) + oec.graphService.readExternalProps(req, List(HierarchyConstants.HIERARCHY)).flatMap(resp => { + val hierarchyString = resp.getResult.toMap.getOrElse(HierarchyConstants.HIERARCHY, "").asInstanceOf[String] + if (StringUtils.isNotBlank(hierarchyString)) { + val hierarchyMap = JsonUtils.deserialize(hierarchyString, classOf[util.HashMap[String, AnyRef]]) + val childIds = getChildrenIdentifiers(hierarchyMap) + if (CollectionUtils.isNotEmpty(childIds)) { + val topicName = Platform.getString("kafka.topics.graph.event", "sunbirddev.learning.graph.events") + childIds.foreach(id => kfClient.send(ScalaJsonUtils.serialize(getLearningGraphEvent(request, id)), topicName)) + RedisCache.delete(childIds.map(id => "hierarchy_" + id): _*) + } + hierarchyMap.putAll(updateMetadataMap) + req.put(HierarchyConstants.HIERARCHY, ScalaJsonUtils.serialize(hierarchyMap)) + oec.graphService.saveExternalProps(req) + } else Future(ResponseHandler.OK()) + }) recover { case e: ResourceNotFoundException => + TelemetryManager.log("No hierarchy is present in cassandra for identifier:" + node.getIdentifier) + throw new ServerException("ERR_CONTENT_RETIRE", "Unable to fetch Hierarchy for Root Node: [" + node.getIdentifier + "]") + } + } else Future(ResponseHandler.OK()) + } + + + private def getChildrenIdentifiers(hierarchyMap: util.HashMap[String, AnyRef]): util.List[String] = { + val childIds: ListBuffer[String] = ListBuffer[String]() + addChildIds(hierarchyMap.getOrElse(HierarchyConstants.CHILDREN, new util.ArrayList[util.HashMap[String, AnyRef]]()).asInstanceOf[util.ArrayList[util.HashMap[String, AnyRef]]], childIds) + bufferAsJavaList(childIds) + } + + private def addChildIds(childrenMaps: util.ArrayList[util.HashMap[String, AnyRef]], childrenIds: ListBuffer[String]): Unit = { + if (CollectionUtils.isNotEmpty(childrenMaps)) { + childrenMaps.filter(child => StringUtils.equalsIgnoreCase(HierarchyConstants.PARENT, child.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])).foreach(child => { + childrenIds += child.get(HierarchyConstants.IDENTIFIER).asInstanceOf[String] + addChildIds(child.get(HierarchyConstants.CHILDREN).asInstanceOf[util.ArrayList[util.HashMap[String, AnyRef]]], childrenIds) + }) + } + } + + private def getLearningGraphEvent(request: Request, id: String): Map[String, Any] = Map("ets" -> System.currentTimeMillis(), "channel" -> request.getContext.get(ContentConstants.CHANNEL), "mid" -> UUID.randomUUID.toString, "nodeType" -> "DATA_NODE", "userId" -> "Ekstep", "createdOn" -> DateUtils.format(new Date()), "objectType" -> "Content", "nodeUniqueId" -> id, "operationType" -> "DELETE", "graphId" -> request.getContext.get("graph_id")) + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/util/ChannelConstants.scala b/content-api/content-actors/src/main/scala/org/sunbird/util/ChannelConstants.scala new file mode 100644 index 000000000..e49708593 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/util/ChannelConstants.scala @@ -0,0 +1,23 @@ +package org.sunbird.util + +object ChannelConstants { + val DEFAULT_LICENSE: String = "defaultLicense" + val NODE_ID: String = "node_id" + val CHANNEL_LICENSE_CACHE_PREFIX: String = "channel_" + val CHANNEL_LICENSE_CACHE_SUFFIX: String = "_license" + val LICENSE_REDIS_KEY: String = "edge_license" + val CONTENT_PRIMARY_CATEGORIES: String = "contentPrimaryCategories" + val COLLECTION_PRIMARY_CATEGORIES: String = "collectionPrimaryCategories" + val ASSET_PRIMARY_CATEGORIES: String = "assetPrimaryCategories" + val CONTENT: String = "content" + val NAME: String = "name" + val OBJECT_CATEGORY: String = "ObjectCategory" + val objectCategoryDefinitionKey = "ObjectCategoryDefinition" + val ERR_VALIDATING_PRIMARY_CATEGORY: String = "ERR_VALIDATING_PRIMARY_CATEGORY" + val CONTENT_ADDITIONAL_CATEGORIES: String = "contentAdditionalCategories" + val COLLECTION_ADDITIONAL_CATEGORIES: String = "collectionAdditionalCategories" + val ASSET_ADDITIONAL_CATEGORIES: String = "assetAdditionalCategories" + val categoryKeyList: List[String] = List(CONTENT_PRIMARY_CATEGORIES, COLLECTION_PRIMARY_CATEGORIES, ASSET_PRIMARY_CATEGORIES, + CONTENT_ADDITIONAL_CATEGORIES, COLLECTION_ADDITIONAL_CATEGORIES, ASSET_ADDITIONAL_CATEGORIES) + +} diff --git a/content-api/content-actors/src/main/scala/org/sunbird/util/HttpUtil.scala b/content-api/content-actors/src/main/scala/org/sunbird/util/HttpUtil.scala new file mode 100644 index 000000000..9284421ae --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/util/HttpUtil.scala @@ -0,0 +1,35 @@ +package org.sunbird.util + +import com.mashape.unirest.http.Unirest +import scala.collection.JavaConverters._ + +/** + * + * We need to move this class to platform-core. + */ + +case class HTTPResponse(status: Int, body: String) extends Serializable + +class HttpUtil extends Serializable { + + def get(url: String, headers: Map[String, String] = Map[String, String]("Content-Type"->"application/json")): HTTPResponse = { + val response = Unirest.get(url).headers(headers.asJava).asString() + HTTPResponse(response.getStatus, response.getBody) + } + + def post(url: String, requestBody: String, headers: Map[String, String] = Map[String, String]("Content-Type"->"application/json")): HTTPResponse = { + val response = Unirest.post(url).headers(headers.asJava).body(requestBody).asString() + HTTPResponse(response.getStatus, response.getBody) + } + + def put(url: String, requestBody: String, headers: Map[String, String] = Map[String, String]("Content-Type"->"application/json")): HTTPResponse = { + val response = Unirest.put(url).headers(headers.asJava).body(requestBody).asString() + HTTPResponse(response.getStatus, response.getBody) + } + + def patch(url: String, requestBody: String, headers: Map[String, String] = Map[String, String]("Content-Type"->"application/json")): HTTPResponse = { + val response = Unirest.patch(url).headers(headers.asJava).body(requestBody).asString() + HTTPResponse(response.getStatus, response.getBody) + } + +} \ No newline at end of file diff --git a/content-api/content-actors/src/main/scala/org/sunbird/util/RequestUtil.scala b/content-api/content-actors/src/main/scala/org/sunbird/util/RequestUtil.scala new file mode 100644 index 000000000..e93949153 --- /dev/null +++ b/content-api/content-actors/src/main/scala/org/sunbird/util/RequestUtil.scala @@ -0,0 +1,21 @@ +package org.sunbird.util + +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.schema.DefinitionNode + +import scala.concurrent.ExecutionContext + +object RequestUtil { + + def restrictProperties(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Unit = { + val graphId = request.getContext.getOrDefault("graph_id","").asInstanceOf[String] + val version = request.getContext.getOrDefault("version","").asInstanceOf[String] + val objectType = request.getContext.getOrDefault("objectType", "").asInstanceOf[String] + val schemaName = request.getContext.getOrDefault("schemaName","").asInstanceOf[String] + val operation = request.getOperation.toLowerCase.replace(objectType.toLowerCase, "") + val restrictedProps =DefinitionNode.getRestrictedProperties(graphId, version, operation, schemaName) + if (restrictedProps.exists(prop => request.getRequest.containsKey(prop))) throw new ClientException("ERROR_RESTRICTED_PROP", "Properties in list " + restrictedProps.mkString("[", ", ", "]") + " are not allowed in request") + } +} diff --git a/learning-api/hierarchy-manager/src/test/resources/application.conf b/content-api/content-actors/src/test/resources/application.conf similarity index 92% rename from learning-api/hierarchy-manager/src/test/resources/application.conf rename to content-api/content-actors/src/test/resources/application.conf index 45c7a335f..06d8e8d15 100644 --- a/learning-api/hierarchy-manager/src/test/resources/application.conf +++ b/content-api/content-actors/src/test/resources/application.conf @@ -407,21 +407,6 @@ telemetry.search.topn=5 installation.id=ekstep -learning.content.copy.invalid_status_list=["Flagged","FlaggedDraft","FraggedReview","Retired", "Processing"] -learning.content.copy.props_to_remove=["downloadUrl", "artifactUrl", "variants", - "createdOn", "collections", "children", "lastUpdatedOn", "SYS_INTERNAL_LAST_UPDATED_ON", - "versionKey", "s3Key", "status", "pkgVersion", "toc_url", "mimeTypesCount", - "contentTypesCount", "leafNodesCount", "childNodes", "prevState", "lastPublishedOn", - "flagReasons", "compatibilityLevel", "size", "publishChecklist", "publishComment", - "LastPublishedBy", "rejectReasons", "rejectComment", "gradeLevel", "subject", - "medium", "board", "topic", "purpose", "subtopic", "contentCredits", - "owner", "collaborators", "creators", "contributors", "badgeAssertions", "dialcodes", - "concepts", "keywords", "reservedDialcodes", "dialcodeRequired", "leafNodes"] - -# Metadata to be added to copied content from origin -learning.content.copy.origin_data=["name", "author", "license", "organisation"] - -learning.content.type.not.copied.list=["Asset"] channel.default="in.ekstep" @@ -431,7 +416,7 @@ dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/sear dialcode.api.authorization=auth_key # Language-Code Configuration -language.graph_ids=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] # Kafka send event to topic enable kafka.topic.send.enable=false @@ -460,19 +445,18 @@ publish.collection.fullecar.disable=true cassandra.lp.consistency.level=QUORUM -content.tagging.backward_enable=false -content.tagging.property="subject,medium" + content.nested.fields="badgeAssertions,targets,badgeAssociations" content.cache.ttl=86400 -content.cache.enable=true -collection.cache.enable=true +content.cache.enable=false +collection.cache.enable=false content.discard.status=["Draft","FlagDraft"] framework.categories_cached=["subject", "medium", "gradeLevel", "board"] framework.cache.ttl=86400 -framework.cache.read=true +framework.cache.read=false # Max size(width/height) of thumbnail in pixels @@ -482,4 +466,46 @@ schema.base_path="../../schemas/" content.hierarchy.removed_props_for_leafNodes=["collections","children","usedByContent","item_sets","methods","libraries","editorState"] collection.keyspace = "hierarchy_store" -content.keyspace = "content_store" \ No newline at end of file +content.keyspace = "content_store" +objectcategorydefinition.keyspace=category_store + + +collection.image.migration.enabled=true + + + +cloud_storage.upload.url.ttl=600 + +content.copy.invalid_statusList=["Flagged","FlaggedDraft","FraggedReview","Retired", "Processing"] +content.copy.props_to_remove=["downloadUrl", "artifactUrl", "variants", + "createdOn", "collections", "children", "lastUpdatedOn", "SYS_INTERNAL_LAST_UPDATED_ON", + "versionKey", "s3Key", "status", "pkgVersion", "toc_url", "mimeTypesCount", + "contentTypesCount", "leafNodesCount", "childNodes", "prevState", "lastPublishedOn", + "flagReasons", "compatibilityLevel", "size", "publishChecklist", "publishComment", + "LastPublishedBy", "rejectReasons", "rejectComment", "gradeLevel", "subject", + "medium", "board", "topic", "purpose", "subtopic", "contentCredits", + "owner", "collaborators", "creators", "contributors", "badgeAssertions", "dialcodes", + "concepts", "keywords", "reservedDialcodes", "dialcodeRequired", "leafNodes", "sYS_INTERNAL_LAST_UPDATED_ON", "prevStatus", "lastPublishedBy", "streamingUrl"] + +content.copy.origin_data=["name", "author", "license", "organisation"] +content.h5p.library.path="https://s3.ap-south-1.amazonaws.com/ekstep-public-dev/content/templates/h5p-library-latest.zip" + +# DIAL Link +dial_service { + api { + base_url = "https://qa.ekstep.in/api" + auth_key = "auth_key" + } +} +content.link_dialcode.validation=true +content.link_dialcode.max_limit=10 +# This is added to handle large artifacts sizes differently +content.artifact.size.for_online=209715200 + +# Import API Config +import { + request_size_limit=2 + output_topic_name="local.auto.creation.job.request" + required_props=["name","code","mimeType","contentType","artifactUrl","framework", "channel"] +} +channel.fetch.suggested_frameworks=false \ No newline at end of file diff --git a/content-api/content-actors/src/test/resources/jpegImage.jpeg b/content-api/content-actors/src/test/resources/jpegImage.jpeg new file mode 100755 index 000000000..ccef81dc8 Binary files /dev/null and b/content-api/content-actors/src/test/resources/jpegImage.jpeg differ diff --git a/content-api/content-actors/src/test/resources/sample.pdf b/content-api/content-actors/src/test/resources/sample.pdf new file mode 100644 index 000000000..dbf091df9 Binary files /dev/null and b/content-api/content-actors/src/test/resources/sample.pdf differ diff --git a/content-api/content-actors/src/test/scala/org/sunbird/channel/TestChannelManager.scala b/content-api/content-actors/src/test/scala/org/sunbird/channel/TestChannelManager.scala new file mode 100644 index 000000000..4d58324f9 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/channel/TestChannelManager.scala @@ -0,0 +1,132 @@ +package org.sunbird.channel + +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.common.dto.Request + +import java.util +import org.apache.commons.collections.CollectionUtils +import org.scalamock.scalatest.MockFactory +import org.sunbird.cache.impl.RedisCache +import org.sunbird.util.{ChannelConstants, HTTPResponse, HttpUtil} +import org.sunbird.channel.managers.ChannelManager +import org.sunbird.common.exception.{ClientException} + + +class TestChannelManager extends AsyncFlatSpec with Matchers with MockFactory { + + implicit val httpUtil: HttpUtil = mock[HttpUtil] + + "ChannelManager" should "return a list of frameworks from search service" in { + val frameworkList = ChannelManager.getAllFrameworkList() + assert(CollectionUtils.isNotEmpty(frameworkList)) + } + + it should "throw exception if map contains invalid language translation" in { + val exception = intercept[ClientException] { + val request = new Request() + request.setRequest(new util.HashMap[String, AnyRef]() { + { + put("translations", new util.HashMap[String, AnyRef]() { + { + put("tyy", "dsk") + } + }) + } + }) + ChannelManager.validateTranslationMap(request) + } + exception.getMessage shouldEqual "Please Provide Valid Language Code For translations. Valid Language Codes are : [as, bn, en, gu, hi, hoc, jun, ka, mai, mr, unx, or, san, sat, ta, te, urd, pj]" + } + + def getRequest(): Request = { + val request = new Request() + request + } + + it should "store license in cache" in { + val request = new Request() + request.getRequest.put("defaultLicense","license1234") + ChannelManager.channelLicenseCache(request, "channel_test") + assert(null != RedisCache.get("channel_channel_test_license")) + } + + it should "return success for valid objectCategory" in { + val request = new Request() + request.setRequest(new util.HashMap[String, AnyRef]() {{ + put(ChannelConstants.CONTENT_PRIMARY_CATEGORIES, new util.ArrayList[String]() {{add("Learning Resource")}}) + put(ChannelConstants.COLLECTION_PRIMARY_CATEGORIES, new util.ArrayList[String]() {{add("Learning Resource")}}) + put(ChannelConstants.ASSET_PRIMARY_CATEGORIES, new util.ArrayList[String]() {{add("Learning Resource")}}) + }}) + ChannelManager.validateObjectCategory(request) + assert(true) + } + + it should "throw exception for invalid objectCategory" in { + val exception = intercept[ClientException] { + val request = new Request() + request.setRequest(new util.HashMap[String, AnyRef]() {{ + put(ChannelConstants.CONTENT_PRIMARY_CATEGORIES, new util.ArrayList[String]() {{add("xyz")}}) + put(ChannelConstants.COLLECTION_PRIMARY_CATEGORIES, new util.ArrayList[String]() {{add("xyz")}}) + put(ChannelConstants.ASSET_PRIMARY_CATEGORIES, new util.ArrayList[String]() {{add("xyz")}}) + }}) + ChannelManager.validateObjectCategory(request) + } + exception.getMessage shouldEqual "Please provide valid : [contentPrimaryCategories,collectionPrimaryCategories,assetPrimaryCategories]" + } + + it should "throw exception for empty objectCategory" in { + val exception = intercept[ClientException] { + val request = new Request() + request.setRequest(new util.HashMap[String, AnyRef]() {{ + put(ChannelConstants.CONTENT_PRIMARY_CATEGORIES, new util.ArrayList[String]()) + }}) + ChannelManager.validateObjectCategory(request) + } + exception.getMessage shouldEqual "Empty list not allowed for contentPrimaryCategories" + } + + it should "throw exception for invalid dataType for objectCategory" in { + val exception = intercept[ClientException] { + val request = new Request() + request.setRequest(new util.HashMap[String, AnyRef]() {{ + put(ChannelConstants.CONTENT_PRIMARY_CATEGORIES, "test-string") + }}) + ChannelManager.validateObjectCategory(request) + } + exception.getMessage shouldEqual "Please provide valid list for contentPrimaryCategories" + } + + it should "add objectCategory into channel read response" in { + val metaDataMap: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() + ChannelManager.setPrimaryAndAdditionCategories(metaDataMap) + assert(metaDataMap.containsKey(ChannelConstants.CONTENT_PRIMARY_CATEGORIES)) + assert(CollectionUtils.isNotEmpty(metaDataMap.get(ChannelConstants.CONTENT_PRIMARY_CATEGORIES).asInstanceOf[util.List[String]])) + } + + it should "not change objectCategory into channel read response" in { + val metaDataMap: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef](){{ + put(ChannelConstants.CONTENT_PRIMARY_CATEGORIES, new util.ArrayList[String]() {{add("Learning Resource")}}) + }} + ChannelManager.setPrimaryAndAdditionCategories(metaDataMap) + assert(metaDataMap.containsKey(ChannelConstants.CONTENT_PRIMARY_CATEGORIES)) + assert(CollectionUtils.isEqualCollection(metaDataMap.get(ChannelConstants.CONTENT_PRIMARY_CATEGORIES).asInstanceOf[util.List[String]], + new util.ArrayList[String]() {{add("Learning Resource")}})) + } + + it should "return primary categories of a channel" in { + (httpUtil.post _).expects(*, """{"request":{"filters":{"objectType":"ObjectCategoryDefinition"},"not_exists":"channel","fields":["name","identifier","targetObjectType"]}}""", *) + .returning(HTTPResponse(200, """{"id":"api.v1.search","ver":"1.0","ts":"2021-02-15T05:25:54.939Z","params":{"resmsgid":"46b5e4b0-6f4e-11eb-90b0-bb9ae961ede2","msgid":"46b4d340-6f4e-11eb-90b0-bb9ae961ede2","status":"successful","err":null,"errmsg":null},"responseCode":"OK","result":{"count":24,"ObjectCategoryDefinition":[{"identifier":"obj-cat:asset_asset_all","name":"Asset","targetObjectType":"Asset","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:certasset_asset_all","name":"CertAsset","targetObjectType":"Asset","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:certificate-template_asset_all","name":"Certificate Template","targetObjectType":"Asset","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:content-playlist_content_all","name":"Content Playlist","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:content-playlist_collection_all","name":"Content Playlist","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:course_content_all","name":"Course","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:course_collection_all","name":"Course","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:course-assessment_content_all","name":"Course Assessment","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:course-unit_content_all","name":"Course Unit","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:course-unit_collection_all","name":"Course Unit","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:digital-textbook_content_all","name":"Digital Textbook","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:digital-textbook_collection_all","name":"Digital Textbook","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:etextbook_content_all","name":"eTextbook","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:explanation-content_content_all","name":"Explanation Content","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:explanation-content_collection_all","name":"Explanation Content","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:learning-resource_content_all","name":"Learning Resource","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:lesson-plan-unit_content_all","name":"Lesson Plan Unit","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:lesson-plan-unit_collection_all","name":"Lesson Plan Unit","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:plugin_content_all","name":"Plugin","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:practice-question-set_content_all","name":"Practice Question Set","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:teacher-resource_content_all","name":"Teacher Resource","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:template_content_all","name":"Template","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:textbook-unit_collection_all","name":"Textbook Unit","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:textbook-unit_content_all","name":"Textbook Unit","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"}]}}""")) + (httpUtil.post _).expects(*, """{"request":{"filters":{"objectType":"ObjectCategoryDefinition", "channel": "01309282781705830427"},"fields":["name","identifier","targetObjectType"]}}""", *) + .returning(HTTPResponse(200, """{"id":"api.v1.search","ver":"1.0","ts":"2021-02-15T05:31:41.939Z","params":{"resmsgid":"1589e430-6f4f-11eb-a956-2bb50a66d58f","msgid":"1588abb0-6f4f-11eb-a956-2bb50a66d58f","status":"successful","err":null,"errmsg":null},"responseCode":"OK","result":{"count":3,"ObjectCategoryDefinition":[{"identifier":"obj-cat:course_collection_01309282781705830427","name":"Course","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:exam-question_content_01309282781705830427","name":"Exam Question","targetObjectType":"Content","objectType":"ObjectCategoryDefinition"},{"identifier":"obj-cat:question-paper_collection_01309282781705830427","name":"Question Paper","targetObjectType":"Collection","objectType":"ObjectCategoryDefinition"}]}}""")) + + val primaryCategories = ChannelManager.getChannelPrimaryCategories("01309282781705830427") + assert(primaryCategories.size() > 0) + } + + it should "return additional categories" in { + (httpUtil.post _).expects(*, """{"request":{"filters":{"objectType":"ObjectCategory"},"fields":["name","identifier"]}}""", *) + .returning(HTTPResponse(200, """{"id":"api.v1.search","ver":"1.0","ts":"2021-02-15T08:06:20.058Z","params":{"resmsgid":"afba7fa0-6f64-11eb-a956-2bb50a66d58f","msgid":"afb8aae0-6f64-11eb-a956-2bb50a66d58f","status":"successful","err":null,"errmsg":null},"responseCode":"OK","result":{"ObjectCategory":[{"identifier":"obj-cat:asset","name":"Asset","objectType":"ObjectCategory"},{"identifier":"obj-cat:certificate-template","name":"Certificate Template","objectType":"ObjectCategory"},{"identifier":"obj-cat:classroom-teaching-video","name":"Classroom Teaching Video","objectType":"ObjectCategory"},{"identifier":"obj-cat:content-playlist","name":"Content Playlist","objectType":"ObjectCategory"},{"identifier":"obj-cat:course","name":"Course","objectType":"ObjectCategory"},{"identifier":"obj-cat:course-assessment","name":"Course Assessment","objectType":"ObjectCategory"}],"count":6}}""")) + val additionalCategories = ChannelManager.getAdditionalCategories() + assert(additionalCategories.size() > 0) + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/BaseSpec.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/BaseSpec.scala new file mode 100644 index 000000000..6d0303d02 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/BaseSpec.scala @@ -0,0 +1,45 @@ +package org.sunbird.content.actors + +import java.util +import java.util.concurrent.TimeUnit + +import akka.actor.{ActorSystem, Props} +import akka.testkit.TestKit +import org.scalatest.{FlatSpec, Matchers} +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Node + +import scala.concurrent.duration.FiniteDuration + +class BaseSpec extends FlatSpec with Matchers { + + val system = ActorSystem.create("system") + + def testUnknownOperation(props: Props, request: Request)(implicit oec: OntologyEngineContext) = { + request.setOperation("unknown") + val response = callActor(request, props) + assert("failed".equals(response.getParams.getStatus)) + } + + def callActor(request: Request, props: Props): Response = { + val probe = new TestKit(system) + val actorRef = system.actorOf(props) + actorRef.tell(request, probe.testActor) + probe.expectMsgType[Response](FiniteDuration.apply(30, TimeUnit.SECONDS)) + } + + def getNode(objectType: String, metadata: Option[util.Map[String, AnyRef]]): Node = { + val node = new Node("domain", "DATA_NODE", objectType) + node.setGraphId("domain") + val nodeMetadata = metadata.getOrElse(new util.HashMap[String, AnyRef]() {{ + put("name", "Sunbird Node") + put("code", "sunbird-node") + put("status", "Draft") + }}) + node.setMetadata(nodeMetadata) + node.setObjectType(objectType) + node.setIdentifier("test_id") + node + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestAppActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestAppActor.scala new file mode 100644 index 000000000..64bffd638 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestAppActor.scala @@ -0,0 +1,119 @@ +package org.sunbird.content.actors + +import akka.actor.Props +import org.apache.hadoop.util.StringUtils +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.Request +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.{GraphService, OntologyEngineContext} + +import scala.concurrent.ExecutionContext.Implicits.global +import java.util +import scala.collection.JavaConverters._ +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.Future + +class TestAppActor extends BaseSpec with MockFactory { + + "AppActor" should "return failed response for 'unknown' operation" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new AppActor()), getRequest()) + } + + it should "return success response for 'create' operation" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + val node = new Node("domain", "DATA_NODE", "App") + node.setIdentifier("android-org.test.sunbird.integration") + node.setObjectType("App") + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(node)) + val request = getRequest() + request.getRequest.put("name", "Test Integration App") + request.getRequest.put("logo", "logo url") + request.getRequest.put("description", "Description of Test Integration App") + request.getRequest.put("provider", Map("name" -> "Test Organisation", "copyright" -> "CC BY 4.0").asJava) + request.getRequest.put("osType", "Android") + request.getRequest.put("osMetadata", Map("packageId" -> "org.test.integration", "appVersion" -> "1.0", "compatibilityVer" -> "1.0").asJava) + request.setOperation("create") + val response = callActor(request, Props(new AppActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "throw client exception to have all the required properties for app register" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getRequest() + request.getRequest.put("name", "Test Integration App") + request.setOperation("create") + val response = callActor(request, Props(new AppActor())) + assert("failed".equals(response.getParams.getStatus)) + } + + it should "return success response for update" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + val request = getRequest() + request.putAll(mapAsJavaMap(Map("description" -> "test desc"))) + request.setOperation("update") + val response = callActor(request, Props(new AppActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.get("identifier").equals("android-org.test.sunbird.integration")) + } + + it should "return success response for read app" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(1) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + val request = getRequest() + request.putAll(mapAsJavaMap(Map("fields" -> ""))) + request.setOperation("read") + val response = callActor(request, Props(new AppActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(StringUtils.equalsIgnoreCase(response.get("app").asInstanceOf[util.Map[String, AnyRef]].get("identifier").asInstanceOf[String], "android-org.test.sunbird.integration")) + assert(StringUtils.equalsIgnoreCase(response.get("app").asInstanceOf[util.Map[String, AnyRef]].get("status").asInstanceOf[String], "Draft")) + } + + private def getRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "App") + put("schemaName", "app") + put("X-Channel-Id", "org.sunbird") + } + }) + request.setObjectType("App") + request + } + + private def getValidNode(): Node = { + val node = new Node() + node.setIdentifier("android-org.test.sunbird.integration") + node.setNodeType("DATA_NODE") + node.setObjectType("App") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "android-org.test.sunbird.integration") + put("status", "Draft") + put("name", "Test Integration App") + put("logo", "logo url") + put("description", "Description of Test Integration App") + put("provider", Map("name" -> "Test Organisation", "copyright" -> "CC BY 4.0").asJava) + put("osType", "Android") + put("osMetadata", Map("packageId" -> "org.test.sunbird.integration", "appVersion" -> "1.0", "compatibilityVer" -> "1.0").asJava) + } + }) + node + } + +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestAssetActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestAssetActor.scala new file mode 100644 index 000000000..070451936 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestAssetActor.scala @@ -0,0 +1,121 @@ +package org.sunbird.content.actors + +import java.util + +import akka.actor.Props +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.graph.dac.model.{Node, SearchCriteria} + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class TestAssetActor extends BaseSpec with MockFactory { + + "AssetActor" should "return failed response for 'unknown' operation" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new AssetActor()), getContentRequest()) + } + + it should "return success response for 'copyAsset'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getNode())) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(node)) + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())).anyNumberOfTimes() + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do_1234") + request.put("identifier","do_1234") + request.setOperation("copy") + val response = callActor(request, Props(new AssetActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.getResult.containsKey("node_id")) + assert("test_321".equals(response.get("versionKey"))) + } + + it should "copy asset with invalid objectType, should through client exception" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getInvalidNode())) + val request = getContentRequest() + request.setOperation("copy") + val response = callActor(request, Props(new AssetActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + assert(response.getParams.getErrmsg == "Only asset can be copied") + } + + private def getNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Asset") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("mimeType", "application/vnd.ekstep.content-archive") + put("status", "Live") + put("name", "Asset_Test") + put("versionKey", "test_321") + put("channel", "in.ekstep") + put("code", "Asset_Test") + put("contentType", "Asset") + put("primaryCategory", "Asset") + put("artifactUrl", "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1234/artifact/file-0130860005482086401.svg") + } + }) + node + } + + private def getContentRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Asset") + put("schemaName", "asset") + put("X-Channel-Id", "in.ekstep") + } + }) + request.setObjectType("Asset") + request + } + + private def getInvalidNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node + } + + def getFrameworkNode(): Node = { + val node = new Node() + node.setIdentifier("NCF") + node.setNodeType("DATA_NODE") + node.setObjectType("Framework") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap(Map("name"-> "NCF"))) + node + } + + def getBoardNode(): Node = { + val node = new Node() + node.setIdentifier("ncf_board_cbse") + node.setNodeType("DATA_NODE") + node.setObjectType("Term") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap(Map("name"-> "CBSE"))) + node + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestCategoryActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestCategoryActor.scala new file mode 100644 index 000000000..7a5014d99 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestCategoryActor.scala @@ -0,0 +1,141 @@ +package org.sunbird.content.actors + +import java.util + +import akka.actor.Props +import org.apache.hadoop.util.StringUtils +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.graph.dac.model.Node + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class TestCategoryActor extends BaseSpec with MockFactory{ + + "CategoryActor" should "return failed response for 'unknown' operation" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new CategoryActor()), getCategoryRequest()) + } + + it should "create a categoryNode and store it in neo4j" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(getValidNode())) + val request = getCategoryRequest() + request.putAll(mapAsJavaMap(Map("name" -> "do_1234"))) + request.setOperation("createCategory") + val response = callActor(request, Props(new CategoryActor())) + assert(response.get("identifier") != null) + assert(response.get("identifier").equals("cat-do_1234")) + } + + it should "return exception for create categoryNode without name" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getCategoryRequest() + request.setOperation("createCategory") + val response = callActor(request, Props(new CategoryActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + assert(StringUtils.equalsIgnoreCase(response.get("messages").asInstanceOf[util.ArrayList[String]].get(0).asInstanceOf[String], "Required Metadata name not set")) + } + + it should "return exception for categoryNode with identifier" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getCategoryRequest() + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + request.setOperation("createCategory") + val response = callActor(request, Props(new CategoryActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + assert(StringUtils.equalsIgnoreCase(response.getParams.getErrmsg, "name will be set as identifier")) + } + + it should "return success response for updateCategory" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(getValidNode())) + implicit val ss = mock[StorageService] + val request = getCategoryRequest() + request.getContext.put("identifier","do_1234") + request.putAll(mapAsJavaMap(Map("description" -> "test desc"))) + request.setOperation("updateCategory") + val response = callActor(request, Props(new CategoryActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + + it should "return success response for readCategory" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(1) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + implicit val ss = mock[StorageService] + val request = getCategoryRequest() + request.getContext.put("identifier","do_1234") + request.putAll(mapAsJavaMap(Map("fields" -> ""))) + request.setOperation("readCategory") + val response = callActor(request, Props(new CategoryActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for retireCategory" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(getValidNode())) + implicit val ss = mock[StorageService] + val request = getCategoryRequest() + request.getContext.put("identifier","do_1234") + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + request.setOperation("retireCategory") + val response = callActor(request, Props(new CategoryActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + private def getCategoryRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Category") + put("schemaName", "category") + + } + }) + request.setObjectType("Category") + request + } + + private def getValidNode(): Node = { + val node = new Node() + node.setIdentifier("cat-do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Category") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "cat-do_1234") + put("objectType", "Category") + put("status", "Live") + put("name", "do_1234") + put("versionKey", "1878141") + } + }) + node + } + +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestChannelActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestChannelActor.scala new file mode 100644 index 000000000..2a14668e4 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestChannelActor.scala @@ -0,0 +1,125 @@ +package org.sunbird.content.actors + +import java.util + +import akka.actor.Props +import org.scalamock.scalatest.MockFactory +import org.sunbird.channel.actors.ChannelActor +import org.sunbird.common.dto.Request +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.{GraphService, OntologyEngineContext} + +import scala.collection.JavaConversions._ +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class TestChannelActor extends BaseSpec with MockFactory { + + "ChannelActor" should "return failed response for 'unknown' operation" in { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new ChannelActor()), getRequest()) + } + + it should "return success response for 'createChannel' operation" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + val node = new Node("domain", "DATA_NODE", "Channel") + node.setIdentifier("channel_test") + node.setObjectType("Channel") + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(node)) + val request = getRequest() + request.getRequest.put("name", "channel_test") + request.getRequest.put("code", "channel_test") + request.setOperation("createChannel") + val response = callActor(request, Props(new ChannelActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "throw exception code is required for createChannel" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getRequest() + request.getRequest.put("name", "channel_test") + request.setOperation("createChannel") + val response = callActor(request, Props(new ChannelActor())) + assert("failed".equals(response.getParams.getStatus)) + } + + it should "throw invalid identifier exception for channelUpdate" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + val node = new Node("domain",mapAsJavaMap(Map("identifier" -> "channel_test", "nodeType"->"DATA_NODE", "objectType"->"Channel"))) + node.setIdentifier("channel_test") + node.setObjectType("Channel") + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + val request = getRequest() + request.getRequest.put("name", "channel_test2") + request.setOperation("updateChannel") + val response = callActor(request, Props(new ChannelActor())) + assert("failed".equals(response.getParams.getStatus)) + } + + ignore should "return success response for 'readChannel' operation" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + val node = new Node("domain",mapAsJavaMap(Map("identifier" -> "channel_test", "nodeType"->"DATA_NODE", "objectType"->"Channel"))) + node.setIdentifier("channel_test") + node.setObjectType("Channel") + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + val request = getRequest() + request.getRequest.put("identifier", "channel_test") + request.setOperation("readChannel") + val response = callActor(request, Props(new ChannelActor())) +// assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'updateChannel' operation" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Channel", None) + node.setObjectType("Channel") + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + (graphDB.upsertNode(_:String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + val request = getRequest() + request.getContext.put("identifier", "channel_test"); + request.getRequest.put("name", "channel_test") + request.setOperation("updateChannel") + val response = callActor(request, Props(new ChannelActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'retireChannel' operation" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Channel", None) + node.setObjectType("Channel") + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + (graphDB.upsertNode(_:String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + val request = getRequest() + request.getContext.put("identifier", "channel_test"); + request.getRequest.put("identifier", "channel_test") + request.setOperation("retireChannel") + val response = callActor(request, Props(new ChannelActor())) + println("Response: retire: " + response) + assert("successful".equals(response.getParams.getStatus)) + } + + private def getRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Channel") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "channel") + } + }) + request.setObjectType("Channel") + request + } + +} \ No newline at end of file diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestCollectionActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestCollectionActor.scala new file mode 100644 index 000000000..6782b4308 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestCollectionActor.scala @@ -0,0 +1,30 @@ +package org.sunbird.content.actors + +import java.util + +import akka.actor.Props +import org.sunbird.common.dto.Request +import org.sunbird.graph.OntologyEngineContext + +class TestCollectionActor extends BaseSpec { + + "CollectionActor" should "return failed response for 'unknown' operation" in { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation( Props(new CollectionActor()), getCollectionRequest()) + } + + private def getCollectionRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "collection") + + } + }) + request.setObjectType("Collection") + request + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestContentActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestContentActor.scala new file mode 100644 index 000000000..b2b8154cf --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestContentActor.scala @@ -0,0 +1,456 @@ +package org.sunbird.content.actors + +import java.io.File +import java.util + +import org.sunbird.graph.dac.model.{Node, SearchCriteria} +import akka.actor.Props +import com.google.common.io.Resources +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.{HttpUtil, JsonUtils} +import org.sunbird.common.dto.{Property, Request, Response, ResponseHandler} +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.utils.ScalaJsonUtils +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.kafka.client.KafkaClient + +import scala.collection.JavaConversions._ +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class TestContentActor extends BaseSpec with MockFactory { + + "ContentActor" should "return failed response for 'unknown' operation" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new ContentActor()), getContentRequest()) + } + + it should "validate input before creating content" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getContentRequest() + val content = mapAsJavaMap(Map("name" -> "New Content", "code" -> "1234", "mimeType"-> "application/pdf", "contentType" -> "Resource", + "framework" -> "NCF", "organisationBoardIds" -> new util.ArrayList[String](){{add("ncf_board_cbse")}})) + request.put("content", content) + assert(true) + val response = callActor(request, Props(new ContentActor())) + println("Response: " + JsonUtils.serialize(response)) + + } + + it should "create a node and store it in neo4j" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + // Uncomment below line if running individual file in local. + //(graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(getValidNode())) + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(new util.ArrayList[Node]() { + { + add(getBoardNode()) + } + })) + val request = getContentRequest() + request.getRequest.putAll( mapAsJavaMap(Map("channel"-> "in.ekstep","name" -> "New Content", "code" -> "1234", "mimeType"-> "application/vnd.ekstep.content-collection", "contentType" -> "Course", "primaryCategory" -> "Learning Resource", "channel" -> "in.ekstep", "targetBoardIds" -> new util.ArrayList[String](){{add("ncf_board_cbse")}}))) + request.setOperation("createContent") + val response = callActor(request, Props(new ContentActor())) + assert(response.get("identifier") != null) + assert(response.get("versionKey") != null) + } + + it should "create a plugin node and store it in neo4j" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getDefinitionNode())).anyNumberOfTimes() + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(getValidNode())) + val request = getContentRequest() + request.getRequest.putAll( mapAsJavaMap(Map("name" -> "New Content", "code" -> "1234", "mimeType"-> "application/vnd.ekstep.plugin-archive", "contentType" -> "Course", "primaryCategory" -> "Learning Resource", "channel" -> "in.ekstep", "framework"-> "NCF", "organisationBoardIds" -> new util.ArrayList[String](){{add("ncf_board_cbse")}}))) + request.setOperation("createContent") + val response = callActor(request, Props(new ContentActor())) + assert(response.get("identifier") != null) + assert(response.get("versionKey") != null) + } + + it should "create a plugin node with invalid request, should through client exception" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + val request = getContentRequest() + request.getRequest.putAll( mapAsJavaMap(Map("name" -> "New Content", "mimeType"-> "application/vnd.ekstep.plugin-archive", "contentType" -> "Course", "primaryCategory" -> "Learning Resource", "channel" -> "in.ekstep"))) + request.setOperation("createContent") + val response = callActor(request, Props(new ContentActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + } + + it should "generate and return presigned url" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getValidNode())) + implicit val ss = mock[StorageService] + (ss.getSignedURL(_: String, _: Option[Int], _: Option[String])).expects(*, *, *).returns("cloud store url") + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("fileName" -> "presigned_url", "filePath" -> "/data/cloudstore/", "type" -> "assets", "identifier" -> "do_1234"))) + request.setOperation("uploadPreSignedUrl") + val response = callActor(request, Props(new ContentActor())) + assert(response.get("identifier") != null) + assert(response.get("pre_signed_url") != null) + assert(response.get("url_expiry") != null) + } + + it should "discard node in draft state should return success" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getValidNodeToDiscard())) + (graphDB.deleteNode(_: String, _: String, _: Request)).expects(*, *, *).returns(Future(true)) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_12346"))) + request.setOperation("discardContent") + val response = callActor(request, Props(new ContentActor())) + assert(response.getResponseCode == ResponseCode.OK) + assert(response.get("identifier") == "do_12346") + assert(response.get("message") == "Draft version of the content with id : do_12346 is discarded") + + } + + it should "discard node in Live state should return client error" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(1) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getInValidNodeToDiscard())) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_12346"))) + request.setOperation("discardContent") + val response = callActor(request, Props(new ContentActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + } + + it should "return client error response for retireContent" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do_1234.img") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234.img"))) + request.setOperation("retireContent") + val response = callActor(request, Props(new ContentActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + } + + private def getContentRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "content") + put("X-Channel-Id", "in.ekstep") + } + }) + request.setObjectType("Content") + request + } + + private def getValidNodeToDiscard(): Node = { + val node = new Node() + node.setIdentifier("do_12346") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12346") + put("mimeType", "application/pdf") + put("status", "Draft") + put("contentType", "Resource") + put("name", "Node To discard") + put("primaryCategory", "Learning Resource") + } + }) + node + } + + private def getInValidNodeToDiscard(): Node = { + val node = new Node() + node.setIdentifier("do_12346") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12346") + put("mimeType", "application/pdf") + put("status", "Live") + put("contentType", "Resource") + put("name", "Node To discard") + put("primaryCategory", "Learning Resource") + } + }) + node + } + + private def getValidNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("mimeType", "application/vnd.ekstep.content-collection") + put("status", "Draft") + put("contentType", "Course") + put("name", "Course_1") + put("versionKey", "1878141") + put("primaryCategory", "Learning Resource") + } + }) + node + } + + it should "return success response for retireContent" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + val node = getNode("Content", None) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.updateNodes(_: String, _: util.List[String], _: util.HashMap[String, AnyRef])).expects(*, *, *).returns(Future(new util.HashMap[String, Node])) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do1234") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + request.setOperation("retireContent") + val response = callActor(request, Props(new ContentActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'readContent'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + val node = getNode("Content", None) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do1234") + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "fields" -> ""))) + request.setOperation("readContent") + val response = callActor(request, Props(new ContentActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'updateContent'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("test_123")))) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do_1234") + request.putAll(mapAsJavaMap(Map("description" -> "test desc", "versionKey" -> "test_123"))) + request.setOperation("updateContent") + val response = callActor(request, Props(new ContentActor())) + assert("successful".equals(response.getParams.getStatus)) + assert("do_1234".equals(response.get("identifier"))) + assert("test_123".equals(response.get("versionKey"))) + } + + it should "return client exception for 'updateContent' with invalid versionKey" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("test_xyz")))) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do_1234") + request.putAll(mapAsJavaMap(Map("description" -> "test desc", "versionKey" -> "test_123"))) + request.setOperation("updateContent") + val response = callActor(request, Props(new ContentActor())) + assert("failed".equals(response.getParams.getStatus)) + assert("CLIENT_ERROR".equals(response.getParams.getErr)) + assert("Invalid version Key".equals(response.getParams.getErrmsg)) + } + + it should "return client exception for 'updateContent' without versionKey" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do1234") + request.putAll(mapAsJavaMap(Map("description" -> "updated description","framework" -> "NCF", "organisationBoardIds" -> new util.ArrayList[String](){{add("ncf_board_cbse")}}))) + request.setOperation("updateContent") + val response = callActor(request, Props(new ContentActor())) + assert("failed".equals(response.getParams.getStatus)) + assert("ERR_INVALID_REQUEST".equals(response.getParams.getErr)) + assert("Please Provide Version Key!".equals(response.getParams.getErrmsg)) + } + + it should "return success response for 'copyContent'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(node)) + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do1234") + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "createdBy" -> "username_1", + "createdFor" -> new util.ArrayList[String]() {{ add("NCF2") }}, "framework" -> "NCF", + "organisation" -> new util.ArrayList[String]() {{ add("NCF2") }}))) + request.setOperation("copy") + val response = callActor(request, Props(new ContentActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.getResult.containsKey("node_id")) + assert("test_321".equals(response.get("versionKey"))) + } + + it should "send events to kafka topic" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val kfClient = mock[KafkaClient] + val hUtil = mock[HttpUtil] + (oec.httpUtil _).expects().returns(hUtil) + val resp :Response = ResponseHandler.OK() + resp.put("content", new util.HashMap[String, AnyRef](){{ + put("framework", "NCF") + put("artifactUrl", "http://test.com/test.pdf") + put("channel", "test") + }}) + (hUtil.get(_: String, _: String, _: util.Map[String, String])).expects(*, *, *).returns(resp) + (oec.kafkaClient _).expects().returns(kfClient) + (kfClient.send(_: String, _: String)).expects(*, *).returns(None) + val request = getContentRequest() + request.getRequest.put("content", new util.HashMap[String, AnyRef](){{ + put("source", "https://dock.sunbirded.org/api/content/v1/read/do_11307822356267827219477") + put("metadata", new util.HashMap[String, AnyRef](){{ + put("name", "Test Content") + put("description", "Test Content") + put("mimeType", "application/pdf") + put("code", "test.res.1") + put("contentType", "Resource") + put("primaryCategory", "Learning Resource") + }}) + }}) + request.setOperation("importContent") + request.setObjectType("Content") + val response = callActor(request, Props(new ContentActor())) + assert(response.get("processId") != null) + } + + it should "return success response for 'uploadContent' with jpeg asset" ignore { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + implicit val ss = mock[StorageService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getAssetNodeToUpload() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("1234")))) + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array("do_1234", "do_1234")) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + val request = getContentRequest() + request.getContext.put("identifier", "do_1234") + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "createdBy" -> "username_1", + "createdFor" -> new util.ArrayList[String]() {{ add("NCF2") }}, "framework" -> "NCF", + "organisation" -> new util.ArrayList[String]() {{ add("NCF2") }}))) + request.put("file", new File(Resources.getResource("jpegImage.jpeg").toURI)) + request.setOperation("uploadContent") + val response = callActor(request, Props(new ContentActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + private def getAssetNodeToUpload(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Asset") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("mimeType", "image/jpeg") + put("contentType", "Asset") + put("name", "Asset_1") + put("versionKey", "test_321") + put("channel", "in.ekstep") + put("code", "Resource_1") + put("primaryCategory", "Asset") + put("versionKey", "1234") + } + }) + node + } + + private def getNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("mimeType", "application/pdf") + put("status", "Live") + put("contentType", "Resource") + put("name", "Resource_1") + put("versionKey", "test_321") + put("channel", "in.ekstep") + put("code", "Resource_1") + put("primaryCategory", "Learning Resource") + } + }) + node + } + + + def getDefinitionNode(): Node = { + val node = new Node() + node.setIdentifier("obj-cat:learning-resource_content_in.ekstep") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap( + ScalaJsonUtils.deserialize[Map[String,AnyRef]]("{\n \"objectCategoryDefinition\": {\n \"name\": \"Learning Resource\",\n \"description\": \"Content Playlist\",\n \"categoryId\": \"obj-cat:learning-resource\",\n \"targetObjectType\": \"Content\",\n \"objectMetadata\": {\n \"config\": {},\n \"schema\": {\n \"required\": [\n \"author\",\n \"copyright\",\n \"license\",\n \"audience\"\n ],\n \"properties\": {\n \"audience\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Student\",\n \"Teacher\"\n ]\n },\n \"default\": [\n \"Student\"\n ]\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"enum\": [\n \"application/pdf\"\n ]\n }\n }\n }\n }\n }\n }"))) + node + } + + def getContentSchema(): util.Map[String, AnyRef] = { + val schema:String = "{\n \"$id\": \"content-schema.json\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"title\": \"Content\",\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"status\",\n \"mimeType\",\n \"channel\",\n \"contentType\",\n \"code\",\n \"contentEncoding\",\n \"contentDisposition\",\n \"mediaType\",\n \"primaryCategory\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 5\n },\n \"code\": {\n \"type\": \"string\"\n },\n \"createdOn\": {\n \"type\": \"string\"\n },\n \"lastUpdatedOn\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"type\": \"string\",\n \"enum\": [\n \"Draft\",\n \"Review\",\n \"Redraft\",\n \"Flagged\",\n \"Live\",\n \"Unlisted\",\n \"Retired\",\n \"Mock\",\n \"Processing\",\n \"FlagDraft\",\n \"FlagReview\",\n \"Failed\"\n ],\n \"default\": \"Draft\"\n },\n \"channel\": {\n \"type\": \"string\"\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"enum\": [\n \"application/vnd.ekstep.ecml-archive\",\n \"application/vnd.ekstep.html-archive\",\n \"application/vnd.android.package-archive\",\n \"application/vnd.ekstep.content-archive\",\n \"application/vnd.ekstep.content-collection\",\n \"application/vnd.ekstep.plugin-archive\",\n \"application/vnd.ekstep.h5p-archive\",\n \"application/epub\",\n \"text/x-url\",\n \"video/x-youtube\",\n \"application/octet-stream\",\n \"application/msword\",\n \"application/pdf\",\n \"image/jpeg\",\n \"image/jpg\",\n \"image/png\",\n \"image/tiff\",\n \"image/bmp\",\n \"image/gif\",\n \"image/svg+xml\",\n \"video/avi\",\n \"video/mpeg\",\n \"video/quicktime\",\n \"video/3gpp\",\n \"video/mp4\",\n \"video/ogg\",\n \"video/webm\",\n \"audio/mp3\",\n \"audio/mp4\",\n \"audio/mpeg\",\n \"audio/ogg\",\n \"audio/webm\",\n \"audio/x-wav\",\n \"audio/wav\"\n ]\n },\n \"osId\": {\n \"type\": \"string\",\n \"default\": \"org.ekstep.launcher\"\n },\n \"contentEncoding\": {\n \"type\": \"string\",\n \"enum\": [\n \"gzip\",\n \"identity\"\n ],\n \"default\": \"gzip\"\n },\n \"contentDisposition\": {\n \"type\": \"string\",\n \"enum\": [\n \"inline\",\n \"online\",\n \"attachment\",\n \"online-only\"\n ],\n \"default\": \"inline\"\n },\n \"mediaType\": {\n \"type\": \"string\",\n \"enum\": [\n \"content\",\n \"collection\",\n \"image\",\n \"video\",\n \"audio\",\n \"voice\",\n \"ecml\",\n \"document\",\n \"pdf\",\n \"text\",\n \"other\"\n ],\n \"default\": \"content\"\n },\n \"os\": {\n \"type\": \"array\",\n \"items\": {\n \"type\" : \"string\",\n \"enum\": [\n \"All\",\n \"Android\",\n \"iOS\",\n \"Windows\"\n ]\n },\n \"default\": [\"All\"]\n },\n \"minOsVersion\": {\n \"type\": \"string\"\n },\n \"compatibilityLevel\": {\n \"type\": \"number\",\n \"default\": 1\n },\n \"minGenieVersion\": {\n \"type\": \"string\"\n },\n \"minSupportedVersion\": {\n \"type\": \"string\"\n },\n \"filter\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"variants\": {\n \"type\": \"object\"\n },\n \"config\": {\n \"type\": \"object\"\n },\n \"visibility\": {\n \"type\": \"string\",\n \"enum\": [\n \"Default\",\n \"Parent\"\n ],\n \"default\": \"Default\"\n },\n \"audience\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Student\",\n \"Teacher\",\n \"Administrator\"\n ]\n },\n \"default\": [\"Student\"]\n },\n \"posterImage\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"badgeAssertions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"targets\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"contentCredits\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"appIcon\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"grayScaleAppIcon\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"thumbnail\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"screenshots\": {\n \"type\": \"string\"\n },\n \"format\": {\n \"type\": \"string\"\n },\n \"duration\": {\n \"type\": \"string\"\n },\n \"size\": {\n \"type\": \"number\"\n },\n \"idealScreenSize\": {\n \"type\": \"string\",\n \"enum\": [\n \"small\",\n \"normal\",\n \"large\",\n \"xlarge\",\n \"other\"\n ],\n \"default\": \"normal\"\n },\n \"idealScreenDensity\": {\n \"type\": \"string\",\n \"enum\": [\n \"ldpi\",\n \"mdpi\",\n \"hdpi\",\n \"xhdpi\",\n \"xxhdpi\",\n \"xxxhdpi\"\n ],\n \"default\": \"hdpi\"\n },\n \"releaseNotes\": {\n \"type\": \"array\"\n },\n \"pkgVersion\": {\n \"type\": \"number\"\n },\n \"semanticVersion\": {\n \"type\": \"string\"\n },\n \"versionKey\": {\n \"type\": \"string\"\n },\n \"resources\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Speaker\",\n \"Touch\",\n \"Microphone\",\n \"GPS\",\n \"Motion Sensor\",\n \"Compass\"\n ]\n }\n },\n \"downloadUrl\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"artifactUrl\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"previewUrl\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"streamingUrl\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"objects\": {\n \"type\": \"array\"\n },\n \"organization\": {\n \"type\": \"array\"\n },\n \"createdFor\": {\n \"type\": \"array\"\n },\n \"developer\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"type\": \"string\"\n },\n \"notes\": {\n \"type\": \"string\"\n },\n \"pageNumber\": {\n \"type\": \"string\"\n },\n \"publication\": {\n \"type\": \"string\"\n },\n \"edition\": {\n \"type\": \"string\"\n },\n \"publisher\": {\n \"type\": \"string\"\n },\n \"author\": {\n \"type\": \"string\"\n },\n \"owner\": {\n \"type\": \"string\"\n },\n \"attributions\": {\n \"type\": \"array\"\n },\n \"collaborators\": {\n \"type\": \"array\"\n },\n \"creators\": {\n \"type\": \"string\"\n },\n \"contributors\": {\n \"type\": \"string\"\n },\n \"voiceCredits\": {\n \"type\": \"array\"\n },\n \"soundCredits\": {\n \"type\": \"array\"\n },\n \"imageCredits\": {\n \"type\": \"array\"\n },\n \"copyright\": {\n \"type\": \"string\"\n },\n \"license\": {\n \"type\": \"string\",\n \"default\": \"CC BY 4.0\"\n },\n \"language\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"English\",\n \"Hindi\",\n \"Assamese\",\n \"Bengali\",\n \"Gujarati\",\n \"Kannada\",\n \"Malayalam\",\n \"Marathi\",\n \"Nepali\",\n \"Odia\",\n \"Punjabi\",\n \"Tamil\",\n \"Telugu\",\n \"Urdu\",\n \"Sanskrit\",\n \"Maithili\",\n \"Munda\",\n \"Santali\",\n \"Juang\",\n \"Ho\",\n \"Other\"\n ]\n },\n \"default\": [\"English\"]\n },\n \"words\": {\n \"type\": \"array\"\n },\n \"text\": {\n \"type\": \"array\"\n },\n \"forkable\": {\n \"type\": \"boolean\"\n },\n \"translatable\": {\n \"type\": \"boolean\"\n },\n \"ageGroup\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"<5\",\n \"5-6\",\n \"6-7\",\n \"7-8\",\n \"8-10\",\n \">10\",\n \"Other\"\n ]\n }\n },\n \"interactivityLevel\": {\n \"type\": \"string\",\n \"enum\": [\n \"High\",\n \"Medium\",\n \"Low\"\n ]\n },\n \"contentType\": {\n \"type\": \"string\",\n \"enum\": [\n \"Resource\",\n \"Collection\",\n \"TextBook\",\n \"LessonPlan\",\n \"Course\",\n \"Template\",\n \"Asset\",\n \"Plugin\",\n \"LessonPlanUnit\",\n \"CourseUnit\",\n \"TextBookUnit\",\n \"TeachingMethod\",\n \"PedagogyFlow\",\n \"FocusSpot\",\n \"LearningOutcomeDefinition\",\n \"PracticeQuestionSet\",\n \"CuriosityQuestionSet\",\n \"MarkingSchemeRubric\",\n \"ExplanationResource\",\n \"ExperientialResource\",\n \"ConceptMap\",\n \"SelfAssess\",\n \"PracticeResource\",\n \"eTextBook\",\n \"OnboardingResource\",\n \"ExplanationVideo\",\n \"ClassroomTeachingVideo\",\n \"ExplanationReadingMaterial\",\n \"LearningActivity\",\n \"PreviousBoardExamPapers\",\n \"LessonPlanResource\",\n \"TVLesson\"\n ]\n },\n \"resourceType\": {\n \"type\": \"string\",\n \"enum\": [\n \"Read\",\n \"Learn\",\n \"Teach\",\n \"Play\",\n \"Test\",\n \"Practice\",\n \"Experiment\",\n \"Collection\",\n \"Book\",\n \"Lesson Plan\",\n \"Course\",\n \"Theory\",\n \"Worksheet\",\n \"Practical\"\n ]\n },\n \"category\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"core\",\n \"learning\",\n \"literacy\",\n \"math\",\n \"science\",\n \"time\",\n \"wordnet\",\n \"game\",\n \"mcq\",\n \"mtf\",\n \"ftb\",\n \"library\"\n ]\n }\n },\n \"templateType\": {\n \"type\": \"string\",\n \"enum\": [\n \"story\",\n \"worksheet\",\n \"mcq\",\n \"ftb\",\n \"mtf\",\n \"recognition\",\n \"activity\",\n \"widget\",\n \"other\"\n ]\n },\n \"genre\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Picture Books\",\n \"Chapter Books\",\n \"Flash Cards\",\n \"Serial Books\",\n \"Alphabet Books\",\n \"Folktales\",\n \"Fiction\",\n \"Non-Fiction\",\n \"Poems/Rhymes\",\n \"Plays\",\n \"Comics\",\n \"Words\"\n ]\n }\n },\n \"theme\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"History\",\n \"Adventure\",\n \"Mystery\",\n \"Science\",\n \"Nature\",\n \"Art\",\n \"Music\",\n \"Funny\",\n \"Family\",\n \"Life Skills\",\n \"Scary\",\n \"School Stories\",\n \"Holidays\",\n \"Hobby\",\n \"Geography\",\n \"Rural\",\n \"Urban\"\n ]\n }\n },\n \"themes\": {\n \"type\": \"array\"\n },\n \"rating\": {\n \"type\": \"number\"\n },\n \"rating_a\": {\n \"type\": \"number\"\n },\n \"quality\": {\n \"type\": \"number\"\n },\n \"genieScore\": {\n \"type\": \"number\"\n },\n \"authoringScore\": {\n \"type\": \"number\"\n },\n \"popularity\": {\n \"type\": \"number\"\n },\n \"downloads\": {\n \"type\": \"number\"\n },\n \"launchUrl\": {\n \"type\": \"string\"\n },\n \"activity_class\": {\n \"type\": \"string\"\n },\n \"draftImage\": {\n \"type\": \"string\"\n },\n \"scaffolding\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Tutorial\",\n \"Help\",\n \"Practice\"\n ]\n }\n },\n \"feedback\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Right/Wrong\",\n \"Reflection\",\n \"Guidance\",\n \"Learn from Mistakes\",\n \"Adaptive Feedback\",\n \"Interrupts\",\n \"Rich Feedback\"\n ]\n }\n },\n \"feedbackType\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Audio\",\n \"Visual\",\n \"Textual\",\n \"Tactile\"\n ]\n }\n },\n \"teachingMode\": {\n \"type\": \"string\",\n \"enum\": [\n \"Abstract\",\n \"Concrete\",\n \"Pictorial\"\n ]\n },\n \"skills\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Listening\",\n \"Speaking\",\n \"Reading\",\n \"Writing\",\n \"Touch\",\n \"Gestures\",\n \"Draw\"\n ]\n }\n },\n \"keywords\": {\n \"type\": \"array\"\n },\n \"domain\": {\n \"type\": \"array\"\n },\n \"dialcodes\": {\n \"type\": \"array\"\n },\n \"optStatus\": {\n \"type\": \"string\",\n \"enum\": [\n \"Pending\",\n \"Processing\",\n \"Error\",\n \"Complete\"\n ]\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"instructions\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"type\": \"string\"\n },\n \"oldBody\": {\n \"type\": \"string\"\n },\n \"stageIcons\": {\n \"type\": \"string\"\n },\n \"editorState\": {\n \"type\": \"string\"\n },\n \"data\": {\n \"type\": \"array\"\n },\n \"loadingMessage\": {\n \"type\": \"string\"\n },\n \"checksum\": {\n \"type\": \"string\"\n },\n \"learningObjective\": {\n \"type\": \"array\"\n },\n \"createdBy\": {\n \"type\": \"string\"\n },\n \"creator\": {\n \"type\": \"string\"\n },\n \"reviewer\": {\n \"type\": \"string\"\n },\n \"lastUpdatedBy\": {\n \"type\": \"string\"\n },\n \"lastSubmittedBy\": {\n \"type\": \"string\"\n },\n \"lastSubmittedOn\": {\n \"type\": \"string\"\n },\n \"lastPublishedBy\": {\n \"type\": \"string\"\n },\n \"lastPublishedOn\": {\n \"type\": \"string\"\n },\n \"versionDate\": {\n \"type\": \"string\"\n },\n \"origin\": {\n \"type\": \"string\"\n },\n \"originData\": {\n \"type\": \"object\"\n },\n \"versionCreatedBy\": {\n \"type\": \"string\"\n },\n \"me_totalSessionsCount\": {\n \"type\": \"number\"\n },\n \"me_creationSessions\": {\n \"type\": \"number\"\n },\n \"me_creationTimespent\": {\n \"type\": \"number\"\n },\n \"me_totalTimespent\": {\n \"type\": \"number\"\n },\n \"me_totalInteractions\": {\n \"type\": \"number\"\n },\n \"me_averageInteractionsPerMin\": {\n \"type\": \"number\"\n },\n \"me_averageSessionsPerDevice\": {\n \"type\": \"number\"\n },\n \"me_totalDevices\": {\n \"type\": \"number\"\n },\n \"me_averageTimespentPerSession\": {\n \"type\": \"number\"\n },\n \"me_averageRating\": {\n \"type\": \"number\"\n },\n \"me_totalDownloads\": {\n \"type\": \"number\"\n },\n \"me_totalSideloads\": {\n \"type\": \"number\"\n },\n \"me_totalRatings\": {\n \"type\": \"number\"\n },\n \"me_totalComments\": {\n \"type\": \"number\"\n },\n \"me_totalUsage\": {\n \"type\": \"number\"\n },\n \"me_totalLiveContentUsage\": {\n \"type\": \"number\"\n },\n \"me_usageLastWeek\": {\n \"type\": \"number\"\n },\n \"me_deletionsLastWeek\": {\n \"type\": \"number\"\n },\n \"me_lastUsedOn\": {\n \"type\": \"string\"\n },\n \"me_lastRemovedOn\": {\n \"type\": \"string\"\n },\n \"me_hierarchyLevel\": {\n \"type\": \"number\"\n },\n \"me_totalDialcodeAttached\": {\n \"type\": \"number\"\n },\n \"me_totalDialcodeLinkedToContent\": {\n \"type\": \"number\"\n },\n \"me_totalDialcode\": {\n \"type\": \"array\"\n },\n \"flagReasons\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Inappropriate Content\",\n \"Copyright Violation\",\n \"Privacy Violation\",\n \"Other\"\n ]\n }\n },\n \"flaggedBy\": {\n \"type\": \"array\"\n },\n \"flags\": {\n \"type\": \"array\"\n },\n \"lastFlaggedOn\": {\n \"type\": \"string\"\n },\n \"tempData\": {\n \"type\": \"string\"\n },\n \"copyType\": {\n \"type\": \"string\"\n },\n \"pragma\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"external\",\n \"ads\"\n ]\n }\n },\n \"publishChecklist\": {\n \"type\": \"array\"\n },\n \"publishComment\": {\n \"type\": \"string\"\n },\n \"rejectReasons\": {\n \"type\": \"array\"\n },\n \"rejectComment\": {\n \"type\": \"string\"\n },\n \"totalQuestions\": {\n \"type\": \"number\"\n },\n \"totalScore\": {\n \"type\": \"number\"\n },\n \"ownershipType\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"createdBy\",\n \"createdFor\"\n ]\n },\n \"default\": [\"createdBy\"]\n },\n \"reservedDialcodes\": {\n \"type\": \"object\"\n },\n \"dialcodeRequired\": {\n \"type\": \"string\",\n \"enum\": [\n \"Yes\",\n \"No\"\n ],\n \"default\": \"No\"\n },\n \"lockKey\": {\n \"type\": \"string\"\n },\n \"badgeAssociations\": {\n \"type\": \"object\"\n },\n \"framework\": {\n \"type\": \"string\",\n \"default\": \"NCF\"\n },\n \"lastStatusChangedOn\": {\n \"type\": \"string\"\n },\n \"uploadError\": {\n \"type\": \"string\"\n },\n \"appId\": {\n \"type\": \"string\"\n },\n \"s3Key\": {\n \"type\": \"string\"\n },\n \"consumerId\": {\n \"type\": \"string\"\n },\n \"organisation\": {\n \"type\": \"array\"\n },\n \"nodeType\": {\n \"type\": \"string\"\n },\n \"prevState\": {\n \"type\": \"string\"\n },\n \"publishError\": {\n \"type\": \"string\"\n },\n \"publish_type\": {\n \"type\": \"string\"\n },\n \"ownedBy\": {\n \"type\": \"string\"\n },\n \"purpose\": {\n \"type\": \"string\"\n },\n \"toc_url\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"reviewError\": {\n \"type\": \"string\"\n },\n \"mimeTypesCount\": {\n \"type\": \"string\"\n },\n \"contentTypesCount\": {\n \"type\": \"string\"\n },\n \"childNodes\": {\n \"type\": \"array\"\n },\n \"leafNodesCount\": {\n \"type\": \"number\"\n },\n \"depth\": {\n \"type\": \"number\"\n },\n \"SYS_INTERNAL_LAST_UPDATED_ON\": {\n \"type\": \"string\"\n },\n \"assets\": {\n \"type\": \"array\"\n },\n \"version\": {\n \"type\": \"number\",\n \"default\": 2\n },\n \"qrCodeProcessId\": {\n \"type\": \"string\"\n },\n \"migratedUrl\": {\n \"type\": \"string\",\n \"format\": \"url\"\n },\n \"totalCompressedSize\": {\n \"type\": \"number\"\n },\n \"programId\": {\n \"type\": \"string\"\n },\n \"leafNodes\": {\n \"type\": \"array\"\n },\n \"editorVersion\": {\n \"type\": \"number\"\n },\n \"unitIdentifiers\": {\n \"type\": \"array\"\n },\n \"questionCategories\": {\n \"type\": \"array\"\n },\n \"certTemplate\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"subject\" : {\n \"type\": \"array\"\n },\n \"medium\" : {\n \"type\": \"array\"\n },\n \"gradeLevel\" : {\n \"type\": \"array\"\n },\n \"topic\" : {\n \"type\": \"array\"\n },\n \"subDomains\" : {\n \"type\": \"array\"\n },\n \"subjectCodes\" : {\n \"type\": \"array\"\n },\n \"difficultyLevel\" : {\n \"type\": \"string\"\n },\n \"board\" : {\n \"type\": \"string\"\n },\n \"licenseterms\" : {\n \"type\": \"string\"\n },\n \"copyrightYear\" : {\n \"type\": \"number\"\n },\n \"organisationId\" : {\n \"type\": \"string\"\n },\n \"programId\": {\n \"type\": \"string\"\n },\n \"itemSetPreviewUrl\": {\n \"type\": \"string\"\n },\n \"textbook_name\" : {\n \"type\": \"array\"\n },\n \"level1Name\" : {\n \"type\": \"array\"\n },\n \"level1Concept\" : {\n \"type\": \"array\"\n },\n \"level2Name\" : {\n \"type\": \"array\"\n },\n \"level2Concept\" : {\n \"type\": \"array\"\n },\n \"level3Name\" : {\n \"type\": \"array\"\n },\n \"level3Concept\" : {\n \"type\": \"array\"\n },\n \"sourceURL\" : {\n \"type\": \"string\"\n },\n \"me_totalTimeSpentInSec\": {\n \"type\": \"object\"\n },\n \"me_totalPlaySessionCount\": {\n \"type\": \"object\"\n },\n \"me_totalRatingsCount\": {\n \"type\": \"number\"\n },\n \"monitorable\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"progress-report\",\n \"score-report\"\n ]\n }\n },\n \"userConsent\": {\n \"type\": \"string\",\n \"enum\": [\n \"Yes\",\n \"No\"\n ]\n },\n \"trackable\": {\n \"type\": \"object\",\n \"properties\": {\n \"enabled\": {\n \"type\": \"string\",\n \"enum\": [\"Yes\",\"No\"],\n \"default\": \"No\"\n },\n \"autoBatch\": {\n \"type\": \"string\",\n \"enum\": [\"Yes\",\"No\"],\n \"default\": \"No\"\n }\n },\n \"default\": {\n \"enabled\": \"No\",\n \"autoBatch\": \"No\"\n },\n \"additionalProperties\": false\n },\n \"credentials\": {\n \"type\": \"object\",\n \"properties\": {\n \"enabled\": {\n \"type\": \"string\",\n \"enum\": [\"Yes\",\"No\"],\n \"default\": \"No\"\n }\n },\n \"default\": {\n \"enabled\": \"No\"\n },\n \"additionalProperties\": false\n },\n \"processId\": {\n \"type\": \"string\"\n },\n \"primaryCategory\": {\n \"type\": \"string\",\n \"enum\": [\n \"Explanation Content\",\n \"Learning Resource\",\n \"Course\",\n \"Practice Question Set\",\n \"eTextbook\",\n \"Teacher Resource\",\n \"Course Assessment\",\n \"Digital Textbook\",\n \"Content Playlist\",\n \"Template\",\n \"Asset\",\n \"Plugin\",\n \"Lesson Plan Unit\",\n \"Course Unit\",\n \"Textbook Unit\"\n ]\n },\n \"additionalCategories\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Textbook\",\n \"TV Lesson\",\n \"Previous Board Exam Papers\",\n \"Pedagogy Flow\",\n \"Marking Scheme Rubric\",\n \"Lesson Plan\",\n \"Learning Outcome Definition\",\n \"Focus Spot\",\n \"Explanation Video\",\n \"Experiential Resource\",\n \"Curiosity Question Set\",\n \"Concept Map\",\n \"Classroom Teaching Video\"\n ]\n }\n },\n \"prevStatus\": {\n \"type\": \"string\"\n },\n \"cloudStorageKey\": {\n \"type\": \"string\"\n },\n \"batches\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"year\": {\n \"type\": \"string\"\n },\n \"plugins\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"showNotification\": {\n \"type\": \"boolean\"\n },\n \"collectionId\" : {\n \"type\": \"string\"\n },\n \"learningOutcome\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n}" + JsonUtils.deserialize(schema, classOf[util.Map[String, AnyRef]]) + } + + def getContentConfig(): util.Map[String, AnyRef] = { + val schema:String = "{\n \"restrictProps\": {\n \"create\" : [\n \"status\", \"dialcodes\"\n ],\n \"copy\" : [\n \"status\"\n ],\n \"update\": []\n },\n \"objectType\": \"Content\",\n \"external\": {\n \"tableName\": \"content_data\",\n \"properties\": {\n \"body\": {\n \"type\": \"blob\"\n },\n \"oldBody\": {\n \"type\": \"blob\"\n },\n \"stageIcons\": {\n \"type\": \"blob\"\n },\n \"screenshots\": {\n \"type\": \"blob\"\n },\n \"last_updated_on\": {\n \"type\": \"timestamp\"\n },\n \"externallink\": {\n \"type\": \"text\"\n }\n },\n \"primaryKey\": [\"content_id\"]\n },\n \"relations\": {\n \"concepts\": {\n \"type\": \"associatedTo\",\n \"direction\": \"out\",\n \"objects\": [\"Concept\"]\n },\n \"questions\": {\n \"type\": \"associatedTo\",\n \"direction\": \"out\",\n \"objects\": [\"AssessmentItem\"]\n },\n \"children\": {\n \"type\": \"hasSequenceMember\",\n \"direction\": \"out\",\n \"objects\": [\"Content\", \"ContentImage\"]\n },\n \"collections\": {\n \"type\": \"hasSequenceMember\",\n \"direction\": \"in\",\n \"objects\": [\"Content\", \"ContentImage\"]\n },\n \"usedByContent\": {\n \"type\": \"associatedTo\",\n \"direction\": \"in\",\n \"objects\": [\"Content\"]\n },\n \"usesContent\": {\n \"type\": \"associatedTo\",\n \"direction\": \"out\",\n \"objects\": [\"Content\"]\n },\n \"itemSets\": {\n \"type\": \"associatedTo\",\n \"direction\": \"out\",\n \"objects\": [\"ItemSet\"]\n }\n },\n \"version\": \"enable\",\n \"versionCheckMode\": \"ON\",\n \"frameworkCategories\": [\"board\",\"medium\",\"subject\",\"gradeLevel\",\"difficultyLevel\",\"topic\", \"subDomains\", \"subjectCodes\"],\n \"edge\": {\n \"properties\": {\n \"license\": \"License\"\n }\n },\n \"copy\": {\n \"scheme\": {\n \"TextBookToCourse\": {\n \"TextBook\": \"Course\",\n \"TextBookUnit\": \"CourseUnit\"\n },\n \"TextBookToLessonPlan\": {\n }\n }\n },\n \"cacheEnabled\": true,\n \"searchProps\": {\n \"status\": [\"Live\"],\n \"softConstraints\": {\n \"medium\": 15,\n \"subject\": 15,\n \"ageGroup\": 1,\n \"gradeLevel\": 7,\n \"board\": 4,\n \"relatedBoards\": 4\n }\n },\n \"schema_restrict_api\": true\n}" + JsonUtils.deserialize(schema, classOf[util.Map[String, AnyRef]]) + } + + def getFrameworkNode(): Node = { + val node = new Node() + node.setIdentifier("NCF") + node.setNodeType("DATA_NODE") + node.setObjectType("Framework") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap(Map("name"-> "NCF"))) + node + } + + def getBoardNode(): Node = { + val node = new Node() + node.setIdentifier("ncf_board_cbse") + node.setNodeType("DATA_NODE") + node.setObjectType("Term") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap(Map("name"-> "CBSE"))) + node + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestEventActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestEventActor.scala new file mode 100644 index 000000000..9951be514 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestEventActor.scala @@ -0,0 +1,212 @@ +package org.sunbird.content.actors + +import akka.actor.Props +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.{GraphService, OntologyEngineContext} + +import java.util +import scala.collection.JavaConversions._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +class TestEventActor extends BaseSpec with MockFactory { + + it should "discard node in draft state should return success" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getValidNodeToDiscard())).anyNumberOfTimes() + (graphDB.deleteNode(_: String, _: String, _: Request)).expects(*, *, *).returns(Future(true)) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_12346"))) + request.setOperation("discardContent") + val response = callActor(request, Props(new EventActor())) + assert(response.getResponseCode == ResponseCode.OK) + assert(response.get("identifier") == "do_12346") + assert(response.get("message") == "Draft version of the content with id : do_12346 is discarded") + + } + + it should "publish node in draft state should return success" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getDraftNode())).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(getDraftNode())) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier", "do_1234") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + request.setOperation("publishContent") + val response = callActor(request, Props(new EventActor())) + assert(response.getResponseCode == ResponseCode.OK) + assert(response.get("identifier") == "do_1234") + + } + + it should "discard node in Live state should return client error" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getInValidNodeToDiscard())).anyNumberOfTimes() + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_12346"))) + request.setOperation("discardContent") + val response = callActor(request, Props(new EventActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + } + + it should "return client error response for retireContent" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do_1234.img") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234.img"))) + request.setOperation("retireContent") + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getInValidNodeToDiscard())) + val response = callActor(request, Props(new EventActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + } + + it should "return success response for retireContent" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getNode("Content", None) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.updateNodes(_: String, _: util.List[String], _: util.HashMap[String, AnyRef])).expects(*, *, *).returns(Future(new util.HashMap[String, Node])) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do1234") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + request.setOperation("retireContent") + val response = callActor(request, Props(new EventActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'updateContent'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + implicit val ss = mock[StorageService] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getDraftNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)) + val request = getContentRequest() + request.getContext.put("identifier","do_1234") + request.putAll(mapAsJavaMap(Map("name" -> "New Content", "code" -> "1234", + "startDate" -> "2021-03-04", "endDate" -> "2021-03-04", "startTime" -> "11:00:00Z", "endTime" -> "11:00:00Z", + "registrationEndDate" -> "2021-03-04", "eventType" -> "Online", "versionKey" -> "test_123"))) + request.setOperation("updateContent") + val response = callActor(request, Props(new EventActor())) + assert("successful".equals(response.getParams.getStatus)) + assert("do_1234".equals(response.get("identifier"))) + assert("test_123".equals(response.get("versionKey"))) + } + + private def getContentRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Event") + put("schemaName", "event") + put("X-Channel-Id", "in.ekstep") + } + }) + request.setObjectType("Event") + request + } + + private def getValidNodeToDiscard(): Node = { + val node = new Node() + node.setIdentifier("do_12346") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12346") + put("mimeType", "application/pdf") + put("status", "Draft") + put("contentType", "Resource") + put("name", "Node To discard") + put("primaryCategory", "Learning Resource") + } + }) + node + } + + private def getInValidNodeToDiscard(): Node = { + val node = new Node() + node.setIdentifier("do_12346") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12346") + put("mimeType", "application/pdf") + put("status", "Live") + put("contentType", "Resource") + put("name", "Node To discard") + put("primaryCategory", "Learning Resource") + } + }) + node + } + + private def getNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Event") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("status", "Live") + put("name", "Resource_1") + put("versionKey", "test_321") + put("channel", "in.ekstep") + put("code", "Resource_1") + put("startDate", "2021-02-02") + put("endDate", "2021-02-02") + put("startTime", "11:00:00Z") + put("endTime", "12:00:00Z") + put("registrationEndDate", "2021-01-02") + put("eventType", "Online") + } + }) + node + } + private def getDraftNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Event") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("status", "Draft") + put("name", "Resource_1") + put("versionKey", "test_321") + put("channel", "in.ekstep") + put("code", "Resource_1") + put("startDate", "2021-02-02") + put("endDate", "2021-02-02") + put("startTime", "11:00:00Z") + put("endTime", "12:00:00Z") + put("registrationEndDate", "2021-01-02") + put("eventType", "Online") + } + }) + node + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestEventSetActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestEventSetActor.scala new file mode 100644 index 000000000..961c4ae1a --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestEventSetActor.scala @@ -0,0 +1,392 @@ +package org.sunbird.content.actors + +import akka.actor.Props +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.JsonUtils +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.common.enums.GraphDACParams +import org.sunbird.graph.dac.model.{Node, Relation, SearchCriteria} +import org.sunbird.graph.{GraphService, OntologyEngineContext} + +import java.util +import scala.collection.JavaConversions.mapAsJavaMap +import scala.collection.JavaConverters.seqAsJavaListConverter +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +class TestEventSetActor extends BaseSpec with MockFactory { + + "EventSetActor" should "return failed response for 'unknown' operation" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new EventSetActor()), getContentRequest()) + } + + it should "validate input before creating event set" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getContentRequest() + val eventSet = mapAsJavaMap(Map( + "name" -> "New Content", "code" -> "1234", + "startDate"-> "2021/01/03", //wrong format + "endDate"-> "2021-01-03", + "schedule" -> + mapAsJavaMap(Map("type" -> "NON_RECURRING", + "value" -> List(mapAsJavaMap(Map("startDate" -> "2021-01-03", "endDate" -> "2021-01-03", "startTime" -> "11:00:00Z", "endTime" -> "13:00:00Z"))).asJava)), + "onlineProvider" -> "Zoom", + "registrationEndDate" -> "2021-02-25", + "eventType" -> "Online")) + request.putAll(eventSet) + assert(true) + val response = callActor(request, Props(new EventSetActor())) + println("Response: " + JsonUtils.serialize(response)) + } + + it should "create an eventset and store it in neo4j" in { + val eventNode = getEventNode() + val eventSetNode = getEventSetNode() + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.addNode _).expects(where { (g: String, n:Node) => { + n.getObjectType.equals("Event") + }}).returns(Future(eventNode)).once() + val loopResult: util.Map[String, Object] = new util.HashMap[String, Object]() + loopResult.put(GraphDACParams.loop.name, new java.lang.Boolean(false)) + (graphDB.checkCyclicLoop _).expects(*, *, *, *).returns(loopResult).anyNumberOfTimes() + (graphDB.addNode _).expects(where { (g: String, n:Node) => n.getObjectType.equals("EventSet")}).returns(Future(eventSetNode)).once() + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(new util.ArrayList[Node]() { + { + add(eventNode) + } + })) + (graphDB.createRelation _).expects(*, *).returns(Future(new Response())) + val request = getContentRequest() + val eventSet = mapAsJavaMap(Map( + "name" -> "New Content", "code" -> "1234", + "startDate"-> "2021-01-03", //wrong format + "endDate"-> "2021-01-03", + "schedule" -> + mapAsJavaMap(Map("type" -> "NON_RECURRING", + "value" -> List(mapAsJavaMap(Map("startDate" -> "2021-01-03", "endDate" -> "2021-01-03", "startTime" -> "11:00:00Z", "endTime" -> "13:00:00Z"))).asJava)), + "onlineProvider" -> "Zoom", + "registrationEndDate" -> "2021-02-25", + "eventType" -> "Online")) + request.putAll(eventSet) + request.setOperation("createContent") + val response = callActor(request, Props(new EventSetActor())) + assert(response.get("identifier") != null) + assert(response.get("versionKey") != null) + } + + it should "update an eventset and store it in neo4j" in { + val eventNode = getEventNode() + val eventSetNode = getEventSetCollectionNode() + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.deleteNode(_: String, _: String, _: Request)).expects(*, *, *).returns(Future(true)) + (graphDB.removeRelation(_: String, _: util.List[util.Map[String, AnyRef]])).expects(*, *).returns(Future(new Response)) + (graphDB.addNode _).expects(where { (g: String, n:Node) => { + n.getObjectType.equals("Event") + }}).returns(Future(eventNode)) + val loopResult: util.Map[String, Object] = new util.HashMap[String, Object]() + loopResult.put(GraphDACParams.loop.name, new java.lang.Boolean(false)) + (graphDB.checkCyclicLoop _).expects(*, *, *, *).returns(loopResult).anyNumberOfTimes() + (graphDB.upsertNode _).expects(*, *, *).returns(Future(eventSetNode)) + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(new util.ArrayList[Node]() { + { + add(eventNode) + } + })) + (graphDB.getNodeByUniqueId _).expects(*, *, *, *).returns(Future(eventSetNode)).anyNumberOfTimes() + (graphDB.createRelation _).expects(*, *).returns(Future(new Response())) + val request = getContentRequest() + val eventSet = mapAsJavaMap(Map( + "name" -> "New Content", "code" -> "1234", + "startDate"-> "2021-01-03", //wrong format + "endDate"-> "2021-01-03", + "schedule" -> + mapAsJavaMap(Map("type" -> "NON_RECURRING", + "value" -> List(mapAsJavaMap(Map("startDate" -> "2021-01-03", "endDate" -> "2021-01-03", "startTime" -> "11:00:00Z", "endTime" -> "13:00:00Z"))).asJava)), + "onlineProvider" -> "Zoom", + "registrationEndDate" -> "2021-02-25", + "eventType" -> "Online", + "versionKey" -> "test_123")) + request.putAll(eventSet) + request.setOperation("updateContent") + val response = callActor(request, Props(new EventSetActor())) + assert(response.get("identifier") != null) + assert(response.get("versionKey") != null) + } + + + it should "discard node in draft state should return success" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getValidDraftNode())).twice() + (graphDB.deleteNode(_: String, _: String, _: Request)).expects(*, *, *).returns(Future(true)) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_12346"))) + request.setOperation("discardContent") + val response = callActor(request, Props(new EventSetActor())) + assert(response.getResponseCode == ResponseCode.OK) + assert(response.get("identifier") == "do_12346") + assert(response.get("message") == "Draft version of the content with id : do_12346 is discarded") + + } + + it should "publish node in draft state should return success" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val eventSetNode = getEventSetCollectionNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(eventSetNode)).anyNumberOfTimes() + (graphDB.upsertNode _).expects(*, *, *).returns(Future(eventSetNode)).anyNumberOfTimes() + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_12346"))) + request.setOperation("publishContent") + val response = callActor(request, Props(new EventSetActor())) + assert(response.getResponseCode == ResponseCode.OK) + assert(response.get("identifier") == "do_12345") + } + + it should "discard node in Live state should return client error" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getLiveEventSetCollectionNode())).anyNumberOfTimes() + (graphDB.updateNodes(_: String, _: util.List[String], _: util.HashMap[String, AnyRef])).expects(*, *, *).returns(Future(new util.HashMap[String, Node])).anyNumberOfTimes() + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_12346"))) + request.setOperation("discardContent") + val response = callActor(request, Props(new EventSetActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + } + + it should "return success response for retireContent" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val node = getEventSetCollectionNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.updateNodes(_: String, _: util.List[String], _: util.HashMap[String, AnyRef])).expects(*, *, *).returns(Future(new util.HashMap[String, Node])).anyNumberOfTimes() + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do1234") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + request.setOperation("retireContent") + val response = callActor(request, Props(new EventSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'readContent'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + val node = getNode("EventSet", None) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do1234") + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "fields" -> ""))) + request.setOperation("readContent") + val response = callActor(request, Props(new EventSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + it should "return success response for 'getHierarchy'" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + val node = getNode("EventSet", None) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)) + implicit val ss = mock[StorageService] + val request = getContentRequest() + request.getContext.put("identifier","do1234") + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234", "fields" -> ""))) + request.setOperation("getHierarchy") + val response = callActor(request, Props(new EventSetActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + private def getContentRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "EventSet") + put("schemaName", "eventset") + put("X-Channel-Id", "in.ekstep") + } + }) + request.setObjectType("EventSet") + request + } + + private def getValidDraftNode(): Node = { + val node = new Node() + node.setIdentifier("do_12346") + node.setNodeType("DATA_NODE") + node.setObjectType("EventSet") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12346") + put("status", "Draft") + put("contentType", "EventSet") + put("name", "Node To discard") + } + }) + node + } + + private def getInValidNodeToDiscard(): Node = { + val node = new Node() + node.setIdentifier("do_12346") + node.setNodeType("DATA_NODE") + node.setObjectType("EventSet") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12346") + put("status", "Live") + put("contentType", "EventSet") + put("name", "Node To discard") + } + }) + node + } + + private def getEventNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Event") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("status", "Live") + put("name", "Event_1") + put("code", "event1") + put("versionKey", "1878141") + put("startDate", "2021-02-02") + put("endDate", "2021-02-02") + put("startTime", "11:00:00Z") + put("endTime", "12:00:00Z") + put("registrationEndDate", "2021-01-02") + put("eventType", "Online") + } + }) + node + } + + private def getEventSetNode(): Node = { + val node = new Node() + node.setIdentifier("do_12345") + node.setNodeType("DATA_NODE") + node.setObjectType("EventSet") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12345") + put("status", "Draft") + put("name", "EventSet_1") + put("code", "eventset1") + put("versionKey", "1878141") + put("startDate", "2021-02-02") + put("endDate", "2021-02-02") + put("registrationEndDate", "2021-01-02") + put("eventType", "Online") + put("schedule", + mapAsJavaMap(Map("type" -> "NON_RECURRING", + "value" -> List(mapAsJavaMap(Map("startDate" -> "2021-01-03", + "endDate" -> "2021-01-03", + "startTime" -> "11:00:00Z", + "endTime" -> "13:00:00Z"))).asJava))) + + } + }) + node + } + + private def getEventSetCollectionNode(): Node = { + val node = new Node() + node.setIdentifier("do_12345") + node.setNodeType("DATA_NODE") + node.setObjectType("EventSet") + val rel: Relation = new Relation() + rel.setEndNodeObjectType("Event") + rel.setEndNodeId("do_12345.1") + rel.setStartNodeId("do_12345") + rel.setRelationType("hasSequenceMember") + node.setOutRelations(new util.ArrayList[Relation](){ + add(rel) + }) + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12345") + put("status", "Draft") + put("name", "EventSet_1") + put("code", "eventset1") + put("versionKey", "1878141") + put("startDate", "2021-02-02") + put("endDate", "2021-02-02") + put("registrationEndDate", "2021-01-02") + put("eventType", "Online") + put("schedule", + mapAsJavaMap(Map("type" -> "NON_RECURRING", + "value" -> List(mapAsJavaMap(Map("startDate" -> "2021-01-03", + "endDate" -> "2021-01-03", + "startTime" -> "11:00:00Z", + "endTime" -> "13:00:00Z", + "status" -> "Draft"))).asJava))) + + } + }) + node + } + + private def getLiveEventSetCollectionNode(): Node = { + val node = new Node() + node.setIdentifier("do_12345") + node.setNodeType("DATA_NODE") + node.setObjectType("EventSet") + val rel: Relation = new Relation() + rel.setEndNodeObjectType("Event") + rel.setEndNodeId("do_12345.1") + node.setOutRelations(new util.ArrayList[Relation](){ + add(rel) + }) + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12345") + put("status", "Live") + put("name", "EventSet_1") + put("code", "eventset1") + put("versionKey", "1878141") + put("startDate", "2021-02-02") + put("endDate", "2021-02-02") + put("registrationEndDate", "2021-01-02") + put("eventType", "Online") + put("schedule", + mapAsJavaMap(Map("type" -> "NON_RECURRING", + "value" -> List(mapAsJavaMap(Map("startDate" -> "2021-01-03", + "endDate" -> "2021-01-03", + "startTime" -> "11:00:00Z", + "endTime" -> "13:00:00Z", + "status" -> "Live"))).asJava))) + + } + }) + node + } + + +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestLicenseActor.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestLicenseActor.scala new file mode 100644 index 000000000..a976c6664 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/actors/TestLicenseActor.scala @@ -0,0 +1,137 @@ +package org.sunbird.content.actors + +import java.util + +import akka.actor.Props +import org.apache.hadoop.util.StringUtils +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.graph.dac.model.Node + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class TestLicenseActor extends BaseSpec with MockFactory { + + "LicenseActor" should "return failed response for 'unknown' operation" in { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + testUnknownOperation(Props(new LicenseActor()), getLicenseRequest()) + } + + it should "create a licenseNode and store it in neo4j" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(getValidNode())) + val request = getLicenseRequest() + request.put("name", "do_1234") + request.setOperation("createLicense") + val response = callActor(request, Props(new LicenseActor())) + assert(response.get("identifier") != null) + assert(response.get("identifier").equals("do_1234")) + } + + it should "return exception for create license without name" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getLicenseRequest() + request.setOperation("createLicense") + val response = callActor(request, Props(new LicenseActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + assert(StringUtils.equalsIgnoreCase(response.get("messages").asInstanceOf[util.ArrayList[String]].get(0).asInstanceOf[String], "Required Metadata name not set")) + } + + it should "return exception for licenseNode with identifier" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val request = getLicenseRequest() + request.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + request.setOperation("createLicense") + val response = callActor(request, Props(new LicenseActor())) + assert(response.getResponseCode == ResponseCode.CLIENT_ERROR) + assert(StringUtils.equalsIgnoreCase(response.getParams.getErrmsg, "name will be set as identifier")) + } + + it should "return success response for updateLicense" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(getValidNode())) + implicit val ss = mock[StorageService] + val request = getLicenseRequest() + request.putAll(mapAsJavaMap(Map("description" -> "test desc"))) + request.setOperation("updateLicense") + val response = callActor(request, Props(new LicenseActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.get("identifier").equals("do_1234")) + } + + it should "return success response for readLicense" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(1) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + implicit val ss = mock[StorageService] + val request = getLicenseRequest() + request.putAll(mapAsJavaMap(Map("fields" -> ""))) + request.setOperation("readLicense") + val response = callActor(request, Props(new LicenseActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(StringUtils.equalsIgnoreCase(response.get("license").asInstanceOf[util.Map[String, AnyRef]].get("identifier").asInstanceOf[String], "do_1234")) + assert(StringUtils.equalsIgnoreCase(response.get("license").asInstanceOf[util.Map[String, AnyRef]].get("status").asInstanceOf[String], "Live")) + } + + it should "return success response for retireLicense" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).repeated(2) + val node = getValidNode() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(getValidNode())) + implicit val ss = mock[StorageService] + val request = getLicenseRequest() + request.setOperation("retireLicense") + val response = callActor(request, Props(new LicenseActor())) + assert("successful".equals(response.getParams.getStatus)) + assert(response.get("identifier").equals("do_1234")) + } + + private def getLicenseRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "License") + put("schemaName", "license") + + } + }) + request.setObjectType("License") + request + } + + private def getValidNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("License") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("objectType", "License") + put("status", "Live") + put("name", "do_1234") + put("versionKey", "1878141") + } + }) + node + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/dial/DIALManagerTest.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/dial/DIALManagerTest.scala new file mode 100644 index 000000000..fd096d0ad --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/dial/DIALManagerTest.scala @@ -0,0 +1,364 @@ +package org.sunbird.content.dial + +import java.util + +import org.scalamock.matchers.Matchers +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.AsyncFlatSpec +import org.sunbird.common.{HttpUtil, JsonUtils} +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.common.exception.{ClientException, ResourceNotFoundException, ResponseCode, ServerException} +import org.sunbird.graph.dac.model.{Node, SearchCriteria} +import org.sunbird.graph.{GraphService, OntologyEngineContext} + +import scala.concurrent.Future + +class DIALManagerTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + val httpUtil = mock[HttpUtil] + + "getRequestData with list input" should "return request data as list with scala types" in { + val reqMap : java.util.Map[String, AnyRef] = new util.HashMap[String, AnyRef](){{ + put("content", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier","do_1111") + put("dialcode", new util.ArrayList[String](){{ + add("ABC111") + add("ABC222") + }}) + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier",new util.ArrayList[String](){{ + add("do_2222") + add("do_3333") + }}) + put("dialcode", "ABC333") + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier",new util.ArrayList[String](){{ + add("do_88888") + add("do_99999") + }}) + put("dialcode", new util.ArrayList[String]()) + }}) + }}) + }} + val request = new Request() + request.putAll(reqMap) + val result: List[Map[String, List[String]]] = DIALManager.getRequestData(request) + assert(null!=result && result.nonEmpty) + assert(result.isInstanceOf[List[AnyRef]]) + assert(result.size==3) + assert(result(1).nonEmpty) + assert(result(1).get("identifier").get.isInstanceOf[List[String]]) + assert(result(1).get("dialcode").get.isInstanceOf[List[String]]) + } + + "getRequestData with map input" should "return request data as list with scala types" in { + val reqMap : java.util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() {{ + put("content", new util.HashMap[String, AnyRef](){{ + put("identifier", "do_123") + put("dialcode", new util.ArrayList[String](){{ + add("ABC123") + add("BCD123") + }}) + }}) + }} + val request = new Request() + request.putAll(reqMap) + val result: List[Map[String, List[String]]] = DIALManager.getRequestData(request) + assert(null!=result && result.nonEmpty) + assert(result.isInstanceOf[List[AnyRef]]) + assert(result.size==1) + assert(result(0).nonEmpty) + assert(result(0).get("identifier").get.isInstanceOf[List[String]]) + assert(result(0).get("dialcode").get.isInstanceOf[List[String]]) + } + + "getRequestData with invalid input" should "throw client exception" in { + val exception = intercept[ClientException] { + DIALManager.getRequestData(new Request()) + } + assert(exception.getMessage == "Invalid Request! Please Provide Valid Request.") + } + + "getList with java list input" should "return scala list" in { + val input = new util.ArrayList[String](){{ + add("ABC123") + add("") + add(" ") + add("BCD123") + }} + val result:List[String] = DIALManager.getList(input) + assert(result.nonEmpty) + assert(result.size==2) + } + + "getList with String input" should "return scala List" in { + val input = "do_123" + val result:List[String] = DIALManager.getList(input) + assert(result.nonEmpty) + assert(result.size==1) + } + + "getList with empty java list" should "return empty scala List" in { + val input = new util.ArrayList[String]() + val result:List[String] = DIALManager.getList(input) + assert(result.isEmpty) + assert(result.size==0) + } + + "validateAndGetRequestMap with valid input" should "return the request map" in { + (oec.httpUtil _).expects().returns(httpUtil) + (httpUtil.post(_: String, _:java.util.Map[String, AnyRef], _:java.util.Map[String, String])).expects(*, *, *).returns(getDIALSearchResponse) + val input = getRequestData() + val result = DIALManager.validateAndGetRequestMap("test", input) + assert(result.nonEmpty) + assert(result.size==5) + assert(result.get("do_88888").get.contains("L4A6W8")) + assert(result.get("do_88888").get.contains("D2E1J9")) + assert(result.get("do_2222").get.size==1) + assert(result.get("do_2222").get.contains("R4X2P2")) + } + + "validateReqStructure with valid request" should "not throw any exception" in { + DIALManager.validateReqStructure(List("ABC123"), List("do_123")) + assert(true) + } + + "validateReqStructure with empty contents" should "throw client exception" in { + val exception = intercept[ClientException] { + DIALManager.validateReqStructure(List("ABC123"), List()) + } + assert(exception.getMessage == "Invalid Request! Please Provide Required Properties In Request.") + } + + "validateReqStructure with more than 10 contents" should "throw client exception" in { + val exception = intercept[ClientException] { + DIALManager.validateReqStructure(List("ABC123"), List("do_111","do_222","do_3333","do_444","do_555","do_1111","do_2222","do_3333","do_4444","do_5555")) + } + assert(exception.getMessage == "Max Limit For Link Content To DIAL Code In A Request Is 10") + } + + "validateDialCodes with valid channel and valid dialcodes" should "return true" in { + (oec.httpUtil _).expects().returns(httpUtil) + (httpUtil.post(_: String, _:java.util.Map[String, AnyRef], _:java.util.Map[String, String])).expects(*, *, *).returns(getDIALSearchResponse) + val result = DIALManager.validateDialCodes("test", List("L4A6W8","BCD123","ABC123","PQR123","JKL123")) + assert(result) + } + + "validateDialCodes with invalid channel and valid dialcodes" should "throw ResourceNotFoundException" in { + (oec.httpUtil _).expects().returns(httpUtil) + val resp = new Response + resp.put("count",0) + resp.put("dialcodes", util.Arrays.asList()) + (httpUtil.post(_: String, _:java.util.Map[String, AnyRef], _:java.util.Map[String, String])).expects(*, *, *).returns(resp) + val exception = intercept[ResourceNotFoundException] { + DIALManager.validateDialCodes("test", List("L4A6W8","BCD123","ABC123","PQR123","JKL123")) + } + assert(exception.getMessage == "DIAL Code Not Found With Id(s): [L4A6W8, BCD123, ABC123, PQR123, JKL123]") + } + + "validateDialCodes with invalid search response" should "throw ServerException" in { + (oec.httpUtil _).expects().returns(httpUtil) + val resp = new Response + resp.setResponseCode(ResponseCode.SERVER_ERROR) + (httpUtil.post(_: String, _:java.util.Map[String, AnyRef], _:java.util.Map[String, String])).expects(*, *, *).returns(resp) + val exception = intercept[ServerException] { + DIALManager.validateDialCodes("test", List("L4A6W8","BCD123","ABC123","PQR123","JKL123")) + } + assert(exception.getMessage == "Something Went Wrong While Processing Your Request. Please Try Again After Sometime!") + } + + "link DIAL with valid request for content" should "update the contents successfully" in { + (oec.httpUtil _).expects().returns(httpUtil) + (oec.graphService _).expects().returns(graphDB).repeated(4) + (httpUtil.post(_: String, _:java.util.Map[String, AnyRef], _:java.util.Map[String, String])).expects(*, *, *).returns(getDIALSearchResponse) + (graphDB.getNodeByUniqueIds(_: String, _: SearchCriteria)).expects(*, *).returns(Future(getNodes())) + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getNode("do_1111"))) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(getNode("do_1111"))) + val request = getContentDIALRequest() + val resFuture = DIALManager.link(request) + resFuture.map(result => { + assert(result.getResponseCode.toString=="OK") + }) + } + + "link DIAL with valid request for collections" should "update the collection content successfully" in { + (oec.httpUtil _).expects().returns(httpUtil) + (httpUtil.post(_: String, _:java.util.Map[String, AnyRef], _:java.util.Map[String, String])).expects(*, *, *).returns(getDIALSearchResponse) + val request = getCollectionDIALRequest() + val response = DIALManager.link(request) + response.map(result => { + assert(result.getResponseCode.toString=="OK") + }) + } + + def getDIALSearchResponse():Response = { + val resString = "{\n \"id\": \"sunbird.dialcode.search\",\n \"ver\": \"3.0\",\n \"ts\": \"2020-04-21T19:39:14ZZ\",\n \"params\": {\n \"resmsgid\": \"1dfcc25b-6c37-49f8-a6c3-7185063e8752\",\n \"msgid\": null,\n \"err\": null,\n \"status\": \"successful\",\n \"errmsg\": null\n },\n \"responseCode\": \"OK\",\n \"result\": {\n \"dialcodes\": [\n {\n \"dialcode_index\": 7609876,\n \"identifier\": \"N4Z7D5\",\n \"channel\": \"testr01\",\n \"batchcode\": \"testPub0001.20200421T193801\",\n \"publisher\": \"testPub0001\",\n \"generated_on\": \"2020-04-21T19:38:01.603+0000\",\n \"status\": \"Draft\",\n \"objectType\": \"DialCode\"\n },\n {\n \"dialcode_index\": 7610113,\n \"identifier\": \"E8B7Z6\",\n \"channel\": \"testr01\",\n \"batchcode\": \"testPub0001.20200421T193801\",\n \"publisher\": \"testPub0001\",\n \"generated_on\": \"2020-04-21T19:38:01.635+0000\",\n \"status\": \"Draft\",\n \"objectType\": \"DialCode\"\n },\n {\n \"dialcode_index\": 7610117,\n \"identifier\": \"R4X2P2\",\n \"channel\": \"testr01\",\n \"batchcode\": \"testPub0001.20200421T193801\",\n \"publisher\": \"testPub0001\",\n \"generated_on\": \"2020-04-21T19:38:01.637+0000\",\n \"status\": \"Draft\",\n \"objectType\": \"DialCode\"\n },\n {\n \"dialcode_index\": 7610961,\n \"identifier\": \"L4A6W8\",\n \"channel\": \"testr01\",\n \"batchcode\": \"testPub0001.20200421T193801\",\n \"publisher\": \"testPub0001\",\n \"generated_on\": \"2020-04-21T19:38:01.734+0000\",\n \"status\": \"Draft\",\n \"objectType\": \"DialCode\"\n },\n {\n \"dialcode_index\": 7611164,\n \"identifier\": \"D2E1J9\",\n \"channel\": \"testr01\",\n \"batchcode\": \"testPub0001.20200421T193801\",\n \"publisher\": \"testPub0001\",\n \"generated_on\": \"2020-04-21T19:38:01.759+0000\",\n \"status\": \"Draft\",\n \"objectType\": \"DialCode\"\n }\n ],\n \"count\": 5\n }\n}"; + JsonUtils.deserialize(resString, classOf[Response]) + } + + def getRequestData(): List[Map[String, List[String]]] = { + val reqMap : java.util.Map[String, AnyRef] = new util.HashMap[String, AnyRef](){{ + put("content", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier","do_1111") + put("dialcode", new util.ArrayList[String](){{ + add("N4Z7D5") + add("E8B7Z6") + }}) + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier",new util.ArrayList[String](){{ + add("do_2222") + add("do_3333") + }}) + put("dialcode", "R4X2P2") + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier",new util.ArrayList[String](){{ + add("do_88888") + add("do_99999") + }}) + put("dialcode", new util.ArrayList[String](){{ + add("L4A6W8") + add("D2E1J9") + }}) + }}) + }}) + }} + val request = new Request() + request.putAll(reqMap) + DIALManager.getRequestData(request) + } + + def getContentDIALRequest(): Request = { + val request = new Request() + request.setObjectType("Content") + request.setContext(getContext()) + request.getContext.put("linkType","content") + val reqMap : java.util.Map[String, AnyRef] = new util.HashMap[String, AnyRef](){{ + put("content", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier","do_1111") + put("dialcode", new util.ArrayList[String](){{ + add("N4Z7D5") + add("E8B7Z6") + add("R4X2P2") + add("L4A6W8") + add("D2E1J9") + }}) + }}) + }}) + }} + request.putAll(reqMap) + request + } + + def getCollectionDIALRequest(): Request = { + val request = new Request() + request.setObjectType("Content") + request.setContext(getContext()) + request.getContext.put("linkType","collection") + request.getContext.put("identifier","do_1111") + request.putAll(getRequest()) + request + } + + def getContext():util.Map[String, AnyRef] = { + val contextMap: java.util.Map[String, AnyRef] = new util.HashMap[String, AnyRef](){{ + put("graph_id", "domain") + put("version" , "1.0") + put("objectType" , "Content") + put("schemaName", "content") + put("channel", "test") + }} + contextMap + } + + def getRequest():util.Map[String, AnyRef] = { + val reqMap : java.util.Map[String, AnyRef] = new util.HashMap[String, AnyRef](){{ + put("content", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier","do_1111") + put("dialcode", new util.ArrayList[String](){{ + add("N4Z7D5") + add("E8B7Z6") + }}) + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier",new util.ArrayList[String](){{ + add("do_2222") + add("do_3333") + }}) + put("dialcode", "R4X2P2") + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier",new util.ArrayList[String](){{ + add("do_4444") + add("do_5555") + }}) + put("dialcode", new util.ArrayList[String](){{ + add("L4A6W8") + add("D2E1J9") + }}) + }}) + }}) + }} + reqMap + } + + def getNodes(): util.List[Node] = { + val result = new util.ArrayList[Node](){{ + add(getNode("do_1111")) + add(getNode("do_2222")) + add(getNode("do_3333")) + add(getNode("do_4444")) + add(getNode("do_5555")) + }} + result + } + + def getNode(identifier: String): Node = { + val node = new Node() + node.setIdentifier(identifier) + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", identifier) + put("name", "Test Content") + put("code", "test.resource") + put("contentType", "Resource") + put("mimeType", "application/pdf") + put("status", "Draft") + put("channel", "test") + put("versionKey", "1234") + put("primaryCategory", "Learning Resource") + } + }) + node + } + + private def getCategoryDefinitionNode(identifier: String): Node = { + val node = new Node() + node.setIdentifier(identifier) + node.setNodeType("DATA_NODE") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", identifier) + put("categoryId", "obj-cat:1234") + put("objectType", "ObjectCategoryDefinition") + put("name", "Test Category Definition") + put("targetObjectType", "Content") + put("objectMetadata", "{\"config\":{},\"schema\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}},\"additionalProperties\":false}}}") + } + }) + node + } + +} \ No newline at end of file diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/upload/mgr/UploadManagerTest.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/upload/mgr/UploadManagerTest.scala new file mode 100644 index 000000000..44ac88da3 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/upload/mgr/UploadManagerTest.scala @@ -0,0 +1,24 @@ +package org.sunbird.content.upload.mgr + +import java.util + +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.graph.dac.model.Node + +class UploadManagerTest extends AsyncFlatSpec with Matchers { + + "getUploadResponse with valid node object" should "return response with artifactUrl" in { + val node = new Node() + node.setIdentifier("do_1234") + node.setMetadata(new util.HashMap[String, AnyRef](){{ + put("artifactUrl", "testurl") + put("versionKey",123456.asInstanceOf[AnyRef]) + }}) + val response = UploadManager.getUploadResponse(node) + val result = response.getResult + assert(null != response) + assert("OK"==response.getResponseCode.toString) + assert(result.size()==5) + assert(result.get("artifactUrl").toString.equals("testurl")) + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/util/CopyManagerTest.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/util/CopyManagerTest.scala new file mode 100644 index 000000000..64e9440d1 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/util/CopyManagerTest.scala @@ -0,0 +1,313 @@ +package org.sunbird.content.util + +import java.util + +import org.apache.commons.collections.MapUtils +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.cloud.storage.util.JSONUtils +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.{Property, Request} +import org.sunbird.common.exception.{ClientException, ResponseCode} +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.utils.ScalaJsonUtils + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.Future + +class CopyManagerTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + + "CopyManager" should "return copied node identifier when content is copied" ignore { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getNode())) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getDefinitionNode_channel())) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getDefinitionNode_channel())) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(getCopiedNode())) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getCopiedNode())) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(getCopiedNode())) + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("1234")))) + CopyManager.copy(getCopyRequest()).map(resp => { + assert(resp != null) + assert(resp.getResponseCode == ResponseCode.OK) + assert(resp.getResult.get("node_id").asInstanceOf[util.HashMap[String, AnyRef]].get("do_1234").asInstanceOf[String] == "do_1234_copy") + }) + } + + it should "return copied node identifier and safe hierarchy in cassandra when collection is copied" in { + assert(true) + } + + "Required property not sent" should "return client error response for" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + implicit val ss = mock[StorageService] + val request = getInvalidCopyRequest_2() + request.getContext.put("identifier","do_1234") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + val exception = intercept[ClientException] { + CopyManager.validateRequest(request) + } + exception.getMessage shouldEqual "Please provide valid value for List(createdBy)" + } + + "Shallow Copy along with copy scheme" should "return client error response for copy content" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + implicit val ss = mock[StorageService] + val request = getInvalidCopyRequest_1() + request.getContext.put("identifier","do_1234") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + val exception = intercept[ClientException] { + CopyManager.validateRequest(request) + } + exception.getMessage shouldEqual "Content can not be shallow copied with copy scheme." + } + + "Wrong CopyScheme sent" should "return client error response for copy content" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + implicit val ss = mock[StorageService] + val request = getInvalidCopyRequest_3() + request.getContext.put("identifier","do_1234") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + val exception = intercept[ClientException] { + CopyManager.validateRequest(request) + } + exception.getMessage shouldEqual "Invalid copy scheme, Please provide valid copy scheme" + } + + "Valid scheme type update" should "should populate new contentType in metadata" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + implicit val ss = mock[StorageService] + val request = getInvalidCopyRequest_3() + request.getContext.put("identifier","do_1234") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "do_1234"))) + val metadata = new util.HashMap[String,Object]() + CopyManager.updateToCopySchemeContentType(getValidCopyRequest_1(), "TextBook", metadata) + assert(MapUtils.isNotEmpty(metadata)) + } + + + private def getNode(): Node = { + val node = new Node() + node.setGraphId("domain") + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("mimeType", "application/pdf") + put("status", "Draft") + put("contentType", "Resource") + put("primaryCategory", "Learning Resource") + put("name", "Copy content") + put("artifactUrl", "https://ntpstagingall.blob.core.windows.net/ntp-content-staging/content/assets/do_212959046431154176151/hindi3.pdf") + put("channel", "in.ekstep") + put("code", "xyz") + put("versionKey", "1234") + } + }) + node + } + + private def getCopiedNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234_copy") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setGraphId("domain") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234_copy") + put("mimeType", "application/pdf") + put("status", "Draft") + put("contentType", "Resource") + put("primaryCategory", "Learning Resource") + put("name", "Copy content") + put("artifactUrl", "https://ntpstagingall.blob.core.windows.net/ntp-content-staging/content/assets/do_212959046431154176151/hindi3.pdf") + put("channel", "in.ekstep") + put("code", "xyz") + put("versionKey", "1234") + } + }) + node + } + + private def getCopyRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "content") + + } + }) + request.setObjectType("Content") + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("createdBy", "EkStep") + put("createdFor", new util.ArrayList[String]() { + { + add("Ekstep") + } + }) + put("organisation", new util.ArrayList[String]() { + { + add("ekstep") + } + }) + put("framework", "DevCon-NCERT") + } + }) + request + } + + private def getInvalidCopyRequest_1(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "content") + put("copyScheme", "TextBookToCourse") + } + }) + request.setObjectType("Content") + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("createdBy", "EkStep") + put("createdFor", new util.ArrayList[String]() { + { + add("Ekstep") + } + }) + put("organisation", new util.ArrayList[String]() { + { + add("ekstep") + } + }) + put("framework", "DevCon-NCERT") + put("copyType", "shallow") + } + }) + request + } + + private def getInvalidCopyRequest_2(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "content") + put("copyScheme", "TextBookToCourse") + } + }) + request.setObjectType("Content") + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("createdFor", new util.ArrayList[String]() { + { + add("Ekstep") + } + }) + put("organisation", new util.ArrayList[String]() { + { + add("ekstep") + } + }) + put("framework", "DevCon-NCERT") + } + }) + request + } + + private def getInvalidCopyRequest_3(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "content") + put("copyScheme", "TextBookToCurriculumCourse") + } + }) + request.setObjectType("Content") + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("createdBy", "EkStep") + put("createdFor", new util.ArrayList[String]() { + { + add("Ekstep") + } + }) + put("organisation", new util.ArrayList[String]() { + { + add("ekstep") + } + }) + put("framework", "DevCon-NCERT") + } + }) + request + } + + private def getValidCopyRequest_1(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "content") + put("copyScheme", "TextBookToCourse") + } + }) + request.setObjectType("Content") + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("createdBy", "EkStep") + put("createdFor", new util.ArrayList[String]() { + { + add("Ekstep") + } + }) + put("organisation", new util.ArrayList[String]() { + { + add("ekstep") + } + }) + put("framework", "DevCon-NCERT") + } + }) + request + } + + def getDefinitionNode_channel(): Node = { + val node = new Node() + node.setIdentifier("obj-cat:learning-resource_content_in.ekstep") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap( + ScalaJsonUtils.deserialize[Map[String,AnyRef]]("{\n \"objectCategoryDefinition\": {\n \"name\": \"Learning Resource\",\n \"description\": \"Content Playlist\",\n \"categoryId\": \"obj-cat:learning-resource\",\n \"targetObjectType\": \"Content\",\n \"objectMetadata\": {\n \"config\": {},\n \"schema\": {\n \"required\": [\n \"author\",\n \"copyright\",\n \"audience\"\n ],\n \"properties\": {\n \"audience\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Student\",\n \"Teacher\"\n ]\n },\n \"default\": [\n \"Student\"\n ]\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"enum\": [\n \"application/pdf\"\n ]\n }\n }\n }\n }\n }\n }"))) + node + } + + def getDefinitionNode(): Node = { + val node = new Node() + node.setIdentifier("obj-cat:learning-resource_content_all") + node.setNodeType("DATA_NODE") + node.setObjectType("Content") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap( + ScalaJsonUtils.deserialize[Map[String,AnyRef]]("{\n \"objectCategoryDefinition\": {\n \"name\": \"Learning Resource\",\n \"description\": \"Content Playlist\",\n \"categoryId\": \"obj-cat:learning-resource\",\n \"targetObjectType\": \"Content\",\n \"objectMetadata\": {\n \"config\": {},\n \"schema\": {\n \"required\": [\n \"author\",\n \"copyright\",\n \"audience\"\n ],\n \"properties\": {\n \"audience\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Student\",\n \"Teacher\"\n ]\n },\n \"default\": [\n \"Student\"\n ]\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"enum\": [\n \"application/pdf\"\n ]\n }\n }\n }\n }\n }\n }"))) + node + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/util/DiscardManagerTest.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/util/DiscardManagerTest.scala new file mode 100644 index 000000000..45f19480c --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/util/DiscardManagerTest.scala @@ -0,0 +1,63 @@ +package org.sunbird.content.util + +import java.util +import java.util.concurrent.CompletionException + +import akka.actor.Props +import org.apache.commons.lang3.BooleanUtils +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.ClientException +import org.sunbird.content.actors.BaseSpec +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.{GraphService, OntologyEngineContext} + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class DiscardManagerTest extends BaseSpec with MockFactory { + + it should "discard node in Live state should return client error" in { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + val request = getContentRequest() + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> ""))) + request.setOperation("discardContent") + val exception = intercept[ClientException] { + DiscardManager.validateRequest(request) + } + exception.getMessage shouldEqual "Please provide valid content identifier" + } + + private def getContentRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "content") + + } + }) + request.setObjectType("Content") + request + } + + private def getInValidNodeToDiscard(): Node = { + val node = new Node() + node.setIdentifier("do_12346") + node.setNodeType("DATA_NODE") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_12346") + put("mimeType", "application/pdf") + put("status", "Live") + put("contentType", "Resource") + put("name", "Node To discard") + } + }) + node + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/util/FlagManagerTest.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/util/FlagManagerTest.scala new file mode 100644 index 000000000..06344afbb --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/util/FlagManagerTest.scala @@ -0,0 +1,29 @@ +package org.sunbird.content.util + +import java.util + +import org.scalamock.scalatest.MockFactory +import org.scalatest.{FlatSpec, Matchers} +class FlagManagerTest extends FlatSpec with Matchers with MockFactory { + + "addFlagReasons with metadata metadata without flagReasons" should "return flaggedList with only list with request flagReasons value" in { + val requestFlagReasons = java.util.Arrays.asList("Not a valid content") + val metadata = new util.HashMap[String, AnyRef]() + val flaggedByList = FlagManager.addDataIntoList(requestFlagReasons, metadata, "flagReasons") + assert(flaggedByList.size()==1) + assert(flaggedByList.containsAll(java.util.Arrays.asList("Not a valid content"))) + } + + "addFlagReasons with metadata with flagReasons as list of string" should "return flaggedList with list of requestFlagReasons and metadata flagReasons value" in { + val requestFlagReasons = new java.util.ArrayList[String] + requestFlagReasons.add("Not a valid content") + val flagReasons = new java.util.ArrayList[String] + flagReasons.add("Others") + val metadata = new util.HashMap[String, AnyRef](){{ + put("flagReasons", flagReasons) + }} + val flaggedByList = FlagManager.addDataIntoList(requestFlagReasons, metadata, "flagReasons") + assert(flaggedByList.size()==2) + assert(flaggedByList.containsAll(java.util.Arrays.asList("Not a valid content", "Others"))) + } +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/util/RequestUtilTest.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/util/RequestUtilTest.scala new file mode 100644 index 000000000..b3cff3429 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/util/RequestUtilTest.scala @@ -0,0 +1,34 @@ +package org.sunbird.content.util + +import java.util + +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{FlatSpec, Matchers} +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.util.RequestUtil + + +class RequestUtilTest extends FlatSpec with Matchers with AsyncMockFactory { + + + it should "throw clientException for invalid request" in { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val exception = intercept[ClientException] { + val context = new util.HashMap[String, AnyRef](){{ + put("graphId", "domain") + put("version", "1.0") + put("schemaName", "content") + put("objectType", "Content") + }} + val request = new Request() + request.setContext(context) + request.setOperation("create") + request.put("status", "Live") + RequestUtil.restrictProperties(request) + } + exception.getErrCode shouldEqual "ERROR_RESTRICTED_PROP" + } + +} diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/util/TestAcceptFlagManager.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/util/TestAcceptFlagManager.scala new file mode 100644 index 000000000..eeef3f2d2 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/util/TestAcceptFlagManager.scala @@ -0,0 +1,60 @@ +package org.sunbird.content.util + +import java.util + +import akka.actor.Props +import org.scalamock.scalatest.MockFactory +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.dto.{Property, Request, Response} +import org.sunbird.content.actors.{BaseSpec, ContentActor} +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.graph.dac.model.Node + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +class TestAcceptFlagManager extends BaseSpec with MockFactory { + + it should "return success response for acceptFlag for Resource" in { + implicit val ss = mock[StorageService] + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + val nodeMetaData = new util.HashMap[String, AnyRef]() {{ + put("name", "Domain") + put("code", "domain") + put("status", "Flagged") + put("identifier", "domain") + put("versionKey", "1234") + put("contentType", "Resource") + put("channel", "Test") + put("mimeType", "application/pdf") + put("primaryCategory", "Learning Resource") + }} + val node = getNode("Content", Option(nodeMetaData)) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.upsertNode(_:String, _: Node, _: Request)).expects(*, *, *).returns(Future(node)).anyNumberOfTimes() + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("1234")))).anyNumberOfTimes() + (graphDB.readExternalProps(_: Request, _: List[String])).expects(*, *).returns(Future(new Response())) + val request = getRequest() + request.getContext.put("identifier","domain") + request.getRequest.putAll(mapAsJavaMap(Map("identifier" -> "domain"))) + request.setOperation("acceptFlag") + val response = callActor(request, Props(new ContentActor())) + assert("successful".equals(response.getParams.getStatus)) + } + + private def getRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "content") + } + }) + request + } +} \ No newline at end of file diff --git a/content-api/content-actors/src/test/scala/org/sunbird/content/util/TestAssetManager.scala b/content-api/content-actors/src/test/scala/org/sunbird/content/util/TestAssetManager.scala new file mode 100644 index 000000000..62dc10c11 --- /dev/null +++ b/content-api/content-actors/src/test/scala/org/sunbird/content/util/TestAssetManager.scala @@ -0,0 +1,113 @@ +package org.sunbird.content.util + +import java.util + +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.common.dto.{Property, Request} +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.{GraphService, OntologyEngineContext} +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.utils.ScalaJsonUtils + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.concurrent.Future + +class TestAssetManager extends AsyncFlatSpec with Matchers with AsyncMockFactory { + "AssetCopyManager" should "return copied node identifier when asset is copied" ignore { + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val graphDB = mock[GraphService] + (oec.graphService _).expects().returns(graphDB).anyNumberOfTimes() + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getNode())) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getDefinitionNode_channel())) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getDefinitionNode_channel())) + (graphDB.addNode(_: String, _: Node)).expects(*, *).returns(Future(getCopiedNode())) + (graphDB.getNodeByUniqueId(_: String, _: String, _: Boolean, _: Request)).expects(*, *, *, *).returns(Future(getCopiedNode())) + (graphDB.upsertNode(_: String, _: Node, _: Request)).expects(*, *, *).returns(Future(getCopiedNode())) + (graphDB.getNodeProperty(_: String, _: String, _: String)).expects(*, *, *).returns(Future(new Property("versionKey", new org.neo4j.driver.internal.value.StringValue("1234")))) + AssetCopyManager.copy(getCopyRequest()).map(resp => { + assert(resp != null) + assert(resp.getResponseCode == ResponseCode.OK) + assert(resp.getResult.get("node_id").asInstanceOf[util.HashMap[String, AnyRef]].get("do_1234").asInstanceOf[String] == "do_1234_copy") + }) + } + + private def getCopyRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Asset") + put("schemaName", "content") + + } + }) + request.setObjectType("Asset") + request.putAll(new util.HashMap[String, AnyRef]() { + { + put("name", "test") + } + }) + request + } + + private def getNode(): Node = { + val node = new Node() + node.setGraphId("domain") + node.setIdentifier("do_1234") + node.setNodeType("DATA_NODE") + node.setObjectType("Asset") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("mimeType", "application/pdf") + put("status", "Draft") + put("contentType", "Resource") + put("primaryCategory", "Learning Resource") + put("name", "Copy content") + put("artifactUrl", "https://ntpstagingall.blob.core.windows.net/ntp-content-staging/content/assets/do_212959046431154176151/hindi3.pdf") + put("channel", "in.ekstep") + put("code", "xyz") + put("versionKey", "1234") + } + }) + node + } + + + def getDefinitionNode_channel(): Node = { + val node = new Node() + node.setIdentifier("obj-cat:learning-resource_content_in.ekstep") + node.setNodeType("DATA_NODE") + node.setObjectType("Asset") + node.setGraphId("domain") + node.setMetadata(mapAsJavaMap( + ScalaJsonUtils.deserialize[Map[String, AnyRef]]("{\n \"objectCategoryDefinition\": {\n \"name\": \"Learning Resource\",\n \"description\": \"Content Playlist\",\n \"categoryId\": \"obj-cat:learning-resource\",\n \"targetObjectType\": \"Content\",\n \"objectMetadata\": {\n \"config\": {},\n \"schema\": {\n \"required\": [\n \"author\",\n \"copyright\",\n \"audience\"\n ],\n \"properties\": {\n \"audience\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"Student\",\n \"Teacher\"\n ]\n },\n \"default\": [\n \"Student\"\n ]\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"enum\": [\n \"application/pdf\"\n ]\n }\n }\n }\n }\n }\n }"))) + node + } + + private def getCopiedNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234_copy") + node.setNodeType("DATA_NODE") + node.setObjectType("Asset") + node.setGraphId("domain") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234_copy") + put("mimeType", "application/pdf") + put("status", "Draft") + put("contentType", "Resource") + put("primaryCategory", "Learning Resource") + put("name", "Copy content") + put("artifactUrl", "https://ntpstagingall.blob.core.windows.net/ntp-content-staging/content/assets/do_212959046431154176151/hindi3.pdf") + put("channel", "in.ekstep") + put("code", "xyz") + put("versionKey", "1234") + } + }) + node + } + +} diff --git a/learning-api/content-service/.gitignore b/content-api/content-service/.gitignore similarity index 100% rename from learning-api/content-service/.gitignore rename to content-api/content-service/.gitignore diff --git a/content-api/content-service/app/controllers/BaseController.scala b/content-api/content-service/app/controllers/BaseController.scala new file mode 100644 index 000000000..4374015b5 --- /dev/null +++ b/content-api/content-service/app/controllers/BaseController.scala @@ -0,0 +1,211 @@ +package controllers + + +import java.io.File +import java.util +import java.util.UUID + +import akka.actor.ActorRef +import akka.pattern.Patterns +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.{DateUtils, Platform} +import org.sunbird.common.dto.{Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ResponseCode} +import play.api.mvc._ +import utils.{Constants, JavaJsonUtils} + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +abstract class BaseController(protected val cc: ControllerComponents)(implicit exec: ExecutionContext) extends AbstractController(cc) { + val categoryMap: java.util.Map[String, AnyRef] = Platform.getAnyRef("contentTypeToPrimaryCategory", + new util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + val categoryMapForMimeType: java.util.Map[String, AnyRef] = Platform.getAnyRef("mimeTypeToPrimaryCategory", + new util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + val categoryMapForResourceType: java.util.Map[String, AnyRef] = Platform.getAnyRef("resourceTypeToPrimaryCategory", + new util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + val mimeTypesToCheck = List("application/vnd.ekstep.h5p-archive", "application/vnd.ekstep.html-archive", "application/vnd.android.package-archive", + "video/webm", "video/x-youtube", "video/mp4") + + def requestBody()(implicit request: Request[AnyContent]) = { + val body = request.body.asJson.getOrElse("{}").toString + JavaJsonUtils.deserialize[java.util.Map[String, Object]](body).getOrDefault("request", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + } + + def requestFormData(identifier: String)(implicit request: Request[AnyContent]) = { + val reqMap = new util.HashMap[String, AnyRef]() + if(!request.body.asMultipartFormData.isEmpty) { + val multipartData = request.body.asMultipartFormData.get + if (null != multipartData.asFormUrlEncoded && !multipartData.asFormUrlEncoded.isEmpty) { + if(multipartData.asFormUrlEncoded.getOrElse("fileUrl",Seq()).length > 0){ + val fileUrl: String = multipartData.asFormUrlEncoded.getOrElse("fileUrl",Seq()).head + if (StringUtils.isNotBlank(fileUrl)) + reqMap.put("fileUrl", fileUrl) + } + if(multipartData.asFormUrlEncoded.getOrElse("filePath",Seq()).length > 0){ + val filePath: String = multipartData.asFormUrlEncoded.getOrElse("filePath",Seq()).head + if (StringUtils.isNotBlank(filePath)) + reqMap.put("filePath", filePath) + } + } + if (null != multipartData.files && !multipartData.files.isEmpty) { + val file: File = new File("/tmp" + File.separator + identifier + "_" + System.currentTimeMillis + "_"+ request.body.asMultipartFormData.get.files.head.filename) + val copiedFile: File = multipartData.files.head.ref.copyTo(file, false).toFile + reqMap.put("file", copiedFile) + } + } + if(StringUtils.isNotBlank(reqMap.getOrDefault("fileUrl", "").asInstanceOf[String]) || null != reqMap.get("file").asInstanceOf[File]){ + reqMap + } else { + throw new ClientException("ERR_INVALID_DATA", "Please Provide Valid File Or File Url!") + } + } + + def commonHeaders(ignoreHeaders: Option[List[String]] = Option(List()))(implicit request: Request[AnyContent]): java.util.Map[String, Object] = { + val customHeaders = Map("x-channel-id" -> "channel", "X-Consumer-ID" -> "consumerId", "X-App-Id" -> "appId").filterKeys(key => !ignoreHeaders.getOrElse(List()).contains(key)) + customHeaders.map(ch => { + val value = request.headers.get(ch._1) + if (value.isDefined && !value.isEmpty) { + collection.mutable.HashMap[String, Object](ch._2 -> value.get).asJava + } else { + collection.mutable.HashMap[String, Object]().asJava + } + }).reduce((a, b) => { + a.putAll(b) + return a + }) + } + + def getRequest(input: java.util.Map[String, AnyRef], context: java.util.Map[String, AnyRef], operation: String, categoryMapping: Boolean = false): org.sunbird.common.dto.Request = { + //Todo mapping and reverse mapping + if (categoryMapping) setContentAndCategoryTypes(input) + new org.sunbird.common.dto.Request(context, input, operation, null); + } + + def getResult(apiId: String, actor: ActorRef, request: org.sunbird.common.dto.Request, categoryMapping: Boolean = false, version: String = "3.0") : Future[Result] = { + val future = Patterns.ask(actor, request, 30000) recoverWith {case e: Exception => Future(ResponseHandler.getErrorResponse(e))} + future.map(f => { + val result: Response = f.asInstanceOf[Response] + result.setId(apiId) + result.setVer(version) + setResponseEnvelope(result) + //TODO Mapping for backward compatibility + if (categoryMapping && result.getResponseCode == ResponseCode.OK) { + setContentAndCategoryTypes(result.getResult.getOrDefault("content", new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]]) + val objectType = result.getResult.getOrDefault("content", new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]].getOrDefault("objectType", "Content").asInstanceOf[String] + setObjectTypeForRead(objectType, result.getResult.getOrDefault("content", new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]]) + } + val response: String = JavaJsonUtils.serialize(result); + result.getResponseCode match { + case ResponseCode.OK => Ok(response).as("application/json") + case ResponseCode.CLIENT_ERROR => BadRequest(response).as("application/json") + case ResponseCode.RESOURCE_NOT_FOUND => NotFound(response).as("application/json") + case ResponseCode.PARTIAL_SUCCESS => MultiStatus(response).as("application/json") + case _ => play.api.mvc.Results.InternalServerError(response).as("application/json") + } + }) + } + + def setResponseEnvelope(response: Response) = { + response.setTs(DateUtils.formatCurrentDate("yyyy-MM-dd'T'HH:mm:ss'Z'XXX")) + response.getParams.setResmsgid(UUID.randomUUID().toString) + } + + def setRequestContext(request: org.sunbird.common.dto.Request, version: String, objectType: String, schemaName: String): Unit = { + val mimeType = request.getRequest.getOrDefault("mimeType", "").asInstanceOf[String] + val contentType = request.getRequest.getOrDefault("contentType", "").asInstanceOf[String] + val primaryCategory = request.getRequest.getOrDefault("primaryCategory", "").asInstanceOf[String] + val contextMap: java.util.Map[String, AnyRef] = if (StringUtils.isNotBlank(mimeType) && StringUtils.equalsIgnoreCase(mimeType, Constants.COLLECTION_MIME_TYPE)) { + request.setObjectType(Constants.COLLECTION_OBJECT_TYPE) + new java.util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", Constants.COLLECTION_VERSION) + put("objectType", Constants.COLLECTION_OBJECT_TYPE) + put("schemaName", Constants.COLLECTION_SCHEMA_NAME) + } + } + } else if ((StringUtils.isNotBlank(contentType) && StringUtils.equalsIgnoreCase(contentType, Constants.ASSET_CONTENT_TYPE)) + || (StringUtils.isNotBlank(primaryCategory) && StringUtils.equalsIgnoreCase(primaryCategory, Constants.ASSET_CONTENT_TYPE))) { + request.setObjectType(Constants.ASSET_OBJECT_TYPE) + new java.util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", Constants.ASSET_VERSION) + put("objectType", Constants.ASSET_OBJECT_TYPE) + put("schemaName", Constants.ASSET_SCHEMA_NAME) + } + } + } else { + request.setObjectType(objectType) + new java.util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", version) + put("objectType", objectType) + put("schemaName", schemaName) + } + } + } + if(StringUtils.isNotBlank(request.getContext.getOrDefault("channel", "").asInstanceOf[String])) + contextMap.put("channel", request.getContext.get("channel").asInstanceOf[String]) + request.setContext(contextMap) + } + + private def setContentAndCategoryTypes(input: java.util.Map[String, AnyRef]): Unit = { + val contentType = input.get("contentType").asInstanceOf[String] + val primaryCategory = input.get("primaryCategory").asInstanceOf[String] + val (updatedContentType, updatedPrimaryCategory): (String, String) = (contentType, primaryCategory) match { + case (x: String, y: String) => (x, y) + case ("Resource", y) => (contentType, getCategoryForResource(input.getOrDefault("mimeType", "").asInstanceOf[String], + input.getOrDefault("resourceType", "").asInstanceOf[String])) + case (x: String, y) => (x, getPrimeryCategory(x)) + case (x, y: String) => (getContentType(y), y) + case _ => (contentType, primaryCategory) + } + input.put("contentType", if (StringUtils.isBlank(updatedContentType)) "Resource" else updatedContentType) + input.put("primaryCategory", updatedPrimaryCategory) + } + + private def getPrimeryCategory(contentType: String): String ={ + val primaryCategory = categoryMap.get(contentType) + if(primaryCategory.isInstanceOf[String]) + primaryCategory.asInstanceOf[String] + else + primaryCategory.asInstanceOf[util.List[String]].asScala.headOption.getOrElse("Learning Resource") + + } + + private def getContentType(primaryCategory: String): String ={ + categoryMap.asScala.filter(entry => (entry._2 match{ + case xs: util.List[_] => xs.asInstanceOf[util.List[String]].contains(primaryCategory) + case _ => StringUtils.equalsIgnoreCase(entry._2.asInstanceOf[String], primaryCategory) + + })).keys.headOption.getOrElse("Resource") + } + + private def getCategoryForResource(mimeType: String, resourceType: String): String = (mimeType, resourceType) match { + case ("", "") => "Learning Resource" + case (x: String, "") => categoryMapForMimeType.get(x).asInstanceOf[util.List[String]].asScala.headOption.getOrElse("Learning Resource") + case (x: String, y: String) => if (mimeTypesToCheck.contains(x)) categoryMapForMimeType.get(x).asInstanceOf[util.List[String]].asScala.headOption.getOrElse("Learning Resource") else categoryMapForResourceType.getOrDefault(y, "Learning Resource").asInstanceOf[String] + case _ => "Learning Resource" + } + + private def setObjectTypeForRead(objectType: String, result: java.util.Map[String, AnyRef]): Unit = { + result.put("objectType", "Content") + } + + def validatePrimaryCategory(input: java.util.Map[String, AnyRef]): Boolean = StringUtils.isNotBlank(input.getOrDefault("primaryCategory", "").asInstanceOf[String]) + + def validateContentType(input: java.util.Map[String, AnyRef]): Boolean = StringUtils.isNotBlank(input.getOrDefault("contentType", "").asInstanceOf[String]) + + + def getErrorResponse(apiId: String, version: String, errCode: String, errMessage: String): Future[Result] = { + val result = ResponseHandler.ERROR(ResponseCode.CLIENT_ERROR, errCode, errMessage) + result.setId(apiId) + result.setVer(version) + setResponseEnvelope(result) + Future(BadRequest(JavaJsonUtils.serialize(result)).as("application/json")) + } + +} diff --git a/content-api/content-service/app/controllers/HealthController.scala b/content-api/content-service/app/controllers/HealthController.scala new file mode 100644 index 000000000..d278b7681 --- /dev/null +++ b/content-api/content-service/app/controllers/HealthController.scala @@ -0,0 +1,31 @@ +package controllers + +import akka.actor.{ActorRef, ActorSystem} +import handlers.SignalHandler +import javax.inject._ +import org.sunbird.common.JsonUtils +import org.sunbird.common.dto.ResponseHandler +import play.api.mvc._ +import utils.{ActorNames, ApiId} + +import scala.concurrent.{ExecutionContext, Future} + +class HealthController @Inject()(@Named(ActorNames.HEALTH_ACTOR) healthActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem, signalHandler: SignalHandler)(implicit exec: ExecutionContext) extends BaseController(cc) { + + def health() = Action.async { implicit request => + if (signalHandler.isShuttingDown) { + Future { ServiceUnavailable } + } else { + getResult(ApiId.APPLICATION_HEALTH, healthActor, new org.sunbird.common.dto.Request()) + } + } + + def serviceHealth() = Action.async { implicit request => + if (signalHandler.isShuttingDown) + Future { ServiceUnavailable } + else { + val response = ResponseHandler.OK().setId(ApiId.APPLICATION_SERVICE_HEALTH).put("healthy", true) + Future { Ok(JsonUtils.serialize(response)).as("application/json") } + } + } +} diff --git a/content-api/content-service/app/controllers/v3/CategoryController.scala b/content-api/content-service/app/controllers/v3/CategoryController.scala new file mode 100644 index 000000000..59bcce644 --- /dev/null +++ b/content-api/content-service/app/controllers/v3/CategoryController.scala @@ -0,0 +1,61 @@ +package controllers.v3 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import controllers.BaseController +import javax.inject.{Inject, Named} +import org.sunbird.content.util.CategoryConstants +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId} + +import scala.collection.JavaConverters._ +import scala.concurrent.ExecutionContext + +@Singleton +class CategoryController @Inject()(@Named(ActorNames.CATEGORY_ACTOR) categoryActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + + val objectType = "Category" + val schemaName: String = "category" + val version = "1.0" + + def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val category = body.getOrDefault("category", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + category.putAll(headers) + val categoryRequest = getRequest(category, headers, CategoryConstants.CREATE_CATEGORY) + setRequestContext(categoryRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_CATEGORY, categoryActor, categoryRequest) + } + + def read(identifier: String, fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val category = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + category.putAll(headers) + category.putAll(Map("identifier" -> identifier, "fields" -> fields.getOrElse("")).asJava) + val categoryRequest = getRequest(category, headers, CategoryConstants.READ_CATEGORY) + setRequestContext(categoryRequest, version, objectType, schemaName) + getResult(ApiId.READ_CATEGORY, categoryActor, categoryRequest) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val category = body.getOrDefault("category", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + category.putAll(headers) + val categoryRequest = getRequest(category, headers, CategoryConstants.UPDATE_CATEGORY) + setRequestContext(categoryRequest, version, objectType, schemaName) + categoryRequest.getContext.put("identifier", identifier) + getResult(ApiId.UPDATE_CATEGORY, categoryActor, categoryRequest) + } + + def retire(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val category = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + category.putAll(headers) + val categoryRequest = getRequest(category, headers, CategoryConstants.RETIRE_CATEGORY) + setRequestContext(categoryRequest, version, objectType, schemaName) + categoryRequest.getContext.put("identifier", identifier) + getResult(ApiId.RETIRE_CATEGORY, categoryActor, categoryRequest) + } +} diff --git a/content-api/content-service/app/controllers/v3/ChannelController.scala b/content-api/content-service/app/controllers/v3/ChannelController.scala new file mode 100644 index 000000000..c798ca511 --- /dev/null +++ b/content-api/content-service/app/controllers/v3/ChannelController.scala @@ -0,0 +1,60 @@ +package controllers.v3 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import controllers.BaseController +import javax.inject.{Inject, Named} +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId} + +import scala.concurrent.{ExecutionContext} + +@Singleton +class ChannelController @Inject()(@Named(ActorNames.CHANNEL_ACTOR) channelActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + + val objectType = "Channel" + val schemaName: String = "channel" + val version = "1.0" + + def create() = Action.async { implicit request => + val headers = commonHeaders(Option(List("x-channel-id"))) + val body = requestBody() + val channel = body.getOrDefault("channel", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + channel.putAll(headers) + val channelRequest = getRequest(channel, headers, "createChannel") + setRequestContext(channelRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_CHANNEL, channelActor, channelRequest) + } + + def read(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val channel = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + channel.put("identifier", identifier) + channel.putAll(headers) + val readRequest = getRequest(channel, headers, "readChannel") + setRequestContext(readRequest, version, objectType, schemaName) + getResult(ApiId.READ_CHANNEL, channelActor, readRequest) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders(Option(List("x-channel-id"))) + val body = requestBody() + val channel = body.getOrDefault("channel", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + channel.putAll(headers) + val channelRequest = getRequest(channel, headers, "updateChannel") + setRequestContext(channelRequest, version, objectType, schemaName) + channelRequest.getContext.put("identifier", identifier); + getResult(ApiId.UPDATE_CHANNEL, channelActor, channelRequest) + } + + def retire(identifier: String) = Action.async { implicit request => + val headers = commonHeaders(Option(List("x-channel-id"))) + val channel = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + channel.putAll(headers) + val channelRequest = getRequest(channel, headers, "retireChannel") + setRequestContext(channelRequest, version, objectType, schemaName) + channelRequest.getContext.put("identifier", identifier); + getResult(ApiId.RETIRE_CHANNEL, channelActor, channelRequest) + } +} + diff --git a/content-api/content-service/app/controllers/v3/ContentController.scala b/content-api/content-service/app/controllers/v3/ContentController.scala new file mode 100644 index 000000000..6a7abdd35 --- /dev/null +++ b/content-api/content-service/app/controllers/v3/ContentController.scala @@ -0,0 +1,269 @@ +package controllers.v3 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import controllers.BaseController +import javax.inject.{Inject, Named} +import org.sunbird.models.UploadParams +import org.sunbird.common.dto.ResponseHandler +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId, JavaJsonUtils} + +import scala.collection.JavaConverters._ + +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class ContentController @Inject()(@Named(ActorNames.CONTENT_ACTOR) contentActor: ActorRef, @Named(ActorNames.COLLECTION_ACTOR) collectionActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + + val objectType = "Content" + val schemaName: String = "content" + val version = "1.0" + + def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault("content", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + content.putAll(headers) + val contentRequest = getRequest(content, headers, "createContent", true) + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_CONTENT, contentActor, contentRequest) + + } + + /** + * This Api end point takes 3 parameters + * Content Identifier the unique identifier of a content + * Mode in which the content can be viewed (default read or edit) + * Fields are metadata that should be returned to visualize + * + * @param identifier + * @param mode + * @param fields + * @return + */ + def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "readContent") + setRequestContext(readRequest, version, objectType, schemaName) + getResult(ApiId.READ_CONTENT, contentActor, readRequest, true) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault("content", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + content.putAll(headers) + val contentRequest = getRequest(content, headers, "updateContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier); + getResult(ApiId.UPDATE_CONTENT, contentActor, contentRequest) + } + + def addHierarchy() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "addHierarchy") + contentRequest.put("mode", "edit"); + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.ADD_HIERARCHY, collectionActor, contentRequest) + } + + def removeHierarchy() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "removeHierarchy") + contentRequest.put("mode", "edit"); + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.REMOVE_HIERARCHY, collectionActor, contentRequest) + } + + def updateHierarchy() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val data = body.getOrDefault("data", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + data.putAll(headers) + val contentRequest = getRequest(data, headers, "updateHierarchy") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.UPDATE_HIERARCHY, collectionActor, contentRequest) + } + + def getHierarchy(identifier: String, mode: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("rootId" -> identifier, "mode" -> mode.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "getHierarchy") + setRequestContext(readRequest, version, objectType, null) + getResult(ApiId.GET_HIERARCHY, collectionActor, readRequest, true) + } + + def getBookmarkHierarchy(identifier: String, bookmarkId: String, mode: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("rootId" -> identifier, "bookmarkId" -> bookmarkId, "mode" -> mode.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "getHierarchy") + setRequestContext(readRequest, version, objectType, null) + getResult(ApiId.GET_HIERARCHY, collectionActor, readRequest, true) + } + + def flag(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val contentRequest = getRequest(content, headers, "flagContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier) + getResult(ApiId.FlAG_CONTENT, contentActor, contentRequest) + } + + def acceptFlag(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val acceptRequest = getRequest(content, headers, "acceptFlag") + setRequestContext(acceptRequest, version, objectType, schemaName) + getResult(ApiId.ACCEPT_FLAG, contentActor, acceptRequest) + } + + def rejectFlag(identifier: String) = Action.async { implicit request => + val result = ResponseHandler.OK() + val response = JavaJsonUtils.serialize(result) + Future(Ok(response).as("application/json")) + } + + def bundle() = Action.async { implicit request => + val result = ResponseHandler.OK() + val response = JavaJsonUtils.serialize(result) + Future(Ok(response).as("application/json")) + } + + def publish(identifier: String) = Action.async { implicit request => + val result = ResponseHandler.OK() + val response = JavaJsonUtils.serialize(result) + Future(Ok(response).as("application/json")) + } + + def review(identfier: String) = Action.async { implicit request => + val result = ResponseHandler.OK() + val response = JavaJsonUtils.serialize(result) + Future(Ok(response).as("application/json")) + } + + def discard(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val discardRequest = getRequest(content, headers, "discardContent") + setRequestContext(discardRequest, version, objectType, schemaName) + getResult(ApiId.DISCARD_CONTENT, contentActor, discardRequest) + } + def retire(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault("content", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.put("identifier", identifier) + content.putAll(headers) + val contentRequest = getRequest(content, headers, "retireContent") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.RETIRE_CONTENT, contentActor, contentRequest) + } + + def linkDialCode() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "linkDIALCode") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("linkType", "content") + getResult(ApiId.LINK_DIAL_CONTENT, contentActor, contentRequest) + } + + def collectionLinkDialCode(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "linkDIALCode") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("linkType", "collection") + contentRequest.getContext.put("identifier", identifier) + getResult(ApiId.LINK_DIAL_COLLECTION, contentActor, contentRequest) + } + + def reserveDialCode(identifier: String) = Action.async { implicit request => + val result = ResponseHandler.OK() + val response = JavaJsonUtils.serialize(result) + Future(Ok(response).as("application/json")) + } + + def releaseDialcodes(identifier: String) = Action.async { implicit request => + val result = ResponseHandler.OK() + val response = JavaJsonUtils.serialize(result) + Future(Ok(response).as("application/json")) + } + + def rejectContent(identifier: String) = Action.async { implicit request => + val result = ResponseHandler.OK() + val response = JavaJsonUtils.serialize(result) + Future(Ok(response).as("application/json")) + } + + def publishUnlisted(identifier: String) = Action.async { implicit request => + val result = ResponseHandler.OK() + val response = JavaJsonUtils.serialize(result) + Future(Ok(response).as("application/json")) + } + + def upload(identifier: String, fileFormat: Option[String], validation: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = requestFormData(identifier) + content.putAll(headers) + val contentRequest = getRequest(content, headers, "uploadContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.putAll(Map("identifier" -> identifier, "params" -> UploadParams(fileFormat, validation.map(_.toBoolean))).asJava) + getResult(ApiId.UPLOAD_CONTENT, contentActor, contentRequest) + } + + + def copy(identifier: String, mode: Option[String], copyType: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault("content", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse(""), "copyType" -> copyType).asJava) + val contentRequest = getRequest(content, headers, "copy") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.COPY_CONTENT, contentActor, contentRequest) + } + + def uploadPreSigned(identifier: String, `type`: Option[String])= Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault("content", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "type" -> `type`.getOrElse("assets")).asJava) + val contentRequest = getRequest(content, headers, "uploadPreSignedUrl") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.UPLOAD_PRE_SIGNED_CONTENT, contentActor, contentRequest) + } + + def importContent() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "importContent") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.IMPORT_CONTENT, contentActor, contentRequest) + } + +} diff --git a/learning-api/content-service/app/controllers/v3/LicenseController.scala b/content-api/content-service/app/controllers/v3/LicenseController.scala similarity index 77% rename from learning-api/content-service/app/controllers/v3/LicenseController.scala rename to content-api/content-service/app/controllers/v3/LicenseController.scala index 0cbfc6487..9543f2759 100644 --- a/learning-api/content-service/app/controllers/v3/LicenseController.scala +++ b/content-api/content-service/app/controllers/v3/LicenseController.scala @@ -4,11 +4,11 @@ import akka.actor.{ActorRef, ActorSystem} import com.google.inject.Singleton import controllers.BaseController import javax.inject.{Inject, Named} -import org.sunbird.utils.LicenseOperations +import org.sunbird.content.util.LicenseConstants import play.api.mvc.ControllerComponents import utils.{ActorNames, ApiId} -import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ import scala.concurrent.ExecutionContext @Singleton @@ -21,9 +21,9 @@ class LicenseController @Inject()(@Named(ActorNames.LICENSE_ACTOR) licenseActor: def create() = Action.async { implicit request => val headers = commonHeaders() val body = requestBody() - val license = body.getOrElse("license", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + val license = body.getOrDefault("license", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] license.putAll(headers) - val licenseRequest = getRequest(license, headers, LicenseOperations.createLicense.name()) + val licenseRequest = getRequest(license, headers, LicenseConstants.CREATE_LICENSE) setRequestContext(licenseRequest, version, objectType, schemaName) getResult(ApiId.CREATE_LICENSE, licenseActor, licenseRequest) } @@ -32,8 +32,8 @@ class LicenseController @Inject()(@Named(ActorNames.LICENSE_ACTOR) licenseActor: val headers = commonHeaders() val license = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] license.putAll(headers) - license.putAll(Map("identifier" -> identifier, "fields" -> fields.getOrElse(""))) - val licenseRequest = getRequest(license, headers, LicenseOperations.readLicense.name()) + license.putAll(Map("identifier" -> identifier, "fields" -> fields.getOrElse("")).asJava) + val licenseRequest = getRequest(license, headers, LicenseConstants.READ_LICENSE) setRequestContext(licenseRequest, version, objectType, schemaName) getResult(ApiId.READ_LICENSE, licenseActor, licenseRequest) } @@ -41,9 +41,9 @@ class LicenseController @Inject()(@Named(ActorNames.LICENSE_ACTOR) licenseActor: def update(identifier: String) = Action.async { implicit request => val headers = commonHeaders() val body = requestBody() - val license = body.getOrElse("license", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + val license = body.getOrDefault("license", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] license.putAll(headers) - val licenseRequest = getRequest(license, headers, LicenseOperations.updateLicense.name()) + val licenseRequest = getRequest(license, headers, LicenseConstants.UPDATE_LICENSE) setRequestContext(licenseRequest, version, objectType, schemaName) licenseRequest.getContext.put("identifier", identifier) getResult(ApiId.UPDATE_LICENSE, licenseActor, licenseRequest) @@ -53,7 +53,7 @@ class LicenseController @Inject()(@Named(ActorNames.LICENSE_ACTOR) licenseActor: val headers = commonHeaders() val license = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] license.putAll(headers) - val licenseRequest = getRequest(license, headers, LicenseOperations.retireLicense.name()) + val licenseRequest = getRequest(license, headers, LicenseConstants.RETIRE_LICENSE) setRequestContext(licenseRequest, version, objectType, schemaName) licenseRequest.getContext.put("identifier", identifier) getResult(ApiId.RETIRE_LICENSE, licenseActor, licenseRequest) diff --git a/content-api/content-service/app/controllers/v4/AppController.scala b/content-api/content-service/app/controllers/v4/AppController.scala new file mode 100644 index 000000000..54e9fc8cb --- /dev/null +++ b/content-api/content-service/app/controllers/v4/AppController.scala @@ -0,0 +1,56 @@ +package controllers.v4 + +import akka.actor.ActorRef +import com.google.inject.Singleton +import controllers.BaseController +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId} + +import javax.inject.{Inject, Named} +import scala.concurrent.ExecutionContext + +import scala.collection.JavaConverters._ + +/*** + * TODO: Re-write this controller after merging the Event and EventSet Controller. + */ + +@Singleton +class AppController @Inject()(@Named(ActorNames.APP_ACTOR) appActor: ActorRef, cc: ControllerComponents)(implicit exec: ExecutionContext) extends BaseController(cc) { + + val objectType = "App" + val schemaName: String = "app" + val version = "1.0" + val apiVersion = "4.0" + + def register() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val metadata = body.getOrDefault("app", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + metadata.putAll(headers) + val appRequest = getRequest(metadata, headers, "create") + setRequestContext(appRequest, version, objectType, schemaName) + getResult(ApiId.REGISTER_APP, appActor, appRequest, version = apiVersion) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val metadata = body.getOrDefault("app", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + metadata.putAll(headers) + val appRequest = getRequest(metadata, headers, "update") + setRequestContext(appRequest, version, objectType, schemaName) + appRequest.getContext.put("identifier", identifier) + getResult(ApiId.UPDATE_APP, appActor, appRequest, version = apiVersion) + } + + def read(identifier: String, fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val app = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + app.putAll(headers) + app.putAll(Map("identifier" -> identifier, "mode" -> "read", "fields" -> fields.getOrElse("")).asJava) + val readRequest = getRequest(app, headers, "read") + setRequestContext(readRequest, version, objectType, schemaName) + getResult(ApiId.READ_APP, appActor, readRequest, version = apiVersion) + } +} diff --git a/content-api/content-service/app/controllers/v4/AssetController.scala b/content-api/content-service/app/controllers/v4/AssetController.scala new file mode 100644 index 000000000..feaf9a6b7 --- /dev/null +++ b/content-api/content-service/app/controllers/v4/AssetController.scala @@ -0,0 +1,106 @@ +package controllers.v4 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import controllers.BaseController +import javax.inject.{Inject, Named} +import org.sunbird.models.UploadParams +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId} + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext} +@Singleton +class AssetController @Inject()(@Named(ActorNames.CONTENT_ACTOR) contentActor: ActorRef, @Named(ActorNames.ASSET_ACTOR) assetActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + val objectType = "Asset" + val schemaName: String = "asset" + val version = "1.0" + val apiVersion = "4.0" + + /** + * This Api end point takes a body + * Content Identifier the unique identifier of a content, can either be provided or will be generated + * primaryCategory, mimeType, name and code are mandatory + * + * @returns identifier and versionKey + */ + def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + if(!validatePrimaryCategory(content)) + getErrorResponse(ApiId.CREATE_ASSET, apiVersion, "VALIDATION_ERROR", "primaryCategory is a mandatory parameter.") + else if(validateContentType(content)) + getErrorResponse(ApiId.CREATE_ASSET, apiVersion, "VALIDATION_ERROR", "contentType cannot be set from request.") + else { + val contentRequest = getRequest(content, headers, "createContent", true) + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_ASSET, contentActor, contentRequest, version = apiVersion) + } + } + + /** + * This Api end point takes 3 parameters + * Content Identifier the unique identifier of a content + * Mode in which the content can be viewed (default read or edit) + * Fields are metadata that should be returned to visualize + * + * @param identifier + * @param mode + * @param fields + * @return + */ + def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "readContent") + setRequestContext(readRequest, version, objectType, schemaName) + getResult(ApiId.READ_ASSET, contentActor, readRequest, version = apiVersion) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + val contentRequest = getRequest(content, headers, "updateContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier) + getResult(ApiId.UPDATE_ASSET, contentActor, contentRequest, version = apiVersion) + } + + def upload(identifier: String, fileFormat: Option[String], validation: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = requestFormData(identifier) + content.putAll(headers) + val contentRequest = getRequest(content, headers, "uploadContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.putAll(Map("identifier" -> identifier, "params" -> UploadParams(fileFormat, validation.map(_.toBoolean))).asJava) + getResult(ApiId.UPLOAD_ASSET, contentActor, contentRequest, version = apiVersion) + } + + def uploadPreSigned(identifier: String, `type`: Option[String])= Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "type" -> `type`.getOrElse("assets")).asJava) + val contentRequest = getRequest(content, headers, "uploadPreSignedUrl") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.UPLOAD_PRE_SIGNED_ASSET, contentActor, contentRequest) + } + + def copy(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val contentRequest = getRequest(content, headers, "copy") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.COPY_ASSET, assetActor, contentRequest, version = apiVersion) + } +} diff --git a/content-api/content-service/app/controllers/v4/CollectionController.scala b/content-api/content-service/app/controllers/v4/CollectionController.scala new file mode 100644 index 000000000..1362bcba8 --- /dev/null +++ b/content-api/content-service/app/controllers/v4/CollectionController.scala @@ -0,0 +1,187 @@ +package controllers.v4 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import controllers.BaseController +import javax.inject.{Inject, Named} +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId} + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext} +@Singleton +class CollectionController @Inject()(@Named(ActorNames.CONTENT_ACTOR) contentActor: ActorRef, @Named(ActorNames.COLLECTION_ACTOR) collectionActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + val objectType = "Collection" + val schemaName: String = "collection" + val version = "1.0" + val apiVersion = "4.0" + + /** + * This Api end point takes a body + * Content Identifier the unique identifier of a content, can either be provided or will be generated + * primaryCategory, mimeType, name and code are mandatory + * + * @returns identifier and versionKey + */ + def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + if(!validatePrimaryCategory(content)) + getErrorResponse(ApiId.CREATE_COLLECTION, apiVersion, "VALIDATION_ERROR", "primaryCategory is a mandatory parameter") + else if(validateContentType(content)) + getErrorResponse(ApiId.CREATE_COLLECTION, apiVersion, "VALIDATION_ERROR", "contentType cannot be set from request.") + else { + val contentRequest = getRequest(content, headers, "createContent", true) + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_COLLECTION, contentActor, contentRequest, version = apiVersion) + } + } + + /** + * This Api end point takes 3 parameters + * Content Identifier the unique identifier of a content + * Mode in which the content can be viewed (default read or edit) + * Fields are metadata that should be returned to visualize + * + * @param identifier + * @param mode + * @param fields + * @return + */ + def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "readContent") + setRequestContext(readRequest, version, objectType, schemaName) + getResult(ApiId.READ_COLLECTION, contentActor, readRequest, version = apiVersion) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + val contentRequest = getRequest(content, headers, "updateContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier) + getResult(ApiId.UPDATE_COLLECTION, contentActor, contentRequest, version = apiVersion) + } + + def addHierarchy() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "addHierarchy") + contentRequest.put("mode", "edit") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.ADD_HIERARCHY_V4, collectionActor, contentRequest) + } + + def removeHierarchy() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "removeHierarchy") + contentRequest.put("mode", "edit") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.REMOVE_HIERARCHY_V4, collectionActor, contentRequest, version = apiVersion) + } + + def updateHierarchy() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val data = body.getOrDefault("data", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + data.putAll(headers) + val contentRequest = getRequest(data, headers, "updateHierarchy") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.UPDATE_HIERARCHY_V4, collectionActor, contentRequest, version = apiVersion) + } + + def getHierarchy(identifier: String, mode: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("rootId" -> identifier, "mode" -> mode.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "getHierarchy") + setRequestContext(readRequest, version, objectType, null) + getResult(ApiId.GET_HIERARCHY_V4, collectionActor, readRequest, version = apiVersion) + } + + def getBookmarkHierarchy(identifier: String, bookmarkId: String, mode: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("rootId" -> identifier, "bookmarkId" -> bookmarkId, "mode" -> mode.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "getHierarchy") + setRequestContext(readRequest, version, objectType, null) + getResult(ApiId.GET_HIERARCHY_V4, collectionActor, readRequest, version = apiVersion) + } + + def flag(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val contentRequest = getRequest(content, headers, "flagContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier) + getResult(ApiId.FlAG_COLLECTION, contentActor, contentRequest, version = apiVersion) + } + + def acceptFlag(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val acceptRequest = getRequest(content, headers, "acceptFlag") + setRequestContext(acceptRequest, version, objectType, schemaName) + getResult(ApiId.ACCEPT_FLAG_COLLECTION, contentActor, acceptRequest, version = apiVersion) + } + + def discard(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val discardRequest = getRequest(content, headers, "discardContent") + setRequestContext(discardRequest, version, objectType, schemaName) + getResult(ApiId.DISCARD_COLLECTION, contentActor, discardRequest, version = apiVersion) + } + def retire(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.put("identifier", identifier) + content.putAll(headers) + val contentRequest = getRequest(content, headers, "retireContent") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.RETIRE_COLLECTION, contentActor, contentRequest, version = apiVersion) + } + + def collectionLinkDialCode(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "linkDIALCode") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("linkType", "collection") + contentRequest.getContext.put("identifier", identifier) + getResult(ApiId.LINK_DIAL_COLLECTION, contentActor, contentRequest, version = apiVersion) + } + + def copy(identifier: String, mode: Option[String], copyType: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse(""), "copyType" -> copyType).asJava) + val contentRequest = getRequest(content, headers, "copy") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.COPY_COLLECTION, contentActor, contentRequest) + } +} diff --git a/content-api/content-service/app/controllers/v4/ContentController.scala b/content-api/content-service/app/controllers/v4/ContentController.scala new file mode 100644 index 000000000..3cf2d92a6 --- /dev/null +++ b/content-api/content-service/app/controllers/v4/ContentController.scala @@ -0,0 +1,166 @@ +package controllers.v4 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import controllers.BaseController +import javax.inject.{Inject, Named} +import org.sunbird.models.UploadParams +import play.api.mvc.ControllerComponents +import utils.{ActorNames, ApiId} + +import scala.collection.JavaConverters._ + +import scala.concurrent.{ExecutionContext} + +@Singleton +class ContentController @Inject()(@Named(ActorNames.CONTENT_ACTOR) contentActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { + + val objectType = "Content" + val schemaName: String = "content" + val version = "1.0" + val apiVersion = "4.0" + + + def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + content.putAll(headers) + if(!validatePrimaryCategory(content)) + getErrorResponse(ApiId.CREATE_CONTENT, apiVersion, "VALIDATION_ERROR", "primaryCategory is a mandatory parameter") + else if(validateContentType(content)) + getErrorResponse(ApiId.CREATE_CONTENT, apiVersion, "VALIDATION_ERROR", "contentType cannot be set from request.") + else { + val contentRequest = getRequest(content, headers, "createContent", true) + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_ASSET, contentActor, contentRequest, version = apiVersion) + } + } + + /** + * This Api end point takes 3 parameters + * Content Identifier the unique identifier of a content + * Mode in which the content can be viewed (default read or edit) + * Fields are metadata that should be returned to visualize + * + * @param identifier Identifier of the content + * @param mode Mode to read the data edit or published + * @param fields List of fields to return in the response + */ + def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "readContent") + setRequestContext(readRequest, version, objectType, schemaName) + getResult(ApiId.READ_CONTENT, contentActor, readRequest, version = apiVersion) + } + + def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + content.putAll(headers) + val contentRequest = getRequest(content, headers, "updateContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier); + getResult(ApiId.UPDATE_CONTENT, contentActor, contentRequest, version = apiVersion) + } + + def flag(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val contentRequest = getRequest(content, headers, "flagContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier) + getResult(ApiId.FlAG_CONTENT, contentActor, contentRequest, version = apiVersion) + } + + def acceptFlag(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val acceptRequest = getRequest(content, headers, "acceptFlag") + setRequestContext(acceptRequest, version, objectType, schemaName) + getResult(ApiId.ACCEPT_FLAG, contentActor, acceptRequest) + } + + + def discard(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier).asJava) + val discardRequest = getRequest(content, headers, "discardContent") + setRequestContext(discardRequest, version, objectType, schemaName) + getResult(ApiId.DISCARD_CONTENT, contentActor, discardRequest) + } + def retire(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.put("identifier", identifier) + content.putAll(headers) + val contentRequest = getRequest(content, headers, "retireContent") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.RETIRE_CONTENT, contentActor, contentRequest, version = apiVersion) + } + + def linkDialCode() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "linkDIALCode") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("linkType", "content") + getResult(ApiId.LINK_DIAL_CONTENT, contentActor, contentRequest, version = apiVersion) + } + + def upload(identifier: String, fileFormat: Option[String], validation: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = requestFormData(identifier) + content.putAll(headers) + val contentRequest = getRequest(content, headers, "uploadContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.putAll(Map("identifier" -> identifier, "params" -> UploadParams(fileFormat, validation.map(_.toBoolean))).asJava) + getResult(ApiId.UPLOAD_CONTENT, contentActor, contentRequest, version = apiVersion) + } + + + def copy(identifier: String, mode: Option[String], copyType: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse(""), "copyType" -> copyType).asJava) + val contentRequest = getRequest(content, headers, "copy") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.COPY_CONTENT, contentActor, contentRequest, version = apiVersion) + } + + def uploadPreSigned(identifier: String, `type`: Option[String])= Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "type" -> `type`.getOrElse("assets")).asJava) + val contentRequest = getRequest(content, headers, "uploadPreSignedUrl") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.UPLOAD_PRE_SIGNED_CONTENT, contentActor, contentRequest, version = apiVersion) + } + + def importContent() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + body.putAll(headers) + val contentRequest = getRequest(body, headers, "importContent") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.IMPORT_CONTENT, contentActor, contentRequest, version = apiVersion) + } + +} diff --git a/content-api/content-service/app/controllers/v4/EventController.scala b/content-api/content-service/app/controllers/v4/EventController.scala new file mode 100644 index 000000000..3b609d32b --- /dev/null +++ b/content-api/content-service/app/controllers/v4/EventController.scala @@ -0,0 +1,70 @@ +package controllers.v4 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import play.api.mvc.{Action, AnyContent, ControllerComponents} +import utils.{ActorNames, ApiId, Constants} + +import javax.inject.{Inject, Named} +import scala.collection.JavaConverters.mapAsJavaMapConverter +import scala.concurrent.ExecutionContext + +@Singleton +class EventController @Inject()(@Named(ActorNames.EVENT_ACTOR) eventActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends ContentController(eventActor, cc, actorSystem) { + + override val objectType = "Event" + override val schemaName: String = "event" + + override def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + content.putAll(headers) + if(validateContentType(content)) + getErrorResponse(ApiId.CREATE_EVENT, apiVersion, "VALIDATION_ERROR", "contentType cannot be set from request.") + else { + val contentRequest = getRequest(content, headers, "createContent", false) + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_EVENT, eventActor, contentRequest, version = apiVersion) + } + } + + override def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap[String, Object]() + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "readContent") + setRequestContext(readRequest, version, objectType, schemaName) + readRequest.getContext.put(Constants.RESPONSE_SCHEMA_NAME, schemaName); + getResult(ApiId.READ_CONTENT, eventActor, readRequest, version = apiVersion) + } + + override def update(identifier: String) = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; + if (content.containsKey("status")) { + getErrorResponse(ApiId.UPDATE_EVENT, apiVersion, "VALIDATION_ERROR", "status update is restricted, use status APIs.") + } else { + content.putAll(headers) + val contentRequest = getRequest(content, headers, "updateContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier); + getResult(ApiId.UPDATE_EVENT, eventActor, contentRequest, version = apiVersion) + } + } + + def publish(identifier: String): Action[AnyContent] = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap[String, Object]() + content.put("status", "Live") + content.put("identifier", identifier) + content.putAll(headers) + val contentRequest = getRequest(content, headers, "publishContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier); + getResult(ApiId.PUBLISH_EVENT, eventActor, contentRequest, version = apiVersion) + } + +} \ No newline at end of file diff --git a/content-api/content-service/app/controllers/v4/EventSetController.scala b/content-api/content-service/app/controllers/v4/EventSetController.scala new file mode 100644 index 000000000..76f014098 --- /dev/null +++ b/content-api/content-service/app/controllers/v4/EventSetController.scala @@ -0,0 +1,78 @@ +package controllers.v4 + +import akka.actor.{ActorRef, ActorSystem} +import com.google.inject.Singleton +import play.api.mvc.{Action, AnyContent, ControllerComponents} +import utils.{ActorNames, ApiId, Constants} + +import javax.inject.{Inject, Named} +import scala.collection.JavaConverters.mapAsJavaMapConverter +import scala.concurrent.ExecutionContext + +@Singleton +class EventSetController @Inject()(@Named(ActorNames.EVENT_SET_ACTOR) eventSetActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends CollectionController(eventSetActor, eventSetActor, cc, actorSystem) { + override val objectType = "EventSet" + override val schemaName: String = "eventset" + + override def create() = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + if(validateContentType(content)) + getErrorResponse(ApiId.CREATE_EVENT_SET, apiVersion, "VALIDATION_ERROR", "contentType cannot be set from request.") + else { + val contentRequest = getRequest(content, headers, "createContent", false) + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.CREATE_EVENT_SET, eventSetActor, contentRequest, version = apiVersion) + } + } + + override def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "readContent") + setRequestContext(readRequest, version, objectType, schemaName) + readRequest.getContext.put(Constants.RESPONSE_SCHEMA_NAME, schemaName); + getResult(ApiId.READ_COLLECTION, eventSetActor, readRequest, version = apiVersion) + } + + def getHierarchy(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] + content.putAll(headers) + content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asJava) + val readRequest = getRequest(content, headers, "getHierarchy") + setRequestContext(readRequest, version, objectType, schemaName) + getResult(ApiId.READ_COLLECTION, eventSetActor, readRequest, version = apiVersion) + } + + override def update(identifier: String): Action[AnyContent] = Action.async { implicit request => + val headers = commonHeaders() + val body = requestBody() + val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]] + if (content.containsKey("status")) { + getErrorResponse(ApiId.UPDATE_EVENT_SET, apiVersion, "VALIDATION_ERROR", "status update is restricted, use status APIs.") + } else { + content.putAll(headers) + val contentRequest = getRequest(content, headers, "updateContent") + setRequestContext(contentRequest, version, objectType, schemaName) + contentRequest.getContext.put("identifier", identifier) + getResult(ApiId.UPDATE_EVENT_SET, eventSetActor, contentRequest, version = apiVersion) + } + } + + def publish(identifier: String): Action[AnyContent] = Action.async { implicit request => + val headers = commonHeaders() + val content = new java.util.HashMap[String, Object]() + content.put("identifier", identifier) + content.putAll(headers) + val contentRequest = getRequest(content, headers, "publishContent") + setRequestContext(contentRequest, version, objectType, schemaName) + getResult(ApiId.PUBLISH_EVENT_SET, eventSetActor, contentRequest, version = apiVersion) + } + + +} \ No newline at end of file diff --git a/content-api/content-service/app/filters/AccessLogFilter.scala b/content-api/content-service/app/filters/AccessLogFilter.scala new file mode 100644 index 000000000..5e30b5579 --- /dev/null +++ b/content-api/content-service/app/filters/AccessLogFilter.scala @@ -0,0 +1,45 @@ +package filters + +import akka.util.ByteString +import javax.inject.Inject +import org.sunbird.telemetry.util.TelemetryAccessEventUtil +import play.api.Logging +import play.api.libs.streams.Accumulator +import play.api.mvc._ + +import scala.concurrent.ExecutionContext +import scala.collection.JavaConverters._ + +class AccessLogFilter @Inject() (implicit ec: ExecutionContext) extends EssentialFilter with Logging { + + val xHeaderNames = Map("x-session-id" -> "X-Session-ID", "X-Consumer-ID" -> "x-consumer-id", "x-device-id" -> "X-Device-ID", "x-app-id" -> "APP_ID", "x-authenticated-userid" -> "X-Authenticated-Userid", "x-channel-id" -> "X-Channel-Id") + + def apply(nextFilter: EssentialAction) = new EssentialAction { + def apply(requestHeader: RequestHeader) = { + + val startTime = System.currentTimeMillis + + val accumulator: Accumulator[ByteString, Result] = nextFilter(requestHeader) + + accumulator.map { result => + val endTime = System.currentTimeMillis + val requestTime = endTime - startTime + + val path = requestHeader.uri + if(!path.contains("/health")){ + val headers = requestHeader.headers.headers.groupBy(_._1).mapValues(_.map(_._2)) + val appHeaders = headers.filter(header => xHeaderNames.keySet.contains(header._1.toLowerCase)) + .map(entry => (xHeaderNames.get(entry._1.toLowerCase()).get, entry._2.head)) + val otherDetails = Map[String, Any]("StartTime" -> startTime, "env" -> "content", + "RemoteAddress" -> requestHeader.remoteAddress, + "ContentLength" -> result.body.contentLength.getOrElse(0), + "Status" -> result.header.status, "Protocol" -> "http", + "path" -> path, + "Method" -> requestHeader.method.toString) + TelemetryAccessEventUtil.writeTelemetryEventLog((otherDetails ++ appHeaders).asInstanceOf[Map[String, AnyRef]].asJava) + } + result.withHeaders("Request-Time" -> requestTime.toString) + } + } + } + } \ No newline at end of file diff --git a/content-api/content-service/app/handlers/SignalHandler.scala b/content-api/content-service/app/handlers/SignalHandler.scala new file mode 100644 index 000000000..4cad301c1 --- /dev/null +++ b/content-api/content-service/app/handlers/SignalHandler.scala @@ -0,0 +1,33 @@ +package handlers + +import java.util.concurrent.TimeUnit + +import akka.actor.ActorSystem +import javax.inject.{Inject, Singleton} +import org.slf4j.LoggerFactory +import play.api.inject.DefaultApplicationLifecycle +import sun.misc.Signal + +import scala.concurrent.duration.Duration + +@Singleton +class SignalHandler @Inject()(implicit actorSystem: ActorSystem, lifecycle: DefaultApplicationLifecycle) { + val LOG = LoggerFactory.getLogger(classOf[SignalHandler]) + val STOP_DELAY = Duration.create(30, TimeUnit.SECONDS) + var isShuttingDown = false + + println("Initializing SignalHandler...") + Signal.handle(new Signal("TERM"), new sun.misc.SignalHandler() { + override def handle(signal: Signal): Unit = { + // $COVERAGE-OFF$ Disabling scoverage as this code is impossible to test + isShuttingDown = true + println("Termination required, swallowing SIGTERM to allow current requests to finish. : " + System.currentTimeMillis()) + actorSystem.scheduler.scheduleOnce(STOP_DELAY)(() => { + println("ApplicationLifecycle stop triggered... : " + System.currentTimeMillis()) + lifecycle.stop() + })(actorSystem.dispatcher) + // $COVERAGE-ON + } + }) +} + diff --git a/content-api/content-service/app/modules/ContentModule.scala b/content-api/content-service/app/modules/ContentModule.scala new file mode 100644 index 000000000..a701a048e --- /dev/null +++ b/content-api/content-service/app/modules/ContentModule.scala @@ -0,0 +1,27 @@ +package modules + +import com.google.inject.AbstractModule +import org.sunbird.channel.actors.ChannelActor +import org.sunbird.content.actors.{AppActor, AssetActor, CategoryActor, CollectionActor, ContentActor, EventActor, EventSetActor, HealthActor, LicenseActor} +import play.libs.akka.AkkaGuiceSupport +import utils.ActorNames + +class ContentModule extends AbstractModule with AkkaGuiceSupport { + + override def configure() = { + // $COVERAGE-OFF$ Disabling scoverage as this code is impossible to test + //super.configure() + bindActor(classOf[HealthActor], ActorNames.HEALTH_ACTOR) + bindActor(classOf[ContentActor], ActorNames.CONTENT_ACTOR) + bindActor(classOf[LicenseActor], ActorNames.LICENSE_ACTOR) + bindActor(classOf[CollectionActor], ActorNames.COLLECTION_ACTOR) + bindActor(classOf[EventActor], ActorNames.EVENT_ACTOR) + bindActor(classOf[EventSetActor], ActorNames.EVENT_SET_ACTOR) + bindActor(classOf[ChannelActor], ActorNames.CHANNEL_ACTOR) + bindActor(classOf[CategoryActor], ActorNames.CATEGORY_ACTOR) + bindActor(classOf[AssetActor], ActorNames.ASSET_ACTOR) + bindActor(classOf[AppActor], ActorNames.APP_ACTOR) + println("Initialized application actors...") + // $COVERAGE-ON + } +} diff --git a/content-api/content-service/app/utils/ActorNames.scala b/content-api/content-service/app/utils/ActorNames.scala new file mode 100644 index 000000000..a98a6ddf8 --- /dev/null +++ b/content-api/content-service/app/utils/ActorNames.scala @@ -0,0 +1,16 @@ +package utils + +object ActorNames { + + final val HEALTH_ACTOR = "healthActor" + final val CONTENT_ACTOR = "contentActor" + final val LICENSE_ACTOR = "licenseActor" + final val COLLECTION_ACTOR = "collectionActor" + final val EVENT_ACTOR = "eventActor" + final val EVENT_SET_ACTOR = "eventSetActor" + final val CHANNEL_ACTOR = "channelActor" + final val CATEGORY_ACTOR = "categoryActor" + final val ASSET_ACTOR = "assetActor" + final val APP_ACTOR = "appActor" + +} diff --git a/content-api/content-service/app/utils/ApiId.scala b/content-api/content-service/app/utils/ApiId.scala new file mode 100644 index 000000000..a4231b43f --- /dev/null +++ b/content-api/content-service/app/utils/ApiId.scala @@ -0,0 +1,88 @@ +package utils + +object ApiId { + + final val APPLICATION_HEALTH = "api.content.health" + final val APPLICATION_SERVICE_HEALTH = "api.content.service.health" + + //Content APIs + val CREATE_CONTENT = "api.content.create" + val READ_CONTENT = "api.content.read" + val UPDATE_CONTENT = "api.content.update" + val UPLOAD_CONTENT = "api.content.upload" + val RETIRE_CONTENT = "api.content.retire" + val COPY_CONTENT = "api.content.copy" + val UPLOAD_PRE_SIGNED_CONTENT = "api.content.upload.url" + val DISCARD_CONTENT = "api.content.discard" + val FlAG_CONTENT = "api.content.flag" + val ACCEPT_FLAG = "api.content.flag.accept" + val LINK_DIAL_CONTENT = "api.content.dialcode.link" + val IMPORT_CONTENT = "api.content.import" + + // Collection APIs + val ADD_HIERARCHY = "api.content.hierarchy.add" + val REMOVE_HIERARCHY = "api.content.hierarchy.remove" + val UPDATE_HIERARCHY = "api.content.hierarchy.update" + val GET_HIERARCHY = "api.content.hierarchy.get" + val LINK_DIAL_COLLECTION = "api.collection.dialcode.link" + + //License APIs + val CREATE_LICENSE = "api.license.create" + val READ_LICENSE = "api.license.read" + val UPDATE_LICENSE = "api.license.update" + val RETIRE_LICENSE = "api.license.retire" + + //Channel APIs + val CREATE_CHANNEL = "api.channel.create" + val READ_CHANNEL = "api.channel.read" + val UPDATE_CHANNEL = "api.channel.update" + val LIST_CHANNEL = "api.channel.list" + val RETIRE_CHANNEL = "api.channel.retire" + + //Category APIs + val CREATE_CATEGORY = "api.category.create" + val READ_CATEGORY = "api.category.read" + val UPDATE_CATEGORY = "api.category.update" + val RETIRE_CATEGORY = "api.category.retire" + + //Asset V4 apis + val CREATE_ASSET = "api.asset.create" + val READ_ASSET = "api.asset.read" + val UPDATE_ASSET = "api.asset.update" + val UPLOAD_ASSET = "api.asset.upload" + val UPLOAD_PRE_SIGNED_ASSET= "api.asset.upload.url" + val COPY_ASSET = "api.asset.copy" + + + + //Collection V4 apis + val CREATE_COLLECTION = "api.collection.create" + val READ_COLLECTION = "api.collection.read" + val UPDATE_COLLECTION = "api.collection.update" + val RETIRE_COLLECTION = "api.collection.retire" + val COPY_COLLECTION = "api.collection.copy" + val DISCARD_COLLECTION = "api.collection.discard" + val FlAG_COLLECTION = "api.collection.flag" + val ACCEPT_FLAG_COLLECTION = "api.collection.flag.accept" + val ADD_HIERARCHY_V4 = "api.collection.hierarchy.add" + val REMOVE_HIERARCHY_V4 = "api.collection.hierarchy.remove" + val UPDATE_HIERARCHY_V4 = "api.collection.hierarchy.update" + val GET_HIERARCHY_V4 = "api.collection.hierarchy.get" + + //App APIs + val REGISTER_APP = "api.app.register" + val READ_APP = "api.app.read" + val UPDATE_APP = "api.app.update" + val APPROVE_APP = "api.app.approve" + val REJECT_APP = "api.app.reject" + val RETIRE_APP = "api.app.retire" + + val CREATE_EVENT = "api.event.create" + val UPDATE_EVENT = "api.event.update" + + val CREATE_EVENT_SET = "api.eventset.create" + val UPDATE_EVENT_SET = "api.eventset.update" + val PUBLISH_EVENT_SET = "api.eventset.publish" + val PUBLISH_EVENT = "api.event.publish" + +} diff --git a/content-api/content-service/app/utils/Constants.scala b/content-api/content-service/app/utils/Constants.scala new file mode 100644 index 000000000..b10c214e5 --- /dev/null +++ b/content-api/content-service/app/utils/Constants.scala @@ -0,0 +1,18 @@ +package utils + +object Constants { + val SCHEMA_NAME: String = "schemaName" + val RESPONSE_SCHEMA_NAME: String = "responseSchemaName" + val VERSION: String = "version" + val CONTENT_SCHEMA_NAME: String = "content" + val COLLECTION_SCHEMA_NAME: String = "collection" + val ASSET_SCHEMA_NAME: String = "asset" + val CONTENT_VERSION: String = "1.0" + val COLLECTION_VERSION: String = "1.0" + val ASSET_VERSION: String = "1.0" + val COLLECTION_MIME_TYPE: String = "application/vnd.ekstep.content-collection" + val ASSET_CONTENT_TYPE: String = "Asset" + val CONTENT_OBJECT_TYPE: String = "Content" + val COLLECTION_OBJECT_TYPE: String = "Collection" + val ASSET_OBJECT_TYPE: String = "Asset" +} \ No newline at end of file diff --git a/content-api/content-service/app/utils/JavaJsonUtils.scala b/content-api/content-service/app/utils/JavaJsonUtils.scala new file mode 100644 index 000000000..2093c2e33 --- /dev/null +++ b/content-api/content-service/app/utils/JavaJsonUtils.scala @@ -0,0 +1,38 @@ +package utils + +import java.lang.reflect.{ParameterizedType, Type} + +import com.fasterxml.jackson.core.`type`.TypeReference +import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper} + +object JavaJsonUtils { + + @transient val mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); +// mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); +// mapper.setSerializationInclusion(Include.NON_NULL); + + @throws(classOf[Exception]) + def serialize(obj: AnyRef): String = { + mapper.writeValueAsString(obj); + } + + @throws(classOf[Exception]) + def deserialize[T: Manifest](value: String): T = mapper.readValue(value, typeReference[T]); + + private[this] def typeReference[T: Manifest] = new TypeReference[T] { + override def getType = typeFromManifest(manifest[T]) + } + + + private[this] def typeFromManifest(m: Manifest[_]): Type = { + if (m.typeArguments.isEmpty) { m.runtimeClass } + // $COVERAGE-OFF$Disabling scoverage as this code is impossible to test + else new ParameterizedType { + def getRawType = m.runtimeClass + def getActualTypeArguments = m.typeArguments.map(typeFromManifest).toArray + def getOwnerType = null + } + // $COVERAGE-ON$ + } +} diff --git a/content-api/content-service/conf/application.conf b/content-api/content-service/conf/application.conf new file mode 100644 index 000000000..770162730 --- /dev/null +++ b/content-api/content-service/conf/application.conf @@ -0,0 +1,676 @@ +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +# ~~~~~ +# Play uses HOCON as its configuration file format. HOCON has a number +# of advantages over other config formats, but there are two things that +# can be used when modifying settings. +# +# You can include other configuration files in this main application.conf file: +#include "extra-config.conf" +# +# You can declare variables and substitute for them: +#mykey = ${some.value} +# +# And if an environment variable exists when there is no other substitution, then +# HOCON will fall back to substituting environment variable: +#mykey = ${JAVA_HOME} + +## Akka +# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration +# https://www.playframework.com/documentation/latest/JavaAkka#Configuration +# ~~~~~ +# Play uses Akka internally and exposes Akka Streams and actors in Websockets and +# other streaming HTTP responses. +akka { + # "akka.log-config-on-start" is extraordinarly useful because it log the complete + # configuration at INFO level, including defaults and overrides, so it s worth + # putting at the very top. + # + # Put the following in your conf/logback.xml file: + # + # + # + # And then uncomment this line to debug the configuration. + # + #log-config-on-start = true + default-dispatcher { + # This will be used if you have set "executor = "fork-join-executor"" + fork-join-executor { + # Min number of threads to cap factor-based parallelism number to + parallelism-min = 8 + + # The parallelism factor is used to determine thread pool size using the + # following formula: ceil(available processors * factor). Resulting size + # is then bounded by the parallelism-min and parallelism-max values. + parallelism-factor = 32.0 + + # Max number of threads to cap factor-based parallelism number to + parallelism-max = 64 + + # Setting to "FIFO" to use queue like peeking mode which "poll" or "LIFO" to use stack + # like peeking mode which "pop". + task-peeking-mode = "FIFO" + } + } + actors-dispatcher { + type = "Dispatcher" + executor = "fork-join-executor" + fork-join-executor { + parallelism-min = 8 + parallelism-factor = 32.0 + parallelism-max = 64 + } + # Throughput for default Dispatcher, set to 1 for as fair as possible + throughput = 1 + } + actor { + deployment { + /contentActor + { + router = smallest-mailbox-pool + nr-of-instances = 10 + dispatcher = actors-dispatcher + } + /healthActor + { + nr-of-instances = 10 + dispatcher = actors-dispatcher + } + /assetActor + { + router = smallest-mailbox-pool + nr-of-instances = 10 + dispatcher = actors-dispatcher + } + } + } +} + +## Secret key +# http://www.playframework.com/documentation/latest/ApplicationSecret +# ~~~~~ +# The secret key is used to sign Play's session cookie. +# This must be changed for production, but we don't recommend you change it in this file. +play.http.secret.key = a-long-secret-to-calm-the-rage-of-the-entropy-gods + +## Modules +# https://www.playframework.com/documentation/latest/Modules +# ~~~~~ +# Control which modules are loaded when Play starts. Note that modules are +# the replacement for "GlobalSettings", which are deprecated in 2.5.x. +# Please see https://www.playframework.com/documentation/latest/GlobalSettings +# for more information. +# +# You can also extend Play functionality by using one of the publically available +# Play modules: https://playframework.com/documentation/latest/ModuleDirectory +play.modules { + # By default, Play will load any class called Module that is defined + # in the root package (the "app" directory), or you can define them + # explicitly below. + # If there are any built-in modules that you want to enable, you can list them here. + enabled += modules.ContentModule + + # If there are any built-in modules that you want to disable, you can list them here. + #disabled += "" +} + +## IDE +# https://www.playframework.com/documentation/latest/IDE +# ~~~~~ +# Depending on your IDE, you can add a hyperlink for errors that will jump you +# directly to the code location in the IDE in dev mode. The following line makes +# use of the IntelliJ IDEA REST interface: +#play.editor="http://localhost:63342/api/file/?file=%s&line=%s" + +## Internationalisation +# https://www.playframework.com/documentation/latest/JavaI18N +# https://www.playframework.com/documentation/latest/ScalaI18N +# ~~~~~ +# Play comes with its own i18n settings, which allow the user's preferred language +# to map through to internal messages, or allow the language to be stored in a cookie. +play.i18n { + # The application languages + langs = [ "en" ] + + # Whether the language cookie should be secure or not + #langCookieSecure = true + + # Whether the HTTP only attribute of the cookie should be set to true + #langCookieHttpOnly = true +} + +## Play HTTP settings +# ~~~~~ +play.http { + ## Router + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # Define the Router object to use for this application. + # This router will be looked up first when the application is starting up, + # so make sure this is the entry point. + # Furthermore, it's assumed your route file is named properly. + # So for an application router like `my.application.Router`, + # you may need to define a router file `conf/my.application.routes`. + # Default to Routes in the root package (aka "apps" folder) (and conf/routes) + #router = my.application.Router + + ## Action Creator + # https://www.playframework.com/documentation/latest/JavaActionCreator + # ~~~~~ + #actionCreator = null + + ## ErrorHandler + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # If null, will attempt to load a class called ErrorHandler in the root package, + #errorHandler = null + + ## Session & Flash + # https://www.playframework.com/documentation/latest/JavaSessionFlash + # https://www.playframework.com/documentation/latest/ScalaSessionFlash + # ~~~~~ + session { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + + # Sets the max-age field of the cookie to 5 minutes. + # NOTE: this only sets when the browser will discard the cookie. Play will consider any + # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, + # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. + #maxAge = 300 + + # Sets the domain on the session cookie. + #domain = "example.com" + } + + flash { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + } +} + +play.http.parser.maxDiskBuffer = 10MB +parsers.anyContent.maxLength = 10MB + +play.server.provider = play.core.server.NettyServerProvider + +## Netty Provider +# https://www.playframework.com/documentation/latest/SettingsNetty +# ~~~~~ +play.server.netty { + # Whether the Netty wire should be logged + log.wire = true + + # If you run Play on Linux, you can use Netty's native socket transport + # for higher performance with less garbage. + transport = "native" +} + +## WS (HTTP Client) +# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS +# ~~~~~ +# The HTTP client primarily used for REST APIs. The default client can be +# configured directly, but you can also create different client instances +# with customized settings. You must enable this by adding to build.sbt: +# +# libraryDependencies += ws // or javaWs if using java +# +play.ws { + # Sets HTTP requests not to follow 302 requests + #followRedirects = false + + # Sets the maximum number of open HTTP connections for the client. + #ahc.maxConnectionsTotal = 50 + + ## WS SSL + # https://www.playframework.com/documentation/latest/WsSSL + # ~~~~~ + ssl { + # Configuring HTTPS with Play WS does not require programming. You can + # set up both trustManager and keyManager for mutual authentication, and + # turn on JSSE debugging in development with a reload. + #debug.handshake = true + #trustManager = { + # stores = [ + # { type = "JKS", path = "exampletrust.jks" } + # ] + #} + } +} + +## Cache +# https://www.playframework.com/documentation/latest/JavaCache +# https://www.playframework.com/documentation/latest/ScalaCache +# ~~~~~ +# Play comes with an integrated cache API that can reduce the operational +# overhead of repeated requests. You must enable this by adding to build.sbt: +# +# libraryDependencies += cache +# +play.cache { + # If you want to bind several caches, you can bind the individually + #bindCaches = ["db-cache", "user-cache", "session-cache"] +} + +## Filter Configuration +# https://www.playframework.com/documentation/latest/Filters +# ~~~~~ +# There are a number of built-in filters that can be enabled and configured +# to give Play greater security. +# +play.filters { + + # Enabled filters are run automatically against Play. + # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. + enabled = [filters.AccessLogFilter] + + # Disabled filters remove elements from the enabled list. + # disabled += filters.CSRFFilter + + + ## CORS filter configuration + # https://www.playframework.com/documentation/latest/CorsFilter + # ~~~~~ + # CORS is a protocol that allows web applications to make requests from the browser + # across different domains. + # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has + # dependencies on CORS settings. + cors { + # Filter paths by a whitelist of path prefixes + #pathPrefixes = ["/some/path", ...] + + # The allowed origins. If null, all origins are allowed. + #allowedOrigins = ["http://www.example.com"] + + # The allowed HTTP methods. If null, all methods are allowed + #allowedHttpMethods = ["GET", "POST"] + } + + ## Security headers filter configuration + # https://www.playframework.com/documentation/latest/SecurityHeaders + # ~~~~~ + # Defines security headers that prevent XSS attacks. + # If enabled, then all options are set to the below configuration by default: + headers { + # The X-Frame-Options header. If null, the header is not set. + #frameOptions = "DENY" + + # The X-XSS-Protection header. If null, the header is not set. + #xssProtection = "1; mode=block" + + # The X-Content-Type-Options header. If null, the header is not set. + #contentTypeOptions = "nosniff" + + # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. + #permittedCrossDomainPolicies = "master-only" + + # The Content-Security-Policy header. If null, the header is not set. + #contentSecurityPolicy = "default-src 'self'" + } + + ## Allowed hosts filter configuration + # https://www.playframework.com/documentation/latest/AllowedHostsFilter + # ~~~~~ + # Play provides a filter that lets you configure which hosts can access your application. + # This is useful to prevent cache poisoning attacks. + hosts { + # Allow requests to example.com, its subdomains, and localhost:9000. + #allowed = [".example.com", "localhost:9000"] + } +} + +play.http.parser.maxMemoryBuffer = 50MB +akka.http.parsing.max-content-length = 50MB + +schema.base_path = "../../schemas/" +content.hierarchy.removed_props_for_leafNodes=["collections","children","usedByContent","item_sets","methods","libraries","editorState"] + +languageCode { + assamese : "as" + bengali : "bn" + english : "en" + gujarati : "gu" + hindi : "hi" + kannada : "ka" + marathi : "mr" + odia : "or" + tamil : "ta" + telugu : "te" +} + +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd"] + +collection.keyspace = "hierarchy_store" +content.keyspace = "content_store" + +# Learning-Service Configuration +content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit", "event"] + +# Cassandra Configuration +//content.keyspace.name=content_store +//content.keyspace.table=content_data +#TODO: Add Configuration for assessment. e.g: question_data +orchestrator.keyspace.name=script_store +orchestrator.keyspace.table=script_data +cassandra.lp.connection="127.0.0.1:9042" +cassandra.lpa.connection="127.0.0.1:9042" + +# Redis Configuration +redis.host=localhost +redis.port=6379 +redis.maxConnections=128 + +#Condition to enable publish locally +content.publish_task.enabled=true + +#directory location where store unzip file +dist.directory=/data/tmp/dist/ +output.zipfile=/data/tmp/story.zip +source.folder=/data/tmp/temp2/ +save.directory=/data/tmp/temp/ + +# Content 2 vec analytics URL +CONTENT_TO_VEC_URL="http://172.31.27.233:9000/content-to-vec" + +# FOR CONTENT WORKFLOW PIPELINE (CWP) + +#--Content Workflow Pipeline Mode +OPERATION_MODE=TEST + +#--Maximum Content Package File Size Limit in Bytes (50 MB) +MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=52428800 + +#--Maximum Asset File Size Limit in Bytes (20 MB) +MAX_ASSET_FILE_SIZE_LIMIT=20971520 + +#--No of Retry While File Download Fails +RETRY_ASSET_DOWNLOAD_COUNT=1 + +#Google-vision-API +google.vision.tagging.enabled = false + +#Orchestrator env properties +env="https://dev.ekstep.in/api/learning" + +#Current environment +cloud_storage.env=dev + + +#Folder configuration +cloud_storage.content.folder=content +cloud_storage.asset.folder=assets +cloud_storage.artefact.folder=artifact +cloud_storage.bundle.folder=bundle +cloud_storage.media.folder=media +cloud_storage.ecar.folder=ecar_files + +# Media download configuration +content.media.base.url="https://dev.open-sunbird.org" +plugin.media.base.url="https://dev.open-sunbird.org" + +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + +# Elasticsearch properties +search.es_conn_info="localhost:9200" +search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","domain","subject","description^10","keywords^25","ageGroup^10","filter^10","theme^10","genre^10","objects^25","contentType^100","language^200","teachingMode^25","skills^10","learningObjective^10","curriculum^100","gradeLevel^100","developer^100","attributions^10","owner^50","text","words","releaseNotes"] +search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] +search.batch.size=500 +search.connection.timeout=30 +platform-api-url="http://localhost:8080/language-service" +MAX_ITERATION_COUNT_FOR_SAMZA_JOB=2 + + +# DIAL Code Configuration +dialcode.keyspace.name="dialcode_store" +dialcode.keyspace.table="dial_code" +dialcode.max_count=1000 + +# System Configuration +system.config.keyspace.name="dialcode_store" +system.config.table="system_config" + +#Publisher Configuration +publisher.keyspace.name="dialcode_store" +publisher.keyspace.table="publisher" + +#DIAL Code Generator Configuration +dialcode.strip.chars="0" +dialcode.length=6.0 +dialcode.large.prime_number=1679979167 + +#DIAL Code ElasticSearch Configuration +dialcode.index=true +dialcode.object_type="DialCode" + +framework.max_term_creation_limit=200 + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + +# Kafka configuration details +kafka.topics.instruction="local.learning.job.request" +kafka.urls="localhost:9092" + +#Youtube Standard Licence Validation +learning.content.youtube.validate.license=true +learning.content.youtube.application.name=fetch-youtube-license +youtube.license.regex.pattern=["\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)","^([A-Za-z0-9\\-\\_]*)"] + +#Top N Config for Search Telemetry +telemetry_env=dev +telemetry.search.topn=5 + +installation.id=ekstep + +content.copy.invalid_statusList=["Flagged","FlaggedDraft","FraggedReview","Retired", "Processing"] +content.copy.props_to_remove=["downloadUrl", "artifactUrl", "variants", + "createdOn", "collections", "children", "lastUpdatedOn", "SYS_INTERNAL_LAST_UPDATED_ON", + "versionKey", "s3Key", "status", "pkgVersion", "toc_url", "mimeTypesCount", + "contentTypesCount", "leafNodesCount", "childNodes", "prevState", "lastPublishedOn", + "flagReasons", "compatibilityLevel", "size", "publishChecklist", "publishComment", + "LastPublishedBy", "rejectReasons", "rejectComment", "gradeLevel", "subject", + "medium", "board", "topic", "purpose", "subtopic", "contentCredits", + "owner", "collaborators", "creators", "contributors", "badgeAssertions", "dialcodes", + "concepts", "keywords", "reservedDialcodes", "dialcodeRequired", "leafNodes", "sYS_INTERNAL_LAST_UPDATED_ON", "prevStatus", "lastPublishedBy", "streamingUrl"] + +content.copy.origin_data=["name", "author", "license", "organisation"] + + +channel.default="in.ekstep" + +# DialCode Link API Config +learning.content.link_dialcode_validation=true +dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/search" +dialcode.api.authorization=auth_key + +# Language-Code Configuration +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] + +# Kafka send event to topic enable +kafka.topic.send.enable=true + +learning.valid_license=["creativeCommon"] +learning.service_provider=["youtube"] + +stream.mime.type=video/mp4 +compositesearch.index.name="compositesearch" + +hierarchy.keyspace.name=hierarchy_store +content.hierarchy.table=content_hierarchy +framework.hierarchy.table=framework_hierarchy +objectcategorydefinition.keyspace=category_store + +# Kafka topic for definition update event. +kafka.topic.system.command="dev.system.command" + +learning.reserve_dialcode.content_type=["TextBook"] +# restrict.metadata.objectTypes=["Content", "ContentImage", "AssessmentItem", "Channel", "Framework", "Category", "CategoryInstance", "Term"] + +#restrict.metadata.objectTypes="Content,ContentImage" + +publish.collection.fullecar.disable=true + +# Consistency Level for Multi Node Cassandra cluster +cassandra.lp.consistency.level=QUORUM + + + + +content.nested.fields="badgeAssertions,targets,badgeAssociations" + +content.cache.ttl=86400 +content.cache.enable=true +collection.cache.enable=true +content.discard.status=["Draft","FlagDraft"] + +framework.categories_cached=["subject", "medium", "gradeLevel", "board"] +framework.cache.ttl=86400 +framework.cache.read=true + + +# Max size(width/height) of thumbnail in pixels +max.thumbnail.size.pixels=150 + +collection.image.migration.enabled=true + + + +cloud_storage.upload.url.ttl=600 + +composite.search.url="https://dev.sunbirded.org/action/composite/v3/search" + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + +content.h5p.library.path="https://s3.ap-south-1.amazonaws.com/ekstep-public-prod/content/templates/h5p-library-v2.zip" + +kafka.topics.graph.event="sunbirddev.learning.graph.events" +content.discard.status=["Draft","FlagDraft"] +content.discard.remove_publish_data=["compatibilityLevel", "lastPublishedOn", "pkgVersion", "leafNodesCount", "downloadUrl", "variants"] + +# DIAL Link +dial_service { + api { + base_url = "https://qa.ekstep.in/api" + auth_key = "auth_key" + } +} +content.link_dialcode.validation=true +content.link_dialcode.max_limit=10 +# This is added to handle large artifacts sizes differently +content.artifact.size.for_online=209715200 + +# Content Import API Config +import { + request_size_limit=2 + output_topic_name="local.auto.creation.job.request" + required_props=["name","code","mimeType","contentType","artifactUrl","framework", "channel"] +} + +contentTypeToPrimaryCategory { + ClassroomTeachingVideo: "Explanation Content" + ConceptMap: "Learning Resource" + Course: ["Course", "Curriculum Course", "Professional Development Course"] + CuriosityQuestionSet: "Practice Question Set" + eTextBook: "eTextbook" + Event: "Event" + EventSet: "Event Set" + ExperientialResource: "Learning Resource" + ExplanationResource: "Explanation Content" + ExplanationVideo: "Explanation Content" + FocusSpot: "Teacher Resource" + LearningOutcomeDefinition: "Teacher Resource" + MarkingSchemeRubric: "Teacher Resource" + PedagogyFlow: "Teacher Resource" + PracticeQuestionSet: "Practice Question Set" + PracticeResource: "Practice Question Set" + SelfAssess: "Course Assessment" + TeachingMethod: "Teacher Resource" + TextBook: "Digital Textbook" + Collection: "Content Playlist" + ExplanationReadingMaterial: "Learning Resource" + LearningActivity: "Learning Resource" + LessonPlan: "Content Playlist" + LessonPlanResource: "Teacher Resource" + PreviousBoardExamPapers: "Learning Resource" + TVLesson: "Explanation Content" + OnboardingResource: "Learning Resource" + ReadingMaterial: "Learning Resource" + Template: "Template" + Asset: "Asset" + Plugin: "Plugin" + LessonPlanUnit: "Lesson Plan Unit" + CourseUnit: "Course Unit" + TextBookUnit: "Textbook Unit" + Asset: "Certificate Template" +} + +resourceTypeToPrimaryCategory { + Learn: "Learning Resource" + Read: "Learning Resource" + Practice: "Learning Resource" + Teach: "Teacher Resource" + Test: "Learning Resource" + Experiment: "Learning Resource" + LessonPlan: "Teacher Resource" +} + +mimeTypeToPrimaryCategory { + "application/vnd.ekstep.h5p-archive": ["Learning Resource"] + "application/vnd.ekstep.html-archive": ["Learning Resource"] + "application/vnd.android.package-archive": ["Learning Resource"] + "video/webm": ["Explanation Content"] + "video/x-youtube": ["Explanation Content"] + "video/mp4": ["Explanation Content"] + "application/pdf": ["Learning Resource", "Teacher Resource"] + "application/epub": ["Learning Resource", "Teacher Resource"] + "application/vnd.ekstep.ecml-archive": ["Learning Resource", "Teacher Resource"] + "text/x-url": ["Learnin Resource", "Teacher Resource"] +} + +#Default objectCategory mapping for channel + +channel { + content{ + primarycategories=["Course Assessment", "Event", "eTextbook", "Explanation Content", "Learning Resource", "Practice Question Set", "Teacher Resource"] + additionalcategories=["Classroom Teaching Video", "Concept Map", "Curiosity Question Set", "Experiential Resource", "Explanation Video", "Focus Spot", "Learning Outcome Definition", "Lesson Plan", "Marking Scheme Rubric", "Pedagogy Flow", "Previous Board Exam Papers", "TV Lesson", "Textbook"] + } + collection { + primarycategories=["Content Playlist", "Course", "Digital Textbook", "Explanation Content", "Event Set"] + additionalcategories=["Textbook", "Lesson Plan", "TV Lesson"] + } + asset { + primarycategories=["Asset", "CertAsset", "Certificate Template"] + additionalcategories=[] + } +} + +#config for primary categories mapping for collection units +collection.primarycategories.mapping.enabled=true +#config for objectType as content for collection units +objecttype.as.content.enabled=true diff --git a/content-api/content-service/conf/logback.xml b/content-api/content-service/conf/logback.xml new file mode 100644 index 000000000..73529d622 --- /dev/null +++ b/content-api/content-service/conf/logback.xml @@ -0,0 +1,28 @@ + + + + + + + + + + %d %msg%n + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content-api/content-service/conf/routes b/content-api/content-service/conf/routes new file mode 100644 index 000000000..23840e8e2 --- /dev/null +++ b/content-api/content-service/conf/routes @@ -0,0 +1,117 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ +GET /health controllers.HealthController.health +GET /service/health controllers.HealthController.serviceHealth + +# Content APIs +POST /content/v3/create controllers.v3.ContentController.create +PATCH /content/v3/update/:identifier controllers.v3.ContentController.update(identifier:String) +GET /content/v3/read/:identifier controllers.v3.ContentController.read(identifier:String, mode:Option[String], fields:Option[String]) +POST /content/v3/upload/url/:identifier controllers.v3.ContentController.uploadPreSigned(identifier:String, type: Option[String]) +POST /content/v3/upload/:identifier controllers.v3.ContentController.upload(identifier:String, fileFormat: Option[String], validation: Option[String]) +POST /content/v3/copy/:identifier controllers.v3.ContentController.copy(identifier:String, mode:Option[String], type:String ?= "deep") +POST /content/v3/dialcode/link controllers.v3.ContentController.linkDialCode() +POST /content/v3/import controllers.v3.ContentController.importContent() + +# Content APIs - with mock response +POST /content/v3/flag/:identifier controllers.v3.ContentController.flag(identifier:String) +POST /content/v3/bundle controllers.v3.ContentController.bundle +POST /content/v3/flag/accept/:identifier controllers.v3.ContentController.acceptFlag(identifier:String) +POST /content/v3/flag/reject/:identifier controllers.v3.ContentController.rejectFlag(identifier:String) +POST /content/v3/publish/:identifier controllers.v3.ContentController.publish(identifier:String) +POST /content/v3/public/publish/:identifier controllers.v3.ContentController.publish(identifier:String) +POST /content/v3/review/:identifier controllers.v3.ContentController.review(identifier:String) +DELETE /content/v3/discard/:identifier controllers.v3.ContentController.discard(identifier:String) +DELETE /content/v3/retire/:identifier controllers.v3.ContentController.retire(identifier:String) +POST /content/v3/dialcode/reserve/:identifier controllers.v3.ContentController.reserveDialCode(identifier:String) +PATCH /content/v3/dialcode/release/:identifier controllers.v3.ContentController.releaseDialcodes(identifier:String) +POST /content/v3/reject/:identifier controllers.v3.ContentController.rejectContent(identifier:String) +POST /content/v3/unlisted/publish/:identifier controllers.v3.ContentController.publishUnlisted(identifier:String) + +# Collection APIs +PATCH /content/v3/hierarchy/add controllers.v3.ContentController.addHierarchy +DELETE /content/v3/hierarchy/remove controllers.v3.ContentController.removeHierarchy +PATCH /content/v3/hierarchy/update controllers.v3.ContentController.updateHierarchy +GET /content/v3/hierarchy/:identifier controllers.v3.ContentController.getHierarchy(identifier:String, mode:Option[String]) +GET /content/v3/hierarchy/:identifier/:bookmarkId controllers.v3.ContentController.getBookmarkHierarchy(identifier: String, bookmarkId: String, mode: Option[String]) +POST /collection/v3/dialcode/link/:identifier @controllers.v3.ContentController.collectionLinkDialCode(identifier:String) + +#License APIs +POST /license/v3/create controllers.v3.LicenseController.create +GET /license/v3/read/:identifier controllers.v3.LicenseController.read(identifier: String, fields:Option[String]) +PATCH /license/v3/update/:identifier controllers.v3.LicenseController.update(identifier: String) +DELETE /license/v3/retire/:identifier controllers.v3.LicenseController.retire(identifier: String) + +#These are routes for Channel +POST /channel/v3/create controllers.v3.ChannelController.create +PATCH /channel/v3/update/:identifier controllers.v3.ChannelController.update(identifier: String) +GET /channel/v3/read/:identifier controllers.v3.ChannelController.read(identifier: String) +DELETE /channel/v3/retire/:identifier controllers.v3.ChannelController.retire(identifier: String) + +# Category APIs +POST /category/v3/create controllers.v3.CategoryController.create +GET /category/v3/read/:identifier controllers.v3.CategoryController.read(identifier: String, fields:Option[String]) +PATCH /category/v3/update/:identifier controllers.v3.CategoryController.update(identifier: String) +DELETE /category/v3/retire/:identifier controllers.v3.CategoryController.retire(identifier: String) + +#Asset V4 Api's +POST /asset/v4/create controllers.v4.AssetController.create +PATCH /asset/v4/update/:identifier controllers.v4.AssetController.update(identifier:String) +GET /asset/v4/read/:identifier controllers.v4.AssetController.read(identifier:String, mode:Option[String], fields:Option[String]) +POST /asset/v4/upload/:identifier controllers.v4.AssetController.upload(identifier:String, fileFormat: Option[String], validation: Option[String]) +POST /asset/v4/upload/url/:identifier controllers.v4.AssetController.uploadPreSigned(identifier:String, type: Option[String]) +POST /asset/v4/copy/:identifier controllers.v4.AssetController.copy(identifier:String) + +# Collection v4 Api's +POST /collection/v4/create controllers.v4.CollectionController.create +PATCH /collection/v4/update/:identifier controllers.v4.CollectionController.update(identifier:String) +GET /collection/v4/read/:identifier controllers.v4.CollectionController.read(identifier:String, mode:Option[String], fields:Option[String]) +POST /collection/v4/flag/:identifier controllers.v4.CollectionController.flag(identifier:String) +POST /collection/v4/flag/accept/:identifier controllers.v4.CollectionController.acceptFlag(identifier:String) +DELETE /collection/v4/discard/:identifier controllers.v4.CollectionController.discard(identifier:String) +DELETE /collection/v4/retire/:identifier controllers.v4.CollectionController.retire(identifier:String) +PATCH /collection/v4/hierarchy/add controllers.v4.CollectionController.addHierarchy +DELETE /collection/v4/hierarchy/remove controllers.v4.CollectionController.removeHierarchy +PATCH /collection/v4/hierarchy/update controllers.v4.CollectionController.updateHierarchy +GET /collection/v4/hierarchy/:identifier controllers.v4.CollectionController.getHierarchy(identifier:String, mode:Option[String]) +GET /collection/v4/hierarchy/:identifier/:bookmarkId controllers.v4.CollectionController.getBookmarkHierarchy(identifier: String, bookmarkId: String, mode: Option[String]) +POST /collection/v4/dialcode/link/:identifier controllers.v4.CollectionController.collectionLinkDialCode(identifier:String) +POST /collection/v4/copy/:identifier controllers.v4.CollectionController.copy(identifier:String, mode:Option[String], type:String ?= "deep") + + +# Content v4 APIs +POST /content/v4/create controllers.v4.ContentController.create +PATCH /content/v4/update/:identifier controllers.v4.ContentController.update(identifier:String) +GET /content/v4/read/:identifier controllers.v4.ContentController.read(identifier:String, mode:Option[String], fields:Option[String]) +POST /content/v4/upload/url/:identifier controllers.v4.ContentController.uploadPreSigned(identifier:String, type: Option[String]) +POST /content/v4/upload/:identifier controllers.v4.ContentController.upload(identifier:String, fileFormat: Option[String], validation: Option[String]) +POST /content/v4/copy/:identifier controllers.v4.ContentController.copy(identifier:String, mode:Option[String], type:String ?= "deep") +POST /content/v4/dialcode/link controllers.v4.ContentController.linkDialCode() +POST /content/v4/import controllers.v4.ContentController.importContent() +POST /content/v4/flag/:identifier controllers.v4.ContentController.flag(identifier:String) +POST /content/v4/flag/accept/:identifier controllers.v4.ContentController.acceptFlag(identifier:String) +DELETE /content/v4/discard/:identifier controllers.v4.ContentController.discard(identifier:String) +DELETE /content/v4/retire/:identifier controllers.v4.ContentController.retire(identifier:String) + +# App v4 APIs +POST /app/v4/register controllers.v4.AppController.register +PATCH /app/v4/update/:identifier controllers.v4.AppController.update(identifier:String) +GET /app/v4/read/:identifier controllers.v4.AppController.read(identifier:String, fields:Option[String]) + +# Event APIs +POST /event/v4/create controllers.v4.EventController.create +PATCH /event/v4/update/:identifier controllers.v4.EventController.update(identifier:String) +POST /event/v4/publish/:identifier controllers.v4.EventController.publish(identifier:String) +GET /event/v4/read/:identifier controllers.v4.EventController.read(identifier:String, mode:Option[String], fields:Option[String]) +DELETE /event/v4/discard/:identifier controllers.v4.EventController.discard(identifier:String) +DELETE /private/event/v4/retire/:identifier controllers.v4.EventController.retire(identifier:String) + +# EventSet v4 Api's +POST /eventset/v4/create controllers.v4.EventSetController.create +PUT /eventset/v4/update/:identifier controllers.v4.EventSetController.update(identifier:String) +POST /eventset/v4/publish/:identifier controllers.v4.EventSetController.publish(identifier:String) +GET /eventset/v4/hierarchy/:identifier controllers.v4.EventSetController.getHierarchy(identifier:String, mode:Option[String], fields:Option[String]) +GET /eventset/v4/read/:identifier controllers.v4.EventSetController.read(identifier:String, mode:Option[String], fields:Option[String]) +DELETE /eventset/v4/discard/:identifier controllers.v4.EventSetController.discard(identifier:String) +DELETE /private/eventset/v4/retire/:identifier controllers.v4.EventSetController.retire(identifier:String) \ No newline at end of file diff --git a/content-api/content-service/pom.xml b/content-api/content-service/pom.xml new file mode 100755 index 000000000..8a2db3be2 --- /dev/null +++ b/content-api/content-service/pom.xml @@ -0,0 +1,188 @@ + + + 4.0.0 + content-service + content-service + play2 + + org.sunbird + content-api + 1.0-SNAPSHOT + + + + scalaz-bintray + Scalaz Bintray - releases + https://dl.bintray.com/scalaz/releases/ + + false + + + + + + typesafe-releases-plugins + https://repo.typesafe.com/typesafe/releases/ + + false + + + + + 2.7.2 + 1.0.0-rc5 + 1.0.0 + 2.11 + + + + + com.google.guava + guava + 18.0 + + + com.google.inject + guice + 3.0 + + + com.google.inject.extensions + guice-assistedinject + 3.0 + + + com.typesafe.play + play_${scala.major.version} + ${play2.version} + + + guava + com.google.guava + + + + + com.typesafe.play + play-guice_${scala.major.version} + ${play2.version} + + + guava + com.google.guava + + + + + com.typesafe.play + filters-helpers_${scala.major.version} + ${play2.version} + + + com.typesafe.play + play-logback_${scala.major.version} + ${play2.version} + runtime + + + com.typesafe.play + play-netty-server_${scala.major.version} + ${play2.version} + runtime + + + org.scala-lang + scala-library + ${scala.version} + + + org.sunbird + content-actors + 1.0-SNAPSHOT + jar + + + slf4j-log4j12 + org.slf4j + + + + + org.scalatest + scalatest_${scala.maj.version} + 3.1.2 + test + + + com.typesafe.play + play-specs2_${scala.maj.version} + ${play2.version} + test + + + guava + com.google.guava + + + + + org.joda + joda-convert + 2.2.1 + + + com.github.danielwegener + logback-kafka-appender + 0.2.0-RC2 + + + + + ${basedir}/app + ${basedir}/test + + + ${basedir}/conf + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + + **/*Spec.java + **/*Test.java + + + + + com.google.code.play2-maven-plugin + play2-maven-plugin + ${play2.plugin.version} + true + + + com.google.code.sbt-compiler-maven-plugin + sbt-compiler-maven-plugin + ${sbt-compiler.plugin.version} + + -feature -deprecation -Xfatal-warnings + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + .*RoutesPrefix.*;.*Routes.*;.*javascript.* + + + + + + diff --git a/content-api/content-service/test/controllers/base/BaseSpec.scala b/content-api/content-service/test/controllers/base/BaseSpec.scala new file mode 100644 index 000000000..24f3f4070 --- /dev/null +++ b/content-api/content-service/test/controllers/base/BaseSpec.scala @@ -0,0 +1,38 @@ +package controllers.base + +import com.typesafe.config.ConfigFactory +import modules.TestModule +import org.specs2.mutable.Specification +import play.api.inject.guice.GuiceApplicationBuilder +import play.api.libs.json.Json +import play.api.mvc.Result +import play.api.test.Helpers.{POST, contentAsString, contentType, defaultAwaitTimeout, route, status, _} +import play.api.test.{FakeHeaders, FakeRequest} + +import scala.concurrent.Future + +class BaseSpec extends Specification { + implicit val app = new GuiceApplicationBuilder() + .disable(classOf[modules.ContentModule]) + .bindings(new TestModule) + .build + implicit val config = ConfigFactory.load(); + + def post(apiURL: String, request: String, h: FakeHeaders = FakeHeaders(Seq())) + : Future[Result] = { + val headers = h.add(("content-type", "application/json")) + route(app, FakeRequest(POST, apiURL, headers, Json.toJson(Json.parse(request)))).get + } + + def isOK(response: Future[Result]) { + status(response) must equalTo(OK) + contentType(response) must beSome.which(_ == "application/json") + contentAsString(response) must contain(""""status":"successful"""") + } + + def hasClientError(response: Future[Result]) { + status(response) must equalTo(BAD_REQUEST) + contentType(response) must beSome.which(_ == "application/json") + contentAsString(response) must contain(""""err":"CLIENT_ERROR","status":"failed"""") + } +} diff --git a/content-api/content-service/test/controllers/v3/BadRequestSpec.scala b/content-api/content-service/test/controllers/v3/BadRequestSpec.scala new file mode 100644 index 000000000..3eb4c7218 --- /dev/null +++ b/content-api/content-service/test/controllers/v3/BadRequestSpec.scala @@ -0,0 +1,19 @@ +package controllers.v3 + +import org.junit.runner._ +import org.specs2.runner._ + +import org.specs2.mutable.Specification +import play.api.inject.guice.GuiceApplicationBuilder +import play.api.test.Helpers._ +import play.api.test.FakeRequest + +@RunWith(classOf[JUnitRunner]) +class BadRequestSpec extends Specification { + implicit val app = new GuiceApplicationBuilder().build + "Application" should { + "send 404 on a bad request - /boum" in { + route(app, FakeRequest(GET, "/boum")) must beSome.which (status(_) == NOT_FOUND) + } + } +} diff --git a/content-api/content-service/test/controllers/v3/CategorySpec.scala b/content-api/content-service/test/controllers/v3/CategorySpec.scala new file mode 100644 index 000000000..eb972da06 --- /dev/null +++ b/content-api/content-service/test/controllers/v3/CategorySpec.scala @@ -0,0 +1,40 @@ +package controllers.v3 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status, _} + +@RunWith(classOf[JUnitRunner]) +class CategorySpec extends BaseSpec { + + "Category Controller " should { + + val controller = app.injector.instanceOf[controllers.v3.CategoryController] + + "return success response for create API" in { + val result = controller.create()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for update API" in { + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val result = controller.read("do_123", None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for retire API" in { + val result = controller.retire("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + } +} diff --git a/content-api/content-service/test/controllers/v3/ChannelSpec.scala b/content-api/content-service/test/controllers/v3/ChannelSpec.scala new file mode 100644 index 000000000..cc9f93db6 --- /dev/null +++ b/content-api/content-service/test/controllers/v3/ChannelSpec.scala @@ -0,0 +1,44 @@ +package controllers.v3 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status} +import play.api.test.Helpers._ + +@RunWith(classOf[JUnitRunner]) +class ChannelSpec extends BaseSpec { + "Channel Controller " should { + "return success response for create API" in { + val controller = app.injector.instanceOf[controllers.v3.ChannelController] + val result = controller.create()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + } + "return success response for update API" in { + val controller = app.injector.instanceOf[controllers.v3.ChannelController] + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val controller = app.injector.instanceOf[controllers.v3.ChannelController] + val result = controller.read("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for retire API" in { + val controller = app.injector.instanceOf[controllers.v3.ChannelController] + val result = controller.retire("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + +} + + + diff --git a/content-api/content-service/test/controllers/v3/ContentSpec.scala b/content-api/content-service/test/controllers/v3/ContentSpec.scala new file mode 100644 index 000000000..40a050f93 --- /dev/null +++ b/content-api/content-service/test/controllers/v3/ContentSpec.scala @@ -0,0 +1,223 @@ +package controllers.v3 + +import java.io.File + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import org.sunbird.models.UploadParams +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status} +import play.api.test.Helpers._ +import play.api.libs.Files.{SingletonTemporaryFileCreator, TemporaryFile} +import play.api.libs.json.JsValue +import play.api.mvc.MultipartFormData +import play.api.mvc.MultipartFormData.{BadPart, FilePart} +import play.api.libs.json.Json + +@RunWith(classOf[JUnitRunner]) +class ContentSpec extends BaseSpec { + + "Content Controller " should { + "return success response for create API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val json: JsValue = Json.parse("""{"request": {"content": {"contentType": "Asset"}}}""") + val fakeRequest = FakeRequest("POST", "/content/v3/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for update API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.read("do_123", None, None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for hierarchy add API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.addHierarchy()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for hierarchy remove API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.removeHierarchy()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for hierarchy get API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.getHierarchy("do_123", None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for flag API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.flag("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for acceptFlag API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.acceptFlag("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for rejectFlag API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.rejectFlag("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for bundle API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.bundle()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for publish API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.publish("0123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for review API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.review("0123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for discard API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.discard("0123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for retire API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.retire("0123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for linkDialCode API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.linkDialCode()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for collectionLinkDialCode API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.collectionLinkDialCode("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for reserveDialCode API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.reserveDialCode("01234")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for releaseDialcodes API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.releaseDialcodes("01234")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for rejectContent API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.rejectContent("01234")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for publishUnlisted API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.publishUnlisted("01234")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for presignedUrl upload API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.uploadPreSigned("01234", None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + } + "return success response for upload API with file" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]](), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", None, None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for upload API with fileUrl" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]]("fileUrl" -> Seq("https://abc.com/content/sample.pdf"), "filePath" -> Seq("/program/id")), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", None, None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for upload API with fileUrl and fileFormat" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]](), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", Some("composed-h5p-zip"), None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for importContent API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.importContent()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for hierarchy update API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val json: JsValue = Json.parse("""{"request": {"data": {"mimeType": "application/vnd.ekstep.content-collection"}}}""") + val fakeRequest = FakeRequest("POST", "/content/v3/hierarchy/update").withJsonBody(json) + val result = controller.updateHierarchy()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for bookmark hierarchy API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val result = controller.getBookmarkHierarchy("do_123", "do_1234", Option.apply("read"))(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for copy API" in { + val controller = app.injector.instanceOf[controllers.v3.ContentController] + val json: JsValue = Json.parse("""{"request": {"content": {"primaryCategory": "Asset"}}}""") + val fakeRequest = FakeRequest("POST", "/content/v3/copy/do_123").withJsonBody(json) + val result = controller.copy("do_123", Option.apply("read"), "shallowCopy")(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } +} diff --git a/content-api/content-service/test/controllers/v3/HealthSpec.scala b/content-api/content-service/test/controllers/v3/HealthSpec.scala new file mode 100644 index 000000000..d8d7b0430 --- /dev/null +++ b/content-api/content-service/test/controllers/v3/HealthSpec.scala @@ -0,0 +1,19 @@ +package controllers.v3 + +import controllers.base.BaseSpec +import org.junit.runner._ +import org.specs2.runner._ +import play.api.test.Helpers._ +import play.api.test._ + +@RunWith(classOf[JUnitRunner]) +class HealthSpec extends BaseSpec { + "Application" should { + "return api health status report - successful" in { + val controller = app.injector.instanceOf[controllers.HealthController] + val result = controller.health()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + } +} diff --git a/content-api/content-service/test/controllers/v3/LicenseSpec.scala b/content-api/content-service/test/controllers/v3/LicenseSpec.scala new file mode 100644 index 000000000..926adfadd --- /dev/null +++ b/content-api/content-service/test/controllers/v3/LicenseSpec.scala @@ -0,0 +1,41 @@ +package controllers.v3 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status} +import play.api.test.Helpers._ + +@RunWith(classOf[JUnitRunner]) +class LicenseSpec extends BaseSpec { + + "License Controller " should { + + val controller = app.injector.instanceOf[controllers.v3.LicenseController] + + "return success response for create API" in { + val result = controller.create()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for update API" in { + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val result = controller.read("do_123", None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for retire API" in { + val result = controller.retire("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + } +} diff --git a/content-api/content-service/test/controllers/v4/AppSpec.scala b/content-api/content-service/test/controllers/v4/AppSpec.scala new file mode 100644 index 000000000..74ea948fe --- /dev/null +++ b/content-api/content-service/test/controllers/v4/AppSpec.scala @@ -0,0 +1,39 @@ +package controllers.v4 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status} +import play.api.test.Helpers._ +import play.api.libs.json.JsValue +import play.api.libs.json.Json + +@RunWith(classOf[JUnitRunner]) +class AppSpec extends BaseSpec { + + "AppController " should { + "return success response for register API" in { + val controller = app.injector.instanceOf[controllers.v4.AppController] + val json: JsValue = Json.parse("""{"request":{"app":{"name":"Test Integration App","description":"Description of Test Integration App","provider":{"name":"Test Organisation","copyright":"CC BY 4.0"},"osType":"android","osMetadata":{"packageId":"org.test.integration","appVersion":"1.0","compatibilityVer":"1.0"},"appTarget":{"mimeType":["application/pdf"]}}}}""") + val fakeRequest = FakeRequest("POST", "/app/v4/register").withJsonBody(json) + val result = controller.register()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for update API" in { + val controller = app.injector.instanceOf[controllers.v4.AppController] + val result = controller.update("android-org.test.integration")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val controller = app.injector.instanceOf[controllers.v4.AppController] + val result = controller.read("android-org.test.integration", None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + } +} diff --git a/content-api/content-service/test/controllers/v4/AssetSpec.scala b/content-api/content-service/test/controllers/v4/AssetSpec.scala new file mode 100644 index 000000000..3f4692e3f --- /dev/null +++ b/content-api/content-service/test/controllers/v4/AssetSpec.scala @@ -0,0 +1,114 @@ +package controllers.v4 + +import java.io.File + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status} +import play.api.test.Helpers._ +import play.api.libs.Files.{SingletonTemporaryFileCreator, TemporaryFile} +import play.api.libs.json.JsValue +import play.api.mvc.MultipartFormData +import play.api.mvc.MultipartFormData.{BadPart, FilePart} +import play.api.libs.json.Json + +@RunWith(classOf[JUnitRunner]) +class AssetSpec extends BaseSpec { + + "AssetController " should { + "return success response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val json: JsValue = Json.parse("""{"request": {"asset": { "primaryCategory": "Asset"}}}""") + val fakeRequest = FakeRequest("POST", "/asset/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + + "return success response for update API" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val result = controller.read("do_123", None, None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for upload API with file" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]](), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", None, None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for upload API with fileUrl" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]]("fileUrl" -> Seq("https://abc.com/content/sample.pdf"), "filePath" -> Seq("/program/id")), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", None, None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for upload API with fileUrl and fileFormat" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]](), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", Some("composed-h5p-zip"), None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for pre signed Url upload API" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val result = controller.uploadPreSigned("01234", None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for copy API" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val json: JsValue = Json.parse("""{"request": {"asset": { "name": "Asset-Test"}}}""") + val fakeRequest = FakeRequest("POST", "/asset/v4/copy ").withJsonBody(json) + val result = controller.copy("01234")(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + } + + "Asset controller with invalid request " should { + "return client error response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val json: JsValue = Json.parse("""{"request": {"asset": { "contentType": "Asset"}}}""") + val fakeRequest = FakeRequest("POST", "/asset/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + status(result) must equalTo(BAD_REQUEST) + } + } + + "Asset controller with invalid request " should { + "return client error response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.AssetController] + val json: JsValue = Json.parse("""{"request": {"asset": { "name": "Asset"}}}""") + val fakeRequest = FakeRequest("POST", "/asset/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + status(result) must equalTo(BAD_REQUEST) + } + } +} diff --git a/content-api/content-service/test/controllers/v4/CollectionSpec.scala b/content-api/content-service/test/controllers/v4/CollectionSpec.scala new file mode 100644 index 000000000..6f570eae5 --- /dev/null +++ b/content-api/content-service/test/controllers/v4/CollectionSpec.scala @@ -0,0 +1,138 @@ +package controllers.v4 + + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status} +import play.api.test.Helpers._ +import play.api.libs.json.JsValue +import play.api.libs.json.Json + +@RunWith(classOf[JUnitRunner]) +class CollectionSpec extends BaseSpec { + + "Collection Controller " should { + "return success response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val json: JsValue = Json.parse("""{"request": {"collection": {"name": "Collection","primaryCategory": "Digital Textbook"}}}""") + val fakeRequest = FakeRequest("POST", "/collection/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for update API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.read("do_123", None, None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for hierarchy add API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.addHierarchy()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for hierarchy remove API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.removeHierarchy()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for hierarchy get API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.getHierarchy("do_123", None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for flag API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.flag("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for acceptFlag API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.acceptFlag("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for discard API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.discard("0123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for retire API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.retire("0123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for collectionLinkDialCode API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.collectionLinkDialCode("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + } + + "return success response for hierarchy update API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val json: JsValue = Json.parse("""{"request": {"data": {"mimeType": "application/vnd.ekstep.content-collection"}}}""") + val fakeRequest = FakeRequest("POST", "/collection/v4/hierarchy/update").withJsonBody(json) + val result = controller.updateHierarchy()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for bookmark hierarchy API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val result = controller.getBookmarkHierarchy("do_123", "do_1234", Option.apply("read"))(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for copy API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val json: JsValue = Json.parse("""{"request": {"collection": {"primaryCategory": "Asset"}}}""") + val fakeRequest = FakeRequest("POST", "/collection/v4/copy/do_123").withJsonBody(json) + val result = controller.copy("do_123", Option.apply("read"), "shallowCopy")(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "Collection Controller with invalid request " should { + "return client error response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val json: JsValue = Json.parse("""{"request": {"collection": { "contentType": "TextBook"}}}""") + val fakeRequest = FakeRequest("POST", "/collection/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + status(result) must equalTo(BAD_REQUEST) + } + } + + "Collection Controller with invalid request " should { + "return client error response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.CollectionController] + val json: JsValue = Json.parse("""{"request": {"collection": { "name": "Textbook"}}}""") + val fakeRequest = FakeRequest("POST", "/collection/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + status(result) must equalTo(BAD_REQUEST) + } + } +} diff --git a/content-api/content-service/test/controllers/v4/ContentSpec.scala b/content-api/content-service/test/controllers/v4/ContentSpec.scala new file mode 100644 index 000000000..98d4deb8d --- /dev/null +++ b/content-api/content-service/test/controllers/v4/ContentSpec.scala @@ -0,0 +1,154 @@ +package controllers.v4 + +import java.io.File + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status} +import play.api.test.Helpers._ +import play.api.libs.Files.{SingletonTemporaryFileCreator, TemporaryFile} +import play.api.libs.json.JsValue +import play.api.mvc.MultipartFormData +import play.api.mvc.MultipartFormData.{BadPart, FilePart} +import play.api.libs.json.Json + +@RunWith(classOf[JUnitRunner]) +class ContentSpec extends BaseSpec { + + "Content Controller " should { + "return success response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val json: JsValue = Json.parse("""{"request": {"content": {"primaryCategory": "Learning Resource"}}}""") + val fakeRequest = FakeRequest("POST", "/content/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for update API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.read("do_123", None, None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for flag API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.flag("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for acceptFlag API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.acceptFlag("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for discard API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.discard("0123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for retire API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.retire("0123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for linkDialCode API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.linkDialCode()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for presignedUrl upload API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.uploadPreSigned("01234", None)(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + } + "return success response for upload API with file" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]](), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", None, None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for upload API with fileUrl" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]]("fileUrl" -> Seq("https://abc.com/content/sample.pdf"), "filePath" -> Seq("/program/id")), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", None, None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for upload API with fileUrl and fileFormat" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val file = new File("test/resources/sample.pdf") + val files = Seq[FilePart[TemporaryFile]](FilePart("file", "sample.pdf", None, SingletonTemporaryFileCreator.create(file.toPath))) + val multipartBody = MultipartFormData(Map[String, Seq[String]](), files, Seq[BadPart]()) + val fakeRequest = FakeRequest().withMultipartFormDataBody(multipartBody) + val result = controller.upload("01234", Some("composed-h5p-zip"), None)(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for importContent API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val result = controller.importContent()(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + + "return success response for copy API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val json: JsValue = Json.parse("""{"request": {"content": {"primaryCategory": "Asset"}}}""") + val fakeRequest = FakeRequest("POST", "/content/v4/copy/do_123").withJsonBody(json) + val result = controller.copy("do_123", Option.apply("read"), "shallowCopy")(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "Content Controller with invalid request " should { + "return client error response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val json: JsValue = Json.parse("""{"request": {"content": { "contentType": "TextBook"}}}""") + val fakeRequest = FakeRequest("POST", "/content/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + status(result) must equalTo(BAD_REQUEST) + } + } + + "Content Controller with invalid request " should { + "return client error response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.ContentController] + val json: JsValue = Json.parse("""{"request": {"content": { "name": "Resource"}}}""") + val fakeRequest = FakeRequest("POST", "/content/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + status(result) must equalTo(BAD_REQUEST) + } + } +} diff --git a/content-api/content-service/test/controllers/v4/EventSetSpec.scala b/content-api/content-service/test/controllers/v4/EventSetSpec.scala new file mode 100644 index 000000000..2fcbcf0ed --- /dev/null +++ b/content-api/content-service/test/controllers/v4/EventSetSpec.scala @@ -0,0 +1,75 @@ +package controllers.v4 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.libs.json.{JsValue, Json} +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status, _} + +@RunWith(classOf[JUnitRunner]) +class EventSetSpec extends BaseSpec { + + "EventSet Controller " should { + "return success response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.EventSetController] + val json: JsValue = Json.parse("""{"request": {"eventset": {"name": "EventSet","primaryCategory": "Event Set"}}}""") + val fakeRequest = FakeRequest("POST", "/eventset/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for update API" in { + val controller = app.injector.instanceOf[controllers.v4.EventSetController] + val result = controller.update("do_123")(FakeRequest("POST", "/eventset/v4/update ")) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val controller = app.injector.instanceOf[controllers.v4.EventSetController] + val result = controller.read("do_123", None, None)(FakeRequest("POST", "/eventset/v4/read ")) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for hierarchy get API" in { + val controller = app.injector.instanceOf[controllers.v4.EventSetController] + val result = controller.getHierarchy("do_123", None, None)(FakeRequest("POST", "/eventset/v4/hierarchy ")) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for discard API" in { + val controller = app.injector.instanceOf[controllers.v4.EventSetController] + val result = controller.discard("0123")(FakeRequest("POST", "/eventset/v4/discard ")) + isOK(result) + status(result) must equalTo(OK) + } + "return success response for retire API" in { + val controller = app.injector.instanceOf[controllers.v4.EventSetController] + val result = controller.retire("0123")(FakeRequest("POST", "/eventset/v4/retire ")) + isOK(result) + status(result) must equalTo(OK) + } + + "return error response when updating status using update API" in { + val controller = app.injector.instanceOf[controllers.v4.EventSetController] + val json: JsValue = Json.parse("""{"request": {"eventset": {"status": "Live"}}}""") + val fakeRequest = FakeRequest("POST", "/eventset/v4/update ").withJsonBody(json) + val result = controller.update("do_123")(fakeRequest) + status(result) must equalTo(BAD_REQUEST) + } + + "return success response for publish API" in { + val controller = app.injector.instanceOf[controllers.v4.EventSetController] + val result = controller.publish("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + } + + +} \ No newline at end of file diff --git a/content-api/content-service/test/controllers/v4/EventSpec.scala b/content-api/content-service/test/controllers/v4/EventSpec.scala new file mode 100644 index 000000000..e184fd701 --- /dev/null +++ b/content-api/content-service/test/controllers/v4/EventSpec.scala @@ -0,0 +1,54 @@ +package controllers.v4 + +import controllers.base.BaseSpec +import org.junit.runner.RunWith +import org.specs2.runner.JUnitRunner +import play.api.libs.json.{JsValue, Json} +import play.api.test.FakeRequest +import play.api.test.Helpers.{OK, status, _} + +@RunWith(classOf[JUnitRunner]) +class EventSpec extends BaseSpec { + + "Event Controller " should { + "return success response for create API" in { + val controller = app.injector.instanceOf[controllers.v4.EventController] + val json: JsValue = Json.parse("""{"request": {"event": {"name": "Event","primaryCategory": "Event"}}}""") + val fakeRequest = FakeRequest("POST", "/event/v4/create ").withJsonBody(json) + val result = controller.create()(fakeRequest) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for read API" in { + val controller = app.injector.instanceOf[controllers.v4.EventController] + val result = controller.read("do_123", None, None)(FakeRequest("POST", "/event/v4/read ")) + isOK(result) + status(result) must equalTo(OK) + } + + "return success response for update API" in { + val controller = app.injector.instanceOf[controllers.v4.EventController] + val result = controller.update("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + "return error response when updating status using update API" in { + val controller = app.injector.instanceOf[controllers.v4.EventController] + val json: JsValue = Json.parse("""{"request": {"event": {"status": "Live"}}}""") + val fakeRequest = FakeRequest("POST", "/event/v4/update ").withJsonBody(json) + val result = controller.update("do_123")(fakeRequest) + status(result) must equalTo(BAD_REQUEST) + } + + "return success response for publish API" in { + val controller = app.injector.instanceOf[controllers.v4.EventController] + val result = controller.publish("do_123")(FakeRequest()) + isOK(result) + status(result) must equalTo(OK) + } + + } + +} \ No newline at end of file diff --git a/content-api/content-service/test/modules/TestModule.scala b/content-api/content-service/test/modules/TestModule.scala new file mode 100644 index 000000000..938b737e2 --- /dev/null +++ b/content-api/content-service/test/modules/TestModule.scala @@ -0,0 +1,35 @@ +package modules + +import com.google.inject.AbstractModule +import org.sunbird.actor.core.BaseActor +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import play.libs.akka.AkkaGuiceSupport +import utils.ActorNames + +import scala.concurrent.{ExecutionContext, Future} + +class TestModule extends AbstractModule with AkkaGuiceSupport { + + override def configure(): Unit = { + bindActor(classOf[TestActor], ActorNames.HEALTH_ACTOR) + bindActor(classOf[TestActor], ActorNames.CONTENT_ACTOR) + bindActor(classOf[TestActor], ActorNames.LICENSE_ACTOR) + bindActor(classOf[TestActor], ActorNames.COLLECTION_ACTOR) + bindActor(classOf[TestActor], ActorNames.CHANNEL_ACTOR) + bindActor(classOf[TestActor], ActorNames.CATEGORY_ACTOR) + bindActor(classOf[TestActor], ActorNames.ASSET_ACTOR) + bindActor(classOf[TestActor], ActorNames.APP_ACTOR) + bindActor(classOf[TestActor], ActorNames.EVENT_SET_ACTOR) + bindActor(classOf[TestActor], ActorNames.EVENT_ACTOR) + println("Test Module is initialized...") + } +} + +class TestActor extends BaseActor { + + implicit val ec: ExecutionContext = getContext().dispatcher + + override def onReceive(request: Request): Future[Response] = { + Future(ResponseHandler.OK) + } +} diff --git a/content-api/content-service/test/resources/sample.pdf b/content-api/content-service/test/resources/sample.pdf new file mode 100644 index 000000000..dbf091df9 Binary files /dev/null and b/content-api/content-service/test/resources/sample.pdf differ diff --git a/content-api/hierarchy-manager/pom.xml b/content-api/hierarchy-manager/pom.xml new file mode 100644 index 000000000..828dac9fc --- /dev/null +++ b/content-api/hierarchy-manager/pom.xml @@ -0,0 +1,136 @@ + + + + content-api + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + + hierarchy-manager + + + + org.sunbird + graph-engine_2.11 + 1.0-SNAPSHOT + jar + + + org.sunbird + platform-common + 1.0-SNAPSHOT + + + org.scala-lang + scala-library + ${scala.version} + + + org.scalatest + scalatest_${scala.maj.version} + 3.1.2 + test + + + org.neo4j + neo4j-bolt + 3.5.0 + test + + + org.neo4j + neo4j-graphdb-api + 3.5.0 + test + + + org.neo4j + neo4j + 3.5.0 + test + + + org.cassandraunit + cassandra-unit + 3.11.2.0 + test + + + httpcore + org.apache.httpcomponents + + + httpclient + org.apache.httpcomponents + + + + + com.mashape.unirest + unirest-java + 1.4.9 + + + + + + src/main/scala + src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + ${scala.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + + test + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + + + \ No newline at end of file diff --git a/content-api/hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala b/content-api/hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala new file mode 100644 index 000000000..86e0efda8 --- /dev/null +++ b/content-api/hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala @@ -0,0 +1,618 @@ +package org.sunbird.managers + +import java.util +import java.util.concurrent.CompletionException + +import com.fasterxml.jackson.databind.ObjectMapper +import org.apache.commons.lang3.StringUtils +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ClientException, ErrorCodes, ResourceNotFoundException, ResponseCode, ServerException} +import org.sunbird.common.{JsonUtils, Platform, Slug} +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.utils.{NodeUtil, ScalaJsonUtils} + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters +import scala.collection.JavaConverters.asJavaIterableConverter +import scala.concurrent.{ExecutionContext, Future} +import com.mashape.unirest.http.HttpResponse +import com.mashape.unirest.http.Unirest +import org.apache.commons.collections4.{CollectionUtils, MapUtils} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.schema.DefinitionNode +import org.sunbird.utils.{HierarchyBackwardCompatibilityUtil, HierarchyConstants, HierarchyErrorCodes} + +object HierarchyManager { + + val schemaName: String = "collection" + val schemaVersion: String = "1.0" + val imgSuffix: String = ".img" + val hierarchyPrefix: String = "hierarchy_" + val statusList = List("Live", "Unlisted", "Flagged") + + val keyTobeRemoved = { + if(Platform.config.hasPath("content.hierarchy.removed_props_for_leafNodes")) + Platform.config.getStringList("content.hierarchy.removed_props_for_leafNodes") + else + java.util.Arrays.asList("collections","children","usedByContent","item_sets","methods","libraries","editorState") + } + + val mapPrimaryCategoriesEnabled: Boolean = if (Platform.config.hasPath("collection.primarycategories.mapping.enabled")) Platform.config.getBoolean("collection.primarycategories.mapping.enabled") else true + val objectTypeAsContentEnabled: Boolean = if (Platform.config.hasPath("objecttype.as.content.enabled")) Platform.config.getBoolean("objecttype.as.content.enabled") else true + + @throws[Exception] + def addLeafNodesToHierarchy(request:Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + validateRequest(request) + val rootNodeFuture = getRootNode(request) + rootNodeFuture.map(rootNode => { + val unitId = request.get("unitId").asInstanceOf[String] + val rootNodeMap = NodeUtil.serialize(rootNode, java.util.Arrays.asList("childNodes", "originData"), schemaName, schemaVersion) + validateShallowCopied(rootNodeMap, "add", rootNode.getIdentifier.replaceAll(imgSuffix, "")) + if(!rootNodeMap.get("childNodes").asInstanceOf[Array[String]].toList.contains(unitId)) { + Future{ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "unitId " + unitId + " does not exist")} + }else { + val hierarchyFuture = fetchHierarchy(request, rootNode.getIdentifier) + hierarchyFuture.map(hierarchy => { + if(hierarchy.isEmpty){ + Future{ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), "hierarchy is empty")} + } else { + val leafNodesFuture = fetchLeafNodes(request) + leafNodesFuture.map(leafNodes => { + updateRootNode(rootNode, request, "add").map(node => { + val updateResponse = updateHierarchy(unitId, hierarchy, leafNodes, node, request, "add") + updateResponse.map(response => { + if(!ResponseHandler.checkError(response)) { + ResponseHandler.OK + .put("rootId", node.getIdentifier.replaceAll(imgSuffix, "")) + .put(unitId, request.get("children")) + }else { + response + } + }) + }).flatMap(f => f) + }).flatMap(f => f) + } + }).flatMap(f => f) + } + }).flatMap(f => f) recoverWith {case e: CompletionException => throw e.getCause} + } + + @throws[Exception] + def removeLeafNodesFromHierarchy(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + validateRequest(request) + val rootNodeFuture = getRootNode(request) + rootNodeFuture.map(rootNode => { + val unitId = request.get("unitId").asInstanceOf[String] + val rootNodeMap = NodeUtil.serialize(rootNode, java.util.Arrays.asList("childNodes", "originData"), schemaName, schemaVersion) + validateShallowCopied(rootNodeMap, "remove", rootNode.getIdentifier.replaceAll(imgSuffix, "")) + if(!rootNodeMap.get("childNodes").asInstanceOf[Array[String]].toList.contains(unitId)) { + Future{ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "unitId " + unitId + " does not exist")} + }else { + val hierarchyFuture = fetchHierarchy(request, rootNode.getIdentifier) + hierarchyFuture.map(hierarchy => { + if(hierarchy.isEmpty){ + Future{ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), "hierarchy is empty")} + } else { + updateRootNode(rootNode, request, "remove").map(node =>{ + val updateResponse = updateHierarchy(unitId, hierarchy, null, node, request, "remove") + updateResponse.map(response => { + if(!ResponseHandler.checkError(response)) { + ResponseHandler.OK.put("rootId", node.getIdentifier.replaceAll(imgSuffix, "")) + } else { + response + } + }) + }).flatMap(f => f) + } + }).flatMap(f => f) + } + }).flatMap(f => f) recoverWith {case e: CompletionException => throw e.getCause} + } + + @throws[Exception] + def getHierarchy(request : Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val mode = request.get("mode").asInstanceOf[String] + if(StringUtils.isNotEmpty(mode) && mode.equals("edit")) + getUnPublishedHierarchy(request) + else + getPublishedHierarchy(request) + } + + @throws[Exception] + def getUnPublishedHierarchy(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val rootNodeFuture = getRootNode(request) + rootNodeFuture.map(rootNode => { + if (StringUtils.equalsIgnoreCase("Retired", rootNode.getMetadata.getOrDefault("status", "").asInstanceOf[String])) { + Future(ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "rootId " + request.get("rootId") + " does not exist")) + } + val bookmarkId = request.get("bookmarkId").asInstanceOf[String] + var metadata: util.Map[String, AnyRef] = NodeUtil.serialize(rootNode, new util.ArrayList[String](), request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + val hierarchy = fetchHierarchy(request, rootNode.getIdentifier) + //TODO: Remove content Mapping for backward compatibility + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(metadata) + hierarchy.map(hierarchy => { + val children = hierarchy.getOrDefault("children", new util.ArrayList[java.util.Map[String, AnyRef]]).asInstanceOf[util.ArrayList[java.util.Map[String, AnyRef]]] + //TODO: Remove content Mapping for backward compatibility + updateContentMappingInChildren(children) + val leafNodeIds = new util.ArrayList[String]() + fetchAllLeafNodes(children, leafNodeIds) + getLatestLeafNodes(leafNodeIds).map(leafNodesMap => { + updateLatestLeafNodes(children, leafNodesMap) + metadata.put("children", children) + metadata.put("identifier", request.get("rootId")) + if(StringUtils.isNotEmpty(bookmarkId)) + metadata = filterBookmarkHierarchy(metadata.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]], bookmarkId) + if (MapUtils.isEmpty(metadata)) { + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "bookmarkId " + bookmarkId + " does not exist") + } else { + ResponseHandler.OK.put("content", metadata) + } + }) + }).flatMap(f => f) + }).flatMap(f => f) recoverWith { case e: ResourceNotFoundException => { + val searchResponse = searchRootIdInElasticSearch(request.get("rootId").asInstanceOf[String]) + searchResponse.map(rootHierarchy => { + if(!rootHierarchy.isEmpty && StringUtils.isNotEmpty(rootHierarchy.asInstanceOf[util.HashMap[String, AnyRef]].get("identifier").asInstanceOf[String])){ + //TODO: Remove content Mapping for backward compatibility + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(rootHierarchy.asInstanceOf[util.HashMap[String, AnyRef]]) + val unPublishedBookmarkHierarchy = getUnpublishedBookmarkHierarchy(request, rootHierarchy.asInstanceOf[util.HashMap[String, AnyRef]].get("identifier").asInstanceOf[String]) + unPublishedBookmarkHierarchy.map(hierarchy => { + if (!hierarchy.isEmpty) { + val children = hierarchy.getOrDefault("children", new util.ArrayList[java.util.Map[String, AnyRef]]).asInstanceOf[util.ArrayList[java.util.Map[String, AnyRef]]] + //TODO: Remove content Mapping for backward compatibility + updateContentMappingInChildren(children) + val leafNodeIds = new util.ArrayList[String]() + fetchAllLeafNodes(children, leafNodeIds) + getLatestLeafNodes(leafNodeIds).map(leafNodesMap => { + updateLatestLeafNodes(children, leafNodesMap) + hierarchy.put("children", children) + }) + ResponseHandler.OK.put("content", hierarchy) + } else + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "rootId " + request.get("rootId") + " does not exist") + }) + } else { + Future(ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "rootId " + request.get("rootId") + " does not exist")) + } + }).flatMap(f => f) + } + } + } + + @throws[Exception] + def getPublishedHierarchy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + val redisHierarchy = RedisCache.get(hierarchyPrefix + request.get("rootId")) + val hierarchyFuture = if (StringUtils.isNotEmpty(redisHierarchy)) { + Future(mapAsJavaMap(Map("content" -> JsonUtils.deserialize(redisHierarchy, classOf[java.util.Map[String, AnyRef]])))) + } else getCassandraHierarchy(request) + hierarchyFuture.map(result => { + if (!result.isEmpty) { + val bookmarkId = request.get("bookmarkId").asInstanceOf[String] + val rootHierarchy = result.get("content").asInstanceOf[util.Map[String, AnyRef]] + if (StringUtils.isEmpty(bookmarkId)) { + ResponseHandler.OK.put("content", 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) + if (MapUtils.isEmpty(bookmarkHierarchy)) { + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "bookmarkId " + bookmarkId + " does not exist") + } else { + ResponseHandler.OK.put("content", bookmarkHierarchy) + } + } + } else + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "rootId " + request.get("rootId") + " does not exist") + }) + } + + def validateRequest(request: Request)(implicit ec: ExecutionContext) = { + val rootId = request.get("rootId").asInstanceOf[String] + val unitId = request.get("unitId").asInstanceOf[String] + val children = request.get("children").asInstanceOf[java.util.List[String]] + + if (StringUtils.isBlank(rootId)) { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "rootId is mandatory") + } + if (StringUtils.isBlank(unitId)) { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "unitId is mandatory") + } + if (null == children || children.isEmpty) { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "children are mandatory") + } + } + + private def getRootNode(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val req = new Request(request) + req.put("identifier", request.get("rootId").asInstanceOf[String]) + req.put("mode", request.get("mode").asInstanceOf[String]) + req.put("fields",request.get("fields").asInstanceOf[java.util.List[String]]) + DataNode.read(req) + } + + def fetchLeafNodes(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + val leafNodes = request.get("children").asInstanceOf[java.util.List[String]] + val req = new Request(request) + req.put("identifiers", leafNodes) + val nodes = DataNode.list(req).map(nodes => { + if(nodes.size() != leafNodes.size()) { + val filteredList = leafNodes.toList.filter(id => !nodes.contains(id)) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Children which are not available are: " + leafNodes) + } + else nodes.toList + }) + nodes + } + + def convertNodeToMap(leafNodes: List[Node])(implicit oec: OntologyEngineContext, ec: ExecutionContext): java.util.List[java.util.Map[String, AnyRef]] = { + leafNodes.map(node => { + val nodeMap:java.util.Map[String,AnyRef] = NodeUtil.serialize(node, null, node.getObjectType.toLowerCase().replace("image", ""), schemaVersion) + nodeMap.keySet().removeAll(keyTobeRemoved) + nodeMap + }) + } + + def addChildrenToUnit(children: java.util.List[java.util.Map[String,AnyRef]], unitId:String, leafNodes: java.util.List[java.util.Map[String, AnyRef]], leafNodeIds: java.util.List[String])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Unit = { + val childNodes = children.filter(child => ("Parent".equalsIgnoreCase(child.get("visibility").asInstanceOf[String]) && unitId.equalsIgnoreCase(child.get("identifier").asInstanceOf[String]))).toList + if(null != childNodes && !childNodes.isEmpty){ + val child = childNodes.get(0) + leafNodes.toList.map(leafNode => validateLeafNodes(child, leafNode)) + val childList = child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]] + val restructuredChildren: java.util.List[java.util.Map[String,AnyRef]] = restructureUnit(childList, leafNodes, leafNodeIds, (child.get("depth").asInstanceOf[Integer] + 1), unitId) + child.put("children", restructuredChildren) + } else { + for(child <- children) { + if(null !=child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) + addChildrenToUnit(child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]], unitId, leafNodes, leafNodeIds) + } + } + } + + def removeChildrenFromUnit(children: java.util.List[java.util.Map[String, AnyRef]], unitId: String, leafNodeIds: java.util.List[String]):Unit = { + val childNodes = children.filter(child => ("Parent".equalsIgnoreCase(child.get("visibility").asInstanceOf[String]) && unitId.equalsIgnoreCase(child.get("identifier").asInstanceOf[String]))).toList + if(null != childNodes && !childNodes.isEmpty){ + val child = childNodes.get(0) + if(null != child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) { + var filteredLeafNodes = child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].filter(existingLeafNode => { + !leafNodeIds.contains(existingLeafNode.get("identifier").asInstanceOf[String]) + }) + var index: Integer = 1 + filteredLeafNodes.toList.sortBy(x => x.get("index").asInstanceOf[Integer]).foreach(node => { + node.put("index", index) + index += 1 + }) + child.put("children", filteredLeafNodes) + } + } else { + for(child <- children) { + if(null !=child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) + removeChildrenFromUnit(child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]], unitId, leafNodeIds) + } + } + } + + def updateRootNode(rootNode: Node, request: Request, operation: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext) = { + val req = new Request(request) + val leafNodes = request.get("children").asInstanceOf[java.util.List[String]] + var childNodes = new java.util.ArrayList[String]() + childNodes.addAll(rootNode.getMetadata.get("childNodes").asInstanceOf[Array[String]].toList) + if(operation.equalsIgnoreCase("add")) + childNodes.addAll(leafNodes) + if(operation.equalsIgnoreCase("remove")) + childNodes.removeAll(leafNodes) + req.put("childNodes", childNodes.distinct.toArray) + req.getContext.put("identifier", rootNode.getIdentifier.replaceAll(imgSuffix, "")) + req.getContext.put("skipValidation", java.lang.Boolean.TRUE) + DataNode.update(req) + } + + def updateHierarchy(unitId: String, hierarchy: java.util.Map[String, AnyRef], leafNodes: List[Node], rootNode: Node, request: Request, operation: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext) = { + val children = hierarchy.get("children").asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] + val leafNodeIds = request.get("children").asInstanceOf[java.util.List[String]] + if("add".equalsIgnoreCase(operation)){ + val leafNodesMap:java.util.List[java.util.Map[String, AnyRef]] = convertNodeToMap(leafNodes) + addChildrenToUnit(children, unitId, leafNodesMap, leafNodeIds) + } + if("remove".equalsIgnoreCase(operation)) { + removeChildrenFromUnit(children,unitId, leafNodeIds) + } + val rootId = rootNode.getIdentifier.replaceAll(imgSuffix, "") + val updatedHierarchy = new java.util.HashMap[String, AnyRef]() + updatedHierarchy.put("identifier", rootId) + updatedHierarchy.put("children", children) + val req = new Request(request) + req.put("hierarchy", ScalaJsonUtils.serialize(updatedHierarchy)) + req.put("identifier", rootNode.getIdentifier) + oec.graphService.saveExternalProps(req) + } + + def restructureUnit(childList: java.util.List[java.util.Map[String, AnyRef]], leafNodes: java.util.List[java.util.Map[String, AnyRef]], leafNodeIds: java.util.List[String], depth: Integer, parent: String): java.util.List[java.util.Map[String, AnyRef]] = { + var maxIndex:Integer = 0 + var leafNodeMap: java.util.Map[String, java.util.Map[String, AnyRef]] = new util.HashMap[String, java.util.Map[String, AnyRef]]() + for(leafNode <- leafNodes){ + leafNodeMap.put(leafNode.get("identifier").asInstanceOf[String], JavaConverters.mapAsJavaMapConverter(leafNode).asJava) + } + var filteredLeafNodes: java.util.List[java.util.Map[String, AnyRef]] = new util.ArrayList[java.util.Map[String, AnyRef]]() + if(null != childList && !childList.isEmpty) { + val childMap:Map[String, java.util.Map[String, AnyRef]] = childList.toList.map(f => f.get("identifier").asInstanceOf[String] -> f).toMap + val existingLeafNodes = childMap.filter(p => leafNodeIds.contains(p._1)) + existingLeafNodes.map(en => { + leafNodeMap.get(en._1).put("index", en._2.get("index").asInstanceOf[Integer]) + }) + filteredLeafNodes = bufferAsJavaList(childList.filter(existingLeafNode => { + !leafNodeIds.contains(existingLeafNode.get("identifier").asInstanceOf[String]) + })) + maxIndex = childMap.values.toList.map(child => child.get("index").asInstanceOf[Integer]).toList.max.asInstanceOf[Integer] + } + leafNodeIds.foreach(id => { + var node = leafNodeMap.get(id) + node.put("parent", parent) + node.put("depth", depth) + if( null == node.get("index")) { + val index:Integer = maxIndex + 1 + node.put("index", index) + maxIndex += 1 + } + filteredLeafNodes.add(node) + }) + filteredLeafNodes + } + + def fetchHierarchy(request: Request, identifier: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Map[String, AnyRef]] = { + val req = new Request(request) + req.put("identifier", identifier) + val responseFuture = oec.graphService.readExternalProps(req, List("hierarchy")) + responseFuture.map(response => { + if (!ResponseHandler.checkError(response)) { + val hierarchyString = response.getResult.toMap.getOrDefault("hierarchy", "").asInstanceOf[String] + if (StringUtils.isNotEmpty(hierarchyString)) { + Future(JsonUtils.deserialize(hierarchyString, classOf[java.util.Map[String, AnyRef]]).toMap) + } else + Future(Map[String, AnyRef]()) + } else if (ResponseHandler.checkError(response) && response.getResponseCode.code() == 404 && Platform.config.hasPath("collection.image.migration.enabled") && Platform.config.getBoolean("collection.image.migration.enabled")) { + req.put("identifier", identifier.replaceAll(".img", "") + ".img") + val responseFuture = oec.graphService.readExternalProps(req, List("hierarchy")) + responseFuture.map(response => { + if (!ResponseHandler.checkError(response)) { + val hierarchyString = response.getResult.toMap.getOrDefault("hierarchy", "").asInstanceOf[String] + if (StringUtils.isNotEmpty(hierarchyString)) { + JsonUtils.deserialize(hierarchyString, classOf[java.util.Map[String, AnyRef]]).toMap + } else + Map[String, AnyRef]() + } else if (ResponseHandler.checkError(response) && response.getResponseCode.code() == 404) + Map[String, AnyRef]() + else + throw new ServerException("ERR_WHILE_FETCHING_HIERARCHY_FROM_CASSANDRA", "Error while fetching hierarchy from cassandra") + }) + } else if (ResponseHandler.checkError(response) && response.getResponseCode.code() == 404) + Future(Map[String, AnyRef]()) + else + throw new ServerException("ERR_WHILE_FETCHING_HIERARCHY_FROM_CASSANDRA", "Error while fetching hierarchy from cassandra") + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + def getCassandraHierarchy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[util.Map[String, AnyRef]] = { + val rootHierarchy: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() + 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])) { + //TODO: Remove mapping + val hierarchyMap = mapPrimaryCategories(hierarchy) + rootHierarchy.put("content", hierarchyMap) + RedisCache.set(hierarchyPrefix + request.get("rootId"), JsonUtils.serialize(hierarchyMap)) + Future(rootHierarchy) + } else { + Future(new util.HashMap[String, AnyRef]()) + } + } else { + val searchResponse = searchRootIdInElasticSearch(request.get("rootId").asInstanceOf[String]) + searchResponse.map(response => { + if (!response.isEmpty) { + if (StringUtils.isNotEmpty(response.getOrDefault("identifier", "").asInstanceOf[String])) { + val parentHierarchy = fetchHierarchy(request, response.get("identifier").asInstanceOf[String]) + parentHierarchy.map(hierarchy => { + if (!hierarchy.isEmpty) { + if (StringUtils.isNoneEmpty(hierarchy.getOrDefault("status", "").asInstanceOf[String]) && statusList.contains(hierarchy.getOrDefault("status", "").asInstanceOf[String]) && CollectionUtils.isNotEmpty(mapAsJavaMap(hierarchy).get("children").asInstanceOf[util.ArrayList[util.HashMap[String, AnyRef]]])) { + val bookmarkHierarchy = filterBookmarkHierarchy(mapAsJavaMap(hierarchy).get("children").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]], request.get("rootId").asInstanceOf[String]) + if (!bookmarkHierarchy.isEmpty) { + //TODO: Remove mapping + val hierarchyMap = mapPrimaryCategories(bookmarkHierarchy) + rootHierarchy.put("content", hierarchyMap) + RedisCache.set(hierarchyPrefix + request.get("rootId"), JsonUtils.serialize(hierarchyMap)) + rootHierarchy + } else { + new util.HashMap[String, AnyRef]() + } + } else { + new util.HashMap[String, AnyRef]() + } + } else { + new util.HashMap[String, AnyRef]() + } + }) + } else { + Future(new util.HashMap[String, AnyRef]()) + } + } else { + Future(new util.HashMap[String, AnyRef]()) + } + }).flatMap(f => f) + } + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + def searchRootIdInElasticSearch(rootId: String)(implicit ec: ExecutionContext): Future[util.Map[String, AnyRef]] = { + val mapper: ObjectMapper = new ObjectMapper() + val searchRequest: util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() { + put("request", new util.HashMap[String, AnyRef]() { + put("filters", new util.HashMap[String, AnyRef]() { + put("status", new util.ArrayList[String]() { + add("Live"); + add("Unlisted") + }) + put("mimeType", "application/vnd.ekstep.content-collection") + put("childNodes", new util.ArrayList[String]() { + add(rootId) + }) + put("visibility", "Default") + }) + put("fields", new util.ArrayList[String]() { + add("identifier") + }) + }) + } + val url: String = if (Platform.config.hasPath("composite.search.url")) Platform.config.getString("composite.search.url") else "https://dev.sunbirded.org/action/composite/v3/search" + val httpResponse: HttpResponse[String] = Unirest.post(url).header("Content-Type", "application/json").body(mapper.writeValueAsString(searchRequest)).asString + if (httpResponse.getStatus == 200) { + val response: Response = JsonUtils.deserialize(httpResponse.getBody, classOf[Response]) + if (response.get("count").asInstanceOf[Integer] > 0 && CollectionUtils.isNotEmpty(response.get("content").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]])) { + Future(response.get("content").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]].get(0)) + } else { + Future(new util.HashMap[String, AnyRef]()) + } + } else { + throw new ServerException("SERVER_ERROR", "Invalid response from search") + } + } + + def filterBookmarkHierarchy(children: util.List[util.Map[String, AnyRef]], bookmarkId: String)(implicit ec: ExecutionContext): util.Map[String, AnyRef] = { + if (CollectionUtils.isNotEmpty(children)) { + val response = children.filter(_.get("identifier") == bookmarkId).toList + if (CollectionUtils.isNotEmpty(response)) { + response.get(0) + } else { + val nextChildren = bufferAsJavaList(children.flatMap(child => { + if (!child.isEmpty && CollectionUtils.isNotEmpty(child.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]])) + child.get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + else new util.ArrayList[util.Map[String, AnyRef]] + })) + filterBookmarkHierarchy(nextChildren, bookmarkId) + } + } else { + new util.HashMap[String, AnyRef]() + } + } + + def getUnpublishedBookmarkHierarchy(request: Request, identifier: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[util.Map[String, AnyRef]] = { + if (StringUtils.isNotEmpty(identifier)) { + val parentHierarchy = fetchHierarchy(request, identifier + imgSuffix) + parentHierarchy.map(hierarchy => { + if (!hierarchy.isEmpty && CollectionUtils.isNotEmpty(mapAsJavaMap(hierarchy).get("children").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]])) { + val bookmarkHierarchy = filterBookmarkHierarchy(mapAsJavaMap(hierarchy).get("children").asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]], request.get("rootId").asInstanceOf[String]) + if (!bookmarkHierarchy.isEmpty) { + bookmarkHierarchy + } else { + new util.HashMap[String, AnyRef]() + } + } else { + new util.HashMap[String, AnyRef]() + } + }) + } else { + Future(new util.HashMap[String, AnyRef]()) + } + } + + def validateShallowCopied(rootNodeMap: util.Map[String, AnyRef], operation: String, identifier: String) = { + val originData = rootNodeMap.getOrDefault("originData", new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + if (StringUtils.equalsIgnoreCase(originData.getOrElse("copyType", "").asInstanceOf[String], HierarchyConstants.COPY_TYPE_SHALLOW)) { + operation match { + case "add"=> throw new ClientException(HierarchyErrorCodes.ERR_ADD_HIERARCHY_DENIED, "Add Hierarchy is not allowed for partially (shallow) copied content : " + identifier) + case "remove"=> throw new ClientException(HierarchyErrorCodes.ERR_REMOVE_HIERARCHY_DENIED, "Remove Hierarchy is not allowed for partially (shallow) copied content : " + identifier) + } + + } + } + + def updateLatestLeafNodes(children: util.List[util.Map[String, AnyRef]], leafNodeMap: util.Map[String, AnyRef]): List[Any] = { + children.toList.map(content => { + if(StringUtils.equalsIgnoreCase("Default", content.getOrDefault("visibility", "").asInstanceOf[String])) { + val metadata: util.Map[String, AnyRef] = leafNodeMap.getOrDefault(content.get("identifier").asInstanceOf[String], new java.util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + if(HierarchyConstants.RETIRED_STATUS.equalsIgnoreCase(metadata.getOrDefault("status", HierarchyConstants.RETIRED_STATUS).asInstanceOf[String])){ + children.remove(content) + } else { + if (objectTypeAsContentEnabled) + HierarchyBackwardCompatibilityUtil.setObjectTypeForRead(metadata, metadata.get("objectType").asInstanceOf[String]) + content.putAll(metadata) + } + } else { + updateLatestLeafNodes(content.getOrDefault("children", new util.ArrayList[Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]], leafNodeMap) + } + }) + } + + def fetchAllLeafNodes(children: util.List[util.Map[String, AnyRef]], leafNodeIds: util.List[String]): List[Any] = { + children.toList.map(content => { + if(StringUtils.equalsIgnoreCase("Default", content.getOrDefault("visibility", "").asInstanceOf[String])) { + leafNodeIds.add(content.get("identifier").asInstanceOf[String]) + leafNodeIds + } else { + fetchAllLeafNodes(content.getOrDefault("children", new util.ArrayList[Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]], leafNodeIds) + } + }) + } + + def getLatestLeafNodes(leafNodeIds : util.List[String])(implicit oec: OntologyEngineContext, ec: ExecutionContext) = { + if(CollectionUtils.isNotEmpty(leafNodeIds)) { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put(HierarchyConstants.GRAPH_ID, HierarchyConstants.TAXONOMY_ID) + } + }) + request.put("identifiers", leafNodeIds) + DataNode.list(request).map(nodes => { + val leafNodeMap: Map[String, AnyRef] = nodes.toList.map(node => (node.getIdentifier, NodeUtil.serialize(node, null, node.getObjectType.toLowerCase.replace("image", ""), HierarchyConstants.SCHEMA_VERSION, true).asInstanceOf[AnyRef])).toMap + val imageNodeIds: util.List[String] = JavaConverters.seqAsJavaListConverter(leafNodeIds.toList.map(id => id + HierarchyConstants.IMAGE_SUFFIX)).asJava + request.put("identifiers", imageNodeIds) + DataNode.list(request).map(imageNodes => { + //val imageLeafNodeMap: Map[String, AnyRef] = imageNodes.toList.map(imageNode => (imageNode.getIdentifier.replaceAll(HierarchyConstants.IMAGE_SUFFIX, ""), NodeUtil.serialize(imageNode, null, HierarchyConstants.CONTENT_SCHEMA_NAME, HierarchyConstants.SCHEMA_VERSION, true).asInstanceOf[AnyRef])).toMap + val imageLeafNodeMap: Map[String, AnyRef] = imageNodes.toList.map(imageNode => { + val identifier = imageNode.getIdentifier.replaceAll(HierarchyConstants.IMAGE_SUFFIX, "") + val metadata = NodeUtil.serialize(imageNode, null, imageNode.getObjectType.toLowerCase.replace("image", ""), HierarchyConstants.SCHEMA_VERSION, true) + metadata.replace("identifier", identifier) + (identifier, metadata.asInstanceOf[AnyRef]) + }).toMap + val updatedMap = leafNodeMap ++ imageLeafNodeMap + JavaConverters.mapAsJavaMapConverter(updatedMap).asJava + }) + }).flatMap(f => f) + } else { + Future{new util.HashMap[String, AnyRef]()} + } + + } + + def updateContentMappingInChildren(children: util.List[util.Map[String, AnyRef]]): List[Any] = { + children.toList.map(content => { + if (mapPrimaryCategoriesEnabled) + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(content, content.get("objectType").asInstanceOf[String]) + if (objectTypeAsContentEnabled) + HierarchyBackwardCompatibilityUtil.setObjectTypeForRead(content, content.get("objectType").asInstanceOf[String]) + updateContentMappingInChildren(content.getOrDefault("children", new util.ArrayList[Map[String, AnyRef]]).asInstanceOf[util.List[util.Map[String, AnyRef]]]) + }) + } + + private def mapPrimaryCategories(hierarchy: java.util.Map[String, AnyRef]):util.Map[String, AnyRef] = { + val updatedHierarchy = new util.HashMap[String, AnyRef](hierarchy) + if (mapPrimaryCategoriesEnabled) + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(updatedHierarchy) + if (objectTypeAsContentEnabled) + HierarchyBackwardCompatibilityUtil.setObjectTypeForRead(updatedHierarchy, updatedHierarchy.get("objectType").asInstanceOf[String]) + val children = new util.HashMap[String, AnyRef](hierarchy).getOrDefault("children", new util.ArrayList[java.util.Map[String, AnyRef]]).asInstanceOf[util.ArrayList[java.util.Map[String, AnyRef]]] + updateContentMappingInChildren(children) + updatedHierarchy + } + + def validateLeafNodes(parentNode: java.util.Map[String, AnyRef], childNode: java.util.Map[String, AnyRef])(implicit oec: OntologyEngineContext, ec: ExecutionContext) = { + val primaryCategory = parentNode.getOrDefault("primaryCategory", "").asInstanceOf[String] + val channel = parentNode.getOrDefault("channel", "_all") + val categoryId = if (StringUtils.isBlank(primaryCategory)) "" else "obj-cat:" + Slug.makeSlug(primaryCategory + "_" + parentNode.getOrDefault("objectType", "").asInstanceOf[String].toLowerCase() + "_" + channel) + val outRelations = DefinitionNode.getOutRelations(HierarchyConstants.GRAPH_ID, "1.0", parentNode.getOrDefault("objectType", "").asInstanceOf[String].toLowerCase().replace("image", ""), categoryId) + val configObjTypes: List[String] = outRelations.find(_.keySet.contains("children")).orNull.getOrElse("children", Map()).asInstanceOf[java.util.Map[String, AnyRef]].getOrElse("objects", new util.ArrayList[String]()).asInstanceOf[java.util.List[String]].toList + if(configObjTypes.nonEmpty && !configObjTypes.contains(childNode.getOrDefault("objectType", "").asInstanceOf[String])) + throw new ClientException("ERR_INVALID_CHILDREN", "Invalid Children objectType "+childNode.get("objectType")+" found for : "+childNode.get("identifier") + "| Please provide children having one of the objectType from "+ configObjTypes.asJava) + } +} diff --git a/content-api/hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala b/content-api/hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala new file mode 100644 index 000000000..687c53551 --- /dev/null +++ b/content-api/hierarchy-manager/src/main/scala/org/sunbird/managers/UpdateHierarchyManager.scala @@ -0,0 +1,507 @@ +package org.sunbird.managers + +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} +import org.sunbird.common.exception.{ClientException, ErrorCodes, ResourceNotFoundException, ServerException} +import org.sunbird.common.{DateUtils, JsonUtils, Platform} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.Identifier +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.external.ExternalPropsManager +import org.sunbird.graph.nodes.DataNode +import org.sunbird.graph.schema.DefinitionNode +import org.sunbird.graph.utils.{NodeUtil, ScalaJsonUtils} +import org.sunbird.telemetry.logger.TelemetryManager +import org.sunbird.utils.{HierarchyBackwardCompatibilityUtil, HierarchyConstants, HierarchyErrorCodes} + +import scala.collection.JavaConversions._ +import scala.collection.mutable +import scala.concurrent.{ExecutionContext, Future} + +object UpdateHierarchyManager { + + @throws[Exception] + def updateHierarchy(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + validateRequest(request) + val nodesModified: java.util.HashMap[String, AnyRef] = request.getRequest.get(HierarchyConstants.NODES_MODIFIED).asInstanceOf[java.util.HashMap[String, AnyRef]] + val hierarchy: java.util.HashMap[String, AnyRef] = request.getRequest.get(HierarchyConstants.HIERARCHY).asInstanceOf[java.util.HashMap[String, AnyRef]] + val rootId: String = getRootId(nodesModified, hierarchy) + request.getContext.put(HierarchyConstants.ROOT_ID, rootId) + getValidatedRootNode(rootId, request).map(node => { + 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) + addChildNodesInNodeList(existingChildren, request, nodes).map(list => (existingHierarchy, list)) + }).flatMap(f => f) + .map(result => { + val nodes = result._2 + TelemetryManager.info("NodeList final size: " + nodes.size) + val duplicates = nodes.groupBy(node => node.getIdentifier).map(t => t._1 -> t._2.size).toMap + //TelemetryManager.info("NodeList for root with duplicates :" + rootId +" :: " + ScalaJsonUtils.serialize(duplicates)) + val nodeMap: Map[String, AnyRef] = nodes.map(node => node.getIdentifier -> node.getMetadata.get("visibility")).toMap + //TelemetryManager.info("NodeList for root id :" + rootId +" :: " + ScalaJsonUtils.serialize(nodeMap)) + val idMap: mutable.Map[String, String] = mutable.Map() + idMap += (rootId -> rootId) + updateNodesModifiedInNodeList(nodes, nodesModified, request, idMap).map(modifiedNodeList => { + + getChildrenHierarchy(modifiedNodeList, rootId, hierarchy, idMap, result._1).map(children => { + TelemetryManager.log("Children for root id :" + rootId +" :: " + JsonUtils.serialize(children)) + updateHierarchyData(rootId, children, modifiedNodeList, request).map(node => { + val response = ResponseHandler.OK() + response.put(HierarchyConstants.CONTENT_ID, rootId) + idMap.remove(rootId) + response.put(HierarchyConstants.IDENTIFIERS, mapAsJavaMap(idMap)) + if (request.getContext.getOrDefault("shouldImageDelete", false.asInstanceOf[AnyRef]).asInstanceOf[Boolean]) + deleteHierarchy(request) + Future(response) + }).flatMap(f => f) + }).flatMap(f => f) + }).flatMap(f => f) + }) + }).flatMap(f => f).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + + private def validateRequest(request: Request)(implicit ec: ExecutionContext): Unit = { + if (!request.getRequest.contains(HierarchyConstants.NODES_MODIFIED) && !request.getRequest.contains(HierarchyConstants.HIERARCHY)) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Hierarchy data is empty") + } + + /** + * Checks if root id is empty, all black or image id + * + * @param nodesModified + * @param hierarchy + * @param ec + * @return + */ + private def getRootId(nodesModified: java.util.HashMap[String, AnyRef], hierarchy: java.util.HashMap[String, AnyRef])(implicit ec: ExecutionContext): String = { + val rootId: String = nodesModified.keySet() + .find(key => nodesModified.get(key).asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.ROOT).asInstanceOf[Boolean]) + .getOrElse(hierarchy.keySet().find(key => hierarchy.get(key).asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.ROOT).asInstanceOf[Boolean]).orNull) + if (StringUtils.isEmpty(rootId) && StringUtils.isAllBlank(rootId) || StringUtils.contains(rootId, HierarchyConstants.IMAGE_SUFFIX)) + throw new ClientException(HierarchyErrorCodes.ERR_INVALID_ROOT_ID, "Please Provide Valid Root Node Identifier") + rootId + } + + //Check if you can combine the below methods + private def getValidatedRootNode(identifier: String, request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val req = new Request(request) + req.put(HierarchyConstants.IDENTIFIER, identifier) + req.put(HierarchyConstants.MODE, HierarchyConstants.EDIT_MODE) + DataNode.read(req).map(rootNode => { + val metadata: java.util.Map[String, AnyRef] = NodeUtil.serialize(rootNode, new java.util.ArrayList[String](), request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String]) + if (!StringUtils.equals(metadata.get(HierarchyConstants.MIME_TYPE).asInstanceOf[String], HierarchyConstants.COLLECTION_MIME_TYPE)) { + throw new ClientException(HierarchyErrorCodes.ERR_INVALID_ROOT_ID, "Invalid MimeType for Root Node Identifier : " + identifier) + TelemetryManager.error("UpdateHierarchyManager.getValidatedRootNode :: Invalid MimeType for Root node id: " + identifier) + } + //Todo: Remove if not required + if (null == metadata.get(HierarchyConstants.VERSION) || metadata.get(HierarchyConstants.VERSION).asInstanceOf[Number].intValue < 2) { + TelemetryManager.error("UpdateHierarchyManager.getValidatedRootNode :: Invalid Content Version for Root node id: " + identifier) + throw new ClientException(HierarchyErrorCodes.ERR_INVALID_ROOT_ID, "The collection version is not up to date " + identifier) + } + val originData = metadata.getOrDefault("originData", new java.util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + if (StringUtils.equalsIgnoreCase(originData.getOrElse("copyType", "").asInstanceOf[String], HierarchyConstants.COPY_TYPE_SHALLOW)) + throw new ClientException(HierarchyErrorCodes.ERR_HIERARCHY_UPDATE_DENIED, "Hierarchy update is not allowed for partially (shallow) copied content : " + identifier) + rootNode.getMetadata.put(HierarchyConstants.VERSION, HierarchyConstants.LATEST_CONTENT_VERSION) + //TODO: Remove the Populate category mapping before updating for backward + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(rootNode.getMetadata) + HierarchyBackwardCompatibilityUtil.setNewObjectType(rootNode) + rootNode + }) + } + + private def getExistingHierarchy(request: Request, rootNode: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[java.util.HashMap[String, AnyRef]] = { + fetchHierarchy(request, rootNode).map(hierarchyString => { + if (!hierarchyString.asInstanceOf[String].isEmpty) { + JsonUtils.deserialize(hierarchyString.asInstanceOf[String], classOf[java.util.HashMap[String, AnyRef]]) + } else new java.util.HashMap[String, AnyRef]() + }) + } + + private def fetchHierarchy(request: Request, rootNode: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Any] = { + val req = new Request(request) + req.put(HierarchyConstants.IDENTIFIER, rootNode.getIdentifier) + oec.graphService.readExternalProps(req, List(HierarchyConstants.HIERARCHY)).map(response => { + if (ResponseHandler.checkError(response) && ResponseHandler.isResponseNotFoundError(response)) { + if (CollectionUtils.containsAny(HierarchyConstants.HIERARCHY_LIVE_STATUS, rootNode.getMetadata.get("status").asInstanceOf[String])) + throw new ServerException(HierarchyErrorCodes.ERR_HIERARCHY_NOT_FOUND, "No hierarchy is present in cassandra for identifier:" + rootNode.getIdentifier) + else { + if (rootNode.getMetadata.containsKey("pkgVersion")) + req.put(HierarchyConstants.IDENTIFIER, rootNode.getIdentifier.replace(HierarchyConstants.IMAGE_SUFFIX, "")) + else { + //TODO: Remove should Image be deleted after migration + request.getContext.put("shouldImageDelete", shouldImageBeDeleted(rootNode).asInstanceOf[AnyRef]) + req.put(HierarchyConstants.IDENTIFIER, if (!rootNode.getIdentifier.endsWith(HierarchyConstants.IMAGE_SUFFIX)) rootNode.getIdentifier + HierarchyConstants.IMAGE_SUFFIX else rootNode.getIdentifier) + } + oec.graphService.readExternalProps(req, List(HierarchyConstants.HIERARCHY)).map(resp => { + resp.getResult.toMap.getOrElse(HierarchyConstants.HIERARCHY, "").asInstanceOf[String] + }) recover { case e: ResourceNotFoundException => TelemetryManager.log("No hierarchy is present in cassandra for identifier:" + rootNode.getIdentifier) } + } + } else Future(response.getResult.toMap.getOrElse(HierarchyConstants.HIERARCHY, "").asInstanceOf[String]) + }).flatMap(f => f) + } + + private def addChildNodesInNodeList(childrenMaps: java.util.List[java.util.Map[String, AnyRef]], request: Request, nodes: scala.collection.immutable.List[Node])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[scala.collection.immutable.List[Node]] = { + if (CollectionUtils.isNotEmpty(childrenMaps)) { + val futures = childrenMaps.map(child => { +// println("Executing for child : " + child.get("identifier")); + addNodeToList(child, request, nodes).map(modifiedList => { + if (!StringUtils.equalsIgnoreCase(HierarchyConstants.DEFAULT, child.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])) { +// println("Calling next level for child : " + child.get("identifier")); + addChildNodesInNodeList(child.get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]], request, modifiedList) + } else + Future(modifiedList) + }).flatMap(f => f) + }).toList + Future.sequence(futures).map(f => f.flatten.distinct) + } else { + Future(nodes) + } + } + + private def addNodeToList(child: java.util.Map[String, AnyRef], request: Request, nodes: scala.collection.immutable.List[Node])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[scala.collection.immutable.List[Node]] = { + if (StringUtils.isNotEmpty(child.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])) + if (StringUtils.equalsIgnoreCase(HierarchyConstants.DEFAULT, child.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])) { + getContentNode(child.getOrDefault(HierarchyConstants.IDENTIFIER, "").asInstanceOf[String], HierarchyConstants.TAXONOMY_ID).map(node => { + node.getMetadata.put(HierarchyConstants.DEPTH, child.get(HierarchyConstants.DEPTH)) + node.getMetadata.put(HierarchyConstants.PARENT, child.get(HierarchyConstants.PARENT)) + node.getMetadata.put(HierarchyConstants.INDEX, child.get(HierarchyConstants.INDEX)) + //TODO: Remove the Populate category mapping before updating for backward + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(node.getMetadata, node.getObjectType) + HierarchyBackwardCompatibilityUtil.setNewObjectType(node) + val updatedNodes = node :: nodes + updatedNodes + }) recoverWith { case e: CompletionException => throw e.getCause } + } else { + val childData: java.util.Map[String, AnyRef] = new java.util.HashMap[String, AnyRef] + childData.putAll(child) + childData.remove(HierarchyConstants.CHILDREN) + childData.put(HierarchyConstants.STATUS, "Draft") + //TODO: Remove the Populate category mapping before updating for backward + val rootNode = getTempNode(nodes, request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String]) + childData.put(HierarchyConstants.CHANNEL, rootNode.getMetadata.get(HierarchyConstants.CHANNEL)) + childData.put(HierarchyConstants.AUDIENCE, rootNode.getMetadata.get(HierarchyConstants.AUDIENCE) ) + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(childData) + val node = NodeUtil.deserialize(childData, request.getContext.get(HierarchyConstants.SCHEMA_NAME).asInstanceOf[String], DefinitionNode.getRelationsMap(request)) + HierarchyBackwardCompatibilityUtil.setNewObjectType(node) + val updatedNodes = node :: nodes + Future(updatedNodes) + } + else { + //println("Visibility is empty for child :" + child) + Future(nodes) + } + } + + + private def updateNodesModifiedInNodeList(nodeList: List[Node], nodesModified: java.util.HashMap[String, AnyRef], request: Request, idMap: mutable.Map[String, String])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + updateRootNode(request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String], nodeList, nodesModified) + val futures = nodesModified.filter(nodeModified => !StringUtils.startsWith(request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String], nodeModified._1)) + .map(nodeModified => { + val metadata = nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].getOrDefault(HierarchyConstants.METADATA, new java.util.HashMap()).asInstanceOf[java.util.HashMap[String, AnyRef]] + metadata.remove(HierarchyConstants.DIALCODES) + metadata.put(HierarchyConstants.STATUS, "Draft") + metadata.put(HierarchyConstants.LAST_UPDATED_ON, DateUtils.formatCurrentDate) + if (nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].containsKey(HierarchyConstants.IS_NEW) + && nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.IS_NEW).asInstanceOf[Boolean]) { + if (!nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.ROOT).asInstanceOf[Boolean]) + metadata.put(HierarchyConstants.VISIBILITY, HierarchyConstants.PARENT) + if (nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].contains(HierarchyConstants.SET_DEFAULT_VALUE)) + createNewNode(nodeModified._1, idMap, metadata, nodeList, request, nodeModified._2.asInstanceOf[java.util.HashMap[String, AnyRef]].get(HierarchyConstants.SET_DEFAULT_VALUE).asInstanceOf[Boolean]) + else + createNewNode(nodeModified._1, idMap, metadata, nodeList, request) + } else { + updateTempNode(nodeModified._1, nodeList, idMap, metadata) + Future(nodeList.distinct) + } + }) + if (CollectionUtils.isNotEmpty(futures)) + Future.sequence(futures.toList).map(f => f.flatten) + else Future(nodeList) + } + + private def updateRootNode(rootId: String, nodeList: List[Node], nodesModified: java.util.HashMap[String, AnyRef])(implicit ec: ExecutionContext): Unit = { + if (nodesModified.containsKey(rootId)) { + val metadata = nodesModified.getOrDefault(rootId, new java.util.HashMap()).asInstanceOf[java.util.HashMap[String, AnyRef]].getOrDefault(HierarchyConstants.METADATA, new java.util.HashMap()).asInstanceOf[java.util.HashMap[String, AnyRef]] + updateNodeList(nodeList, rootId, metadata) + nodesModified.remove(rootId) + } + } + + private def createNewNode(nodeId: String, idMap: mutable.Map[String, String], metadata: java.util.HashMap[String, AnyRef], nodeList: List[Node], request: Request, setDefaultValue: Boolean = true)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + val identifier: String = Identifier.getIdentifier(HierarchyConstants.TAXONOMY_ID, Identifier.getUniqueIdFromTimestamp) + idMap += (nodeId -> identifier) + metadata.put(HierarchyConstants.IDENTIFIER, identifier) + metadata.put(HierarchyConstants.CODE, nodeId) + metadata.put(HierarchyConstants.VERSION_KEY, System.currentTimeMillis + "") + metadata.put(HierarchyConstants.CREATED_ON, DateUtils.formatCurrentDate) + metadata.put(HierarchyConstants.LAST_STATUS_CHANGED_ON, DateUtils.formatCurrentDate) + val rootNode = getTempNode(nodeList, request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String]) + metadata.put(HierarchyConstants.CHANNEL, rootNode.getMetadata.get(HierarchyConstants.CHANNEL)) + metadata.put(HierarchyConstants.AUDIENCE, rootNode.getMetadata.get(HierarchyConstants.AUDIENCE) ) + val createRequest: Request = new Request(request) + //TODO: Remove the Populate category mapping before updating for backward + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(metadata) + createRequest.setRequest(metadata) + DefinitionNode.validate(createRequest, setDefaultValue).map(node => { + node.setGraphId(HierarchyConstants.TAXONOMY_ID) + node.setNodeType(HierarchyConstants.DATA_NODE) + //Object type mapping + HierarchyBackwardCompatibilityUtil.setNewObjectType(node) + val updatedList = node :: nodeList + updatedList.distinct + }) + } + + private def updateTempNode(nodeId: String, nodeList: List[Node], idMap: mutable.Map[String, String], metadata: java.util.HashMap[String, AnyRef])(implicit ec: ExecutionContext): Unit = { + val tempNode: Node = getTempNode(nodeList, nodeId) + if (null != tempNode && StringUtils.isNotBlank(tempNode.getIdentifier)) { + metadata.put(HierarchyConstants.IDENTIFIER, tempNode.getIdentifier) + idMap += (nodeId -> tempNode.getIdentifier) + updateNodeList(nodeList, tempNode.getIdentifier, metadata) + } else throw new ResourceNotFoundException(HierarchyErrorCodes.ERR_CONTENT_NOT_FOUND, "Content not found with identifier: " + nodeId) + } + + private def validateNodes(nodeList: java.util.List[Node], rootId: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + val nodesToValidate = nodeList.filter(node => StringUtils.equals(HierarchyConstants.PARENT, node.getMetadata.get(HierarchyConstants.VISIBILITY).asInstanceOf[String]) || StringUtils.equalsAnyIgnoreCase(rootId, node.getIdentifier)).toList + DefinitionNode.updateJsonPropsInNodes(nodeList.toList, HierarchyConstants.TAXONOMY_ID, HierarchyConstants.COLLECTION_SCHEMA_NAME, HierarchyConstants.SCHEMA_VERSION) + //TODO: Use actual object schema instead of collection, when another object with visibility parent introduced. + DefinitionNode.validateContentNodes(nodesToValidate, HierarchyConstants.TAXONOMY_ID, HierarchyConstants.COLLECTION_SCHEMA_NAME, HierarchyConstants.SCHEMA_VERSION) + } + + def constructHierarchy(list: List[java.util.Map[String, AnyRef]]): java.util.Map[String, AnyRef] = { + val hierarchy: java.util.Map[String, AnyRef] = list.filter(root => root.get(HierarchyConstants.DEPTH).asInstanceOf[Number].intValue() == 0).head + if (MapUtils.isNotEmpty(hierarchy)) { + val maxDepth = list.map(node => node.get(HierarchyConstants.DEPTH).asInstanceOf[Number].intValue()).max + for (i <- 0 to maxDepth) { + val depth = i + val currentLevelNodes: Map[String, List[java.util.Map[String, Object]]] = list.filter(node => node.get(HierarchyConstants.DEPTH).asInstanceOf[Number].intValue() == depth).groupBy(_.get("identifier").asInstanceOf[String].replaceAll(".img", "")) + val nextLevel: List[java.util.Map[String, AnyRef]] = list.filter(node => node.get(HierarchyConstants.DEPTH).asInstanceOf[Number].intValue() == (depth + 1)) + if (CollectionUtils.isNotEmpty(nextLevel) && MapUtils.isNotEmpty(currentLevelNodes)) { + nextLevel.foreach(e => { + val parentId = e.get("parent").asInstanceOf[String] + currentLevelNodes.getOrDefault(parentId, List[java.util.Map[String, AnyRef]]()).foreach(parent => { + val children = parent.getOrDefault(HierarchyConstants.CHILDREN, new java.util.ArrayList[java.util.Map[String, AnyRef]]()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] + children.add(e) + parent.put(HierarchyConstants.CHILDREN, sortByIndex(children)) + }) + }) + } + } + } + hierarchy + } + + @throws[Exception] + private def getChildrenHierarchy(nodeList: List[Node], rootId: String, hierarchyData: java.util.HashMap[String, AnyRef], idMap: mutable.Map[String, String], existingHierarchy: java.util.Map[String, AnyRef])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[java.util.List[java.util.Map[String, AnyRef]]] = { + val childrenIdentifiersMap: Map[String, Map[String, Int]] = getChildrenIdentifiersMap(hierarchyData, idMap, existingHierarchy) +// TelemetryManager.log("Children Id map for root id :" + rootId + " :: " + ScalaJsonUtils.serialize(childrenIdentifiersMap)) + getPreparedHierarchyData(nodeList, rootId, childrenIdentifiersMap).map(nodeMaps => { + TelemetryManager.info("prepared hierarchy list without filtering: " + nodeMaps.size()) + val filteredNodeMaps = nodeMaps.filter(nodeMap => null != nodeMap.get(HierarchyConstants.DEPTH)).toList + TelemetryManager.info("prepared hierarchy list with filtering: " + filteredNodeMaps.size()) +// TelemetryManager.log("filteredNodeMaps for root id :" + rootId + " :: " + ScalaJsonUtils.serialize(filteredNodeMaps)) + val hierarchyMap = constructHierarchy(filteredNodeMaps) + if (MapUtils.isNotEmpty(hierarchyMap)) { + hierarchyMap.getOrDefault(HierarchyConstants.CHILDREN, new java.util.ArrayList[java.util.Map[String, AnyRef]]()).asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] + .filter(child => MapUtils.isNotEmpty(child)) + } + else + new java.util.ArrayList[java.util.Map[String, AnyRef]]() + + }) + } + + private def getChildrenIdentifiersMap(hierarchyData: java.util.Map[String, AnyRef], idMap: mutable.Map[String, String], existingHierarchy: java.util.Map[String, AnyRef]): Map[String, Map[String, Int]] = { + if (MapUtils.isNotEmpty(hierarchyData)) { + hierarchyData.map(entry => idMap.getOrDefault(entry._1, entry._1) -> entry._2.asInstanceOf[java.util.HashMap[String, AnyRef]] + .get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.ArrayList[String]] + .map(id => idMap.getOrDefault(id, id)).zipWithIndex.toMap).toMap + } else { + val tempChildMap: java.util.Map[String, Map[String, Int]] = new java.util.HashMap[String, Map[String, Int]]() + val tempResourceMap: java.util.Map[String, Map[String, Int]] = new java.util.HashMap[String, Map[String, Int]]() + getChildrenIdMapFromExistingHierarchy(existingHierarchy, tempChildMap, tempResourceMap) + tempChildMap.putAll(tempResourceMap) + tempChildMap.toMap + } + } + + private def getChildrenIdMapFromExistingHierarchy(existingHierarchy: java.util.Map[String, AnyRef], tempChildMap: java.util.Map[String, Map[String, Int]], tempResourceMap: java.util.Map[String, Map[String, Int]]): Unit = { + if (existingHierarchy.containsKey(HierarchyConstants.CHILDREN) && CollectionUtils.isNotEmpty(existingHierarchy.get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.ArrayList[java.util.HashMap[String, AnyRef]]])) { + tempChildMap.put(existingHierarchy.get(HierarchyConstants.IDENTIFIER).asInstanceOf[String], existingHierarchy.get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.ArrayList[java.util.HashMap[String, AnyRef]]] + .map(child => child.get(HierarchyConstants.IDENTIFIER).asInstanceOf[String] -> child.get(HierarchyConstants.INDEX).asInstanceOf[Int]).toMap) + existingHierarchy.get(HierarchyConstants.CHILDREN).asInstanceOf[java.util.ArrayList[java.util.HashMap[String, AnyRef]]] + .foreach(child => getChildrenIdMapFromExistingHierarchy(child, tempChildMap, tempResourceMap)) + } else + tempResourceMap.put(existingHierarchy.get(HierarchyConstants.IDENTIFIER).asInstanceOf[String], Map[String, Int]()) + } + + @throws[Exception] + private def getPreparedHierarchyData(nodeList: List[Node], rootId: String, childrenIdentifiersMap: Map[String, Map[String, Int]])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[java.util.List[java.util.Map[String, AnyRef]]] = { + if (MapUtils.isNotEmpty(childrenIdentifiersMap)) { + val updatedNodeList = getTempNode(nodeList, rootId) :: List() + updateHierarchyRelatedData(childrenIdentifiersMap.getOrElse(rootId, Map[String, Int]()), 1, + rootId, nodeList, childrenIdentifiersMap, updatedNodeList).map(finalEnrichedNodeList => { + TelemetryManager.info("Final enriched list size: " + finalEnrichedNodeList.size) + val childNodeIds = finalEnrichedNodeList.map(node => node.getIdentifier).filterNot(id => rootId.equalsIgnoreCase(id)).distinct + TelemetryManager.info("Final enriched ids (childNodes): " + childNodeIds + " :: size: " + childNodeIds.size) + // UNDERSTANDING: below we used nodeList to update DEPTH and CHILD_NODES. It automatically updated to finalEnrichedNodeList. + // Because, the Node object is a Java POJO with metadata using java.util.Map. + updateNodeList(nodeList, rootId, new java.util.HashMap[String, AnyRef]() { + put(HierarchyConstants.DEPTH, 0.asInstanceOf[AnyRef]) + put(HierarchyConstants.CHILD_NODES, new java.util.ArrayList[String](childNodeIds)) + }) + validateNodes(finalEnrichedNodeList, rootId).map(result => HierarchyManager.convertNodeToMap(finalEnrichedNodeList)) + }).flatMap(f => f) + } else { + updateNodeList(nodeList, rootId, new java.util.HashMap[String, AnyRef]() { + { + put(HierarchyConstants.DEPTH, 0.asInstanceOf[AnyRef]) + } + }) + validateNodes(nodeList, rootId).map(result => HierarchyManager.convertNodeToMap(nodeList)) + } + } + + @throws[Exception] + private def updateHierarchyRelatedData(childrenIds: Map[String, Int], depth: Int, parent: String, nodeList: List[Node], hierarchyStructure: Map[String, Map[String, Int]], enrichedNodeList: scala.collection.immutable.List[Node])(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[List[Node]] = { + val futures = childrenIds.map(child => { + val id = child._1 + val index = child._2 + 1 + val tempNode = getTempNode(nodeList, id) + if (null != tempNode && StringUtils.equalsIgnoreCase(HierarchyConstants.PARENT, tempNode.getMetadata.get(HierarchyConstants.VISIBILITY).asInstanceOf[String])) { + populateHierarchyRelatedData(tempNode, depth, index, parent) + val nxtEnrichedNodeList = tempNode :: enrichedNodeList + if (MapUtils.isNotEmpty(hierarchyStructure.getOrDefault(child._1, Map[String, Int]()))) + updateHierarchyRelatedData(hierarchyStructure.getOrDefault(child._1, Map[String, Int]()), + tempNode.getMetadata.get(HierarchyConstants.DEPTH).asInstanceOf[Int] + 1, id, nodeList, hierarchyStructure, nxtEnrichedNodeList) + else + Future(nxtEnrichedNodeList) + } else { +// TelemetryManager.info("Get ContentNode as TempNode is null for ID: " + id) + getContentNode(id, HierarchyConstants.TAXONOMY_ID).map(node => { + val parentNode: Node = nodeList.find(p => p.getIdentifier.equals(parent)).orNull + val parentMetadata: java.util.Map[String, AnyRef] = NodeUtil.serialize(parentNode, new java.util.ArrayList[String](), parentNode.getObjectType.toLowerCase, "1.0") + val childMetadata: java.util.Map[String, AnyRef] = NodeUtil.serialize(node, new java.util.ArrayList[String](), node.getObjectType.toLowerCase, "1.0") + HierarchyManager.validateLeafNodes(parentMetadata, childMetadata) + populateHierarchyRelatedData(node, depth, index, parent) + node.getMetadata.put(HierarchyConstants.VISIBILITY, HierarchyConstants.DEFAULT) + //TODO: Populate category mapping before updating for backward + HierarchyBackwardCompatibilityUtil.setContentAndCategoryTypes(node.getMetadata, node.getObjectType) + HierarchyBackwardCompatibilityUtil.setNewObjectType(node) + val nxtEnrichedNodeList = node :: enrichedNodeList + if (MapUtils.isNotEmpty(hierarchyStructure.getOrDefault(id, Map[String, Int]()))) { + updateHierarchyRelatedData(hierarchyStructure.getOrDefault(id, Map[String, Int]()), node.getMetadata.get(HierarchyConstants.DEPTH).asInstanceOf[Int] + 1, id, nodeList, hierarchyStructure, nxtEnrichedNodeList) + } else + Future(nxtEnrichedNodeList) + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause } + } + }) + if (CollectionUtils.isNotEmpty(futures)) { + val listOfFutures = Future.sequence(futures.toList) + listOfFutures.map(f => f.flatten.distinct) + } else + Future(enrichedNodeList) + } + + private def populateHierarchyRelatedData(tempNode: Node, depth: Int, index: Int, parent: String) = { + tempNode.getMetadata.put(HierarchyConstants.DEPTH, depth.asInstanceOf[AnyRef]) + tempNode.getMetadata.put(HierarchyConstants.PARENT, parent.replaceAll(".img", "")) + tempNode.getMetadata.put(HierarchyConstants.INDEX, index.asInstanceOf[AnyRef]) + } + + /** + * This method is to check if all the children of the parent entity are present in the populated map + * + * @param children + * @param populatedChildMap + * @return + */ + def isFullyPopulated(children: List[String], populatedChildMap: mutable.Map[_, _]): Boolean = { + children.forall(child => populatedChildMap.containsKey(child)) + } + + def updateHierarchyData(rootId: String, children: java.util.List[java.util.Map[String, AnyRef]], nodeList: List[Node], request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val node = getTempNode(nodeList, rootId) + val updatedHierarchy = new java.util.HashMap[String, AnyRef]() + updatedHierarchy.put(HierarchyConstants.IDENTIFIER, rootId) + updatedHierarchy.put(HierarchyConstants.CHILDREN, children) + val req = new Request(request) + req.getContext.put(HierarchyConstants.IDENTIFIER, rootId) + val metadata = cleanUpRootData(node) + req.getRequest.putAll(metadata) + req.put(HierarchyConstants.HIERARCHY, ScalaJsonUtils.serialize(updatedHierarchy)) + req.put(HierarchyConstants.IDENTIFIER, rootId) + req.put(HierarchyConstants.CHILDREN, new java.util.ArrayList()) + req.put(HierarchyConstants.CONCEPTS, new java.util.ArrayList()) + DataNode.update(req) + } + + private def cleanUpRootData(node: Node)(implicit oec: OntologyEngineContext, ec: ExecutionContext): java.util.Map[String, AnyRef] = { + DefinitionNode.getRestrictedProperties(HierarchyConstants.TAXONOMY_ID, HierarchyConstants.SCHEMA_VERSION, HierarchyConstants.OPERATION_UPDATE_HIERARCHY, HierarchyConstants.COLLECTION_SCHEMA_NAME) + .foreach(key => node.getMetadata.remove(key)) + node.getMetadata.remove(HierarchyConstants.STATUS) + node.getMetadata.remove(HierarchyConstants.LAST_UPDATED_ON) + node.getMetadata.remove(HierarchyConstants.LAST_STATUS_CHANGED_ON) + node.getMetadata + } + + /** + * Get the Node with ID provided from List else return Null. + * + * @param nodeList + * @param id + * @return + */ + private def getTempNode(nodeList: List[Node], id: String) = { + nodeList.find(node => StringUtils.startsWith(node.getIdentifier, id)).orNull + } + + private def updateNodeList(nodeList: List[Node], id: String, metadata: java.util.HashMap[String, AnyRef]): Unit = { + nodeList.foreach(node => { + if(node.getIdentifier.startsWith(id)){ + node.getMetadata.putAll(metadata) + } + }) + } + + def getContentNode(identifier: String, graphId: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val request: Request = new Request() + request.setContext(new java.util.HashMap[String, AnyRef]() { + { + put(HierarchyConstants.GRAPH_ID, graphId) + put(HierarchyConstants.VERSION, HierarchyConstants.SCHEMA_VERSION) + put(HierarchyConstants.OBJECT_TYPE, HierarchyConstants.CONTENT_OBJECT_TYPE) + put(HierarchyConstants.SCHEMA_NAME, HierarchyConstants.CONTENT_SCHEMA_NAME) + } + }) + request.setObjectType(HierarchyConstants.CONTENT_OBJECT_TYPE) + request.put(HierarchyConstants.IDENTIFIER, identifier) + request.put(HierarchyConstants.MODE, HierarchyConstants.READ_MODE) + request.put(HierarchyConstants.FIELDS, new java.util.ArrayList[String]()) + DataNode.read(request) + } + + private def shouldImageBeDeleted(rootNode: Node): Boolean = { + val flag = if (Platform.config.hasPath("collection.image.migration.enabled")) Platform.config.getBoolean("collection.image.migration.enabled") else false + // flag && !CollectionUtils.containsAny(HierarchyConstants.HIERARCHY_LIVE_STATUS, rootNode.getMetadata.get(HierarchyConstants.STATUS).asInstanceOf[String]) && + // !rootNode.getMetadata.containsKey("pkgVersion") + flag + } + + def sortByIndex(childrenMaps: java.util.List[java.util.Map[String, AnyRef]]): java.util.List[java.util.Map[String, AnyRef]] = { + bufferAsJavaList(childrenMaps.sortBy(_.get("index").asInstanceOf[Int])) + } + + + def deleteHierarchy(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + val req = new Request(request) + val rootId = request.getContext.get(HierarchyConstants.ROOT_ID).asInstanceOf[String] + req.put(HierarchyConstants.IDENTIFIERS, if (rootId.contains(HierarchyConstants.IMAGE_SUFFIX)) List(rootId) else List(rootId + HierarchyConstants.IMAGE_SUFFIX)) + oec.graphService.deleteExternalProps(req) + } + +} diff --git a/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyBackwardCompatibilityUtil.scala b/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyBackwardCompatibilityUtil.scala new file mode 100644 index 000000000..83aef6b53 --- /dev/null +++ b/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyBackwardCompatibilityUtil.scala @@ -0,0 +1,70 @@ +package org.sunbird.utils + +import java.util + +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.Platform +import org.sunbird.graph.dac.model.Node + +import scala.collection.JavaConverters._ + +object HierarchyBackwardCompatibilityUtil { + + val categoryMap: java.util.Map[String, AnyRef] = Platform.getAnyRef("contentTypeToPrimaryCategory", + new util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + val categoryMapForMimeType: java.util.Map[String, AnyRef] = Platform.getAnyRef("mimeTypeToPrimaryCategory", + new util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + val categoryMapForResourceType: java.util.Map[String, AnyRef] = Platform.getAnyRef("resourceTypeToPrimaryCategory", + new util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + val mimeTypesToCheck = List("application/vnd.ekstep.h5p-archive", "application/vnd.ekstep.html-archive", "application/vnd.android.package-archive", + "video/webm", "video/x-youtube", "video/mp4") + val objectTypes = List("Content", "Collection") + + def setContentAndCategoryTypes(input: util.Map[String, AnyRef], objType: String = ""): Unit = { + if(StringUtils.isBlank(objType) || objectTypes.contains(objType)) { + val contentType = input.get("contentType").asInstanceOf[String] + val primaryCategory = input.get("primaryCategory").asInstanceOf[String] + val (updatedContentType, updatedPrimaryCategory): (String, String) = (contentType, primaryCategory) match { + case (x: String, y: String) => (x, y) + case ("Resource", y) => (contentType, getCategoryForResource(input.getOrDefault("mimeType", "").asInstanceOf[String], + input.getOrDefault("resourceType", "").asInstanceOf[String])) + case (x: String, y) => (x, categoryMap.get(x).asInstanceOf[String]) + case (x, y: String) => (categoryMap.asScala.filter(entry => StringUtils.equalsIgnoreCase(entry._2.asInstanceOf[String], y)).keys.headOption.getOrElse(""), y) + case _ => (contentType, primaryCategory) + } + + input.put("contentType", updatedContentType) + input.put("primaryCategory", updatedPrimaryCategory) + } + } + + private def getCategoryForResource(mimeType: String, resourceType: String): String = (mimeType, resourceType) match { + case ("", "") => "Learning Resource" + case (x: String, "") => categoryMapForMimeType.get(x).asInstanceOf[util.List[String]].asScala.headOption.getOrElse("Learning Resource") + case (x: String, y: String) => if (mimeTypesToCheck.contains(x)) categoryMapForMimeType.get(x).asInstanceOf[util.List[String]].asScala.headOption.getOrElse("Learning Resource") else categoryMapForResourceType.getOrDefault(y, "Learning Resource").asInstanceOf[String] + case _ => "Learning Resource" + } + def setObjectTypeForRead(result: java.util.Map[String, AnyRef], objectType: String = ""): Unit = { + if(objectTypes.contains(objectType)) + result.put("objectType", "Content") + } + + def setNewObjectType(node: Node) = { + val metadata = node.getMetadata + val mimeType = metadata.getOrDefault("mimeType", "").asInstanceOf[String] + val contentType = metadata.getOrDefault("contentType", "").asInstanceOf[String] + val objectType = metadata.getOrDefault("objectType", "").asInstanceOf[String] + val primaryCategory = metadata.getOrDefault("primaryCategory", "").asInstanceOf[String] + + if (StringUtils.isNotBlank(mimeType) && StringUtils.equalsIgnoreCase(mimeType, HierarchyConstants.COLLECTION_MIME_TYPE)) { + metadata.put(HierarchyConstants.OBJECT_TYPE, HierarchyConstants.COLLECTION_OBJECT_TYPE) + node.setObjectType(HierarchyConstants.COLLECTION_OBJECT_TYPE) + } else if ((StringUtils.isNotBlank(contentType) && StringUtils.equalsIgnoreCase(contentType, HierarchyConstants.ASSET_CONTENT_TYPE)) + || (StringUtils.isNotBlank(primaryCategory) && StringUtils.equalsIgnoreCase(primaryCategory, HierarchyConstants.ASSET_CONTENT_TYPE))) { + metadata.put(HierarchyConstants.OBJECT_TYPE, HierarchyConstants.ASSET_OBJECT_TYPE) + node.setObjectType(HierarchyConstants.ASSET_OBJECT_TYPE) + } else { + metadata.put(HierarchyConstants.OBJECT_TYPE, objectType) + } + } +} \ No newline at end of file diff --git a/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala b/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala new file mode 100644 index 000000000..83c5d1da7 --- /dev/null +++ b/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyConstants.scala @@ -0,0 +1,63 @@ +package org.sunbird.utils + +object HierarchyConstants { + val DATA: String = "data" + val DATA_NODE: String = "DATA_NODE" + val NODES_MODIFIED: String = "nodesModified" + val HIERARCHY: String = "hierarchy" + val ROOT: String = "root" + val SET_DEFAULT_VALUE: String = "setDefaultValue" + val COLLECTION_MIME_TYPE: String = "application/vnd.ekstep.content-collection" + val COLLECTION_SCHEMA_NAME: String = "collection" + val EVENT_SET_SCHEMA_NAME: String = "eventset" + val LATEST_CONTENT_VERSION: Integer = 2 + val VERSION: String = "version" + val IDENTIFIER: String = "identifier" + val DEPTH: String = "depth" + val PARENT: String = "Parent" + val INDEX: String = "index" + val CHILDREN: String = "children" + val VISIBILITY: String = "visibility" + val TAXONOMY_ID: String = "domain" + val METADATA: String = "metadata" + val IS_NEW: String = "isNew" + val DIALCODES: String = "dialcodes" + val CONTENT_OBJECT_TYPE: String = "Content" + val OBJECT_TYPE = "objectType" + val STATUS: String = "status" + val LAST_UPDATED_ON: String = "lastUpdatedOn" + val CODE: String = "code" + val VERSION_KEY: String = "versionKey" + val CREATED_ON: String = "createdOn" + val LAST_STATUS_CHANGED_ON: String = "lastStatusChangedOn" + val CHILD_NODES: String = "childNodes" + val CONTENT_SCHEMA_NAME: String = "content" + val SCHEMA_NAME: String = "schemaName" + val SCHEMA_VERSION: String = "1.0" + val CONTENT_ID: String = "content_id" + val IDENTIFIERS: String = "identifiers" + val DEFAULT: String = "Default" + val CHANNEL: String = "channel" + val ROOT_ID: String = "rootId" + val HIERARCHY_LIVE_STATUS: List[String] = List("Live", "Unlisted", "Flagged") + val IMAGE_SUFFIX: String = ".img" + val GRAPH_ID: String = "graph_id" + val MODE: String = "mode" + val EDIT_MODE: String = "edit" + val READ_MODE: String = "read" + val CONCEPTS: String = "concepts" + val FIELDS: String = "fields" + val MIME_TYPE: String = "mimeType" + val COPY_TYPE_SHALLOW: String = "shallow" + val RETIRED_STATUS: String = "Retired" + val AUDIENCE: String = "audience" + val ASSET_SCHEMA_NAME: String = "asset" + val CONTENT_VERSION: String = "1.0" + val COLLECTION_VERSION: String = "1.0" + val ASSET_VERSION: String = "1.0" + val ASSET_CONTENT_TYPE: String = "Asset" + val COLLECTION_OBJECT_TYPE: String = "Collection" + val ASSET_OBJECT_TYPE: String = "Asset" + val OPERATION_UPDATE_HIERARCHY: String = "updateHierarchy" + +} diff --git a/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyErrorCodes.scala b/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyErrorCodes.scala new file mode 100644 index 000000000..7b7e8b9ec --- /dev/null +++ b/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/HierarchyErrorCodes.scala @@ -0,0 +1,11 @@ +package org.sunbird.utils + +object HierarchyErrorCodes { + val ERR_INVALID_ROOT_ID: String = "ERR_INVALID_ROOT_ID" + val ERR_CONTENT_NOT_FOUND: String = "ERR_CONTENT_NOT_FOUND" + val ERR_HIERARCHY_NOT_FOUND: String = "ERR_HIERARCHY_NOT_FOUND" + val ERR_HIERARCHY_UPDATE_DENIED: String = "ERR_HIERARCHY_UPDATE_DENIED" + val ERR_ADD_HIERARCHY_DENIED: String = "ERR_ADD_HIERARCHY_DENIED" + val ERR_REMOVE_HIERARCHY_DENIED: String = "ERR_REMOVE_HIERARCHY_DENIED" + +} diff --git a/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/NodeUtil.scala b/content-api/hierarchy-manager/src/main/scala/org/sunbird/utils/NodeUtil.scala new file mode 100644 index 000000000..e69de29bb diff --git a/content-api/hierarchy-manager/src/test/resources/application.conf b/content-api/hierarchy-manager/src/test/resources/application.conf new file mode 100644 index 000000000..046ae1a25 --- /dev/null +++ b/content-api/hierarchy-manager/src/test/resources/application.conf @@ -0,0 +1,536 @@ +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +# ~~~~~ +# Play uses HOCON as its configuration file format. HOCON has a number +# of advantages over other config formats, but there are two things that +# can be used when modifying settings. +# +# You can include other configuration files in this main application.conf file: +#include "extra-config.conf" +# +# You can declare variables and substitute for them: +#mykey = ${some.value} +# +# And if an environment variable exists when there is no other substitution, then +# HOCON will fall back to substituting environment variable: +#mykey = ${JAVA_HOME} + +## Akka +# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration +# https://www.playframework.com/documentation/latest/JavaAkka#Configuration +# ~~~~~ +# Play uses Akka internally and exposes Akka Streams and actors in Websockets and +# other streaming HTTP responses. +akka { + # "akka.log-config-on-start" is extraordinarly useful because it log the complete + # configuration at INFO level, including defaults and overrides, so it s worth + # putting at the very top. + # + # Put the following in your conf/logback.xml file: + # + # + # + # And then uncomment this line to debug the configuration. + # + #log-config-on-start = true +} + +## Secret key +# http://www.playframework.com/documentation/latest/ApplicationSecret +# ~~~~~ +# The secret key is used to sign Play's session cookie. +# This must be changed for production, but we don't recommend you change it in this file. +play.http.secret.key = a-long-secret-to-calm-the-rage-of-the-entropy-gods + +## Modules +# https://www.playframework.com/documentation/latest/Modules +# ~~~~~ +# Control which modules are loaded when Play starts. Note that modules are +# the replacement for "GlobalSettings", which are deprecated in 2.5.x. +# Please see https://www.playframework.com/documentation/latest/GlobalSettings +# for more information. +# +# You can also extend Play functionality by using one of the publically available +# Play modules: https://playframework.com/documentation/latest/ModuleDirectory +play.modules { + # By default, Play will load any class called Module that is defined + # in the root package (the "app" directory), or you can define them + # explicitly below. + # If there are any built-in modules that you want to enable, you can list them here. + #enabled += my.application.Module + + # If there are any built-in modules that you want to disable, you can list them here. + #disabled += "" +} + +## IDE +# https://www.playframework.com/documentation/latest/IDE +# ~~~~~ +# Depending on your IDE, you can add a hyperlink for errors that will jump you +# directly to the code location in the IDE in dev mode. The following line makes +# use of the IntelliJ IDEA REST interface: +#play.editor="http://localhost:63342/api/file/?file=%s&line=%s" + +## Internationalisation +# https://www.playframework.com/documentation/latest/JavaI18N +# https://www.playframework.com/documentation/latest/ScalaI18N +# ~~~~~ +# Play comes with its own i18n settings, which allow the user's preferred language +# to map through to internal messages, or allow the language to be stored in a cookie. +play.i18n { + # The application languages + langs = [ "en" ] + + # Whether the language cookie should be secure or not + #langCookieSecure = true + + # Whether the HTTP only attribute of the cookie should be set to true + #langCookieHttpOnly = true +} + +## Play HTTP settings +# ~~~~~ +play.http { + ## Router + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # Define the Router object to use for this application. + # This router will be looked up first when the application is starting up, + # so make sure this is the entry point. + # Furthermore, it's assumed your route file is named properly. + # So for an application router like `my.application.Router`, + # you may need to define a router file `conf/my.application.routes`. + # Default to Routes in the root package (aka "apps" folder) (and conf/routes) + #router = my.application.Router + + ## Action Creator + # https://www.playframework.com/documentation/latest/JavaActionCreator + # ~~~~~ + #actionCreator = null + + ## ErrorHandler + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # If null, will attempt to load a class called ErrorHandler in the root package, + #errorHandler = null + + ## Session & Flash + # https://www.playframework.com/documentation/latest/JavaSessionFlash + # https://www.playframework.com/documentation/latest/ScalaSessionFlash + # ~~~~~ + session { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + + # Sets the max-age field of the cookie to 5 minutes. + # NOTE: this only sets when the browser will discard the cookie. Play will consider any + # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, + # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. + #maxAge = 300 + + # Sets the domain on the session cookie. + #domain = "example.com" + } + + flash { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + } +} + +## Netty Provider +# https://www.playframework.com/documentation/latest/SettingsNetty +# ~~~~~ +play.server.netty { + # Whether the Netty wire should be logged + log.wire = true + + # If you run Play on Linux, you can use Netty's native socket transport + # for higher performance with less garbage. + transport = "native" +} + +## WS (HTTP Client) +# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS +# ~~~~~ +# The HTTP client primarily used for REST APIs. The default client can be +# configured directly, but you can also create different client instances +# with customized settings. You must enable this by adding to build.sbt: +# +# libraryDependencies += ws // or javaWs if using java +# +play.ws { + # Sets HTTP requests not to follow 302 requests + #followRedirects = false + + # Sets the maximum number of open HTTP connections for the client. + #ahc.maxConnectionsTotal = 50 + + ## WS SSL + # https://www.playframework.com/documentation/latest/WsSSL + # ~~~~~ + ssl { + # Configuring HTTPS with Play WS does not require programming. You can + # set up both trustManager and keyManager for mutual authentication, and + # turn on JSSE debugging in development with a reload. + #debug.handshake = true + #trustManager = { + # stores = [ + # { type = "JKS", path = "exampletrust.jks" } + # ] + #} + } +} + +## Cache +# https://www.playframework.com/documentation/latest/JavaCache +# https://www.playframework.com/documentation/latest/ScalaCache +# ~~~~~ +# Play comes with an integrated cache API that can reduce the operational +# overhead of repeated requests. You must enable this by adding to build.sbt: +# +# libraryDependencies += cache +# +play.cache { + # If you want to bind several caches, you can bind the individually + #bindCaches = ["db-cache", "user-cache", "session-cache"] +} + +## Filter Configuration +# https://www.playframework.com/documentation/latest/Filters +# ~~~~~ +# There are a number of built-in filters that can be enabled and configured +# to give Play greater security. +# +play.filters { + + # Enabled filters are run automatically against Play. + # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. + enabled = [] + + # Disabled filters remove elements from the enabled list. + # disabled += filters.CSRFFilter + + + ## CORS filter configuration + # https://www.playframework.com/documentation/latest/CorsFilter + # ~~~~~ + # CORS is a protocol that allows web applications to make requests from the browser + # across different domains. + # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has + # dependencies on CORS settings. + cors { + # Filter paths by a whitelist of path prefixes + #pathPrefixes = ["/some/path", ...] + + # The allowed origins. If null, all origins are allowed. + #allowedOrigins = ["http://www.example.com"] + + # The allowed HTTP methods. If null, all methods are allowed + #allowedHttpMethods = ["GET", "POST"] + } + + ## Security headers filter configuration + # https://www.playframework.com/documentation/latest/SecurityHeaders + # ~~~~~ + # Defines security headers that prevent XSS attacks. + # If enabled, then all options are set to the below configuration by default: + headers { + # The X-Frame-Options header. If null, the header is not set. + #frameOptions = "DENY" + + # The X-XSS-Protection header. If null, the header is not set. + #xssProtection = "1; mode=block" + + # The X-Content-Type-Options header. If null, the header is not set. + #contentTypeOptions = "nosniff" + + # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. + #permittedCrossDomainPolicies = "master-only" + + # The Content-Security-Policy header. If null, the header is not set. + #contentSecurityPolicy = "default-src 'self'" + } + + ## Allowed hosts filter configuration + # https://www.playframework.com/documentation/latest/AllowedHostsFilter + # ~~~~~ + # Play provides a filter that lets you configure which hosts can access your application. + # This is useful to prevent cache poisoning attacks. + hosts { + # Allow requests to example.com, its subdomains, and localhost:9000. + #allowed = [".example.com", "localhost:9000"] + } +} + +# Learning-Service Configuration +content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit"] + +# Cassandra Configuration +content.keyspace.name=content_store +content.keyspace.table=content_data +#TODO: Add Configuration for assessment. e.g: question_data +orchestrator.keyspace.name=script_store +orchestrator.keyspace.table=script_data +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" + +# Redis Configuration +redis.host=localhost +redis.port=6379 +redis.maxConnections=128 + +#Condition to enable publish locally +content.publish_task.enabled=true + +#directory location where store unzip file +dist.directory=/data/tmp/dist/ +output.zipfile=/data/tmp/story.zip +source.folder=/data/tmp/temp2/ +save.directory=/data/tmp/temp/ + +# Content 2 vec analytics URL +CONTENT_TO_VEC_URL="http://172.31.27.233:9000/content-to-vec" + +# FOR CONTENT WORKFLOW PIPELINE (CWP) + +#--Content Workflow Pipeline Mode +OPERATION_MODE=TEST + +#--Maximum Content Package File Size Limit in Bytes (50 MB) +MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=52428800 + +#--Maximum Asset File Size Limit in Bytes (20 MB) +MAX_ASSET_FILE_SIZE_LIMIT=20971520 + +#--No of Retry While File Download Fails +RETRY_ASSET_DOWNLOAD_COUNT=1 + +#Google-vision-API +google.vision.tagging.enabled = false + +#Orchestrator env properties +env="https://dev.ekstep.in/api/learning" + +#Current environment +cloud_storage.env=dev + + +#Folder configuration +cloud_storage.content.folder=content +cloud_storage.asset.folder=assets +cloud_storage.artefact.folder=artifact +cloud_storage.bundle.folder=bundle +cloud_storage.media.folder=media +cloud_storage.ecar.folder=ecar_files + +# Media download configuration +content.media.base.url="https://dev.open-sunbird.org" +plugin.media.base.url="https://dev.open-sunbird.org" + +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + +# Elasticsearch properties +search.es_conn_info="localhost:9200" +search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","domain","subject","description^10","keywords^25","ageGroup^10","filter^10","theme^10","genre^10","objects^25","contentType^100","language^200","teachingMode^25","skills^10","learningObjective^10","curriculum^100","gradeLevel^100","developer^100","attributions^10","owner^50","text","words","releaseNotes"] +search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] +search.batch.size=500 +search.connection.timeout=30 +platform-api-url="http://localhost:8080/language-service" +MAX_ITERATION_COUNT_FOR_SAMZA_JOB=2 + + +# DIAL Code Configuration +dialcode.keyspace.name="dialcode_store" +dialcode.keyspace.table="dial_code" +dialcode.max_count=1000 + +# System Configuration +system.config.keyspace.name="dialcode_store" +system.config.table="system_config" + +#Publisher Configuration +publisher.keyspace.name="dialcode_store" +publisher.keyspace.table="publisher" + +#DIAL Code Generator Configuration +dialcode.strip.chars="0" +dialcode.length=6.0 +dialcode.large.prime_number=1679979167 + +#DIAL Code ElasticSearch Configuration +dialcode.index=true +dialcode.object_type="DialCode" + +framework.max_term_creation_limit=200 + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + +# Kafka configuration details +kafka.topics.instruction="local.learning.job.request" +kafka.urls="localhost:9092" + +#Youtube Standard Licence Validation +learning.content.youtube.validate.license=true +learning.content.youtube.application.name=fetch-youtube-license +youtube.license.regex.pattern=["\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)","^([A-Za-z0-9\\-\\_]*)"] + +#Top N Config for Search Telemetry +telemetry_env=dev +telemetry.search.topn=5 + +installation.id=ekstep + + +channel.default="in.ekstep" + +# DialCode Link API Config +learning.content.link_dialcode_validation=true +dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/search" +dialcode.api.authorization=auth_key + +# Language-Code Configuration +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] + +# Kafka send event to topic enable +kafka.topic.send.enable=false + +learning.valid_license=["creativeCommon"] +learning.service_provider=["youtube"] + +stream.mime.type=video/mp4 +compositesearch.index.name="compositesearch" + +hierarchy.keyspace.name=hierarchy_store +content.hierarchy.table=content_hierarchy +framework.hierarchy.table=framework_hierarchy + +# Kafka topic for definition update event. +kafka.topic.system.command="dev.system.command" + +learning.reserve_dialcode.content_type=["TextBook"] +# restrict.metadata.objectTypes=["Content", "ContentImage", "AssessmentItem", "Channel", "Framework", "Category", "CategoryInstance", "Term"] + +#restrict.metadata.objectTypes="Content,ContentImage" + +publish.collection.fullecar.disable=true + +# Consistency Level for Multi Node Cassandra cluster +cassandra.lp.consistency.level=QUORUM + + + + +content.nested.fields="badgeAssertions,targets,badgeAssociations" + +content.cache.ttl=86400 +content.cache.enable=true +collection.cache.enable=true +content.discard.status=["Draft","FlagDraft"] + +framework.categories_cached=["subject", "medium", "gradeLevel", "board"] +framework.cache.ttl=86400 +framework.cache.read=true + + +# Max size(width/height) of thumbnail in pixels +max.thumbnail.size.pixels=150 + +schema.base_path="../../schemas/" +content.hierarchy.removed_props_for_leafNodes=["collections","children","usedByContent","item_sets","methods","libraries","editorState"] + +collection.keyspace = "hierarchy_store" +content.keyspace = "content_store" + +collection.image.migration.enabled=true + + + +# This is added to handle large artifacts sizes differently +content.artifact.size.for_online=209715200 + +contentTypeToPrimaryCategory { + ClassroomTeachingVideo: "Explanation Content" + ConceptMap: "Learning Resource" + Course: "Course" + CuriosityQuestionSet: "Practice Question Set" + eTextBook: "eTextbook" + ExperientialResource: "Learning Resource" + ExplanationResource: "Explanation Content" + ExplanationVideo: "Explanation Content" + FocusSpot: "Teacher Resource" + LearningOutcomeDefinition: "Teacher Resource" + MarkingSchemeRubric: "Teacher Resource" + PedagogyFlow: "Teacher Resource" + PracticeQuestionSet: "Practice Question Set" + PracticeResource: "Practice Question Set" + SelfAssess: "Course Assessment" + TeachingMethod: "Teacher Resource" + TextBook: "Digital Textbook" + Collection: "Content Playlist" + ExplanationReadingMaterial: "Learning Resource" + LearningActivity: "Learning Resource" + LessonPlan: "Content Playlist" + LessonPlanResource: "Teacher Resource" + PreviousBoardExamPapers: "Learning Resource" + TVLesson: "Explanation Content" + OnboardingResource: "Learning Resource" + ReadingMaterial: "Learning Resource" + Template: "Template" + Asset: "Asset" + Plugin: "Plugin" + LessonPlanUnit: "Lesson Plan Unit" + CourseUnit: "Course Unit" + TextBookUnit: "Textbook Unit" +} + +resourceTypeToPrimaryCategory { + Learn: "Learning Resource" + Read: "Learning Resource" + Practice: "Learning Resource" + Teach: "Teacher Resource" + Test: "Learning Resource" + Experiment: "Learning Resource" + LessonPlan: "Teacher Resource" +} + +mimeTypeToPrimaryCategory { + "application/vnd.ekstep.h5p-archive": ["Learning Resource"] + "application/vnd.ekstep.html-archive": ["Learning Resource"] + "application/vnd.android.package-archive": ["Learning Resource"] + "video/webm": ["Explanation Content"] + "video/x-youtube": ["Explanation Content"] + "video/mp4": ["Explanation Content"] + "application/pdf": ["Learning Resource", "Teacher Resource"] + "application/epub": ["Learning Resource", "Teacher Resource"] + "application/vnd.ekstep.ecml-archive": ["Learning Resource", "Teacher Resource"] + "text/x-url": ["Learnin Resource", "Teacher Resource"] +} + +objectcategorydefinition.keyspace=category_store diff --git a/content-api/hierarchy-manager/src/test/resources/cassandra-unit.yaml b/content-api/hierarchy-manager/src/test/resources/cassandra-unit.yaml new file mode 100755 index 000000000..a965a8fe5 --- /dev/null +++ b/content-api/hierarchy-manager/src/test/resources/cassandra-unit.yaml @@ -0,0 +1,590 @@ +# Cassandra storage config YAML + +# NOTE: +# See http://wiki.apache.org/cassandra/StorageConfiguration for +# full explanations of configuration directives +# /NOTE + +# The name of the cluster. This is mainly used to prevent machines in +# one logical cluster from joining another. +cluster_name: 'Test Cluster' + +# You should always specify InitialToken when setting up a production +# cluster for the first time, and often when adding capacity later. +# The principle is that each node should be given an equal slice of +# the token ring; see http://wiki.apache.org/cassandra/Operations +# for more details. +# +# If blank, Cassandra will request a token bisecting the range of +# the heaviest-loaded existing node. If there is no load information +# available, such as is the case with a new cluster, it will pick +# a random token, which will lead to hot spots. +#initial_token: + +# See http://wiki.apache.org/cassandra/HintedHandoff +hinted_handoff_enabled: true +# this defines the maximum amount of time a dead host will have hints +# generated. After it has been dead this long, new hints for it will not be +# created until it has been seen alive and gone down again. +max_hint_window_in_ms: 10800000 # 3 hours +# Maximum throttle in KBs per second, per delivery thread. This will be +# reduced proportionally to the number of nodes in the cluster. (If there +# are two nodes in the cluster, each delivery thread will use the maximum +# rate; if there are three, each will throttle to half of the maximum, +# since we expect two nodes to be delivering hints simultaneously.) +hinted_handoff_throttle_in_kb: 1024 +# Number of threads with which to deliver hints; +# Consider increasing this number when you have multi-dc deployments, since +# cross-dc handoff tends to be slower +max_hints_delivery_threads: 2 + +hints_directory: target/embeddedCassandra/hints + +# The following setting populates the page cache on memtable flush and compaction +# WARNING: Enable this setting only when the whole node's data fits in memory. +# Defaults to: false +# populate_io_cache_on_flush: false + +# Authentication backend, implementing IAuthenticator; used to identify users +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, +# PasswordAuthenticator}. +# +# - AllowAllAuthenticator performs no checks - set it to disable authentication. +# - PasswordAuthenticator relies on username/password pairs to authenticate +# users. It keeps usernames and hashed passwords in system_auth.credentials table. +# Please increase system_auth keyspace replication factor if you use this authenticator. +authenticator: AllowAllAuthenticator + +# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, +# CassandraAuthorizer}. +# +# - AllowAllAuthorizer allows any action to any user - set it to disable authorization. +# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please +# increase system_auth keyspace replication factor if you use this authorizer. +authorizer: AllowAllAuthorizer + +# Validity period for permissions cache (fetching permissions can be an +# expensive operation depending on the authorizer, CassandraAuthorizer is +# one example). Defaults to 2000, set to 0 to disable. +# Will be disabled automatically for AllowAllAuthorizer. +permissions_validity_in_ms: 2000 + + +# The partitioner is responsible for distributing rows (by key) across +# nodes in the cluster. Any IPartitioner may be used, including your +# own as long as it is on the classpath. Out of the box, Cassandra +# provides org.apache.cassandra.dht.{Murmur3Partitioner, RandomPartitioner +# ByteOrderedPartitioner, OrderPreservingPartitioner (deprecated)}. +# +# - RandomPartitioner distributes rows across the cluster evenly by md5. +# This is the default prior to 1.2 and is retained for compatibility. +# - Murmur3Partitioner is similar to RandomPartioner but uses Murmur3_128 +# Hash Function instead of md5. When in doubt, this is the best option. +# - ByteOrderedPartitioner orders rows lexically by key bytes. BOP allows +# scanning rows in key order, but the ordering can generate hot spots +# for sequential insertion workloads. +# - OrderPreservingPartitioner is an obsolete form of BOP, that stores +# - keys in a less-efficient format and only works with keys that are +# UTF8-encoded Strings. +# - CollatingOPP collates according to EN,US rules rather than lexical byte +# ordering. Use this as an example if you need custom collation. +# +# See http://wiki.apache.org/cassandra/Operations for more on +# partitioners and token selection. +partitioner: org.apache.cassandra.dht.Murmur3Partitioner + +# directories where Cassandra should store data on disk. +data_file_directories: + - target/embeddedCassandra/data + +# commit log +commitlog_directory: target/embeddedCassandra/commitlog + +cdc_raw_directory: target/embeddedCassandra/cdc + +# policy for data disk failures: +# stop: shut down gossip and Thrift, leaving the node effectively dead, but +# can still be inspected via JMX. +# best_effort: stop using the failed disk and respond to requests based on +# remaining available sstables. This means you WILL see obsolete +# data at CL.ONE! +# ignore: ignore fatal errors and let requests fail, as in pre-1.2 Cassandra +disk_failure_policy: stop + + +# Maximum size of the key cache in memory. +# +# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the +# minimum, sometimes more. The key cache is fairly tiny for the amount of +# time it saves, so it's worthwhile to use it at large numbers. +# The row cache saves even more time, but must store the whole values of +# its rows, so it is extremely space-intensive. It's best to only use the +# row cache if you have hot rows or static rows. +# +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. +key_cache_size_in_mb: + +# Duration in seconds after which Cassandra should +# safe the keys cache. Caches are saved to saved_caches_directory as +# specified in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 14400 or 4 hours. +key_cache_save_period: 14400 + +# Number of keys from the key cache to save +# Disabled by default, meaning all keys are going to be saved +# key_cache_keys_to_save: 100 + +# Maximum size of the row cache in memory. +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is 0, to disable row caching. +row_cache_size_in_mb: 0 + +# Duration in seconds after which Cassandra should +# safe the row cache. Caches are saved to saved_caches_directory as specified +# in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 0 to disable saving the row cache. +row_cache_save_period: 0 + +# Number of keys from the row cache to save +# Disabled by default, meaning all keys are going to be saved +# row_cache_keys_to_save: 100 + +# saved caches +saved_caches_directory: target/embeddedCassandra/saved_caches + +# commitlog_sync may be either "periodic" or "batch." +# When in batch mode, Cassandra won't ack writes until the commit log +# has been fsynced to disk. It will wait up to +# commitlog_sync_batch_window_in_ms milliseconds for other writes, before +# performing the sync. +# +# commitlog_sync: batch +# commitlog_sync_batch_window_in_ms: 50 +# +# the other option is "periodic" where writes may be acked immediately +# and the CommitLog is simply synced every commitlog_sync_period_in_ms +# milliseconds. +commitlog_sync: periodic +commitlog_sync_period_in_ms: 10000 + +# The size of the individual commitlog file segments. A commitlog +# segment may be archived, deleted, or recycled once all the data +# in it (potentially from each columnfamily in the system) has been +# flushed to sstables. +# +# The default size is 32, which is almost always fine, but if you are +# archiving commitlog segments (see commitlog_archiving.properties), +# then you probably want a finer granularity of archiving; 8 or 16 MB +# is reasonable. +commitlog_segment_size_in_mb: 32 + +# any class that implements the SeedProvider interface and has a +# constructor that takes a Map of parameters will do. +seed_provider: + # Addresses of hosts that are deemed contact points. + # Cassandra nodes use this list of hosts to find each other and learn + # the topology of the ring. You must change this if you are running + # multiple nodes! + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + # seeds is actually a comma-delimited list of addresses. + # Ex: ",," + - seeds: "127.0.0.1" + + +# For workloads with more data than can fit in memory, Cassandra's +# bottleneck will be reads that need to fetch data from +# disk. "concurrent_reads" should be set to (16 * number_of_drives) in +# order to allow the operations to enqueue low enough in the stack +# that the OS and drives can reorder them. +# +# On the other hand, since writes are almost never IO bound, the ideal +# number of "concurrent_writes" is dependent on the number of cores in +# your system; (8 * number_of_cores) is a good rule of thumb. +concurrent_reads: 32 +concurrent_writes: 32 + +# Total memory to use for memtables. Cassandra will flush the largest +# memtable when this much memory is used. +# If omitted, Cassandra will set it to 1/3 of the heap. +# memtable_total_space_in_mb: 2048 + +# Total space to use for commitlogs. +# If space gets above this value (it will round up to the next nearest +# segment multiple), Cassandra will flush every dirty CF in the oldest +# segment and remove it. +# commitlog_total_space_in_mb: 4096 + +# This sets the amount of memtable flush writer threads. These will +# be blocked by disk io, and each one will hold a memtable in memory +# while blocked. If you have a large heap and many data directories, +# you can increase this value for better flush performance. +# By default this will be set to the amount of data directories defined. +#memtable_flush_writers: 1 + +# the number of full memtables to allow pending flush, that is, +# waiting for a writer thread. At a minimum, this should be set to +# the maximum number of secondary indexes created on a single CF. +#memtable_flush_queue_size: 4 + +# Whether to, when doing sequential writing, fsync() at intervals in +# order to force the operating system to flush the dirty +# buffers. Enable this to avoid sudden dirty buffer flushing from +# impacting read latencies. Almost always a good idea on SSD:s; not +# necessarily on platters. +trickle_fsync: false +trickle_fsync_interval_in_kb: 10240 + +# TCP port, for commands and data +storage_port: 0 + +# SSL port, for encrypted communication. Unused unless enabled in +# encryption_options +ssl_storage_port: 7011 + +# Address to bind to and tell other Cassandra nodes to connect to. You +# _must_ change this if you want multiple nodes to be able to +# communicate! +# +# Leaving it blank leaves it up to InetAddress.getLocalHost(). This +# will always do the Right Thing *if* the node is properly configured +# (hostname, name resolution, etc), and the Right Thing is to use the +# address associated with the hostname (it might not be). +# +# Setting this to 0.0.0.0 is always wrong. +listen_address: 127.0.0.1 + +start_native_transport: true +# port for the CQL native transport to listen for clients on +native_transport_port: 9042 + +# Whether to start the thrift rpc server. +start_rpc: true + +# Address to broadcast to other Cassandra nodes +# Leaving this blank will set it to the same value as listen_address +# broadcast_address: 1.2.3.4 + +# The address to bind the Thrift RPC service to -- clients connect +# here. Unlike ListenAddress above, you *can* specify 0.0.0.0 here if +# you want Thrift to listen on all interfaces. +# +# Leaving this blank has the same effect it does for ListenAddress, +# (i.e. it will be based on the configured hostname of the node). +rpc_address: localhost +# port for Thrift to listen for clients on +rpc_port: 0 + +# enable or disable keepalive on rpc connections +rpc_keepalive: true + +# Cassandra provides three options for the RPC Server: +# +# sync -> One connection per thread in the rpc pool (see below). +# For a very large number of clients, memory will be your limiting +# factor; on a 64 bit JVM, 128KB is the minimum stack size per thread. +# Connection pooling is very, very strongly recommended. +# +# async -> Nonblocking server implementation with one thread to serve +# rpc connections. This is not recommended for high throughput use +# cases. Async has been tested to be about 50% slower than sync +# or hsha and is deprecated: it will be removed in the next major release. +# +# hsha -> Stands for "half synchronous, half asynchronous." The rpc thread pool +# (see below) is used to manage requests, but the threads are multiplexed +# across the different clients. +# +# The default is sync because on Windows hsha is about 30% slower. On Linux, +# sync/hsha performance is about the same, with hsha of course using less memory. +rpc_server_type: sync + +# Uncomment rpc_min|max|thread to set request pool size. +# You would primarily set max for the sync server to safeguard against +# misbehaved clients; if you do hit the max, Cassandra will block until one +# disconnects before accepting more. The defaults for sync are min of 16 and max +# unlimited. +# +# For the Hsha server, the min and max both default to quadruple the number of +# CPU cores. +# +# This configuration is ignored by the async server. +# +# rpc_min_threads: 16 +# rpc_max_threads: 2048 + +# uncomment to set socket buffer sizes on rpc connections +# rpc_send_buff_size_in_bytes: +# rpc_recv_buff_size_in_bytes: + +# Frame size for thrift (maximum field length). +# 0 disables TFramedTransport in favor of TSocket. This option +# is deprecated; we strongly recommend using Framed mode. +thrift_framed_transport_size_in_mb: 15 + +# The max length of a thrift message, including all fields and +# internal thrift overhead. +thrift_max_message_length_in_mb: 16 + +# Set to true to have Cassandra create a hard link to each sstable +# flushed or streamed locally in a backups/ subdirectory of the +# Keyspace data. Removing these links is the operator's +# responsibility. +incremental_backups: false + +# Whether or not to take a snapshot before each compaction. Be +# careful using this option, since Cassandra won't clean up the +# snapshots for you. Mostly useful if you're paranoid when there +# is a data format change. +snapshot_before_compaction: false + +# Whether or not a snapshot is taken of the data before keyspace truncation +# or dropping of column families. The STRONGLY advised default of true +# should be used to provide data safety. If you set this flag to false, you will +# lose data on truncation or drop. +auto_snapshot: false + +# Add column indexes to a row after its contents reach this size. +# Increase if your column values are large, or if you have a very large +# number of columns. The competing causes are, Cassandra has to +# deserialize this much of the row to read a single column, so you want +# it to be small - at least if you do many partial-row reads - but all +# the index data is read for each access, so you don't want to generate +# that wastefully either. +column_index_size_in_kb: 64 + +# Size limit for rows being compacted in memory. Larger rows will spill +# over to disk and use a slower two-pass compaction process. A message +# will be logged specifying the row key. +#in_memory_compaction_limit_in_mb: 64 + +# Number of simultaneous compactions to allow, NOT including +# validation "compactions" for anti-entropy repair. Simultaneous +# compactions can help preserve read performance in a mixed read/write +# workload, by mitigating the tendency of small sstables to accumulate +# during a single long running compactions. The default is usually +# fine and if you experience problems with compaction running too +# slowly or too fast, you should look at +# compaction_throughput_mb_per_sec first. +# +# This setting has no effect on LeveledCompactionStrategy. +# +# concurrent_compactors defaults to the number of cores. +# Uncomment to make compaction mono-threaded, the pre-0.8 default. +#concurrent_compactors: 1 + +# Multi-threaded compaction. When enabled, each compaction will use +# up to one thread per core, plus one thread per sstable being merged. +# This is usually only useful for SSD-based hardware: otherwise, +# your concern is usually to get compaction to do LESS i/o (see: +# compaction_throughput_mb_per_sec), not more. +#multithreaded_compaction: false + +# Throttles compaction to the given total throughput across the entire +# system. The faster you insert data, the faster you need to compact in +# order to keep the sstable count down, but in general, setting this to +# 16 to 32 times the rate you are inserting data is more than sufficient. +# Setting this to 0 disables throttling. Note that this account for all types +# of compaction, including validation compaction. +compaction_throughput_mb_per_sec: 16 + +# Track cached row keys during compaction, and re-cache their new +# positions in the compacted sstable. Disable if you use really large +# key caches. +#compaction_preheat_key_cache: true + +# Throttles all outbound streaming file transfers on this node to the +# given total throughput in Mbps. This is necessary because Cassandra does +# mostly sequential IO when streaming data during bootstrap or repair, which +# can lead to saturating the network connection and degrading rpc performance. +# When unset, the default is 200 Mbps or 25 MB/s. +# stream_throughput_outbound_megabits_per_sec: 200 + +# How long the coordinator should wait for read operations to complete +read_request_timeout_in_ms: 5000 +# How long the coordinator should wait for seq or index scans to complete +range_request_timeout_in_ms: 10000 +# How long the coordinator should wait for writes to complete +write_request_timeout_in_ms: 2000 +# How long a coordinator should continue to retry a CAS operation +# that contends with other proposals for the same row +cas_contention_timeout_in_ms: 1000 +# How long the coordinator should wait for truncates to complete +# (This can be much longer, because unless auto_snapshot is disabled +# we need to flush first so we can snapshot before removing the data.) +truncate_request_timeout_in_ms: 60000 +# The default timeout for other, miscellaneous operations +request_timeout_in_ms: 10000 + +# Enable operation timeout information exchange between nodes to accurately +# measure request timeouts. If disabled, replicas will assume that requests +# were forwarded to them instantly by the coordinator, which means that +# under overload conditions we will waste that much extra time processing +# already-timed-out requests. +# +# Warning: before enabling this property make sure to ntp is installed +# and the times are synchronized between the nodes. +cross_node_timeout: false + +# Enable socket timeout for streaming operation. +# When a timeout occurs during streaming, streaming is retried from the start +# of the current file. This _can_ involve re-streaming an important amount of +# data, so you should avoid setting the value too low. +# Default value is 0, which never timeout streams. +# streaming_socket_timeout_in_ms: 0 + +# phi value that must be reached for a host to be marked down. +# most users should never need to adjust this. +# phi_convict_threshold: 8 + +# endpoint_snitch -- Set this to a class that implements +# IEndpointSnitch. The snitch has two functions: +# - it teaches Cassandra enough about your network topology to route +# requests efficiently +# - it allows Cassandra to spread replicas around your cluster to avoid +# correlated failures. It does this by grouping machines into +# "datacenters" and "racks." Cassandra will do its best not to have +# more than one replica on the same "rack" (which may not actually +# be a physical location) +# +# IF YOU CHANGE THE SNITCH AFTER DATA IS INSERTED INTO THE CLUSTER, +# YOU MUST RUN A FULL REPAIR, SINCE THE SNITCH AFFECTS WHERE REPLICAS +# ARE PLACED. +# +# Out of the box, Cassandra provides +# - SimpleSnitch: +# Treats Strategy order as proximity. This improves cache locality +# when disabling read repair, which can further improve throughput. +# Only appropriate for single-datacenter deployments. +# - PropertyFileSnitch: +# Proximity is determined by rack and data center, which are +# explicitly configured in cassandra-topology.properties. +# - RackInferringSnitch: +# Proximity is determined by rack and data center, which are +# assumed to correspond to the 3rd and 2nd octet of each node's +# IP address, respectively. Unless this happens to match your +# deployment conventions (as it did Facebook's), this is best used +# as an example of writing a custom Snitch class. +# - Ec2Snitch: +# Appropriate for EC2 deployments in a single Region. Loads Region +# and Availability Zone information from the EC2 API. The Region is +# treated as the Datacenter, and the Availability Zone as the rack. +# Only private IPs are used, so this will not work across multiple +# Regions. +# - Ec2MultiRegionSnitch: +# Uses public IPs as broadcast_address to allow cross-region +# connectivity. (Thus, you should set seed addresses to the public +# IP as well.) You will need to open the storage_port or +# ssl_storage_port on the public IP firewall. (For intra-Region +# traffic, Cassandra will switch to the private IP after +# establishing a connection.) +# +# You can use a custom Snitch by setting this to the full class name +# of the snitch, which will be assumed to be on your classpath. +endpoint_snitch: SimpleSnitch + +# controls how often to perform the more expensive part of host score +# calculation +dynamic_snitch_update_interval_in_ms: 100 +# controls how often to reset all host scores, allowing a bad host to +# possibly recover +dynamic_snitch_reset_interval_in_ms: 600000 +# if set greater than zero and read_repair_chance is < 1.0, this will allow +# 'pinning' of replicas to hosts in order to increase cache capacity. +# The badness threshold will control how much worse the pinned host has to be +# before the dynamic snitch will prefer other replicas over it. This is +# expressed as a double which represents a percentage. Thus, a value of +# 0.2 means Cassandra would continue to prefer the static snitch values +# until the pinned host was 20% worse than the fastest. +dynamic_snitch_badness_threshold: 0.1 + +# request_scheduler -- Set this to a class that implements +# RequestScheduler, which will schedule incoming client requests +# according to the specific policy. This is useful for multi-tenancy +# with a single Cassandra cluster. +# NOTE: This is specifically for requests from the client and does +# not affect inter node communication. +# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place +# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of +# client requests to a node with a separate queue for each +# request_scheduler_id. The scheduler is further customized by +# request_scheduler_options as described below. +request_scheduler: org.apache.cassandra.scheduler.NoScheduler + +# Scheduler Options vary based on the type of scheduler +# NoScheduler - Has no options +# RoundRobin +# - throttle_limit -- The throttle_limit is the number of in-flight +# requests per client. Requests beyond +# that limit are queued up until +# running requests can complete. +# The value of 80 here is twice the number of +# concurrent_reads + concurrent_writes. +# - default_weight -- default_weight is optional and allows for +# overriding the default which is 1. +# - weights -- Weights are optional and will default to 1 or the +# overridden default_weight. The weight translates into how +# many requests are handled during each turn of the +# RoundRobin, based on the scheduler id. +# +# request_scheduler_options: +# throttle_limit: 80 +# default_weight: 5 +# weights: +# Keyspace1: 1 +# Keyspace2: 5 + +# request_scheduler_id -- An identifer based on which to perform +# the request scheduling. Currently the only valid option is keyspace. +# request_scheduler_id: keyspace + +# index_interval controls the sampling of entries from the primrary +# row index in terms of space versus time. The larger the interval, +# the smaller and less effective the sampling will be. In technicial +# terms, the interval coresponds to the number of index entries that +# are skipped between taking each sample. All the sampled entries +# must fit in memory. Generally, a value between 128 and 512 here +# coupled with a large key cache size on CFs results in the best trade +# offs. This value is not often changed, however if you have many +# very small rows (many to an OS page), then increasing this will +# often lower memory usage without a impact on performance. +index_interval: 128 + +# Enable or disable inter-node encryption +# Default settings are TLS v1, RSA 1024-bit keys (it is imperative that +# users generate their own keys) TLS_RSA_WITH_AES_128_CBC_SHA as the cipher +# suite for authentication, key exchange and encryption of the actual data transfers. +# NOTE: No custom encryption options are enabled at the moment +# The available internode options are : all, none, dc, rack +# +# If set to dc cassandra will encrypt the traffic between the DCs +# If set to rack cassandra will encrypt the traffic between the racks +# +# The passwords used in these options must match the passwords used when generating +# the keystore and truststore. For instructions on generating these files, see: +# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore +# +encryption_options: + internode_encryption: none + keystore: conf/.keystore + keystore_password: cassandra + truststore: conf/.truststore + truststore_password: cassandra + # More advanced defaults below: + # protocol: TLS + # algorithm: SunX509 + # store_type: JKS + # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA] \ No newline at end of file diff --git a/content-api/hierarchy-manager/src/test/resources/logback.xml b/content-api/hierarchy-manager/src/test/resources/logback.xml new file mode 100644 index 000000000..73529d622 --- /dev/null +++ b/content-api/hierarchy-manager/src/test/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + + + + + + %d %msg%n + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala b/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala new file mode 100644 index 000000000..8081f2397 --- /dev/null +++ b/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/BaseSpec.scala @@ -0,0 +1,118 @@ +package org.sunbird.managers + +import java.io.{File, IOException} + +import com.datastax.driver.core.{ResultSet, Session} +import org.apache.commons.io.FileUtils +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.neo4j.graphdb.GraphDatabaseService +import org.neo4j.graphdb.factory.GraphDatabaseFactory +import org.neo4j.graphdb.factory.GraphDatabaseSettings.Connector.ConnectorType +import org.neo4j.kernel.configuration.BoltConnector +import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll, BeforeAndAfterEach, Matchers} +import org.sunbird.cassandra.CassandraConnector +import org.sunbird.common.Platform + +class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach { + + var graphDb: GraphDatabaseService = null + var session: Session = null + + private val script_1 = "CREATE KEYSPACE IF NOT EXISTS content_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_2 = "CREATE TABLE IF NOT EXISTS content_store.content_data (content_id text, last_updated_on timestamp,body blob,oldBody blob,stageIcons blob,PRIMARY KEY (content_id));" + private val script_5 = "CREATE KEYSPACE IF NOT EXISTS category_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_6 = "CREATE TABLE IF NOT EXISTS category_store.category_definition_data (identifier text, objectmetadata map, forms map ,PRIMARY KEY (identifier));" + private val script_7 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_content_all', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_8 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:course_collection_all', {'config': '{}', 'schema': '{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"}},\"default\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"additionalProperties\":false},\"additionalCategories\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"default\":\"Textbook\"}},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}'});" + private val script_9 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:course_content_all',{'config': '{}', 'schema': '{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"}},\"default\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"additionalProperties\":false},\"additionalCategories\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"default\":\"Textbook\"}},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}'});" + private val script_10 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_collection_all', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_11 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_content_in.ekstep', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_12 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_collection_in.ekstep', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + + def setUpEmbeddedNeo4j(): Unit = { + if(null == graphDb) { + val bolt: BoltConnector = new BoltConnector("0") + println("GraphDB : " + Platform.config.getString("graph.dir")) + graphDb = new GraphDatabaseFactory() + .newEmbeddedDatabaseBuilder(new File(Platform.config.getString("graph.dir"))) + .setConfig(bolt.`type`, ConnectorType.BOLT.name()) + .setConfig(bolt.enabled, "true").setConfig(bolt.listen_address, "localhost:7687").newGraphDatabase + registerShutdownHook(graphDb) + } + } + + private def registerShutdownHook(graphDb: GraphDatabaseService): Unit = { + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = { + try { + tearEmbeddedNeo4JSetup + System.out.println("cleanup Done!!") + } catch { + case e: Exception => + e.printStackTrace() + } + } + }) + } + + + @throws[Exception] + private def tearEmbeddedNeo4JSetup(): Unit = { + if (null != graphDb) graphDb.shutdown + Thread.sleep(2000) + deleteEmbeddedNeo4j(new File(Platform.config.getString("graph.dir"))) + } + + private def deleteEmbeddedNeo4j(emDb: File): Unit = { + try{ + FileUtils.deleteDirectory(emDb) + }catch{ + case e: Exception => + e.printStackTrace() + } + } + + + def setUpEmbeddedCassandra(): Unit = { + System.setProperty("cassandra.unsafesystem", "true") + EmbeddedCassandraServerHelper.startEmbeddedCassandra("/cassandra-unit.yaml", 100000L) + } + + override def beforeAll(): Unit = { + tearEmbeddedNeo4JSetup() + setUpEmbeddedNeo4j() + setUpEmbeddedCassandra() + executeCassandraQuery(script_1, script_2, script_5, script_6, script_7, script_8, script_9, script_10, script_11, script_12) + } + + override def afterAll(): Unit = { + tearEmbeddedNeo4JSetup() + if(null != session && !session.isClosed) + session.close() + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + } + + + def executeCassandraQuery(queries: String*): Unit = { + if(null == session || session.isClosed){ + session = CassandraConnector.getSession + } + for(query <- queries) { + session.execute(query) + } + } + + def readFromCassandra(query: String) : ResultSet = { + if(null == session || session.isClosed){ + session = CassandraConnector.getSession + } + session.execute(query) + } + + def createRelationData(): Unit = { + graphDb.execute("UNWIND [{identifier:\"Num:C3:SC2\",code:\"Num:C3:SC2\",keywords:[\"Subconcept\",\"Class 3\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",subject:\"numeracy\",channel:\"in.ekstep\",description:\"Multiplication\",versionKey:\"1484389136575\",gradeLevel:[\"Grade 3\",\"Grade 4\"],IL_FUNC_OBJECT_TYPE:\"Concept\",name:\"Multiplication\",lastUpdatedOn:\"2016-06-15T17:15:45.951+0000\",IL_UNIQUE_ID:\"Num:C3:SC2\",status:\"Live\"}, {code:\"31d521da-61de-4220-9277-21ca7ce8335c\",previewUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",downloadUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",channel:\"in.ekstep\",language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790848197_do_11232724509261824014_2.0_spine.ecar\\\",\\\"size\\\":890.0}}\",mimeType:\"application/pdf\",streamingUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",idealScreenSize:\"normal\",createdOn:\"2017-09-07T13:24:20.720+0000\",contentDisposition:\"inline\",artifactUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",contentEncoding:\"identity\",lastUpdatedOn:\"2017-09-07T13:25:53.595+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2017-09-07T13:27:28.417+0000\",contentType:\"Resource\",lastUpdatedBy:\"Ekstep\",audience:[\"Student\"],visibility:\"Default\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",consumerId:\"e84015d2-a541-4c07-a53f-e31d4553312b\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",pkgVersion:2,versionKey:\"1504790848417\",license:\"Creative Commons Attribution (CC BY)\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",size:4864851,lastPublishedOn:\"2017-09-07T13:27:27.410+0000\",createdBy:\"390\",compatibilityLevel:4,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Untitled Content\",publisher:\"EkStep\",IL_UNIQUE_ID:\"do_11232724509261824014\",status:\"Live\",resourceType:[\"Study material\"]}" + + ",{owner:\"in.ekstep\",code:\"NCF\",IL_SYS_NODE_TYPE:\"DATA_NODE\",apoc_json:\"{\\\"batch\\\": true}\",consumerId:\"9393568c-3a56-47dd-a9a3-34da3c821638\",channel:\"in.ekstep\",description:\"NCF \",type:\"K-12\",createdOn:\"2018-01-23T09:53:50.189+0000\",versionKey:\"1545195552163\",apoc_text:\"APOC\",appId:\"dev.sunbird.portal\",IL_FUNC_OBJECT_TYPE:\"Framework\",name:\"State (Uttar Pradesh)\",lastUpdatedOn:\"2018-12-19T04:59:12.163+0000\",IL_UNIQUE_ID:\"NCF\",status:\"Live\",apoc_num:1}" + + ",{owner:\"in.ekstep\",code:\"K-12\",IL_SYS_NODE_TYPE:\"DATA_NODE\",apoc_json:\"{\\\"batch\\\": true}\",consumerId:\"9393568c-3a56-47dd-a9a3-34da3c821638\",channel:\"in.ekstep\",description:\"NCF \",type:\"K-12\",createdOn:\"2018-01-23T09:53:50.189+0000\",versionKey:\"1545195552163\",apoc_text:\"APOC\",appId:\"dev.sunbird.portal\",IL_FUNC_OBJECT_TYPE:\"Framework\",name:\"State (Uttar Pradesh)\",lastUpdatedOn:\"2018-12-19T04:59:12.163+0000\",IL_UNIQUE_ID:\"K-12\",status:\"Live\",apoc_num:1}" + + ",{owner:\"in.ekstep\",code:\"tpd\",IL_SYS_NODE_TYPE:\"DATA_NODE\",apoc_json:\"{\\\"batch\\\": true}\",consumerId:\"9393568c-3a56-47dd-a9a3-34da3c821638\",channel:\"in.ekstep\",description:\"NCF \",type:\"K-12\",createdOn:\"2018-01-23T09:53:50.189+0000\",versionKey:\"1545195552163\",apoc_text:\"APOC\",appId:\"dev.sunbird.portal\",IL_FUNC_OBJECT_TYPE:\"Framework\",name:\"State (Uttar Pradesh)\",lastUpdatedOn:\"2018-12-19T04:59:12.163+0000\",IL_UNIQUE_ID:\"tpd\",status:\"Live\",apoc_num:1}] as row CREATE (n:domain) SET n += row") + } +} diff --git a/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestHierarchy.scala b/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestHierarchy.scala new file mode 100644 index 000000000..af250f2cf --- /dev/null +++ b/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestHierarchy.scala @@ -0,0 +1,579 @@ +package org.sunbird.managers + +import java.util + +import org.apache.commons.collections4.CollectionUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.OntologyEngineContext + +class TestHierarchy extends BaseSpec { + + private val script_1 = "CREATE KEYSPACE IF NOT EXISTS hierarchy_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_2 = "CREATE TABLE IF NOT EXISTS hierarchy_store.content_hierarchy (identifier text, hierarchy text,PRIMARY KEY (identifier));" + private val script_3 = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11283193441064550414', '{\"identifier\":\"do_11283193441064550414\",\"children\":[{\"parent\":\"do_11283193441064550414\",\"identifier\":\"do_11283193463014195215\",\"copyright\":\"Sunbird\",\"lastStatusChangedOn\":\"2019-08-21T14:37:50.281+0000\",\"code\":\"2e837725-d663-45da-8ace-9577ab111982\",\"visibility\":\"Parent\",\"index\":1,\"mimeType\":\"application/vnd.ekstep.content-collection\",\"createdOn\":\"2019-08-21T14:37:50.281+0000\",\"versionKey\":\"1566398270281\",\"framework\":\"tpd\",\"depth\":1,\"children\":[],\"name\":\"U1\",\"lastUpdatedOn\":\"2019-08-21T14:37:50.281+0000\",\"contentType\":\"CourseUnit\",\"status\":\"Draft\", \"objectType\":\"Collection\"}]}');" + private val script_4 = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11283193441064550414', '{\"status\":\"Live\",\"children\":[{\"parent\":\"do_11283193441064550414\",\"identifier\":\"do_11283193463014195215\",\"copyright\":\"Sunbird\",\"lastStatusChangedOn\":\"2019-08-21T14:37:50.281+0000\",\"code\":\"2e837725-d663-45da-8ace-9577ab111982\",\"visibility\":\"Parent\",\"index\":1,\"mimeType\":\"application/vnd.ekstep.content-collection\",\"createdOn\":\"2019-08-21T14:37:50.281+0000\",\"versionKey\":\"1566398270281\",\"framework\":\"tpd\",\"depth\":1,\"children\":[],\"name\":\"U1\",\"lastUpdatedOn\":\"2019-08-21T14:37:50.281+0000\",\"contentType\":\"CourseUnit\",\"status\":\"Draft\"}]}}');" + private val script_5 = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11323126798764441611181', '{\"identifier\":\"do_11323126798764441611181\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_11323126798764441611181\",\"code\":\"U1\",\"keywords\":[],\"credentials\":{\"enabled\":\"No\"},\"channel\":\"sunbird\",\"description\":\"U1-For Content\",\"language\":[\"English\"],\"mimeType\":\"application/vnd.ekstep.content-collection\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2021-03-07T19:24:58.991+0000\",\"objectType\":\"Collection\",\"primaryCategory\":\"Textbook Unit\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_11323126865092608011182\",\"copyright\":\"Kerala State\",\"previewUrl\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/content/assets/do_11307457137049600011786/eng-presentation_1597086905822.pdf\",\"keywords\":[\"By the Hands of the Nature\"],\"subject\":[\"Geography\"],\"channel\":\"0126202691023585280\",\"downloadUrl\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/ecar_files/do_11307457137049600011786/by-the-hands-of-the-nature_1597087677810_do_11307457137049600011786_1.0.ecar\",\"organisation\":[\"Kerala State\"],\"textbook_name\":[\"Contemporary India - I\"],\"showNotification\":true,\"language\":[\"English\"],\"source\":\"Kl 4\",\"mimeType\":\"application/pdf\",\"variants\":{\"spine\":{\"ecarUrl\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/ecar_files/do_11307457137049600011786/by-the-hands-of-the-nature_1597087678819_do_11307457137049600011786_1.0_spine.ecar\",\"size\":36508.0}},\"objectType\":\"Content\",\"sourceURL\":\"https://diksha.gov.in/play/content/do_312783564254150656111171\",\"gradeLevel\":[\"Class 9\"],\"me_totalRatingsCount\":36,\"appIcon\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_312783564254150656111171/artifact/screenshot-from-2019-06-14-11-59-39_1560493827569.thumb.png\",\"primaryCategory\":\"Learning Resource\",\"level2Name\":[\"Physical Features of India\"],\"appId\":\"prod.diksha.portal\",\"contentEncoding\":\"identity\",\"artifactUrl\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/content/assets/do_11307457137049600011786/eng-presentation_1597086905822.pdf\",\"me_totalPlaySessionCount\":{\"portal\":28},\"sYS_INTERNAL_LAST_UPDATED_ON\":\"2020-08-10T19:27:58.911+0000\",\"contentType\":\"Resource\",\"identifier\":\"do_11307457137049600011786\",\"audience\":[\"Student\"],\"me_totalTimeSpentInSec\":{\"portal\":2444},\"visibility\":\"Default\",\"author\":\"Kerala State\",\"consumerId\":\"89490534-126f-4f0b-82ac-3ff3e49f3468\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"languageCode\":[\"en\"],\"lastPublishedBy\":\"ee2a003a-10a9-4152-8907-a905b9e1f943\",\"version\":2,\"pragma\":[\"external\"],\"license\":\"CC BY 4.0\",\"prevState\":\"Draft\",\"size\":1.000519E7,\"lastPublishedOn\":\"2020-08-10T19:27:57.797+0000\",\"name\":\"By the Hands of the Nature\",\"status\":\"Live\",\"code\":\"6633b233-bc5a-4936-a7a0-da37ebd33868\",\"prevStatus\":\"Processing\",\"origin\":\"do_312783564254150656111171\",\"streamingUrl\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/content/assets/do_11307457137049600011786/eng-presentation_1597086905822.pdf\",\"medium\":[\"English\"],\"posterImage\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_312783565429334016112815/artifact/screenshot-from-2019-06-14-11-59-39_1560493827569.png\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-07-29T10:03:33.003+0000\",\"copyrightYear\":2019,\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-08-10T19:27:57.376+0000\",\"originData\":{\"identifier\":\"do_312783564254150656111171\",\"repository\":\"https://dock.sunbirded.org/do_312783564254150656111171\"},\"level1Concept\":[\"Physical Features of India\"],\"dialcodeRequired\":\"No\",\"owner\":\"Kerala SCERT\",\"lastStatusChangedOn\":\"2020-08-10T19:27:58.907+0000\",\"createdFor\":[\"0126202691023585280\"],\"creator\":\"SAJEEV THOMAS\",\"os\":[\"All\"],\"level1Name\":[\"Contemporary India - I\"],\"pkgVersion\":1.0,\"versionKey\":\"1597087677376\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"kl_k-12\",\"depth\":2,\"s3Key\":\"ecar_files/do_11307457137049600011786/by-the-hands-of-the-nature_1597087677810_do_11307457137049600011786_1.0.ecar\",\"me_averageRating\":3,\"createdBy\":\"f20a4bbf-df17-425b-8e43-bd3dd57bde83\",\"compatibilityLevel\":4,\"ownedBy\":\"0126202691023585280\",\"board\":\"CBSE\",\"resourceType\":\"Learn\"}],\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2021-03-07T19:24:58.990+0000\",\"contentEncoding\":\"gzip\",\"contentType\":\"TextBookUnit\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_11323126865092608011182\",\"lastStatusChangedOn\":\"2021-03-07T19:24:58.991+0000\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Parent\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.launcher\",\"languageCode\":[\"en\"],\"version\":2,\"versionKey\":\"1615145098991\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"depth\":1,\"compatibilityLevel\":1,\"name\":\"U1\",\"status\":\"Draft\"},{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_11323126798764441611181\",\"code\":\"U2\",\"keywords\":[],\"credentials\":{\"enabled\":\"No\"},\"channel\":\"sunbird\",\"description\":\"U2-For Other Objects\",\"language\":[\"English\"],\"mimeType\":\"application/vnd.ekstep.content-collection\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2021-03-07T19:24:58.993+0000\",\"objectType\":\"Collection\",\"primaryCategory\":\"Textbook Unit\",\"children\":[{\"parent\":\"do_11323126865095065611184\",\"code\":\"finemanfine\",\"previewUrl\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/questionset/do_113212597854404608111/do_113212597854404608111_html_1612875515166.html\",\"allowSkip\":\"Yes\",\"containsUserData\":\"No\",\"downloadUrl\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/questionset/do_113212597854404608111/test-question-set_1612875514981_do_113212597854404608111_5_SPINE.ecar\",\"description\":\"Updated QS Description\",\"language\":[\"English\"],\"mimeType\":\"application/vnd.sunbird.questionset\",\"showHints\":\"No\",\"variants\":{\"spine\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/questionset/do_113212597854404608111/test-question-set_1612875514981_do_113212597854404608111_5_SPINE.ecar\",\"online\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/questionset/do_113212597854404608111/test-question-set_1612875515115_do_113212597854404608111_5_ONLINE.ecar\"},\"createdOn\":\"2021-02-09T10:19:09.026+0000\",\"objectType\":\"QuestionSet\",\"pdfUrl\":\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/questionset/do_113212597854404608111/do_113212597854404608111_pdf_1612875515932.pdf\",\"primaryCategory\":\"Practice Question Set\",\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2021-02-09T12:58:36.155+0000\",\"contentEncoding\":\"gzip\",\"contentType\":\"PracticeResource\",\"showSolutions\":\"Yes\",\"allowAnonymousAccess\":\"Yes\",\"identifier\":\"do_113212597854404608111\",\"lastStatusChangedOn\":\"2021-02-09T12:58:36.155+0000\",\"requiresSubmit\":\"Yes\",\"visibility\":\"Default\",\"showTimer\":\"No\",\"summaryType\":\"Complete\",\"consumerId\":\"fa13b438-8a3d-41b1-8278-33b0c50210e4\",\"childNodes\":[\"do_113212598840246272112\",\"do_113212599692050432114\",\"do_113212600505057280116\"],\"index\":1,\"setType\":\"materialised\",\"languageCode\":[\"en\"],\"version\":1,\"pkgVersion\":5,\"versionKey\":\"1612875494848\",\"showFeedback\":\"Yes\",\"license\":\"CC BY 4.0\",\"depth\":2,\"lastPublishedOn\":\"2021-02-09T12:58:34.976+0000\",\"compatibilityLevel\":5,\"name\":\"Test Question Set\",\"navigationMode\":\"linear\",\"shuffle\":true,\"status\":\"Live\"}],\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2021-03-07T19:24:58.993+0000\",\"contentEncoding\":\"gzip\",\"contentType\":\"TextBookUnit\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_11323126865095065611184\",\"lastStatusChangedOn\":\"2021-03-07T19:24:58.993+0000\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Parent\",\"index\":2,\"mediaType\":\"content\",\"osId\":\"org.ekstep.launcher\",\"languageCode\":[\"en\"],\"version\":2,\"versionKey\":\"1615145098993\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"depth\":1,\"compatibilityLevel\":1,\"name\":\"U2\",\"status\":\"Draft\"}]}');" + private val script_6 = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_26543193441064550414', '{\"identifier\":\"do_26543193441064550414\",\"children\":[{\"parent\":\"do_26543193441064550414\",\"identifier\":\"do_11283193463014195215\",\"copyright\":\"Sunbird\",\"lastStatusChangedOn\":\"2019-08-21T14:37:50.281+0000\",\"code\":\"2e837725-d663-45da-8ace-9577ab111982\",\"visibility\":\"Parent\",\"index\":1,\"mimeType\":\"application/vnd.ekstep.content-collection\",\"createdOn\":\"2019-08-21T14:37:50.281+0000\",\"versionKey\":\"1566398270281\",\"framework\":\"tpd\",\"depth\":1,\"children\":[],\"name\":\"U1\",\"lastUpdatedOn\":\"2019-08-21T14:37:50.281+0000\",\"contentType\":\"CourseUnit\",\"status\":\"Draft\", \"objectType\":\"Collection\"}]}');" + implicit val oec: OntologyEngineContext = new OntologyEngineContext + + override def beforeAll(): Unit = { + super.beforeAll() + graphDb.execute("UNWIND [{code:\"questionId\",subject:[\"Health and Physical Education\"],language:[\"English\"],medium:[\"English\"],mimeType:\"application/vnd.sunbird.question\",createdOn:\"2021-01-13T09:29:06.255+0000\",IL_FUNC_OBJECT_TYPE:\"Question\",gradeLevel:[\"Class 6\"],contentDisposition:\"inline\",lastUpdatedOn:\"2021-02-08T11:19:08.989+0000\",contentEncoding:\"gzip\",showSolutions:\"No\",allowAnonymousAccess:\"Yes\",IL_UNIQUE_ID:\"do_113193462958120275141\",lastStatusChangedOn:\"2021-02-08T11:19:08.989+0000\",visibility:\"Parent\",showTimer:\"No\",author:\"Vaibhav\",qType:\"SA\",languageCode:[\"en\"],version:1,versionKey:\"1611554879383\",showFeedback:\"No\",license:\"CC BY 4.0\",prevState:\"Review\",compatibilityLevel:4,name:\"Subjective\",topic:[\"Leaves\"],board:\"CBSE\",status:\"Live\"},{code:\"questionId\",subject:[\"Health and Physical Education\"],language:[\"English\"],medium:[\"English\"],mimeType:\"application/vnd.sunbird.question\",createdOn:\"2021-01-13T09:29:06.255+0000\",IL_FUNC_OBJECT_TYPE:\"Question\",gradeLevel:[\"Class 6\"],contentDisposition:\"inline\",lastUpdatedOn:\"2021-02-08T11:19:08.989+0000\",contentEncoding:\"gzip\",showSolutions:\"No\",allowAnonymousAccess:\"Yes\",IL_UNIQUE_ID:\"do_113193462958120960141\",lastStatusChangedOn:\"2021-02-08T11:19:08.989+0000\",visibility:\"Parent\",showTimer:\"No\",author:\"Vaibhav\",qType:\"SA\",languageCode:[\"en\"],version:1,versionKey:\"1611554879383\",showFeedback:\"No\",license:\"CC BY 4.0\",prevState:\"Review\",compatibilityLevel:4,name:\"Subjective\",topic:[\"Leaves\"],board:\"CBSE\",status:\"Draft\"},{ownershipType:[\"createdBy\"],copyright:\"ORG_002\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",channel:\"01246944855007232011\",organisation:[\"ORG_002\"],showNotification:true,language:[\"English\"],mimeType:\"video/mp4\",variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389714022_do_112831862871203840114_1.0_spine.ecar\\\",\\\"size\\\":35757.0}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112831862871203840114/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"dev.sunbird.portal\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",contentEncoding:\"identity\",lockKey:\"be6bc445-c75e-471d-b46f-71fefe4a1d2f\",contentType:\"Resource\",lastUpdatedBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",audience:[\"Student\"],visibility:\"Default\",consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:1,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",lastPublishedOn:\"2019-08-21T12:15:13.652+0000\",size:416488,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Test Resource Cert\",status:\"Live\",code:\"7e6630c7-3818-4319-92ac-4d08c33904d8\",streamingUrl:\"https://sunbirddevmedia-inct.streaming.media.azure.net/25d7a94c-9be3-471c-926b-51eb5d3c4c2c/small.ism/manifest(format=m3u8-aapl-v3)\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T12:11:50.644+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T12:15:13.020+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-08-21T12:30:16.783+0000\",dialcodeRequired:\"No\",creator:\"Pradyumna\",lastStatusChangedOn:\"2019-08-21T12:15:14.384+0000\",createdFor:[\"01246944855007232011\"],os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566389713020\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",framework:\"K-12\",createdBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",compatibilityLevel:1,IL_UNIQUE_ID:\"do_112831862871203840114\",resourceType:\"Learn\"},{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",certTemplate:\"[{\\\"name\\\":\\\"100PercentCompletionCertificate\\\",\\\"issuer\\\":{\\\"name\\\":\\\"Gujarat Council of Educational Research and Training\\\",\\\"url\\\":\\\"https://gcert.gujarat.gov.in/gcert/\\\",\\\"publicKey\\\":[\\\"1\\\",\\\"2\\\"]},\\\"signatoryList\\\":[{\\\"name\\\":\\\"CEO Gujarat\\\",\\\"id\\\":\\\"CEO\\\",\\\"designation\\\":\\\"CEO\\\",\\\"image\\\":\\\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\\\"}],\\\"htmlTemplate\\\":\\\"https://drive.google.com/uc?authuser=1&id=1ryB71i0Oqn2c3aqf9N6Lwvet-MZKytoM&export=download\\\",\\\"notifyTemplate\\\":{\\\"subject\\\":\\\"Course completion certificate\\\",\\\"stateImgUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png\\\",\\\"regardsperson\\\":\\\"Chairperson\\\",\\\"regards\\\":\\\"Minister of Gujarat\\\",\\\"emailTemplateType\\\":\\\"defaultCertTemp\\\"}}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398314186_do_11283193441064550414_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550414/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550414/artifact/do_11283193441064550414_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Collection\",name:\"test prad course cert\",status:\"Live\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11283193441064550414\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\"}] as row CREATE (n:domain) SET n += row") + executeCassandraQuery(script_1, script_2, script_3) + RedisCache.delete("hierarchy_do_11283193441064550414") + } + + + "addLeafNodesToHierarchy" should "addLeafNodesToHierarchy" in { + executeCassandraQuery(script_3) + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Collection") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11283193441064550414") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_112831862871203840114")) + request.put("mode","edit") + val future = HierarchyManager.addLeafNodesToHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].containsAll(request.get("children").asInstanceOf[util.List[String]])) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(!response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].contains("do_11283193463014195215")) + assert(hierarchy.contains("do_112831862871203840114")) + }) + } + + "addLeafNodesToHierarchy with children having different objectType than supported one" should "throw ClientException" in { + executeCassandraQuery(script_3) + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"SC-2200_3eac25ae-a0c9-4d7c-87be-954406824cb8\",channel:\"sunbird\",description:\"Test-Add/Remove Leaf Node\",language:[\"English\"],mimeType:\"video/mp4\",idealScreenSize:\"normal\",createdOn:\"2021-03-07T19:23:38.025+0000\",IL_FUNC_OBJECT_TYPE:\"Asset\",contentDisposition:\"inline\",additionalCategories:[\"Textbook\"],lastUpdatedOn:\"2021-03-07T19:24:59.023+0000\",contentEncoding:\"gzip\",contentType:\"Resource\",dialcodeRequired:\"No\",IL_UNIQUE_ID:\"do_asset_001\",lastStatusChangedOn:\"2021-03-07T19:23:38.025+0000\",audience:[\"Student\"],os:[\"All\"],visibility:\"Default\",mediaType:\"asset\",osId:\"org.ekstep.quiz.app\",languageCode:[\"en\"],version:2,versionKey:\"1615145099023\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,compatibilityLevel:1,userConsent:\"Yes\",name:\"SC-2200-TextBook\",status:\"Draft\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Collection") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11283193441064550414") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_asset_001")) + request.put("mode","edit") + recoverToSucceededIf[ClientException](HierarchyManager.addLeafNodesToHierarchy(request)) + } + + ignore should "add the Question with draft status into hierarchy" in { + executeCassandraQuery(script_3) + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Collection") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11283193441064550414") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_113193462958120960141")) + request.put("mode","edit") + val future = HierarchyManager.addLeafNodesToHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].containsAll(request.get("children").asInstanceOf[util.List[String]])) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(!response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].contains("do_11283193463014195215")) + assert(hierarchy.contains("do_113193462958120960141")) + }) + } + + ignore should "add Question as Leaf Node" in { + executeCassandraQuery(script_3) + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Collection") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11283193441064550414") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_113193462958120275141")) + request.put("mode","edit") + val future = HierarchyManager.addLeafNodesToHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].containsAll(request.get("children").asInstanceOf[util.List[String]])) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(!response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].contains("do_11283193463014195215")) + assert(hierarchy.contains("do_113193462958120275141")) + }) + } + + "addLeafNodesToHierarchy for QuestionSet object" should "addLeafNodesToHierarchy" in { + executeCassandraQuery(script_3) + graphDb.execute("UNWIND [{copyright:\"Hello\",code:\"do_113193433773948928111\",allowSkip:\"Yes\",keywords:[\"135\",\"666667\"],containsUserData:\"No\",subject:[\"Hindi\"],description:\"Hello\",language:[\"English\"],medium:[\"Hindi\"],mimeType:\"application/vnd.sunbird.questionset\",showHints:\"No\",createdOn:\"2021-01-13T08:29:43.736+0000\",IL_FUNC_OBJECT_TYPE:\"QuestionSet\",gradeLevel:[\"Class 7\"],contentDisposition:\"inline\",additionalCategories:[\"Classroom Teaching Video\",\"Concept Map\",\"Textbook\",\"Curiosity Question Set\"],lastUpdatedOn:\"2021-02-08T12:20:33.201+0000\",contentEncoding:\"gzip\",showSolutions:\"Yes\",allowAnonymousAccess:\"Yes\",IL_UNIQUE_ID:\"do_113193433773948928111\",lastStatusChangedOn:\"2021-01-29T06:13:52.095+0000\",audience:[\"Teacher\"],requiresSubmit:\"Yes\",visibility:\"Default\",showTimer:\"Yes\",author:\"Hello\",summaryType:\"Complete\",consumerId:\"fa13b438-8a3d-41b1-8278-33b0c50210e4\",childNodes:[\"do_113193462958120960141\",\"do_113193463656955904143\",\"do_113197944463515648120\",\"do_113209072358883328150\",\"do_113193462438895616139\"],setType:\"materialised\",languageCode:[\"en\"],version:1,versionKey:\"1612786833201\",showFeedback:\"Yes\",license:\"CC BY 4.0\",prevState:\"Draft\",framework:\"ekstep_ncert_k-12\",depth:0,compatibilityLevel:4,name:\"u0926u0941u0903u0916 u0915u093E u0905u0927u093Fu0915u093Eu0930\",navigationMode:\"linear\",topic:[\"Leaves\",\"Water\"],shuffle:true,attributions:[\"Hello\"],board:\"CBSE\",status:\"Review\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Collection") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11283193441064550414") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_113193433773948928111")) + request.put("mode","edit") + val future = HierarchyManager.addLeafNodesToHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].containsAll(request.get("children").asInstanceOf[util.List[String]])) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(!response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].contains("do_11283193463014195215")) + assert(hierarchy.contains("do_113193433773948928111")) + }) + } + + ignore should "remove Question object from hierarchy" in { + executeCassandraQuery(script_6) + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"SC-2200_3eac25ae-a0c9-4d7c-87be-954406824cb8\",channel:\"sunbird\",description:\"Test-Add/Remove Leaf Node\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2021-03-07T19:23:38.025+0000\",IL_FUNC_OBJECT_TYPE:\"Collection\",contentDisposition:\"inline\",additionalCategories:[\"Textbook\"],lastUpdatedOn:\"2021-03-07T19:24:59.023+0000\",contentEncoding:\"gzip\",contentType:\"TextBook\",dialcodeRequired:\"No\",IL_UNIQUE_ID:\"do_26543193441064550414\",lastStatusChangedOn:\"2021-03-07T19:23:38.025+0000\",audience:[\"Student\"],os:[\"All\"],visibility:\"Default\",childNodes:[\"do_11307457137049600011786\",\"do_11323126865092608011182\",\"do_113212597854404608111\",\"do_11323126865095065611184\"],mediaType:\"content\",osId:\"org.ekstep.quiz.app\",languageCode:[\"en\"],version:2,versionKey:\"1615145099023\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,compatibilityLevel:1,userConsent:\"Yes\",name:\"SC-2200-TextBook\",status:\"Draft\"},{copyright:\"Hello\",code:\"do_113193433773948928111\",allowSkip:\"Yes\",keywords:[\"135\",\"666667\"],containsUserData:\"No\",subject:[\"Hindi\"],description:\"Hello\",language:[\"English\"],medium:[\"Hindi\"],mimeType:\"application/vnd.sunbird.questionset\",showHints:\"No\",createdOn:\"2021-01-13T08:29:43.736+0000\",IL_FUNC_OBJECT_TYPE:\"QuestionSet\",gradeLevel:[\"Class 7\"],contentDisposition:\"inline\",additionalCategories:[\"Classroom Teaching Video\",\"Concept Map\",\"Textbook\",\"Curiosity Question Set\"],lastUpdatedOn:\"2021-02-08T12:20:33.201+0000\",contentEncoding:\"gzip\",showSolutions:\"Yes\",allowAnonymousAccess:\"Yes\",IL_UNIQUE_ID:\"do_113193433773948928111\",lastStatusChangedOn:\"2021-01-29T06:13:52.095+0000\",audience:[\"Teacher\"],requiresSubmit:\"Yes\",visibility:\"Default\",showTimer:\"Yes\",author:\"Hello\",summaryType:\"Complete\",consumerId:\"fa13b438-8a3d-41b1-8278-33b0c50210e4\",childNodes:[\"do_113193462958120960141\",\"do_113193463656955904143\",\"do_113197944463515648120\",\"do_113209072358883328150\",\"do_113193462438895616139\"],setType:\"materialised\",languageCode:[\"en\"],version:1,versionKey:\"1612786833201\",showFeedback:\"Yes\",license:\"CC BY 4.0\",prevState:\"Draft\",framework:\"ekstep_ncert_k-12\",depth:0,compatibilityLevel:4,name:\"u0926u0941u0903u0916 u0915u093E u0905u0927u093Fu0915u093Eu0930\",navigationMode:\"linear\",topic:[\"Leaves\",\"Water\"],shuffle:true,attributions:[\"Hello\"],board:\"CBSE\",status:\"Review\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Collection") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_26543193441064550414") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_113193433773948928111")) + request.put("mode","edit") + val future = HierarchyManager.addLeafNodesToHierarchy(request) + future.map(response => { + println("result ::::=="+response.getResult) + assert(response.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_26543193441064550414.img'") + .one().getString("hierarchy") + assert(hierarchy.contains("do_113193433773948928111")) + val removeFuture = HierarchyManager.removeLeafNodesFromHierarchy(request) + removeFuture.map(resp => { + assert(resp.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_26543193441064550414.img'") + .one().getString("hierarchy") + assert(!hierarchy.contains("do_113193433773948928111")) + }) + }).flatMap(f => f) + } + + ignore should "removeLeafNodesToHierarchy" in { + executeCassandraQuery(script_3) + graphDb.execute("UNWIND [{copyright:\"Hello\",code:\"do_113193433773948928111\",allowSkip:\"Yes\",keywords:[\"135\",\"666667\"],containsUserData:\"No\",subject:[\"Hindi\"],description:\"Hello\",language:[\"English\"],medium:[\"Hindi\"],mimeType:\"application/vnd.sunbird.questionset\",showHints:\"No\",createdOn:\"2021-01-13T08:29:43.736+0000\",IL_FUNC_OBJECT_TYPE:\"QuestionSet\",gradeLevel:[\"Class 7\"],contentDisposition:\"inline\",additionalCategories:[\"Classroom Teaching Video\",\"Concept Map\",\"Textbook\",\"Curiosity Question Set\"],lastUpdatedOn:\"2021-02-08T12:20:33.201+0000\",contentEncoding:\"gzip\",showSolutions:\"Yes\",allowAnonymousAccess:\"Yes\",IL_UNIQUE_ID:\"do_113193433773948928111\",lastStatusChangedOn:\"2021-01-29T06:13:52.095+0000\",audience:[\"Teacher\"],requiresSubmit:\"Yes\",visibility:\"Default\",showTimer:\"Yes\",author:\"Hello\",summaryType:\"Complete\",consumerId:\"fa13b438-8a3d-41b1-8278-33b0c50210e4\",childNodes:[\"do_113193462958120960141\",\"do_113193463656955904143\",\"do_113197944463515648120\",\"do_113209072358883328150\",\"do_113193462438895616139\"],setType:\"materialised\",languageCode:[\"en\"],version:1,versionKey:\"1612786833201\",showFeedback:\"Yes\",license:\"CC BY 4.0\",prevState:\"Draft\",framework:\"ekstep_ncert_k-12\",depth:0,compatibilityLevel:4,name:\"u0926u0941u0903u0916 u0915u093E u0905u0927u093Fu0915u093Eu0930\",navigationMode:\"linear\",topic:[\"Leaves\",\"Water\"],shuffle:true,attributions:[\"Hello\"],board:\"CBSE\",status:\"Review\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Collection") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11283193441064550414") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_113193462958120275141")) + request.put("mode","edit") + val future = HierarchyManager.addLeafNodesToHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(hierarchy.contains("do_113193462958120275141")) + val removeFuture = HierarchyManager.removeLeafNodesFromHierarchy(request) + removeFuture.map(resp => { + assert(resp.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(!hierarchy.contains("do_113193462958120275141")) + }) + }).flatMap(f => f) + } + + "removeLeafNodesToHierarchy" should "removeLeafNodesToHierarchy" in { + executeCassandraQuery(script_3) + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Collection") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11283193441064550414") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_112831862871203840114")) + request.put("mode","edit") + val future = HierarchyManager.addLeafNodesToHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(hierarchy.contains("do_112831862871203840114")) + val removeFuture = HierarchyManager.removeLeafNodesFromHierarchy(request) + removeFuture.map(resp => { + assert(resp.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(!hierarchy.contains("do_112831862871203840114")) + }) + }).flatMap(f => f) + } + + "addLeafNodesToHierarchy for shallowcopied" should "throw client exception " in { + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",certTemplate:\"[{\\\"name\\\":\\\"100PercentCompletionCertificate\\\",\\\"issuer\\\":{\\\"name\\\":\\\"Gujarat Council of Educational Research and Training\\\",\\\"url\\\":\\\"https://gcert.gujarat.gov.in/gcert/\\\",\\\"publicKey\\\":[\\\"1\\\",\\\"2\\\"]},\\\"signatoryList\\\":[{\\\"name\\\":\\\"CEO Gujarat\\\",\\\"id\\\":\\\"CEO\\\",\\\"designation\\\":\\\"CEO\\\",\\\"image\\\":\\\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\\\"}],\\\"htmlTemplate\\\":\\\"https://drive.google.com/uc?authuser=1&id=1ryB71i0Oqn2c3aqf9N6Lwvet-MZKytoM&export=download\\\",\\\"notifyTemplate\\\":{\\\"subject\\\":\\\"Course completion certificate\\\",\\\"stateImgUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png\\\",\\\"regardsperson\\\":\\\"Chairperson\\\",\\\"regards\\\":\\\"Minister of Gujarat\\\",\\\"emailTemplateType\\\":\\\"defaultCertTemp\\\"}}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11298480837245337614/test-prad-course-cert_1566398313947_do_11298480837245337614_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11298480837245337614/test-prad-course-cert_1566398314186_do_11298480837245337614_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11298480837245337614/test-prad-course-cert_1566398313947_do_11298480837245337614_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11298480837245337614/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11298480837245337614/artifact/do_11298480837245337614_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Content\",name:\"test prad course cert\",status:\"Live\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11298480837245337614/test-prad-course-cert_1566398313947_do_11298480837245337614_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11298480837245337614\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\",originData:\"{\\\"name\\\":\\\"Copy Collecction Testing For shallow Copy\\\",\\\"copyType\\\":\\\"shallow\\\",\\\"license\\\":\\\"CC BY 4.0\\\",\\\"organisation\\\":[\\\"test\\\"]}\"}] as row CREATE (n:domain) SET n += row"); + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11298480837245337614") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_112831862871203840114")) + request.put("mode","edit") + + recoverToSucceededIf[ClientException](HierarchyManager.addLeafNodesToHierarchy(request)) + + } + + "removeLeafNodesToHierarchy for shallowcopied" should "throw client exception " in { + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",certTemplate:\"[{\\\"name\\\":\\\"100PercentCompletionCertificate\\\",\\\"issuer\\\":{\\\"name\\\":\\\"Gujarat Council of Educational Research and Training\\\",\\\"url\\\":\\\"https://gcert.gujarat.gov.in/gcert/\\\",\\\"publicKey\\\":[\\\"1\\\",\\\"2\\\"]},\\\"signatoryList\\\":[{\\\"name\\\":\\\"CEO Gujarat\\\",\\\"id\\\":\\\"CEO\\\",\\\"designation\\\":\\\"CEO\\\",\\\"image\\\":\\\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\\\"}],\\\"htmlTemplate\\\":\\\"https://drive.google.com/uc?authuser=1&id=1ryB71i0Oqn2c3aqf9N6Lwvet-MZKytoM&export=download\\\",\\\"notifyTemplate\\\":{\\\"subject\\\":\\\"Course completion certificate\\\",\\\"stateImgUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png\\\",\\\"regardsperson\\\":\\\"Chairperson\\\",\\\"regards\\\":\\\"Minister of Gujarat\\\",\\\"emailTemplateType\\\":\\\"defaultCertTemp\\\"}}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11298480837245337614/test-prad-course-cert_1566398313947_do_11298480837245337614_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11298480837245337614/test-prad-course-cert_1566398314186_do_11298480837245337614_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11298480837245337614/test-prad-course-cert_1566398313947_do_11298480837245337614_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11298480837245337614/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11298480837245337614/artifact/do_11298480837245337614_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Content\",name:\"test prad course cert\",status:\"Live\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11298480837245337614/test-prad-course-cert_1566398313947_do_11298480837245337614_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11298480837245337614\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\",originData:\"{\\\"name\\\":\\\"Copy Collecction Testing For shallow Copy\\\",\\\"copyType\\\":\\\"shallow\\\",\\\"license\\\":\\\"CC BY 4.0\\\",\\\"organisation\\\":[\\\"test\\\"]}\"}] as row CREATE (n:domain) SET n += row"); + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + + request.put("rootId", "do_11298480837245337614") + request.put("unitId", "do_11283193463014195215") + request.put("children", util.Arrays.asList("do_112831862871203840114")) + request.put("mode","edit") + + recoverToSucceededIf[ClientException](HierarchyManager.removeLeafNodesFromHierarchy(request)) + + } + +// "getHierarchyWithInvalidIdentifier" should "Resourse_Not_Found" in { +// val request = new Request() +// request.setContext(new util.HashMap[String, AnyRef]() { +// { +// put("objectType", "Content") +// put("graph_id", "domain") +// put("version", "1.0") +// put("schemaName", "collection") +// } +// }) +// request.put("rootId", "1234") +// val future = HierarchyManager.getHierarchy(request) +// future.map(response => { +// assert(response.getResponseCode.code() == 404) +// }) +// } + + "getHierarchyForPublishedContent" should "getHierarchy" in { + val request = new Request() + executeCassandraQuery(script_4) + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + } + }) + request.put("rootId", "do_11283193441064550414") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.getResult.get("content")) + assert(null != response.getResult.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children")) + }) + } + + "getHierarchyWithEditMode" should "getHierarchy" in { + val request = new Request() + executeCassandraQuery(script_3) + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + } + }) + request.put("mode","edit") + request.put("rootId", "do_11283193441064550414") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.getResult.get("content")) + assert(null != response.getResult.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children")) + }) + } + + "getHierarchyWithEditMode having QuestionSet object" should "return the hierarchy" in { + val request = new Request() + executeCassandraQuery(script_5) + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"SC-2200_3eac25ae-a0c9-4d7c-87be-954406824cb8\",channel:\"sunbird\",description:\"Test-Add/Remove Leaf Node\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2021-03-07T19:23:38.025+0000\",IL_FUNC_OBJECT_TYPE:\"Collection\",contentDisposition:\"inline\",additionalCategories:[\"Textbook\"],lastUpdatedOn:\"2021-03-07T19:24:59.023+0000\",contentEncoding:\"gzip\",contentType:\"TextBook\",dialcodeRequired:\"No\",IL_UNIQUE_ID:\"do_11323126798764441611181\",lastStatusChangedOn:\"2021-03-07T19:23:38.025+0000\",audience:[\"Student\"],os:[\"All\"],visibility:\"Default\",childNodes:[\"do_11307457137049600011786\",\"do_11323126865092608011182\",\"do_113212597854404608111\",\"do_11323126865095065611184\"],mediaType:\"content\",osId:\"org.ekstep.quiz.app\",languageCode:[\"en\"],version:2,versionKey:\"1615145099023\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,compatibilityLevel:1,userConsent:\"Yes\",name:\"SC-2200-TextBook\",status:\"Draft\"}] as row CREATE (n:domain) SET n += row") + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"Kerala State\",previewUrl:\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/content/assets/do_11307457137049600011786/eng-presentation_1597086905822.pdf\",keywords:[\"By the Hands of the Nature\"],subject:[\"Geography\"],channel:\"0126202691023585280\",downloadUrl:\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/ecar_files/do_11307457137049600011786/by-the-hands-of-the-nature_1597087677810_do_11307457137049600011786_1.0.ecar\",organisation:[\"Kerala State\"],textbook_name:[\"Contemporary India - I\"],showNotification:true,language:[\"English\"],source:\"Kl 4\",mimeType:\"application/pdf\",IL_FUNC_OBJECT_TYPE:\"Content\",sourceURL:\"https://diksha.gov.in/play/content/do_312783564254150656111171\",gradeLevel:[\"Class 9\"],me_totalRatingsCount:36,appIcon:\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_312783564254150656111171/artifact/screenshot-from-2019-06-14-11-59-39_1560493827569.thumb.png\",level2Name:[\"Physical Features of India\"],appId:\"prod.diksha.portal\",contentEncoding:\"identity\",artifactUrl:\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/content/assets/do_11307457137049600011786/eng-presentation_1597086905822.pdf\",sYS_INTERNAL_LAST_UPDATED_ON:\"2020-08-10T19:27:58.911+0000\",contentType:\"Resource\",IL_UNIQUE_ID:\"do_11307457137049600011786\",audience:[\"Student\"],visibility:\"Default\",author:\"Kerala State\",consumerId:\"89490534-126f-4f0b-82ac-3ff3e49f3468\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",languageCode:[\"en\"],lastPublishedBy:\"ee2a003a-10a9-4152-8907-a905b9e1f943\",version:2,pragma:[\"external\"],license:\"CC BY 4.0\",prevState:\"Draft\",size:10005190,lastPublishedOn:\"2020-08-10T19:27:57.797+0000\",name:\"By the Hands of the Nature\",status:\"Live\",code:\"6633b233-bc5a-4936-a7a0-da37ebd33868\",prevStatus:\"Processing\",origin:\"do_312783564254150656111171\",streamingUrl:\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/content/assets/do_11307457137049600011786/eng-presentation_1597086905822.pdf\",medium:[\"English\"],posterImage:\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_312783565429334016112815/artifact/screenshot-from-2019-06-14-11-59-39_1560493827569.png\",idealScreenSize:\"normal\",createdOn:\"2020-07-29T10:03:33.003+0000\",copyrightYear:2019,contentDisposition:\"inline\",lastUpdatedOn:\"2020-08-10T19:27:57.376+0000\",level1Concept:[\"Physical Features of India\"],dialcodeRequired:\"No\",owner:\"Kerala SCERT\",lastStatusChangedOn:\"2020-08-10T19:27:58.907+0000\",createdFor:[\"0126202691023585280\"],creator:\"SAJEEV THOMAS\",os:[\"All\"],level1Name:[\"Contemporary India - I\"],pkgVersion:1,versionKey:\"1597087677376\",idealScreenDensity:\"hdpi\",framework:\"kl_k-12\",s3Key:\"ecar_files/do_11307457137049600011786/by-the-hands-of-the-nature_1597087677810_do_11307457137049600011786_1.0.ecar\",me_averageRating:3,createdBy:\"f20a4bbf-df17-425b-8e43-bd3dd57bde83\",compatibilityLevel:4,ownedBy:\"0126202691023585280\",board:\"CBSE\",resourceType:\"Learn\"},{code:\"finemanfine\",previewUrl:\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/questionset/do_113212597854404608111/do_113212597854404608111_html_1612875515166.html\",allowSkip:\"Yes\",containsUserData:\"No\",downloadUrl:\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/questionset/do_113212597854404608111/test-question-set_1612875514981_do_113212597854404608111_5_SPINE.ecar\",description:\"Updated QS Description\",language:[\"English\"],mimeType:\"application/vnd.sunbird.questionset\",showHints:\"No\",createdOn:\"2021-02-09T10:19:09.026+0000\",IL_FUNC_OBJECT_TYPE:\"QuestionSet\",pdfUrl:\"https://dockstorage.blob.core.windows.net/sunbird-content-dock/questionset/do_113212597854404608111/do_113212597854404608111_pdf_1612875515932.pdf\",contentDisposition:\"inline\",lastUpdatedOn:\"2021-02-09T12:58:36.155+0000\",contentEncoding:\"gzip\",showSolutions:\"Yes\",allowAnonymousAccess:\"Yes\",IL_UNIQUE_ID:\"do_113212597854404608111\",lastStatusChangedOn:\"2021-02-09T12:58:36.155+0000\",requiresSubmit:\"Yes\",visibility:\"Default\",showTimer:\"No\",summaryType:\"Complete\",consumerId:\"fa13b438-8a3d-41b1-8278-33b0c50210e4\",childNodes:[\"do_113212598840246272112\",\"do_113212599692050432114\",\"do_113212600505057280116\"],setType:\"materialised\",languageCode:[\"en\"],version:1,pkgVersion:5,versionKey:\"1612875494848\",showFeedback:\"Yes\",license:\"CC BY 4.0\",depth:0,lastPublishedOn:\"2021-02-09T12:58:34.976+0000\",compatibilityLevel:5,name:\"Test Question Set\",navigationMode:\"linear\",shuffle:true,status:\"Live\"}] as row CREATE (n:domain) SET n += row") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + } + }) + request.put("mode","edit") + request.put("rootId", "do_11323126798764441611181") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.getResult.get("content")) + println("hierarchy ::::: "+response.getResult.get("content")) + //assert(null != response.getResult.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children")) + val children = response.getResult.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children").asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] + assert(null!=children && !children.isEmpty) + }) + } + + "getHierarchyForDraftAfterUpdateHierarchyWithoutMode" should "getHierarchy" in { + val request = new Request() + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"ORG_002\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",channel:\"01246944855007232011\",organisation:[\"ORG_002\"],showNotification:true,language:[\"English\"],mimeType:\"video/mp4\",variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389714022_do_112831862871203840114_1.0_spine.ecar\\\",\\\"size\\\":35757.0}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112831862871203840114/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"dev.sunbird.portal\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",contentEncoding:\"identity\",lockKey:\"be6bc445-c75e-471d-b46f-71fefe4a1d2f\",contentType:\"Resource\",lastUpdatedBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",audience:[\"Student\"],visibility:\"Default\",consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:1,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",lastPublishedOn:\"2019-08-21T12:15:13.652+0000\",size:416488,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Test Resource Cert\",status:\"Draft\",code:\"7e6630c7-3818-4319-92ac-4d08c33904d8\",streamingUrl:\"https://sunbirddevmedia-inct.streaming.media.azure.net/25d7a94c-9be3-471c-926b-51eb5d3c4c2c/small.ism/manifest(format=m3u8-aapl-v3)\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T12:11:50.644+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T12:15:13.020+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-08-21T12:30:16.783+0000\",dialcodeRequired:\"No\",creator:\"Pradyumna\",lastStatusChangedOn:\"2019-08-21T12:15:14.384+0000\",createdFor:[\"01246944855007232011\"],os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566389713020\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",framework:\"K-12\",createdBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",compatibilityLevel:1,IL_UNIQUE_ID:\"do_112831862871203840114\",resourceType:\"Learn\"},{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",certTemplate:\"[{\\\"name\\\":\\\"100PercentCompletionCertificate\\\",\\\"issuer\\\":{\\\"name\\\":\\\"Gujarat Council of Educational Research and Training\\\",\\\"url\\\":\\\"https://gcert.gujarat.gov.in/gcert/\\\",\\\"publicKey\\\":[\\\"1\\\",\\\"2\\\"]},\\\"signatoryList\\\":[{\\\"name\\\":\\\"CEO Gujarat\\\",\\\"id\\\":\\\"CEO\\\",\\\"designation\\\":\\\"CEO\\\",\\\"image\\\":\\\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\\\"}],\\\"htmlTemplate\\\":\\\"https://drive.google.com/uc?authuser=1&id=1ryB71i0Oqn2c3aqf9N6Lwvet-MZKytoM&export=download\\\",\\\"notifyTemplate\\\":{\\\"subject\\\":\\\"Course completion certificate\\\",\\\"stateImgUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png\\\",\\\"regardsperson\\\":\\\"Chairperson\\\",\\\"regards\\\":\\\"Minister of Gujarat\\\",\\\"emailTemplateType\\\":\\\"defaultCertTemp\\\"}}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398314186_do_11283193441064550411_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550411/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550411/artifact/do_11283193441064550411_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Content\",name:\"test prad course cert\",status:\"Draft\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11283193441064550411\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\"}] as row CREATE (n:domain) SET n += row") + executeCassandraQuery("INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11283193441064550411', '{\"identifier\":\"do_11283193441064550411\",\"children\":[{\"parent\":\"do_11283193441064550411\",\"identifier\":\"do_11283193463014195215\",\"copyright\":\"Sunbird\",\"lastStatusChangedOn\":\"2019-08-21T14:37:50.281+0000\",\"code\":\"2e837725-d663-45da-8ace-9577ab111982\",\"visibility\":\"Parent\",\"index\":1,\"mimeType\":\"application/vnd.ekstep.content-collection\",\"createdOn\":\"2019-08-21T14:37:50.281+0000\",\"versionKey\":\"1566398270281\",\"framework\":\"tpd\",\"depth\":1,\"children\":[],\"name\":\"U1\",\"lastUpdatedOn\":\"2019-08-21T14:37:50.281+0000\",\"contentType\":\"CourseUnit\",\"status\":\"Draft\"}]}')") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + request.put("rootId", "do_11283193441064550411") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 404) + }) + } + + "getHierarchyForDraftAfterUpdateHierarchyWithMode" should "getHierarchy" in { + val request = new Request() + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"ORG_002\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",channel:\"01246944855007232011\",organisation:[\"ORG_002\"],showNotification:true,language:[\"English\"],mimeType:\"video/mp4\",variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389714022_do_112831862871203840114_1.0_spine.ecar\\\",\\\"size\\\":35757.0}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112831862871203840114/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"dev.sunbird.portal\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",contentEncoding:\"identity\",lockKey:\"be6bc445-c75e-471d-b46f-71fefe4a1d2f\",contentType:\"Resource\",lastUpdatedBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",audience:[\"Student\"],visibility:\"Default\",consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:1,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",lastPublishedOn:\"2019-08-21T12:15:13.652+0000\",size:416488,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Test Resource Cert\",status:\"Draft\",code:\"7e6630c7-3818-4319-92ac-4d08c33904d8\",streamingUrl:\"https://sunbirddevmedia-inct.streaming.media.azure.net/25d7a94c-9be3-471c-926b-51eb5d3c4c2c/small.ism/manifest(format=m3u8-aapl-v3)\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T12:11:50.644+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T12:15:13.020+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-08-21T12:30:16.783+0000\",dialcodeRequired:\"No\",creator:\"Pradyumna\",lastStatusChangedOn:\"2019-08-21T12:15:14.384+0000\",createdFor:[\"01246944855007232011\"],os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566389713020\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",framework:\"K-12\",createdBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",compatibilityLevel:1,IL_UNIQUE_ID:\"do_112831862871203840114\",resourceType:\"Learn\"},{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",certTemplate:\"[{\\\"name\\\":\\\"100PercentCompletionCertificate\\\",\\\"issuer\\\":{\\\"name\\\":\\\"Gujarat Council of Educational Research and Training\\\",\\\"url\\\":\\\"https://gcert.gujarat.gov.in/gcert/\\\",\\\"publicKey\\\":[\\\"1\\\",\\\"2\\\"]},\\\"signatoryList\\\":[{\\\"name\\\":\\\"CEO Gujarat\\\",\\\"id\\\":\\\"CEO\\\",\\\"designation\\\":\\\"CEO\\\",\\\"image\\\":\\\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\\\"}],\\\"htmlTemplate\\\":\\\"https://drive.google.com/uc?authuser=1&id=1ryB71i0Oqn2c3aqf9N6Lwvet-MZKytoM&export=download\\\",\\\"notifyTemplate\\\":{\\\"subject\\\":\\\"Course completion certificate\\\",\\\"stateImgUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png\\\",\\\"regardsperson\\\":\\\"Chairperson\\\",\\\"regards\\\":\\\"Minister of Gujarat\\\",\\\"emailTemplateType\\\":\\\"defaultCertTemp\\\"}}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398314186_do_11283193441064550411_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550411/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550411/artifact/do_11283193441064550411_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Content\",name:\"test prad course cert\",status:\"Draft\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11283193441064550411\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\"}] as row CREATE (n:domain) SET n += row") + executeCassandraQuery("INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11283193441064550411', '{\"identifier\":\"do_11283193441064550411\",\"children\":[{\"parent\":\"do_11283193441064550411\",\"identifier\":\"do_11283193463014195215\",\"copyright\":\"Sunbird\",\"lastStatusChangedOn\":\"2019-08-21T14:37:50.281+0000\",\"code\":\"2e837725-d663-45da-8ace-9577ab111982\",\"visibility\":\"Parent\",\"index\":1,\"mimeType\":\"application/vnd.ekstep.content-collection\",\"createdOn\":\"2019-08-21T14:37:50.281+0000\",\"versionKey\":\"1566398270281\",\"framework\":\"tpd\",\"depth\":1,\"children\":[],\"name\":\"U1\",\"lastUpdatedOn\":\"2019-08-21T14:37:50.281+0000\",\"contentType\":\"CourseUnit\",\"status\":\"Draft\"}]}')") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + request.put("mode","edit") + request.put("rootId", "do_11283193441064550411") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.getResult.get("content")) + assert(null != response.getResult.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children")) + }) + } + + "getHierarchyFromCache" should "getHierarchy" in { + val request = new Request() + executeCassandraQuery(script_4) + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + request.put("rootId", "do_11283193441064550414") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != RedisCache.get("hierarchy_do_11283193441064550414")) + }) + } + + "getHierarchyBeforeUpdateHierarchyWithoutMode" should "getHierarchyWithoutChildren" in { + val request = new Request() + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"ORG_002\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",channel:\"01246944855007232011\",organisation:[\"ORG_002\"],showNotification:true,language:[\"English\"],mimeType:\"video/mp4\",variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389714022_do_112831862871203840114_1.0_spine.ecar\\\",\\\"size\\\":35757.0}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112831862871203840114/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"dev.sunbird.portal\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",contentEncoding:\"identity\",lockKey:\"be6bc445-c75e-471d-b46f-71fefe4a1d2f\",contentType:\"Resource\",lastUpdatedBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",audience:[\"Student\"],visibility:\"Default\",consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:1,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",lastPublishedOn:\"2019-08-21T12:15:13.652+0000\",size:416488,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Test Resource Cert\",status:\"Draft\",code:\"7e6630c7-3818-4319-92ac-4d08c33904d8\",streamingUrl:\"https://sunbirddevmedia-inct.streaming.media.azure.net/25d7a94c-9be3-471c-926b-51eb5d3c4c2c/small.ism/manifest(format=m3u8-aapl-v3)\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T12:11:50.644+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T12:15:13.020+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-08-21T12:30:16.783+0000\",dialcodeRequired:\"No\",creator:\"Pradyumna\",lastStatusChangedOn:\"2019-08-21T12:15:14.384+0000\",createdFor:[\"01246944855007232011\"],os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566389713020\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",framework:\"K-12\",createdBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",compatibilityLevel:1,IL_UNIQUE_ID:\"do_112831862871203840114\",resourceType:\"Learn\"},{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",certTemplate:\"[{\\\"name\\\":\\\"100PercentCompletionCertificate\\\",\\\"issuer\\\":{\\\"name\\\":\\\"Gujarat Council of Educational Research and Training\\\",\\\"url\\\":\\\"https://gcert.gujarat.gov.in/gcert/\\\",\\\"publicKey\\\":[\\\"1\\\",\\\"2\\\"]},\\\"signatoryList\\\":[{\\\"name\\\":\\\"CEO Gujarat\\\",\\\"id\\\":\\\"CEO\\\",\\\"designation\\\":\\\"CEO\\\",\\\"image\\\":\\\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\\\"}],\\\"htmlTemplate\\\":\\\"https://drive.google.com/uc?authuser=1&id=1ryB71i0Oqn2c3aqf9N6Lwvet-MZKytoM&export=download\\\",\\\"notifyTemplate\\\":{\\\"subject\\\":\\\"Course completion certificate\\\",\\\"stateImgUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png\\\",\\\"regardsperson\\\":\\\"Chairperson\\\",\\\"regards\\\":\\\"Minister of Gujarat\\\",\\\"emailTemplateType\\\":\\\"defaultCertTemp\\\"}}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398314186_do_11283193441064550411_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550411/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550411/artifact/do_11283193441064550411_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Content\",name:\"test prad course cert\",status:\"Draft\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11283193441064550411/test-prad-course-cert_1566398313947_do_11283193441064550411_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11283193441064550411\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\"}] as row CREATE (n:domain) SET n += row") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + request.put("rootId", "do_11283193441064550411") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 404) + }) + } + + "getHierarchyBeforeUpdateHierarchyWithMode" should "getHierarchyWithoutChildren" in { + val request = new Request() + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"ORG_002\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",channel:\"01246944855007232011\",organisation:[\"ORG_002\"],showNotification:true,language:[\"English\"],mimeType:\"video/mp4\",variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389714022_do_112831862871203840114_1.0_spine.ecar\\\",\\\"size\\\":35757.0}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112831862871203840114/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"dev.sunbird.portal\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",contentEncoding:\"identity\",lockKey:\"be6bc445-c75e-471d-b46f-71fefe4a1d2f\",contentType:\"Resource\",lastUpdatedBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",audience:[\"Student\"],visibility:\"Default\",consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:1,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",lastPublishedOn:\"2019-08-21T12:15:13.652+0000\",size:416488,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Test Resource Cert\",status:\"Draft\",code:\"7e6630c7-3818-4319-92ac-4d08c33904d8\",streamingUrl:\"https://sunbirddevmedia-inct.streaming.media.azure.net/25d7a94c-9be3-471c-926b-51eb5d3c4c2c/small.ism/manifest(format=m3u8-aapl-v3)\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T12:11:50.644+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T12:15:13.020+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-08-21T12:30:16.783+0000\",dialcodeRequired:\"No\",creator:\"Pradyumna\",lastStatusChangedOn:\"2019-08-21T12:15:14.384+0000\",createdFor:[\"01246944855007232011\"],os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566389713020\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",framework:\"K-12\",createdBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",compatibilityLevel:1,IL_UNIQUE_ID:\"do_112831862871203840114\",resourceType:\"Learn\"},{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",certTemplate:\"[{\\\"name\\\":\\\"100PercentCompletionCertificate\\\",\\\"issuer\\\":{\\\"name\\\":\\\"Gujarat Council of Educational Research and Training\\\",\\\"url\\\":\\\"https://gcert.gujarat.gov.in/gcert/\\\",\\\"publicKey\\\":[\\\"1\\\",\\\"2\\\"]},\\\"signatoryList\\\":[{\\\"name\\\":\\\"CEO Gujarat\\\",\\\"id\\\":\\\"CEO\\\",\\\"designation\\\":\\\"CEO\\\",\\\"image\\\":\\\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\\\"}],\\\"htmlTemplate\\\":\\\"https://drive.google.com/uc?authuser=1&id=1ryB71i0Oqn2c3aqf9N6Lwvet-MZKytoM&export=download\\\",\\\"notifyTemplate\\\":{\\\"subject\\\":\\\"Course completion certificate\\\",\\\"stateImgUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png\\\",\\\"regardsperson\\\":\\\"Chairperson\\\",\\\"regards\\\":\\\"Minister of Gujarat\\\",\\\"emailTemplateType\\\":\\\"defaultCertTemp\\\"}}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550412/test-prad-course-cert_1566398313947_do_11283193441064550412_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550412/test-prad-course-cert_1566398314186_do_11283193441064550412_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550412/test-prad-course-cert_1566398313947_do_11283193441064550412_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550412/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550412/artifact/do_11283193441064550412_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Content\",name:\"test prad course cert\",status:\"Draft\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11283193441064550412/test-prad-course-cert_1566398313947_do_11283193441064550412_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11283193441064550412\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\"}] as row CREATE (n:domain) SET n += row") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") + } + }) + request.put("mode","edit") + request.put("rootId", "do_11283193441064550412") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.get("content")) + assert(CollectionUtils.isEmpty(response.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children").asInstanceOf[util.List[Map[String, AnyRef]]])) + }) + } + + "getHierarchy mode=edit" should "return latest leafNodes" in { + val query = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11300156035268608015', '{\"identifier\":\"do_11300156035268608015\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_11300156035268608015\",\"code\":\"2cb4d698-dc19-4f0c-9990-96f49daff753\",\"channel\":\"in.ekstep\",\"description\":\"Test_TextBookUnit_desc_8330194200\",\"language\":[\"English\"],\"mimeType\":\"application/vnd.ekstep.content-collection\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-04-17T11:53:04.855+0530\",\"objectType\":\"Content\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_11300156075913216016\",\"code\":\"test-Resourcce\",\"channel\":\"in.ekstep\",\"language\":[\"English\"],\"mimeType\":\"application/pdf\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-04-17T11:51:30.230+0530\",\"objectType\":\"Content\",\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-04-17T11:51:30.230+0530\",\"contentEncoding\":\"identity\",\"contentType\":\"Resource\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_11300155996401664014\",\"lastStatusChangedOn\":\"2020-04-17T11:51:30.230+0530\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Default\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"languageCode\":[\"en\"],\"version\":2,\"versionKey\":\"1587104490230\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"NCF\",\"depth\":2,\"concepts\":[{\"identifier\":\"Num:C2:SC1\",\"name\":\"Counting\",\"description\":\"Counting\",\"objectType\":\"Concept\",\"relation\":\"associatedTo\",\"status\":\"Retired\"}],\"compatibilityLevel\":1,\"name\":\"test resource\",\"status\":\"Draft\"}],\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-04-17T11:53:04.855+0530\",\"contentEncoding\":\"gzip\",\"contentType\":\"TextBookUnit\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_11300156075913216016\",\"lastStatusChangedOn\":\"2020-04-17T11:53:04.855+0530\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Parent\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.launcher\",\"languageCode\":[\"en\"],\"versionKey\":\"1587104584855\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"depth\":1,\"compatibilityLevel\":1,\"name\":\"Test_TextBookUnit_name_7240493202\",\"status\":\"Draft\"}]}')" + executeCassandraQuery(query) + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"txtbk\",channel:\"in.ekstep\",description:\"Text Book Test\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2020-04-17T11:52:15.303+0530\",contentDisposition:\"inline\",contentEncoding:\"gzip\",lastUpdatedOn:\"2020-04-17T11:53:05.434+0530\",contentType:\"Course\",dialcodeRequired:\"No\",identifier:\"do_11300156035268608015\",audience:[\"Student\"],lastStatusChangedOn:\"2020-04-17T11:52:15.303+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",childNodes:[\"do_11300156075913216016\",\"do_11300155996401664014\"],mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1587104585434\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"TextBook\",IL_UNIQUE_ID:\"do_11300156035268608015\",status:\"Draft\"},{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",prevStatus:\"Live\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-04-17T11:51:30.230+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-04-17T13:38:24.720+0530\",contentType:\"Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-04-17T13:38:22.954+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1587110904720\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"updated\",IL_UNIQUE_ID:\"do_11300155996401664014\",status:\"Draft\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "in.ekstep") + } + }) + request.put("mode","edit") + request.put("rootId", "do_11300156035268608015") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.get("content")) + val children = response.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children").asInstanceOf[util.List[Map[String, AnyRef]]] + assert(CollectionUtils.isNotEmpty(children)) + }) + } + + "getHierarchy mode=edit with bookmark" should "return latest leafNodes for bookmark" in { + val query = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_113054617607118848121','{\"identifier\":\"do_113054617607118848121\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_113054617607118848121\",\"code\":\"TestBookUnit-01\",\"keywords\":[],\"channel\":\"in.ekstep\",\"description\":\"U-1\",\"language\":[\"English\"],\"mimeType\":\"application/vnd.ekstep.content-collection\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-07-01T05:30:02.464+0000\",\"objectType\":\"Content\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_113054618848985088126\",\"code\":\"test.res.1\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_test_content_1/artifact/test_1592831799259.pdf\",\"prevStatus\":\"Live\",\"channel\":\"in.ekstep\",\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_test_content_1/g-test-pdf-1_1592831801712_do_test_content_1_1.0.ecar\",\"language\":[\"English\"],\"streamingUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_test_content_1/artifact/test_1592831799259.pdf\",\"mimeType\":\"application/pdf\",\"variants\":{\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_test_content_1/g-test-pdf-1_1592831801948_do_test_content_1_1.0_spine.ecar\",\"size\":849}},\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-06-22T13:16:39.135+0000\",\"objectType\":\"ContentImage\",\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-06-22T13:16:40.506+0000\",\"contentEncoding\":\"identity\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_test_content_1/artifact/test_1592831799259.pdf\",\"sYS_INTERNAL_LAST_UPDATED_ON\":\"2020-06-22T13:16:42.230+0000\",\"contentType\":\"Resource\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_test_content_1\",\"lastStatusChangedOn\":\"2020-06-23T12:07:01.047+0000\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Default\",\"cloudStorageKey\":\"content/do_test_content_1/artifact/test_1592831799259.pdf\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"languageCode\":[\"en\"],\"lastPublishedBy\":\"EkStep\",\"version\":2,\"pragma\":[\"external\"],\"pkgVersion\":1,\"versionKey\":\"1592914021107\",\"license\":\"CC BY 4.0\",\"prevState\":\"Draft\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"NCFCOPY\",\"depth\":2,\"s3Key\":\"ecar_files/do_test_content_1/g-test-pdf-1_1592831801712_do_test_content_1_1.0.ecar\",\"size\":1946,\"lastPublishedOn\":\"2020-06-22T13:16:40.672+0000\",\"compatibilityLevel\":4,\"name\":\"G-TEST-PDF-1\",\"status\":\"Live\",\"description\":\"updated for do_test_content_1\"}],\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-07-01T05:30:02.463+0000\",\"contentEncoding\":\"gzip\",\"contentType\":\"TextBookUnit\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_113054618848985088126\",\"lastStatusChangedOn\":\"2020-07-01T05:30:02.464+0000\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Parent\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.launcher\",\"languageCode\":[\"en\"],\"versionKey\":\"1593581402464\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"depth\":1,\"compatibilityLevel\":1,\"name\":\"U-1\",\"status\":\"Live\"},{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_113054617607118848121\",\"code\":\"TestBookUnit-02\",\"keywords\":[],\"channel\":\"in.ekstep\",\"description\":\"U-2\",\"language\":[\"English\"],\"mimeType\":\"application/vnd.ekstep.content-collection\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-07-01T05:30:02.458+0000\",\"objectType\":\"Content\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_113054618848935936124\",\"code\":\"test.res.2\",\"previewUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_test_content_2/artifact/test_1592831800654.pdf\",\"prevStatus\":\"Live\",\"channel\":\"in.ekstep\",\"downloadUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_test_content_2/g-test-pdf-2_1592831806405_do_test_content_2_1.0.ecar\",\"language\":[\"English\"],\"streamingUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_test_content_2/artifact/test_1592831800654.pdf\",\"mimeType\":\"application/pdf\",\"variants\":{\"spine\":{\"ecarUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_test_content_2/g-test-pdf-2_1592831806559_do_test_content_2_1.0_spine.ecar\",\"size\":847}},\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-06-22T13:16:40.626+0000\",\"objectType\":\"ContentImage\",\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-06-22T13:16:42.293+0000\",\"contentEncoding\":\"identity\",\"artifactUrl\":\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_test_content_2/artifact/test_1592831800654.pdf\",\"sYS_INTERNAL_LAST_UPDATED_ON\":\"2020-06-22T13:16:46.836+0000\",\"contentType\":\"Resource\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_test_content_2\",\"lastStatusChangedOn\":\"2020-06-23T12:07:01.269+0000\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Default\",\"cloudStorageKey\":\"content/do_test_content_2/artifact/test_1592831800654.pdf\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"languageCode\":[\"en\"],\"lastPublishedBy\":\"EkStep\",\"version\":2,\"pragma\":[\"external\"],\"pkgVersion\":1,\"versionKey\":\"1592914021297\",\"license\":\"CC BY 4.0\",\"prevState\":\"Draft\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"NCFCOPY\",\"depth\":2,\"s3Key\":\"ecar_files/do_test_content_2/g-test-pdf-2_1592831806405_do_test_content_2_1.0.ecar\",\"size\":1941,\"lastPublishedOn\":\"2020-06-22T13:16:42.447+0000\",\"compatibilityLevel\":4,\"name\":\"G-TEST-PDF-2\",\"status\":\"Live\",\"description\":\"updated for do_test_content_2\"}],\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-07-01T05:30:02.457+0000\",\"contentEncoding\":\"gzip\",\"contentType\":\"TextBookUnit\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_113054618848935936124\",\"lastStatusChangedOn\":\"2020-07-01T05:30:02.458+0000\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Parent\",\"index\":2,\"mediaType\":\"content\",\"osId\":\"org.ekstep.launcher\",\"languageCode\":[\"en\"],\"versionKey\":\"1593581402458\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"depth\":1,\"compatibilityLevel\":1,\"name\":\"U-2\",\"status\":\"Live\"}]}')" + executeCassandraQuery(query) + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"test.book.1\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2020-07-01T05:27:30.873+0000\",objectType:\"Content\",contentDisposition:\"inline\",lastUpdatedOn:\"2020-07-01T05:30:02.963+0000\",contentEncoding:\"gzip\",contentType:\"TextBook\",dialcodeRequired:\"No\",identifier:\"do_113054617607118848121\",lastStatusChangedOn:\"2020-07-01T05:27:30.873+0000\",audience:[\"Student\"],os:[\"All\"],visibility:\"Default\",childNodes:[\"do_test_content_1\",\"do_113054618848985088126\",\"do_test_content_2\",\"do_113054618848935936124\"],mediaType:\"content\",osId:\"org.ekstep.quiz.app\",languageCode:[\"en\"],version:2,versionKey:1593581402963,license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",framework:\"NCF\",depth:0,compatibilityLevel:1,name:\"Test-Get Hierarchy\",status:\"Draft\",IL_UNIQUE_ID:\"do_113054617607118848121\",IL_FUNC_OBJECT_TYPE:\"Content\",IL_SYS_NODE_TYPE:\"DATA_NODE\"},\n{ownershipType:[\"createdBy\"],code:\"test.res.1\",prevStatus:\"Live\",channel:\"in.ekstep\",description:\"updated for do_test_content_1\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-06-22T13:16:39.135+0000\",objectType:\"ContentImage\",contentDisposition:\"inline\",lastUpdatedOn:\"2020-06-22T13:16:40.506+0000\",contentEncoding:\"identity\",sYS_INTERNAL_LAST_UPDATED_ON:\"2020-06-22T13:16:42.230+0000\",contentType:\"Resource\",dialcodeRequired:\"No\",identifier:\"do_test_content_1\",lastStatusChangedOn:\"2020-06-23T12:07:01.047+0000\",audience:[\"Student\"],os:[\"All\"],visibility:\"Default\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"EkStep\",languageCode:[\"en\"],version:2,pragma:[\"external\"],pkgVersion:1,versionKey:1592914021107,license:\"CC BY 4.0\",prevState:\"Draft\",idealScreenDensity:\"hdpi\",framework:\"NCFCOPY\",size:1946,lastPublishedOn:\"2020-06-22T13:16:40.672+0000\",compatibilityLevel:4,name:\"G-TEST-PDF-1\",status:\"Draft\",IL_UNIQUE_ID:\"do_test_content_1\",IL_FUNC_OBJECT_TYPE:\"Content\",IL_SYS_NODE_TYPE:\"DATA_NODE\"},\n{ownershipType:[\"createdBy\"],code:\"test.res.2\",prevStatus:\"Live\",channel:\"in.ekstep\",description:\"updated for do_test_content_2\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-06-22T13:16:40.626+0000\",objectType:\"ContentImage\",contentDisposition:\"inline\",lastUpdatedOn:\"2020-06-22T13:16:42.293+0000\",contentEncoding:\"identity\",sYS_INTERNAL_LAST_UPDATED_ON:\"2020-06-22T13:16:46.836+0000\",contentType:\"Resource\",dialcodeRequired:\"No\",identifier:\"do_test_content_2\",lastStatusChangedOn:\"2020-06-23T12:07:01.269+0000\",audience:[\"Student\"],os:[\"All\"],visibility:\"Default\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"EkStep\",languageCode:[\"en\"],version:2,pragma:[\"external\"],pkgVersion:1,versionKey:1592914021297,license:\"CC BY 4.0\",prevState:\"Draft\",idealScreenDensity:\"hdpi\",framework:\"NCFCOPY\",s3Key:\"ecar_files/do_test_content_2/g-test-pdf-2_1592831806405_do_test_content_2_1.0.ecar\",size:1941,lastPublishedOn:\"2020-06-22T13:16:42.447+0000\",compatibilityLevel:4,name:\"G-TEST-PDF-2\",status:\"Draft\",IL_UNIQUE_ID:\"do_test_content_2\",IL_FUNC_OBJECT_TYPE:\"Content\",IL_SYS_NODE_TYPE:\"DATA_NODE\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "in.ekstep") + } + }) + request.put("mode","edit") + request.put("bookmarkId","do_113054618848985088126") + request.put("rootId", "do_113054617607118848121") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.get("content")) + val children = response.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children").asInstanceOf[util.List[Map[String, AnyRef]]] + assert(CollectionUtils.isNotEmpty(children)) + assert(children.size()==1) + val childrenMap = children.get(0).asInstanceOf[util.Map[String, AnyRef]] + assert(StringUtils.equalsIgnoreCase(childrenMap.get("status").asInstanceOf[String],"Draft")) + assert(StringUtils.equalsIgnoreCase(childrenMap.get("identifier").asInstanceOf[String],"do_test_content_1")) + }) + } + + + "getHierarchy mode=edit" should "return removing retired or deleted leafNodes" in { + val query = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11300156035268608015', '{\"identifier\":\"do_11300156035268608015\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_11300156035268608015\",\"code\":\"2cb4d698-dc19-4f0c-9990-96f49daff753\",\"channel\":\"in.ekstep\",\"description\":\"Test_TextBookUnit_desc_8330194200\",\"language\":[\"English\"],\"mimeType\":\"application/vnd.ekstep.content-collection\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-04-17T11:53:04.855+0530\",\"objectType\":\"Content\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_11300156075913216016\",\"code\":\"test-Resourcce\",\"channel\":\"in.ekstep\",\"language\":[\"English\"],\"mimeType\":\"application/pdf\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-04-17T11:51:30.230+0530\",\"objectType\":\"Content\",\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-04-17T11:51:30.230+0530\",\"contentEncoding\":\"identity\",\"contentType\":\"Resource\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_1130015599640166\",\"lastStatusChangedOn\":\"2020-04-17T11:51:30.230+0530\",\"audience\":[\"Learner\"],\"os\":[\"All\"],\"visibility\":\"Default\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"languageCode\":[\"en\"],\"version\":2,\"versionKey\":\"1587104490230\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"NCF\",\"depth\":2,\"concepts\":[{\"identifier\":\"Num:C2:SC1\",\"name\":\"Counting\",\"description\":\"Counting\",\"objectType\":\"Concept\",\"relation\":\"associatedTo\",\"status\":\"Retired\"}],\"compatibilityLevel\":1,\"name\":\"test resource\",\"status\":\"Draft\"}],\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-04-17T11:53:04.855+0530\",\"contentEncoding\":\"gzip\",\"contentType\":\"TextBookUnit\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_11300156075913216016\",\"lastStatusChangedOn\":\"2020-04-17T11:53:04.855+0530\",\"audience\":[\"Learner\"],\"os\":[\"All\"],\"visibility\":\"Parent\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.launcher\",\"languageCode\":[\"en\"],\"versionKey\":\"1587104584855\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"depth\":1,\"compatibilityLevel\":1,\"name\":\"Test_TextBookUnit_name_7240493202\",\"status\":\"Draft\"}]}')" + executeCassandraQuery(query) + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"txtbk\",channel:\"in.ekstep\",description:\"Text Book Test\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2020-04-17T11:52:15.303+0530\",contentDisposition:\"inline\",contentEncoding:\"gzip\",lastUpdatedOn:\"2020-04-17T11:53:05.434+0530\",contentType:\"Course\",dialcodeRequired:\"No\",identifier:\"do_11300156035268608015\",audience:[\"Learner\"],lastStatusChangedOn:\"2020-04-17T11:52:15.303+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",childNodes:[\"do_11300156075913216016\",\"do_11300155996401664014\"],mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1587104585434\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"TextBook\",IL_UNIQUE_ID:\"do_11300156035268608015\",status:\"Draft\"},{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",prevStatus:\"Live\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-04-17T11:51:30.230+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-04-17T13:38:24.720+0530\",contentType:\"Resource\",dialcodeRequired:\"No\",audience:[\"Learner\"],lastStatusChangedOn:\"2020-04-17T13:38:22.954+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1587110904720\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"updated\",IL_UNIQUE_ID:\"do_11300155996401664014\",status:\"Draft\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("objectType", "Content") + put("graph_id", "domain") + put("version", "1.0") + put("schemaName", "collection") + put("channel", "in.ekstep") + } + }) + request.put("mode","edit") + request.put("rootId", "do_11300156035268608015") + val future = HierarchyManager.getHierarchy(request) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.get("content")) + val children = response.get("content").asInstanceOf[util.Map[String, AnyRef]].get("children").asInstanceOf[util.List[Map[String, AnyRef]]] + assert(CollectionUtils.isNotEmpty(children)) + assert(CollectionUtils.isEmpty(children.get(0).asInstanceOf[util.Map[String, AnyRef]].get("children").asInstanceOf[util.List[Map[String, AnyRef]]])) + }) + } +} diff --git a/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestUpdateHierarchy.scala b/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestUpdateHierarchy.scala new file mode 100644 index 000000000..64929da34 --- /dev/null +++ b/content-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestUpdateHierarchy.scala @@ -0,0 +1,509 @@ +package org.sunbird.managers + +import java.util + +import org.apache.commons.lang3.BooleanUtils +import org.parboiled.common.StringUtils +import org.sunbird.common.JsonUtils +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.{ClientException, ResourceNotFoundException} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.utils.HierarchyConstants + +class TestUpdateHierarchy extends BaseSpec { + + private val KEYSPACE_CREATE_SCRIPT = "CREATE KEYSPACE IF NOT EXISTS hierarchy_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val TABLE_CREATE_SCRIPT = "CREATE TABLE IF NOT EXISTS hierarchy_store.content_hierarchy (identifier text, hierarchy text,PRIMARY KEY (identifier));" + private val HIERARCHY_TO_MIGRATE_SCRIPT = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11283193441064550414.img', '{\"identifier\":\"do_11283193441064550414\",\"children\":[{\"parent\":\"do_11283193441064550414\",\"identifier\":\"do_11283193463014195215\",\"copyright\":\"Sunbird\",\"lastStatusChangedOn\":\"2019-08-21T14:37:50.281+0000\",\"code\":\"2e837725-d663-45da-8ace-9577ab111982\",\"visibility\":\"Parent\",\"index\":1,\"mimeType\":\"application/vnd.ekstep.content-collection\",\"createdOn\":\"2019-08-21T14:37:50.281+0000\",\"versionKey\":\"1566398270281\",\"framework\":\"tpd\",\"depth\":1,\"children\":[],\"name\":\"U1\",\"lastUpdatedOn\":\"2019-08-21T14:37:50.281+0000\",\"contentType\":\"CourseUnit\",\"primaryCategory\":\"Learning Resource\",\"status\":\"Draft\"}]}');" + + private val CATEGORY_STORE_KEYSPACE = "CREATE KEYSPACE IF NOT EXISTS category_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val CATEGORY_DEF_DATA_TABLE = "CREATE TABLE IF NOT EXISTS category_store.category_definition_data (identifier text PRIMARY KEY, forms map, objectmetadata map);" + private val CATEGORY_DEF_INPUT = List("INSERT INTO category_store.category_definition_data(identifier) values ('obj-cat:digital-textbook_collection_all')", + "INSERT INTO category_store.category_definition_data(identifier) values ('obj-cat:textbook-unit_collection_all')", + "INSERT INTO category_store.category_definition_data(identifier) values ('obj-cat:course-unit_collection_all')", + "INSERT INTO category_store.category_definition_data(identifier) values ('obj-cat:content-playlist_collection_all')", + "INSERT INTO category_store.category_definition_data(identifier) values ('obj-cat:asset_asset_all')", + "INSERT INTO category_store.category_definition_data(identifier) values ('obj-cat:explanation-content_content_all')") + +// private val EXISTING_HIERARCHY_do_112949210157768704111 = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_112949210157768704111', '{\"identifier\":\"do_112949210157768704111\",\"children\":[{\"ownershipType\":[\"createdBy\"],\"parent\":\"do_112949210157768704111\",\"code\":\"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\",\"channel\":\"in.ekstep\",\"description\":\"Test_CourseUnit_desc_1\",\"language\":[\"English\"],\"mimeType\":\"application/vnd.ekstep.content-collection\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2020-02-04T10:53:23.491+0530\",\"objectType\":\"Content\",\"children\":[{\"ownershipType\":[\"createdFor\"],\"parent\":\"do_11294986283819827217\",\"previewUrl\":\"https://youtu.be/v7YZhQ86Adw\",\"keywords\":[\"10th\",\"Science\",\"Jnana Prabodhini\",\"Maharashtra Board\",\"#gyanqr\"],\"subject\":[\"Science\"],\"channel\":\"01261732844414566415\",\"downloadUrl\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/ecar_files/do_312776559940476928110909/vnsptiinmdhye-laingik-prjnn_1560157123850_do_312776559940476928110909_1.0.ecar\",\"organisation\":[\"Jnana Prabodhini\"],\"language\":[\"English\"],\"mimeType\":\"video/x-youtube\",\"variants\":{\"spine\":{\"ecarUrl\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/ecar_files/do_312776559940476928110909/vnsptiinmdhye-laingik-prjnn_1560157123878_do_312776559940476928110909_1.0_spine.ecar\",\"size\":51205.0}},\"objectType\":\"Content\",\"gradeLevel\":[\"Class 10\"],\"appIcon\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_312776559940476928110909/artifact/10th_mar_2_1547715340679.thumb.png\",\"appId\":\"prod.diksha.portal\",\"contentEncoding\":\"identity\",\"artifactUrl\":\"https://youtu.be/v7YZhQ86Adw\",\"lockKey\":\"772b40b3-4de0-44c3-8474-0fe8f8ec2d91\",\"sYS_INTERNAL_LAST_UPDATED_ON\":\"2019-07-31T01:57:11.210+0000\",\"contentType\":\"Resource\",\"lastUpdatedBy\":\"bf4df886-bb42-4f91-9f33-c88da1653535\",\"identifier\":\"do_312776559940476928110909\",\"audience\":[\"Student\"],\"visibility\":\"Default\",\"consumerId\":\"89490534-126f-4f0b-82ac-3ff3e49f3468\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"lastPublishedBy\":\"7a3358d5-e290-49a4-b7ea-3e3d47a2af30\",\"languageCode\":[\"en\"],\"version\":1,\"pragma\":[\"external\"],\"license\":\"Creative Commons Attribution (CC BY)\",\"prevState\":\"Review\",\"size\":51204.0,\"lastPublishedOn\":\"2019-06-10T08:58:43.846+0000\",\"name\":\"वनस्पतींमध्ये लैंगिक प्रजनन\",\"attributions\":[\"Jnana Prabodhini\"],\"status\":\"Live\",\"code\":\"99a9c6e4-ec56-40ab-9a8c-b66e3a551273\",\"creators\":\"Jnana Prabodhini\",\"description\":\"सजीवांतील जीवनप्रक्रिया भाग - २\",\"streamingUrl\":\"https://youtu.be/v7YZhQ86Adw\",\"medium\":[\"Marathi\"],\"posterImage\":\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_31267888406854041612953/artifact/10th_mar_2_1547715340679.png\",\"idealScreenSize\":\"normal\",\"createdOn\":\"2018-09-06T06:35:10.427+0000\",\"contentDisposition\":\"online\",\"lastUpdatedOn\":\"6019-06-10T08:35:10.582+0000\",\"dialcodeRequired\":\"No\",\"owner\":\"Jnana Prabodhini\",\"lastStatusChangedOn\":\"2019-06-10T08:58:43.925+0000\",\"createdFor\":[\"01261732844414566415\"],\"creator\":\"Pallavi Paradkar\",\"os\":[\"All\"],\"pkgVersion\":1.0,\"versionKey\":\"1560157123651\",\"idealScreenDensity\":\"hdpi\",\"framework\":\"NCF\",\"depth\":2,\"s3Key\":\"ecar_files/do_312776559940476928110909/vnsptiinmdhye-laingik-prjnn_1560157123850_do_312776559940476928110909_1.0.ecar\",\"me_averageRating\":3.0,\"lastSubmittedOn\":\"2019-06-04T09:02:44.995+0000\",\"createdBy\":\"bf4df886-bb42-4f91-9f33-c88da1653535\",\"compatibilityLevel\":4,\"ownedBy\":\"01261732844414566415\",\"board\":\"State (Maharashtra)\",\"resourceType\":\"Learn\"}],\"contentDisposition\":\"inline\",\"lastUpdatedOn\":\"2020-02-04T10:53:23.490+0530\",\"contentEncoding\":\"gzip\",\"contentType\":\"TextBookUnit\",\"dialcodeRequired\":\"No\",\"identifier\":\"do_11294986283819827217\",\"lastStatusChangedOn\":\"2020-02-04T10:53:23.492+0530\",\"audience\":[\"Student\"],\"os\":[\"All\"],\"visibility\":\"Parent\",\"index\":1,\"mediaType\":\"content\",\"osId\":\"org.ekstep.quiz.app\",\"languageCode\":[\"en\"],\"versionKey\":\"1580793803491\",\"license\":\"CC BY 4.0\",\"idealScreenDensity\":\"hdpi\",\"depth\":1,\"compatibilityLevel\":1,\"name\":\"Test_CourseUnit_1\",\"status\":\"Draft\"}]}');" + + implicit val oec: OntologyEngineContext = new OntologyEngineContext + + override def beforeAll(): Unit = { + super.beforeAll() + graphDb.execute("UNWIND [" + + "{identifier:\"obj-cat:learning-resource_content_0123221617357783046602\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:learning-resource\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:learning-resource_content_0123221617357783046602\"}," + + "{identifier:\"obj-cat:learning-resource_content_all\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:learning-resource\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:learning-resource_content_all\"}," + + "{ownershipType:[\"createdBy\"],code:\"citrusCode\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2020-01-29T17:45:55.620+0530\",contentDisposition:\"inline\",contentEncoding:\"gzip\",lastUpdatedOn:\"2020-01-29T17:46:35.471+0530\",contentType:\"TextBook\",primaryCategory:\"Digital Textbook\",dialcodeRequired:\"No\",identifier:\"do_11294581887465881611\",audience:[\"Student\"],lastStatusChangedOn:\"2020-01-29T17:45:55.620+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",nodeType:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1580300195471\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Collection\",name:\"Update Hierarchy Test_03\",IL_UNIQUE_ID:\"do_11294581887465881611\",status:\"Draft\"}," + + "{ownershipType:[\"createdBy\"],code:\"citrusCode\",channel:\"in.ekstep\",description:\"New text book description_01\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2020-02-03T12:45:30.605+0530\",contentDisposition:\"inline\",contentEncoding:\"gzip\",lastUpdatedOn:\"2020-02-03T12:46:01.439+0530\",contentType:\"TextBook\",primaryCategory:\"Digital Textbook\",dialcodeRequired:\"No\",identifier:\"do_112949210157768704111\",audience:[\"Student\"],lastStatusChangedOn:\"2020-02-03T12:45:30.605+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",nodeType:\"DATA_NODE\",childNodes:[\"do_112949210410000384114\"],mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1580714161439\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Collection\",name:\"Update Hierarchy Test_01\",IL_UNIQUE_ID:\"do_112949210157768704111\",status:\"Draft\"}," + + "{ownershipType:[\"createdBy\"],previewUrl:\"https://www.youtube.com/watch?v=JEjUtGkUqus\",downloadUrl:\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/ecar_files/do_31250856200414822416938/mh_chapter-1_science-part-2_grade-10_2_1539192600492_do_31250856200414822416938_2.0.ecar\",channel:\"0123221617357783046602\",showNotification:true,language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/ecar_files/do_31250856200414822416938/mh_chapter-1_science-part-2_grade-10_2_1539192600579_do_31250856200414822416938_2.0_spine.ecar\\\",\\\"size\\\":29518}}\",mimeType:\"video/x-youtube\",appIcon:\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_31250856200414822416938/artifact/logo_4221_1513150964_1513150964262.thumb.jpg\",appId:\"prod.diksha.app\",artifactUrl:\"https://www.youtube.com/watch?v=JEjUtGkUqus\",contentEncoding:\"identity\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",lastUpdatedBy:\"ekstep\",audience:[\"Student\"],visibility:\"Default\",consumerId:\"89490534-126f-4f0b-82ac-3ff3e49f3468\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"ekstep\",version:1,pragma:[\"external\"],license:\"CC BY-SA 4.0\",prevState:\"Live\",size:29519,lastPublishedOn:\"2018-10-10T17:30:00.491+0000\",IL_FUNC_OBJECT_TYPE:\"Content\",name:\"MH_Chapter 1_Science Part 2_Grade 10_2\",attributions:[\"MSCERT\"],status:\"Live\",code:\"b2099ea5-0070-4930-ae13-b08aa83e5853\",description:\"अानुवांशिकता और उत्क्रांती\",streamingUrl:\"https://www.youtube.com/watch?v=JEjUtGkUqus\",posterImage:\"https://ntpproductionall.blob.core.windows.net/ntp-content-production/content/do_31239573269507276815296/artifact/logo_4221_1513150964_1513150964262.jpg\",idealScreenSize:\"normal\",createdOn:\"0021-02-04T08:05:49.480+0000\",contentDisposition:\"online\",lastUpdatedOn:\"25182518-10-10T17:29:59.456+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-07-31T01:57:26.148+0000\",dialcodeRequired:\"No\",lastStatusChangedOn:\"2019-06-17T05:41:05.507+0000\",createdFor:[\"0123221617357783046602\"],creator:\"Alaka Potdar\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",nodeType:\"DATA_NODE\",pkgVersion:2,versionKey:\"1539192599456\",idealScreenDensity:\"hdpi\",framework:\"NCF\",s3Key:\"ecar_files/do_31250856200414822416938/mh_chapter-1_science-part-2_grade-10_2_1539192600492_do_31250856200414822416938_2.0.ecar\",me_averageRating:3,lastSubmittedOn:\"2018-05-21T17:37:16.466+0000\",createdBy:\"c5d09e49-6f1d-474b-b6cc-2e590ae15ef8\",compatibilityLevel:4,IL_UNIQUE_ID:\"do_31250856200414822416938\",resourceType:\"Learn\"}," + + "{copyright:\"\",code:\"org.ekstep.asset.Pant.1773380908\",sources:\"\",channel:\"in.ekstep\",downloadUrl:\"https://ekstep-public-prod.s3-ap-south-1.amazonaws.com/content/do_111112224444/artifact/clothes-1294974_960_720_658_1483340550_1483340551056.png\",language:[\"English\"],mimeType:\"video/mp4\",variants:\"{\\\"high\\\":\\\"https://ekstep-public-prod.s3-ap-south-1.amazonaws.com/content/do_111112224444/artifact/clothes-1294974_960_720_658_1483340550_1483340551056.png\\\",\\\"low\\\":\\\"https://ekstep-public-prod.s3-ap-south-1.amazonaws.com/content/do_111112224444/artifact/clothes-1294974_960_720_658_1483340550_1483340551056.low.png\\\",\\\"medium\\\":\\\"https://ekstep-public-prod.s3-ap-south-1.amazonaws.com/content/do_111112224444/artifact/clothes-1294974_960_720_658_1483340550_1483340551056.medium.png\\\"}\",idealScreenSize:\"normal\",createdOn:\"2017-01-02T07:02:31.021+0000\",contentDisposition:\"inline\",artifactUrl:\"https://ekstep-public-prod.s3-ap-south-1.amazonaws.com/content/do_111112224444/artifact/clothes-1294974_960_720_658_1483340550_1483340551056.png\",contentEncoding:\"identity\",lastUpdatedOn:\"2017-03-10T20:48:09.283+0000\",contentType:\"Resource\",owner:\"\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",nodeType:\"DATA_NODE\",portalOwner:\"658\",mediaType:\"image\",osId:\"org.ekstep.quiz.app\",ageGroup:[\"5-6\"],versionKey:\"1489178889283\",license:\"CC BY-SA 4.0\",idealScreenDensity:\"hdpi\",framework:\"NCF\",s3Key:\"content/do_111112224444/artifact/clothes-1294974_960_720_658_1483340550_1483340551056.png\",size:167939,createdBy:\"658\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Pant\",publisher:\"\",IL_UNIQUE_ID:\"do_111112224444\",status:\"Live\"}," + + "{ownershipType:[\"createdBy\"],code:\"citrusCode\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2020-01-29T17:45:55.620+0530\",contentDisposition:\"inline\",contentEncoding:\"gzip\",lastUpdatedOn:\"2020-01-29T17:46:35.471+0530\",contentType:\"TextBook\",primaryCategory:\"Digital Textbook\",dialcodeRequired:\"No\",identifier:\"do_test_book_1\",audience:[\"Student\"],lastStatusChangedOn:\"2020-01-29T17:45:55.620+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",nodeType:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1580300195471\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Collection\",name:\"Update Hierarchy Test For Origin Data\",IL_UNIQUE_ID:\"do_test_book_1\",status:\"Draft\"}," + + "{identifier:\"obj-cat:learning-resource_content_all\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:learning-resource\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:learning-resource_content_all\"}," + + "{identifier:\"obj-cat:learning-resource_content_b00bc992ef25f1a9a8d63291e20efc8d\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:learning-resource\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:learning-resource_content_b00bc992ef25f1a9a8d63291e20efc8d\"}," + + "{identifier:\"obj-cat:digital-textbook_collection_all\",name:\"Digital Textbook\",description:\"Learning resource\",categoryId:\"obj-cat:digital-textbook\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:digital-textbook_collection_all\"}," + + "{identifier:\"obj-cat:textbook-unit_collection_all\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:textbook-unit\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:textbook-unit_collection_all\"}," + + "{identifier:\"obj-cat:course_collection_all\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:course\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:course_collection_all\"}," + + "{identifier:\"obj-cat:course-unit_collection_all\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:course_unit\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:course-unit_collection_all\"}," + + "{identifier:\"obj-cat:asset_asset_all\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:asset\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:asset_asset_all\"}," + + "{identifier:\"obj-cat:content-playlist_collection_all\",name:\"Learning Resource\",description:\"Learning resource\",categoryId:\"obj-cat:content-playlist\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:content-playlist_collection_all\"}" + + "] as row CREATE (n:domain) SET n += row") + executeCassandraQuery(KEYSPACE_CREATE_SCRIPT, TABLE_CREATE_SCRIPT, CATEGORY_STORE_KEYSPACE, CATEGORY_DEF_DATA_TABLE) + executeCassandraQuery(CATEGORY_DEF_INPUT:_*) + insert20NodesAndOneCourse() + } + + override def beforeEach(): Unit = { + executeCassandraQuery( HIERARCHY_TO_MIGRATE_SCRIPT) + } + + def getContext(): java.util.Map[String, AnyRef] = { + new util.HashMap[String, AnyRef](){{ + put(HierarchyConstants.GRAPH_ID, HierarchyConstants.TAXONOMY_ID) + put(HierarchyConstants.VERSION , HierarchyConstants.SCHEMA_VERSION) + put(HierarchyConstants.OBJECT_TYPE , HierarchyConstants.COLLECTION_OBJECT_TYPE) + put(HierarchyConstants.SCHEMA_NAME, "content") + put(HierarchyConstants.CHANNEL, "b00bc992ef25f1a9a8d63291e20efc8d") + }} + } + + "deleteHierarchy" should "Delete the hierarchy data from cassandra from identifier with .img extension" in { + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + context.put(HierarchyConstants.ROOT_ID, "do_11283193441064550414") + request.setContext(context) + val oldHierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(StringUtils.isNotEmpty(oldHierarchy)) + UpdateHierarchyManager.deleteHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + assert(BooleanUtils.isFalse(readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'").iterator().hasNext)) + }) + } + + "deleteHierarchy with image id" should "Delete the hierarchy data from cassandra from identifier with .img extension" in { + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + context.put(HierarchyConstants.ROOT_ID, "do_11283193441064550414.img") + request.setContext(context) + val oldHierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(StringUtils.isNotEmpty(oldHierarchy)) + UpdateHierarchyManager.deleteHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + assert(BooleanUtils.isFalse(readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'").iterator().hasNext)) + }) + } + + "deleteHierarchy with invalid id" should "Delete the hierarchy data from cassandra from identifier with .img extension" in { + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + context.put(HierarchyConstants.ROOT_ID, "123") + request.setContext(context) + UpdateHierarchyManager.deleteHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + .one().getString("hierarchy") + assert(StringUtils.isNotEmpty(hierarchy)) + }) + } + +// ResourceId = "do_31250856200414822416938" and TextBook id ="do_112945818874658816" + "updateHierarchy with One New Unit and One Live Resource" should "update text book node, create unit and store the hierarchy in cassandra" in { + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.setObjectType(HierarchyConstants.COLLECTION_OBJECT_TYPE) + request.put(HierarchyConstants.NODES_MODIFIED, getNodesModified_1()) + request.put(HierarchyConstants.HIERARCHY, getHierarchy_1()) + UpdateHierarchyManager.updateHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11294581887465881611'") + .one().getString("hierarchy") + assert(StringUtils.isNotEmpty(hierarchy)) + }) + } + + "updateHierarchy with One New Unit and Quesstion Object" should "update text book node, create unit and store the hierarchy in cassandra" in { + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"SC-2200_3eac25ae-a0c9-4d7c-87be-954406824cb8\",channel:\"sunbird\",description:\"Test-Add/Remove Leaf Node\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2021-03-07T19:23:38.025+0000\",IL_FUNC_OBJECT_TYPE:\"Collection\",contentDisposition:\"inline\",additionalCategories:[\"Textbook\"],lastUpdatedOn:\"2021-03-07T19:24:59.023+0000\",contentEncoding:\"gzip\",contentType:\"TextBook\",dialcodeRequired:\"No\",IL_UNIQUE_ID:\"do_111111111222222222\",lastStatusChangedOn:\"2021-03-07T19:23:38.025+0000\",audience:[\"Student\"],os:[\"All\"],visibility:\"Default\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",languageCode:[\"en\"],version:2,versionKey:\"1615145099023\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,compatibilityLevel:1,userConsent:\"Yes\",name:\"SC-2200-TextBook\",status:\"Draft\"},{code:\"questionId\",subject:[\"Health and Physical Education\"],language:[\"English\"],medium:[\"English\"],mimeType:\"application/vnd.sunbird.question\",createdOn:\"2021-01-13T09:29:06.255+0000\",IL_FUNC_OBJECT_TYPE:\"Question\",gradeLevel:[\"Class 6\"],contentDisposition:\"inline\",lastUpdatedOn:\"2021-02-08T11:19:08.989+0000\",contentEncoding:\"gzip\",showSolutions:\"No\",allowAnonymousAccess:\"Yes\",IL_UNIQUE_ID:\"do_113193462958120275141\",lastStatusChangedOn:\"2021-02-08T11:19:08.989+0000\",visibility:\"Default\",showTimer:\"No\",author:\"Vaibhav\",qType:\"SA\",languageCode:[\"en\"],version:1,versionKey:\"1611554879383\",showFeedback:\"No\",license:\"CC BY 4.0\",prevState:\"Review\",compatibilityLevel:4,name:\"Subjective\",topic:[\"Leaves\"],board:\"CBSE\",status:\"Live\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.setObjectType(HierarchyConstants.COLLECTION_OBJECT_TYPE) + request.put(HierarchyConstants.NODES_MODIFIED, getNodesModified_3()) + request.put(HierarchyConstants.HIERARCHY, getHierarchy_3("do_111111111222222222", "do_113193462958120275141")) + UpdateHierarchyManager.updateHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_111111111222222222'") + .one().getString("hierarchy") + assert(StringUtils.isNotEmpty(hierarchy)) + }) + } + + "updateHierarchy with One New Unit and QuesstionSet Object" should "update text book node, create unit and store the hierarchy in cassandra" in { + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"SC-2200_3eac25ae-a0c9-4d7c-87be-954406824cb8\",channel:\"sunbird\",description:\"Test-Add/Remove Leaf Node\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2021-03-07T19:23:38.025+0000\",IL_FUNC_OBJECT_TYPE:\"Collection\",contentDisposition:\"inline\",additionalCategories:[\"Textbook\"],lastUpdatedOn:\"2021-03-07T19:24:59.023+0000\",contentEncoding:\"gzip\",contentType:\"TextBook\",dialcodeRequired:\"No\",IL_UNIQUE_ID:\"do_1111111112222222222223\",lastStatusChangedOn:\"2021-03-07T19:23:38.025+0000\",audience:[\"Student\"],os:[\"All\"],visibility:\"Default\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",languageCode:[\"en\"],version:2,versionKey:\"1615145099023\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,compatibilityLevel:1,userConsent:\"Yes\",name:\"SC-2200-TextBook\",status:\"Draft\"},{code:\"questionId\",subject:[\"Health and Physical Education\"],language:[\"English\"],medium:[\"English\"],mimeType:\"application/vnd.sunbird.questionset\",createdOn:\"2021-01-13T09:29:06.255+0000\",IL_FUNC_OBJECT_TYPE:\"QuestionSet\",gradeLevel:[\"Class 6\"],contentDisposition:\"inline\",lastUpdatedOn:\"2021-02-08T11:19:08.989+0000\",contentEncoding:\"gzip\",showSolutions:\"No\",allowAnonymousAccess:\"Yes\",IL_UNIQUE_ID:\"do_113193462958120277239\",lastStatusChangedOn:\"2021-02-08T11:19:08.989+0000\",visibility:\"Default\",showTimer:\"No\",author:\"Vaibhav\",qType:\"SA\",languageCode:[\"en\"],version:1,versionKey:\"1611554879383\",showFeedback:\"No\",license:\"CC BY 4.0\",prevState:\"Review\",compatibilityLevel:4,name:\"Subjective\",topic:[\"Leaves\"],board:\"CBSE\",status:\"Live\"}] as row CREATE (n:domain) SET n += row") + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.setObjectType(HierarchyConstants.COLLECTION_OBJECT_TYPE) + request.put(HierarchyConstants.NODES_MODIFIED, getNodesModified_3()) + request.put(HierarchyConstants.HIERARCHY, getHierarchy_3("do_1111111112222222222223", "do_113193462958120277239")) + UpdateHierarchyManager.updateHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_1111111112222222222223'") + .one().getString("hierarchy") + assert(StringUtils.isNotEmpty(hierarchy)) + }) + } + + "updateHierarchy on already existing hierarchy" should "recompose the hierarchy structure and store in in cassandra and also update neo4j" in { + UpdateHierarchyManager.getContentNode("do_31250856200414822416938", HierarchyConstants.TAXONOMY_ID).map(node => { + println("Node data from neo4j ----- id: " + node.getIdentifier + "node type: " + node.getNodeType + " node metadata : " + node.getMetadata) + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.put(HierarchyConstants.NODES_MODIFIED, getNodesModified_1()) + request.put(HierarchyConstants.HIERARCHY, getHierarchy_1()) + UpdateHierarchyManager.updateHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + val identifiers = response.get(HierarchyConstants.IDENTIFIERS).asInstanceOf[util.Map[String, AnyRef]] + val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11294581887465881611'") + .one().getString(HierarchyConstants.HIERARCHY) + assert(StringUtils.isNotEmpty(hierarchy)) + request.put(HierarchyConstants.NODES_MODIFIED, getNodesModified_2("do_11294581887465881611", identifiers.get("b9a50833-eff6-4ef5-a2a4-2413f2d51f6c").asInstanceOf[String])) + request.put(HierarchyConstants.HIERARCHY, getHierarchy_2("do_11294581887465881611", identifiers.get("b9a50833-eff6-4ef5-a2a4-2413f2d51f6c").asInstanceOf[String])) + UpdateHierarchyManager.updateHierarchy(request).map(resp => { + assert(response.getResponseCode.code() == 200) + val hierarchy_updated = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11294581887465881611'") + .one().getString(HierarchyConstants.HIERARCHY) + assert(!StringUtils.equalsIgnoreCase(hierarchy, hierarchy_updated)) + }) + }).flatMap(f => f) + }).flatMap(f => f) + } + + "updateHierarchy with New Unit and Invalid Resource" should "throw resource not found exception" in { + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.put(HierarchyConstants.NODES_MODIFIED, getNodesModified_1()) + request.put(HierarchyConstants.HIERARCHY, getHierarchy_Content_Resource_Invalid_ID()) + recoverToSucceededIf[ResourceNotFoundException](UpdateHierarchyManager.updateHierarchy(request)) + } + + "updateHierarchy with empty nodes modified and hierarchy" should "throw client exception" in { + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.put(HierarchyConstants.NODES_MODIFIED, new util.HashMap()) + request.put(HierarchyConstants.HIERARCHY, new util.HashMap()) + val exception = intercept[ClientException] { + UpdateHierarchyManager.updateHierarchy(request) + } + exception.getMessage shouldEqual "Please Provide Valid Root Node Identifier" + } + + "updateHierarchy test proper ordering" should "succeed with proper hierarchy structure" in { + val nodesModified = "{\"do_113031517435822080121\":{\"metadata\":{\"license\":\"CC BY 4.0\"},\"root\":true,\"isNew\":false},\"u1\":{\"metadata\":{\"name\":\"U1\",\"dialcodeRequired\":\"No\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"contentType\":\"CourseUnit\",\"primaryCategory\": \"Course Unit\",\"license\":\"CC BY 4.0\"},\"root\":false,\"isNew\":true},\"u2\":{\"metadata\":{\"name\":\"U2\",\"dialcodeRequired\":\"No\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"contentType\":\"CourseUnit\",\"primaryCategory\": \"Course Unit\",\"license\":\"CC BY 4.0\"},\"root\":false,\"isNew\":true}}"; + val hierarchy = "{\"do_113031517435822080121\":{\"children\":[\"u1\",\"u2\"],\"root\":true,\"name\":\"Untitled Textbook\",\"contentType\":\"Course\",\"primaryCategory\": \"Learning Resource\"},\"u1\":{\"children\":[\"do_11303151546543308811\",\"do_11303151571010355212\",\"do_11303151584773734413\",\"do_11303151594263347214\",\"do_11303151604402585615\",\"do_11303151612719104016\",\"do_11303151623148339217\",\"do_11303151631740928018\",\"do_11303151638961356819\",\"do_113031516469411840110\"],\"root\":false,\"name\":\"U1\",\"contentType\":\"CourseUnit\",\"primaryCategory\": \"Learning Resource\"},\"u2\":{\"children\":[\"do_113031516541870080111\",\"do_113031516616491008112\",\"do_113031516693184512113\",\"do_113031516791406592114\",\"do_113031516862660608115\",\"do_113031516945334272116\",\"do_113031517024190464117\",\"do_113031517104939008118\",\"do_113031517200171008119\",\"do_113031517276520448120\"],\"root\":false,\"name\":\"U2\",\"contentType\":\"CourseUnit\",\"primaryCategory\": \"Learning Resource\"},\"do_11303151546543308811\":{\"name\":\"prad PDF Content-1\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_11303151571010355212\":{\"name\":\"prad PDF Content-2\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_11303151584773734413\":{\"name\":\"prad PDF Content-3\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_11303151594263347214\":{\"name\":\"prad PDF Content-4\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_11303151604402585615\":{\"name\":\"prad PDF Content-5\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_11303151612719104016\":{\"name\":\"prad PDF Content-6\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_11303151623148339217\":{\"name\":\"prad PDF Content-7\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_11303151631740928018\":{\"name\":\"prad PDF Content-8\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_11303151638961356819\":{\"name\":\"prad PDF Content-9\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031516469411840110\":{\"name\":\"prad PDF Content-10\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031516541870080111\":{\"name\":\"prad PDF Content-11\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031516616491008112\":{\"name\":\"prad PDF Content-12\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031516693184512113\":{\"name\":\"prad PDF Content-13\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031516791406592114\":{\"name\":\"prad PDF Content-14\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031516862660608115\":{\"name\":\"prad PDF Content-15\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031516945334272116\":{\"name\":\"prad PDF Content-16\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031517024190464117\":{\"name\":\"prad PDF Content-17\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031517104939008118\":{\"name\":\"prad PDF Content-18\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031517200171008119\":{\"name\":\"prad PDF Content-19\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false},\"do_113031517276520448120\":{\"name\":\"prad PDF Content-20\",\"contentType\":\"Resource\",\"primaryCategory\": \"Learning Resource\",\"children\":[],\"root\":false}}"; + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.put(HierarchyConstants.NODES_MODIFIED, JsonUtils.deserialize(nodesModified, classOf[util.HashMap[String, AnyRef]])) + request.put(HierarchyConstants.HIERARCHY, JsonUtils.deserialize(hierarchy, classOf[util.HashMap[String, AnyRef]])) + UpdateHierarchyManager.updateHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + val identifiers = response.get(HierarchyConstants.IDENTIFIERS).asInstanceOf[util.Map[String, AnyRef]] + val hierarchyResp = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_113031517435822080121'") + .one().getString(HierarchyConstants.HIERARCHY) + assert(StringUtils.isNotEmpty(hierarchyResp)) + val children = JsonUtils.deserialize(hierarchyResp, classOf[util.HashMap[String, AnyRef]]).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(children.get(0).get("identifier").asInstanceOf[String], identifiers.get("u1").asInstanceOf[String])) + val u1Children = children.get(0).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(u1Children.get(0).get("identifier").asInstanceOf[String], "do_11303151546543308811")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(1).get("identifier").asInstanceOf[String], "do_11303151571010355212")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(2).get("identifier").asInstanceOf[String], "do_11303151584773734413")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(3).get("identifier").asInstanceOf[String], "do_11303151594263347214")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(4).get("identifier").asInstanceOf[String], "do_11303151604402585615")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(5).get("identifier").asInstanceOf[String], "do_11303151612719104016")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(6).get("identifier").asInstanceOf[String], "do_11303151623148339217")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(7).get("identifier").asInstanceOf[String], "do_11303151631740928018")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(8).get("identifier").asInstanceOf[String], "do_11303151638961356819")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(9).get("identifier").asInstanceOf[String], "do_113031516469411840110")) + + assert(StringUtils.equalsIgnoreCase(children.get(1).get("identifier").asInstanceOf[String], identifiers.get("u2").asInstanceOf[String])) + val u2Children = children.get(1).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(u2Children.get(0).get("identifier").asInstanceOf[String], "do_113031516541870080111")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(1).get("identifier").asInstanceOf[String], "do_113031516616491008112")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(2).get("identifier").asInstanceOf[String], "do_113031516693184512113")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(3).get("identifier").asInstanceOf[String], "do_113031516791406592114")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(4).get("identifier").asInstanceOf[String], "do_113031516862660608115")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(5).get("identifier").asInstanceOf[String], "do_113031516945334272116")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(6).get("identifier").asInstanceOf[String], "do_113031517024190464117")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(7).get("identifier").asInstanceOf[String], "do_113031517104939008118")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(8).get("identifier").asInstanceOf[String], "do_113031517200171008119")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(9).get("identifier").asInstanceOf[String], "do_113031517276520448120")) + + val nodesModified = "{\"do_113031517435822080121\":{\"metadata\":{\"license\":\"CC BY 4.0\"},\"root\":true,\"isNew\":false}}"; + val request1 = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request1.setContext(context) + request1.put(HierarchyConstants.NODES_MODIFIED, JsonUtils.deserialize(nodesModified, classOf[util.HashMap[String, AnyRef]])) + request1.put(HierarchyConstants.HIERARCHY, new util.HashMap()) + UpdateHierarchyManager.updateHierarchy(request1).map(response => { + assert(response.getResponseCode.code() == 200) + val hierarchyResp = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_113031517435822080121'") + .one().getString(HierarchyConstants.HIERARCHY) + assert(StringUtils.isNotEmpty(hierarchyResp)) + val children = JsonUtils.deserialize(hierarchyResp, classOf[util.HashMap[String, AnyRef]]).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + val u1Children = children.get(0).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(u1Children.get(0).get("identifier").asInstanceOf[String], "do_11303151546543308811")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(1).get("identifier").asInstanceOf[String], "do_11303151571010355212")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(2).get("identifier").asInstanceOf[String], "do_11303151584773734413")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(3).get("identifier").asInstanceOf[String], "do_11303151594263347214")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(4).get("identifier").asInstanceOf[String], "do_11303151604402585615")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(5).get("identifier").asInstanceOf[String], "do_11303151612719104016")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(6).get("identifier").asInstanceOf[String], "do_11303151623148339217")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(7).get("identifier").asInstanceOf[String], "do_11303151631740928018")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(8).get("identifier").asInstanceOf[String], "do_11303151638961356819")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(9).get("identifier").asInstanceOf[String], "do_113031516469411840110")) + + val u2Children = children.get(1).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(u2Children.get(0).get("identifier").asInstanceOf[String], "do_113031516541870080111")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(1).get("identifier").asInstanceOf[String], "do_113031516616491008112")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(2).get("identifier").asInstanceOf[String], "do_113031516693184512113")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(3).get("identifier").asInstanceOf[String], "do_113031516791406592114")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(4).get("identifier").asInstanceOf[String], "do_113031516862660608115")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(5).get("identifier").asInstanceOf[String], "do_113031516945334272116")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(6).get("identifier").asInstanceOf[String], "do_113031517024190464117")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(7).get("identifier").asInstanceOf[String], "do_113031517104939008118")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(8).get("identifier").asInstanceOf[String], "do_113031517200171008119")) + assert(StringUtils.equalsIgnoreCase(u2Children.get(9).get("identifier").asInstanceOf[String], "do_113031517276520448120")) + }) + }).flatMap(f => f) + } + + "updateHierarchy test proper ordering" should "succeed with proper hierarchy structure on same resources" in { + val nodesModified = "{\"do_113031517435822080121\":{\"metadata\":{},\"root\":false,\"isNew\":false},\"u1\":{\"metadata\":{\"name\":\"U1\",\"dialcodeRequired\":\"No\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"primaryCategory\": \"Content Playlist\",\"contentType\":\"Collection\",\"license\":\"CC BY 4.0\"},\"root\":false,\"isNew\":true},\"u1.1\":{\"metadata\":{\"name\":\"U1.1\",\"dialcodeRequired\":\"No\",\"mimeType\":\"application/vnd.ekstep.content-collection\",\"primaryCategory\": \"Content Playlist\",\"contentType\":\"Collection\",\"license\":\"CC BY 4.0\"},\"root\":false,\"isNew\":true}}"; + val hierarchy = "{\"do_113031517435822080121\":{\"children\":[\"u1\"],\"root\":true,\"name\":\"CollectionTest\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"Collection\"},\"u1\":{\"children\":[\"do_11303151546543308811\",\"do_11303151571010355212\",\"do_11303151584773734413\",\"do_11303151594263347214\",\"u1.1\"],\"root\":false,\"name\":\"U1\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"Collection\"},\"u1.1\":{\"children\":[\"do_11303151594263347214\",\"do_11303151584773734413\",\"do_11303151571010355212\",\"do_11303151546543308811\"],\"root\":false,\"name\":\"U1.1\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"Collection\"},\"do_11303151546543308811\":{\"name\":\"prad PDF Content-1\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"Resource\",\"children\":[],\"root\":false},\"do_11303151571010355212\":{\"name\":\"prad PDF Content-2\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"Resource\",\"children\":[],\"root\":false},\"do_11303151584773734413\":{\"name\":\"prad PDF Content-3\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"Resource\",\"children\":[],\"root\":false},\"do_11303151594263347214\":{\"name\":\"prad PDF Content-4\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"Resource\",\"children\":[],\"root\":false}}"; + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.put(HierarchyConstants.NODES_MODIFIED, JsonUtils.deserialize(nodesModified, classOf[util.HashMap[String, AnyRef]])) + request.put(HierarchyConstants.HIERARCHY, JsonUtils.deserialize(hierarchy, classOf[util.HashMap[String, AnyRef]])) + UpdateHierarchyManager.updateHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + val identifiers = response.get(HierarchyConstants.IDENTIFIERS).asInstanceOf[util.Map[String, AnyRef]] + val hierarchyResp = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_113031517435822080121'") + .one().getString(HierarchyConstants.HIERARCHY) + assert(StringUtils.isNotEmpty(hierarchyResp)) + val children = JsonUtils.deserialize(hierarchyResp, classOf[util.HashMap[String, AnyRef]]).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(children.get(0).get("identifier").asInstanceOf[String], identifiers.get("u1").asInstanceOf[String])) + val u1Children = children.get(0).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(u1Children.get(0).get("identifier").asInstanceOf[String], "do_11303151546543308811")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(1).get("identifier").asInstanceOf[String], "do_11303151571010355212")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(2).get("identifier").asInstanceOf[String], "do_11303151584773734413")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(3).get("identifier").asInstanceOf[String], "do_11303151594263347214")) + assert(StringUtils.equalsIgnoreCase(u1Children.get(4).get("identifier").asInstanceOf[String], identifiers.get("u1.1").asInstanceOf[String])) + val u11Children = u1Children.get(4).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(u11Children.get(3).get("identifier").asInstanceOf[String], "do_11303151546543308811")) + assert(StringUtils.equalsIgnoreCase(u11Children.get(2).get("identifier").asInstanceOf[String], "do_11303151571010355212")) + assert(StringUtils.equalsIgnoreCase(u11Children.get(1).get("identifier").asInstanceOf[String], "do_11303151584773734413")) + assert(StringUtils.equalsIgnoreCase(u11Children.get(0).get("identifier").asInstanceOf[String], "do_11303151594263347214")) + }) + } + + "updateHierarchy with originData under root metadata" should "successfully store originData in root node" in { + val nodesModified = "{\"do_test_book_1\":{\"isNew\":true,\"root\":true,\"metadata\":{\"origin\":\"do_113000859727618048110\",\"originData\":{\"channel\":\"012983850117177344161\"}}},\"TestBookUnit-01\":{\"isNew\":true,\"root\":false,\"metadata\":{\"mimeType\":\"application/vnd.ekstep.content-collection\",\"keywords\":[],\"name\":\"U-1\",\"description\":\"U-1\",\"primaryCategory\": \"Textbook Unit\",\"contentType\":\"TextBookUnit\",\"code\":\"TestBookUnit-01\"}},\"TestBookUnit-02\":{\"isNew\":true,\"root\":false,\"metadata\":{\"mimeType\":\"application/vnd.ekstep.content-collection\",\"keywords\":[],\"name\":\"U-2\",\"description\":\"U-2\",\"primaryCategory\": \"Textbook Unit\",\"contentType\":\"TextBookUnit\",\"code\":\"TestBookUnit-02\"}}}" + val hierarchy = "{\"do_test_book_1\":{\"name\":\"Update Hierarchy Test For Origin Data\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"TextBook\",\"children\":[\"TestBookUnit-01\",\"TestBookUnit-02\"],\"root\":true},\"TestBookUnit-01\":{\"name\":\"U-1\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"TextBookUnit\",\"children\":[],\"root\":false},\"TestBookUnit-02\":{\"name\":\"U-2\",\"primaryCategory\": \"Learning Resource\",\"contentType\":\"TextBookUnit\",\"children\":[],\"root\":false}}" + val request = new Request() + val context = getContext() + context.put(HierarchyConstants.SCHEMA_NAME, "collection") + request.setContext(context) + request.put(HierarchyConstants.NODES_MODIFIED, JsonUtils.deserialize(nodesModified, classOf[util.HashMap[String, AnyRef]])) + request.put(HierarchyConstants.HIERARCHY, JsonUtils.deserialize(hierarchy, classOf[util.HashMap[String, AnyRef]])) + UpdateHierarchyManager.updateHierarchy(request).map(response => { + assert(response.getResponseCode.code() == 200) + val identifiers = response.get(HierarchyConstants.IDENTIFIERS).asInstanceOf[util.Map[String, AnyRef]] + val hierarchyResp = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_test_book_1'") + .one().getString(HierarchyConstants.HIERARCHY) + assert(StringUtils.isNotEmpty(hierarchyResp)) + val children = JsonUtils.deserialize(hierarchyResp, classOf[util.HashMap[String, AnyRef]]).get("children").asInstanceOf[util.List[util.Map[String, AnyRef]]] + assert(StringUtils.equalsIgnoreCase(children.get(0).get("identifier").asInstanceOf[String], identifiers.get("TestBookUnit-01").asInstanceOf[String])) + val getHierarchyReq = new Request() + val reqContext = getContext() + reqContext.put(HierarchyConstants.SCHEMA_NAME, "collection") + getHierarchyReq.setContext(reqContext) + getHierarchyReq.put("rootId", "do_test_book_1") + getHierarchyReq.put("mode","edit") + val future = HierarchyManager.getHierarchy(getHierarchyReq) + future.map(response => { + assert(response.getResponseCode.code() == 200) + assert(null != response.getResult.get("content")) + val content = response.getResult.get("content").asInstanceOf[util.Map[String, AnyRef]] + assert(null != content.get("originData")) + assert(null != content.get("children")) + }) + }).flatMap(f=>f) + } + + + //Text Book -> root, New Unit + def getNodesModified_1(): util.HashMap[String, AnyRef] = { + val nodesModifiedString: String = "{\n"+ + " \"do_11294581887465881611\": {\n"+ + " \"isNew\": false,\n"+ + " \"root\": true\n"+ + " },\n"+ + " \"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\": {\n"+ + " \"isNew\": true,\n"+ + " \"root\": false,\n"+ + " \"metadata\": {\n"+ + " \"mimeType\": \"application/vnd.ekstep.content-collection\",\n"+ + " \"contentType\": \"TextBookUnit\",\n"+ + " \"code\": \"updateHierarchy\",\n"+ + " \"name\": \"Test_CourseUnit_1\",\n"+ + " \"description\": \"updated hierarchy\",\n"+ + " \"channel\": \"in.ekstep\",\n"+ + " \"primaryCategory\": \"Textbook Unit\"\n"+ + " }\n"+ + " }\n"+ + " }" + JsonUtils.deserialize(nodesModifiedString, classOf[util.HashMap[String, AnyRef]]) + } + //Text + def getHierarchy_1(): util.HashMap[String, AnyRef] = { + val hierarchyString = "{\n"+ + " \t\"do_11294581887465881611\" : {\n"+ + " \t\t\"root\": true,\n"+ + " \t\t\"children\": [\"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\"]\n"+ + " \t},\n"+ + " \t\"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\": {\n"+ + " \t\t\"root\": false,\n"+ + " \t\t\"children\": [\"do_31250856200414822416938\"]\n"+ + " \t}\n"+ + " }" + JsonUtils.deserialize(hierarchyString, classOf[util.HashMap[String, AnyRef]]) + } + + def getNodesModified_2(rootId:String, unit_1: String): util.HashMap[String, AnyRef] = { + val nodesModifiedString: String = "{\n"+ + " \""+ rootId +"\": {\n"+ + " \"isNew\": false,\n"+ + " \"root\": true,\n"+ + " \"metadata\": {\n"+ + " \"name\": \"updated text book name check\"\n"+ + " }\n"+ + " },\n"+ + " \""+ unit_1 +"\": {\n"+ + " \"isNew\": false,\n"+ + " \"root\": false\n"+ + " },\n"+ + " \"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\": {\n"+ + " \"isNew\": true,\n"+ + " \"root\": false,\n"+ + " \"metadata\": {\n"+ + " \"mimeType\": \"application/vnd.ekstep.content-collection\",\n"+ + " \"contentType\": \"TextBookUnit\",\n"+ + " \"code\": \"updateHierarchy\",\n"+ + " \"name\": \"Test_CourseUnit_1\",\n"+ + " \"description\": \"Test_CourseUnit_desc_1\",\n"+ + " \"primaryCategory\": \"Textbook Unit\"\n"+ + " }\n"+ + " }" + + "}" + JsonUtils.deserialize(nodesModifiedString, classOf[util.HashMap[String, AnyRef]]) + } + + def getHierarchy_2(rootId: String, unit_2: String): util.HashMap[String, AnyRef] = { + val hierarchyString: String = "{\n" + + " \"" + rootId + "\": {\n" + + " \"root\": true,\n" + + " \"children\": [\n" + + " \"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\"\n" + + " ]\n" + + " },\n" + + " \"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\": {\n" + + " \"root\": false,\n" + + " \"children\": [\n" + + " \"" + unit_2 + "\",\n" + + " \"do_111112224444\"\n" + + " ]\n" + + " },\n" + + " \"" + unit_2 + "\": {\n" + + " \"root\": false,\n" + + " \"children\": [\n" + + " \"do_31250856200414822416938\"\n" + + " ]\n" + + " }\n" + + " }" + JsonUtils.deserialize(hierarchyString, classOf[util.HashMap[String, AnyRef]]) + } + + //Text + def getHierarchy_Content_Resource_Invalid_ID(): util.HashMap[String, AnyRef] = { + val hierarchyString = "{\n"+ + " \t\"do_11294581887465881611\" : {\n"+ + " \t\t\"root\": true,\n"+ + " \t\t\"children\": [\"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\"]\n"+ + " \t},\n"+ + " \t\"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\": {\n"+ + " \t\t\"root\": false,\n"+ + " \t\t\"children\": [\"do_3125085620041482241\"]\n"+ + " \t}\n"+ + " }" + JsonUtils.deserialize(hierarchyString, classOf[util.HashMap[String, AnyRef]]) + } + + + def insert20NodesAndOneCourse() = { + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],code:\"txtbk\",channel:\"in.ekstep\",description:\"Text Book Test\",language:[\"English\"],mimeType:\"application/vnd.ekstep.content-collection\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:40:05.744+0530\",contentDisposition:\"inline\",contentEncoding:\"gzip\",lastUpdatedOn:\"2020-05-29T23:19:49.635+0530\",contentType:\"Course\",primaryCategory:\"Course\",dialcodeRequired:\"No\",identifier:\"do_113031517435822080121\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:40:05.744+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590774589635\",license:\"CC BY 4.0\",idealScreenDensity:\"hdpi\",depth:0,framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Collection\",name:\"TextBook\",IL_UNIQUE_ID:\"do_113031517435822080121\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:38:16.618+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:38:16.618+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:38:16.618+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761296618\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-11\",IL_UNIQUE_ID:\"do_113031516541870080111\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:36:35.092+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:36:35.092+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:36:35.092+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761195092\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-2\",IL_UNIQUE_ID:\"do_11303151571010355212\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:38:25.737+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:38:25.737+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:38:25.737+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761305737\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-12\",IL_UNIQUE_ID:\"do_113031516616491008112\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:37:15.852+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:37:15.852+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:37:15.852+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761235852\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-5\",IL_UNIQUE_ID:\"do_11303151604402585615\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:37:03.470+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:37:03.470+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:37:03.470+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761223470\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-4\",IL_UNIQUE_ID:\"do_11303151594263347214\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:39:15.501+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:39:15.501+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:39:15.501+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761355501\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-17\",IL_UNIQUE_ID:\"do_113031517024190464117\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:38:47.078+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:38:47.078+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:38:47.078+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761327078\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-14\",IL_UNIQUE_ID:\"do_113031516791406592114\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:37:58.033+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:37:58.033+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:37:58.033+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761278033\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-9\",IL_UNIQUE_ID:\"do_11303151638961356819\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:39:46.297+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:39:46.297+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:39:46.297+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761386297\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-20\",IL_UNIQUE_ID:\"do_113031517276520448120\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:36:51.893+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:36:51.893+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:36:51.893+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761211893\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-3\",IL_UNIQUE_ID:\"do_11303151584773734413\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:39:25.353+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:39:25.353+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:39:25.353+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761365353\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-18\",IL_UNIQUE_ID:\"do_113031517104939008118\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:38:35.090+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:38:35.090+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:38:35.090+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761315090\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-13\",IL_UNIQUE_ID:\"do_113031516693184512113\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:39:05.869+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:39:05.869+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:39:05.869+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761345869\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-16\",IL_UNIQUE_ID:\"do_113031516945334272116\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:38:07.777+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:38:07.777+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:38:07.777+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761287777\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-10\",IL_UNIQUE_ID:\"do_113031516469411840110\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:36:08.181+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:36:08.181+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:36:08.181+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761168181\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-1\",IL_UNIQUE_ID:\"do_11303151546543308811\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:39:36.978+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:39:36.978+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:39:36.978+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761376978\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-19\",IL_UNIQUE_ID:\"do_113031517200171008119\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:37:25.999+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:37:25.999+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:37:25.999+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761245999\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-6\",IL_UNIQUE_ID:\"do_11303151612719104016\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:37:38.731+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:37:38.731+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:37:38.731+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761258731\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-7\",IL_UNIQUE_ID:\"do_11303151623148339217\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:37:49.223+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:37:49.223+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:37:49.223+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761269223\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-8\",IL_UNIQUE_ID:\"do_11303151631740928018\",status:\"Draft\"},\n{ownershipType:[\"createdBy\"],code:\"test-Resourcce\",channel:\"in.ekstep\",language:[\"English\"],mimeType:\"application/pdf\",idealScreenSize:\"normal\",createdOn:\"2020-05-29T19:38:55.782+0530\",contentDisposition:\"inline\",contentEncoding:\"identity\",lastUpdatedOn:\"2020-05-29T19:38:55.782+0530\",contentType:\"Resource\",primaryCategory:\"Learning Resource\",dialcodeRequired:\"No\",audience:[\"Student\"],lastStatusChangedOn:\"2020-05-29T19:38:55.782+0530\",os:[\"All\"],visibility:\"Default\",IL_SYS_NODE_TYPE:\"DATA_NODE\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",version:2,versionKey:\"1590761335782\",idealScreenDensity:\"hdpi\",license:\"CC BY 4.0\",framework:\"NCF\",compatibilityLevel:1,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"prad PDF Content-15\",IL_UNIQUE_ID:\"do_113031516862660608115\",status:\"Draft\"}" + + ",{owner:\"in.ekstep\",code:\"NCF\",IL_SYS_NODE_TYPE:\"DATA_NODE\",apoc_json:\"{\\\"batch\\\": true}\",consumerId:\"9393568c-3a56-47dd-a9a3-34da3c821638\",channel:\"in.ekstep\",description:\"NCF \",type:\"K-12\",createdOn:\"2018-01-23T09:53:50.189+0000\",versionKey:\"1545195552163\",apoc_text:\"APOC\",appId:\"dev.sunbird.portal\",IL_FUNC_OBJECT_TYPE:\"Framework\",name:\"State (Uttar Pradesh)\",lastUpdatedOn:\"2018-12-19T04:59:12.163+0000\",IL_UNIQUE_ID:\"NCF\",status:\"Live\",apoc_num:1}" + + ",{owner:\"in.ekstep\",code:\"K-12\",IL_SYS_NODE_TYPE:\"DATA_NODE\",apoc_json:\"{\\\"batch\\\": true}\",consumerId:\"9393568c-3a56-47dd-a9a3-34da3c821638\",channel:\"in.ekstep\",description:\"NCF \",type:\"K-12\",createdOn:\"2018-01-23T09:53:50.189+0000\",versionKey:\"1545195552163\",apoc_text:\"APOC\",appId:\"dev.sunbird.portal\",IL_FUNC_OBJECT_TYPE:\"Framework\",name:\"State (Uttar Pradesh)\",lastUpdatedOn:\"2018-12-19T04:59:12.163+0000\",IL_UNIQUE_ID:\"K-12\",status:\"Live\",apoc_num:1}" + + ",{owner:\"in.ekstep\",code:\"tpd\",IL_SYS_NODE_TYPE:\"DATA_NODE\",apoc_json:\"{\\\"batch\\\": true}\",consumerId:\"9393568c-3a56-47dd-a9a3-34da3c821638\",channel:\"in.ekstep\",description:\"NCF \",type:\"K-12\",createdOn:\"2018-01-23T09:53:50.189+0000\",versionKey:\"1545195552163\",apoc_text:\"APOC\",appId:\"dev.sunbird.portal\",IL_FUNC_OBJECT_TYPE:\"Framework\",name:\"State (Uttar Pradesh)\",lastUpdatedOn:\"2018-12-19T04:59:12.163+0000\",IL_UNIQUE_ID:\"tpd\",status:\"Live\",apoc_num:1}] as row CREATE (n:domain) SET n += row;"); + } + + def getNodesModified_3(): util.HashMap[String, AnyRef] = { + val nodesModifiedString: String = "{\n"+ + " \"U1\": {\n"+ + " \"isNew\": true,\n"+ + " \"root\": false,\n"+ + " \"metadata\": {\n"+ + " \"mimeType\": \"application/vnd.ekstep.content-collection\",\n"+ + " \"contentType\": \"TextBookUnit\",\n"+ + " \"code\": \"updateHierarchy\",\n"+ + " \"name\": \"U1\",\n"+ + " \"description\": \"updated hierarchy\",\n"+ + " \"channel\": \"in.ekstep\",\n"+ + " \"primaryCategory\": \"Textbook Unit\"\n"+ + " }\n"+ + " }\n"+ + " }" + JsonUtils.deserialize(nodesModifiedString, classOf[util.HashMap[String, AnyRef]]) + } + def getHierarchy_3(rootId: String,childrenId: String): util.HashMap[String, AnyRef] = { + val hierarchyString = "{\n"+ + " \t\""+rootId+"\" : {\n"+ + " \t\t\"root\": true,\n"+ + " \t\t\"children\": [\"U1\"]\n"+ + " \t},\n"+ + " \t\"b9a50833-eff6-4ef5-a2a4-2413f2d51f6c\": {\n"+ + " \t\t\"root\": false,\n"+ + " \t\t\"children\": [\""+childrenId+"\"]\n"+ + " \t}\n"+ + " }" + JsonUtils.deserialize(hierarchyString, classOf[util.HashMap[String, AnyRef]]) + } +} diff --git a/learning-api/orchestrator/src/main/java/org/sunbird/actors/CollectionActor.java b/content-api/orchestrator/src/main/java/org/sunbird/actors/CollectionActor.java similarity index 65% rename from learning-api/orchestrator/src/main/java/org/sunbird/actors/CollectionActor.java rename to content-api/orchestrator/src/main/java/org/sunbird/actors/CollectionActor.java index dfb15c4c3..3642e9e7b 100644 --- a/learning-api/orchestrator/src/main/java/org/sunbird/actors/CollectionActor.java +++ b/content-api/orchestrator/src/main/java/org/sunbird/actors/CollectionActor.java @@ -4,6 +4,7 @@ import org.sunbird.common.dto.Request; import org.sunbird.common.dto.Response; import org.sunbird.managers.HierarchyManager; +import org.sunbird.managers.UpdateHierarchyManager; import scala.concurrent.Future; public class CollectionActor extends BaseActor { @@ -16,6 +17,8 @@ public Future onReceive(Request request) throws Throwable { switch (operation) { case "addHierarchy": return addLeafNodesToHierarchy(request); case "removeHierarchy": return removeLeafNodesFromHierarchy(request); + case "updateHierarchy": return updateHierarchy(request); + case "getHierarchy": return getHierarchy(request); default: return ERROR(operation); } } @@ -29,4 +32,13 @@ private Future removeLeafNodesFromHierarchy(Request request) throws Ex request.getContext().put("schemaName", SCHEMA_NAME); return HierarchyManager.removeLeafNodesFromHierarchy(request, getContext().dispatcher()); } + + private Future updateHierarchy(Request request) throws Exception { + request.getContext().put("schemaName", SCHEMA_NAME); + return UpdateHierarchyManager.updateHierarchy(request, getContext().dispatcher()); + } + private Future getHierarchy(Request request) throws Exception { + request.getContext().put("schemaName", SCHEMA_NAME); + return HierarchyManager.getHierarchy(request, getContext().dispatcher()); + } } diff --git a/content-api/orchestrator/src/test/resources/application.conf b/content-api/orchestrator/src/test/resources/application.conf new file mode 100644 index 000000000..a07b73257 --- /dev/null +++ b/content-api/orchestrator/src/test/resources/application.conf @@ -0,0 +1,199 @@ +# Learning-Service Configuration +content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit", "event"] + +# Cassandra Configuration +content.keyspace.name=content_store +content.keyspace.table=content_data +#TODO: Add Configuration for assessment. e.g: question_data +orchestrator.keyspace.name=script_store +orchestrator.keyspace.table=script_data +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" + +# Redis Configuration +redis.host=localhost +redis.port=6379 +redis.maxConnections=128 + +#Condition to enable publish locally +content.publish_task.enabled=true + +#directory location where store unzip file +dist.directory=/data/tmp/dist/ +output.zipfile=/data/tmp/story.zip +source.folder=/data/tmp/temp2/ +save.directory=/data/tmp/temp/ + +# Content 2 vec analytics URL +CONTENT_TO_VEC_URL="http://172.31.27.233:9000/content-to-vec" + +# FOR CONTENT WORKFLOW PIPELINE (CWP) + +#--Content Workflow Pipeline Mode +OPERATION_MODE=TEST + +#--Maximum Content Package File Size Limit in Bytes (50 MB) +MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=52428800 + +#--Maximum Asset File Size Limit in Bytes (20 MB) +MAX_ASSET_FILE_SIZE_LIMIT=20971520 + +#--No of Retry While File Download Fails +RETRY_ASSET_DOWNLOAD_COUNT=1 + +#Google-vision-API +google.vision.tagging.enabled = false + +#Orchestrator env properties +env="https://dev.ekstep.in/api/learning" + +#Current environment +cloud_storage.env=dev + + +#Folder configuration +cloud_storage.content.folder=content +cloud_storage.asset.folder=assets +cloud_storage.artefact.folder=artifact +cloud_storage.bundle.folder=bundle +cloud_storage.media.folder=media +cloud_storage.ecar.folder=ecar_files + +# Media download configuration +content.media.base.url="https://dev.open-sunbird.org" +plugin.media.base.url="https://dev.open-sunbird.org" + +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + +# Elasticsearch properties +search.es_conn_info="localhost:9200" +search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","domain","subject","description^10","keywords^25","ageGroup^10","filter^10","theme^10","genre^10","objects^25","contentType^100","language^200","teachingMode^25","skills^10","learningObjective^10","curriculum^100","gradeLevel^100","developer^100","attributions^10","owner^50","text","words","releaseNotes"] +search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] +search.batch.size=500 +search.connection.timeout=30 +platform-api-url="http://localhost:8080/language-service" +MAX_ITERATION_COUNT_FOR_SAMZA_JOB=2 + + +# DIAL Code Configuration +dialcode.keyspace.name="dialcode_store" +dialcode.keyspace.table="dial_code" +dialcode.max_count=1000 + +# System Configuration +system.config.keyspace.name="dialcode_store" +system.config.table="system_config" + + +#Publisher Configuration +publisher.keyspace.name="dialcode_store" +publisher.keyspace.table="publisher" + +#DIAL Code Generator Configuration +dialcode.strip.chars="0" +dialcode.length=6.0 +dialcode.large.prime_number=1679979167 + +#DIAL Code ElasticSearch Configuration +dialcode.index=true +dialcode.object_type="DialCode" + +framework.max_term_creation_limit=200 + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + +# Kafka configuration details +kafka.topics.instruction="local.learning.job.request" +kafka.urls="localhost:9092" + +#Youtube Standard Licence Validation +learning.content.youtube.validate.license=true +learning.content.youtube.application.name=fetch-youtube-license +youtube.license.regex.pattern=["\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)","^([A-Za-z0-9\\-\\_]*)"] + +#Top N Config for Search Telemetry +telemetry_env=dev +telemetry.search.topn=5 + +installation.id=ekstep + +channel.default="in.ekstep" + +# DialCode Link API Config +learning.content.link_dialcode_validation=true +dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/search" +dialcode.api.authorization=auth_key + +# Language-Code Configuration +language.graph_ids=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] + +# Kafka send event to topic enable +kafka.topic.send.enable=false + +learning.valid_license=["creativeCommon"] +learning.service_provider=["youtube"] + +stream.mime.type=video/mp4 +compositesearch.index.name="compositesearch" + +hierarchy.keyspace.name=hierarchy_store +content.hierarchy.table=content_hierarchy +framework.hierarchy.table=framework_hierarchy + +# Kafka topic for definition update event. +kafka.topic.system.command="dev.system.command" + +learning.reserve_dialcode.content_type=["TextBook"] +# restrict.metadata.objectTypes=["Content", "ContentImage", "AssessmentItem", "Channel", "Framework", "Category", "CategoryInstance", "Term"] + +#restrict.metadata.objectTypes="Content,ContentImage" + +publish.collection.fullecar.disable=true + +# Consistency Level for Multi Node Cassandra cluster +cassandra.lp.consistency.level=QUORUM + + + + +content.nested.fields="badgeAssertions,targets,badgeAssociations" + +content.cache.ttl=86400 +content.cache.enable=true +collection.cache.enable=true +content.discard.status=["Draft","FlagDraft"] + +framework.categories_cached=["subject", "medium", "gradeLevel", "board"] +framework.cache.ttl=86400 +framework.cache.read=true + + +# Max size(width/height) of thumbnail in pixels +max.thumbnail.size.pixels=150 + +schema.base_path="../../schemas/" +content.hierarchy.removed_props_for_leafNodes=["collections","children","usedByContent","item_sets","methods","libraries","editorState"] + +collection.keyspace = "hierarchy_store" +content.keyspace = "content_store" +collection.image.migration.enabled=true +objectcategorydefinition.keyspace=category_store + diff --git a/learning-api/pom.xml b/content-api/pom.xml similarity index 66% rename from learning-api/pom.xml rename to content-api/pom.xml index 1a0ad3564..ff480cc54 100755 --- a/learning-api/pom.xml +++ b/content-api/pom.xml @@ -7,23 +7,19 @@ 1.0-SNAPSHOT ../pom.xml - learning-api + content-api 1.0-SNAPSHOT pom - learning-api + content-api - 2.3.1 - 1.8 - 1.8 UTF-8 UTF-8 - 1.1.1 - 2.11.8 + 2.11.12 content-service hierarchy-manager - orchestrator + content-actors @@ -32,22 +28,13 @@ maven-assembly-plugin - 2.3 + 3.3.0 src/assembly/bin.xml - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.8 - 1.8 - - org.scoverage scoverage-maven-plugin diff --git a/definition-scripts/Asset.sh b/definition-scripts/Asset.sh new file mode 100644 index 000000000..a857b9625 --- /dev/null +++ b/definition-scripts/Asset.sh @@ -0,0 +1,15 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request":{ + "objectCategoryDefinition":{ + "categoryId": "obj-cat:asset", + "targetObjectType": "Asset", + "objectMetadata":{ + "config":{}, + "schema":{} + } + } + } +}' + diff --git a/definition-scripts/Cert_Asset.sh b/definition-scripts/Cert_Asset.sh new file mode 100644 index 000000000..3aaf665f1 --- /dev/null +++ b/definition-scripts/Cert_Asset.sh @@ -0,0 +1,14 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request":{ + "objectCategoryDefinition":{ + "categoryId": "obj-cat:certasset", + "targetObjectType": "Asset", + "objectMetadata":{ + "config":{}, + "schema":{} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Certificate_Template.sh b/definition-scripts/Certificate_Template.sh new file mode 100644 index 000000000..97c808da6 --- /dev/null +++ b/definition-scripts/Certificate_Template.sh @@ -0,0 +1,56 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:certificate-template", + "targetObjectType": "Asset", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "issuer": { + "type": "object" + }, + "signatoryList": { + "type": "array", + "items": { + "type": "object", + "properties": { + "image": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "designation": { + "type": "string" + } + } + } + }, + "logos": { + "type": "array", + "items": { + "type": "string" + } + }, + "certType": { + "type": "string", + "enum": [ + "cert template layout", + "cert template" + ] + }, + "data": { + "type": "object" + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Content_playlist.sh b/definition-scripts/Content_playlist.sh new file mode 100644 index 000000000..a60e27c17 --- /dev/null +++ b/definition-scripts/Content_playlist.sh @@ -0,0 +1,14 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:content-playlist", + "targetObjectType": "Collection", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Content_playlist_content.sh b/definition-scripts/Content_playlist_content.sh new file mode 100644 index 000000000..27ca078bd --- /dev/null +++ b/definition-scripts/Content_playlist_content.sh @@ -0,0 +1,14 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:content-playlist", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Course.sh b/definition-scripts/Course.sh new file mode 100644 index 000000000..43fe150a1 --- /dev/null +++ b/definition-scripts/Course.sh @@ -0,0 +1,78 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:course", + "targetObjectType": "Collection", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "Yes" + }, + "autoBatch": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "Yes" + } + }, + "default": { + "enabled": "Yes", + "autoBatch": "Yes" + }, + "additionalProperties": false + }, + "monitorable": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "progress-report", + "score-report" + ] + } + }, + "credentials": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "Yes" + } + }, + "default": { + "enabled": "Yes" + }, + "additionalProperties": false + }, + "userConsent": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "Yes" + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/CourseUnit.sh b/definition-scripts/CourseUnit.sh new file mode 100644 index 000000000..1329104fb --- /dev/null +++ b/definition-scripts/CourseUnit.sh @@ -0,0 +1,14 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:course-unit", + "targetObjectType": "Collection", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/CourseUnit_content.sh b/definition-scripts/CourseUnit_content.sh new file mode 100644 index 000000000..581d83467 --- /dev/null +++ b/definition-scripts/CourseUnit_content.sh @@ -0,0 +1,14 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:course-unit", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Course_assessment.sh b/definition-scripts/Course_assessment.sh new file mode 100644 index 000000000..1cab8852d --- /dev/null +++ b/definition-scripts/Course_assessment.sh @@ -0,0 +1,39 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:course-assessment", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + }, + "autoBatch": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + } + }, + "additionalProperties": false + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Course_content.sh b/definition-scripts/Course_content.sh new file mode 100644 index 000000000..9d0ba5f03 --- /dev/null +++ b/definition-scripts/Course_content.sh @@ -0,0 +1,75 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:course", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "Yes" + }, + "autoBatch": { + "type": "string", + "enum": ["Yes","No"], + "default": "Yes" + } + }, + "default": { + "enabled": "Yes", + "autoBatch": "Yes" + }, + "additionalProperties": false + }, + "monitorable": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "progress-report", + "score-report" + ] + } + }, + "credentials": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "Yes" + } + }, + "default": { + "enabled": "Yes" + }, + "additionalProperties": false + }, + "userConsent": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "Yes" + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Digital_textbook.sh b/definition-scripts/Digital_textbook.sh new file mode 100644 index 000000000..a606e5d6b --- /dev/null +++ b/definition-scripts/Digital_textbook.sh @@ -0,0 +1,59 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:digital-textbook", + "targetObjectType": "Collection", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + }, + "autoBatch": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + } + }, + "default": { + "enabled": "No", + "autoBatch": "No" + }, + "additionalProperties": false + }, + "additionalCategories": { + "type": "array", + "items": { + "type": "string", + "default": "Textbook" + } + }, + "userConsent": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "Yes" + } + } + } + } + } + } +}' + diff --git a/definition-scripts/Digital_textbook_content.sh b/definition-scripts/Digital_textbook_content.sh new file mode 100644 index 000000000..ab77d99f5 --- /dev/null +++ b/definition-scripts/Digital_textbook_content.sh @@ -0,0 +1,59 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:digital-textbook", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + }, + "autoBatch": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + } + }, + "default": { + "enabled": "No", + "autoBatch": "No" + }, + "additionalProperties": false + }, + "additionalCategories": { + "type": "array", + "items": { + "type": "string", + "default": "Textbook" + } + }, + "userConsent": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + } + } + } + } + } + } +}' + diff --git a/definition-scripts/Explanation_collection.sh b/definition-scripts/Explanation_collection.sh new file mode 100644 index 000000000..cd7806260 --- /dev/null +++ b/definition-scripts/Explanation_collection.sh @@ -0,0 +1,30 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:explanation-content", + "targetObjectType": "Collection", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + } + } + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Explanation_content.sh b/definition-scripts/Explanation_content.sh new file mode 100644 index 000000000..7f3ee4bed --- /dev/null +++ b/definition-scripts/Explanation_content.sh @@ -0,0 +1,30 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:explanation-content", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + } + } + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/FTB_QUESTION.sh b/definition-scripts/FTB_QUESTION.sh new file mode 100755 index 000000000..1b6a1d3b9 --- /dev/null +++ b/definition-scripts/FTB_QUESTION.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +curl -L -X POST '/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:ftb-question", + "targetObjectType": "Question", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Learning_resource.sh b/definition-scripts/Learning_resource.sh new file mode 100644 index 000000000..ae8aa2434 --- /dev/null +++ b/definition-scripts/Learning_resource.sh @@ -0,0 +1,14 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:learning-resource", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/LessonPlan_unit.sh b/definition-scripts/LessonPlan_unit.sh new file mode 100755 index 000000000..fe9f31dd5 --- /dev/null +++ b/definition-scripts/LessonPlan_unit.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:lesson-plan-unit", + "targetObjectType": "Collection", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/LessonPlan_unit_content.sh b/definition-scripts/LessonPlan_unit_content.sh new file mode 100755 index 000000000..80fc0e16b --- /dev/null +++ b/definition-scripts/LessonPlan_unit_content.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:lesson-plan-unit", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Multiple_Choice_Question.sh b/definition-scripts/Multiple_Choice_Question.sh new file mode 100755 index 000000000..8a4862367 --- /dev/null +++ b/definition-scripts/Multiple_Choice_Question.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:multiple-choice-question", + "targetObjectType": "Question", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Plugin_Content.sh b/definition-scripts/Plugin_Content.sh new file mode 100644 index 000000000..e107ab4cc --- /dev/null +++ b/definition-scripts/Plugin_Content.sh @@ -0,0 +1,15 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request":{ + "objectCategoryDefinition":{ + "categoryId": "obj-cat:plugin", + "targetObjectType": "Content", + "objectMetadata":{ + "config":{}, + "schema":{} + } + } + } +}' + diff --git a/definition-scripts/Subjective_Question.sh b/definition-scripts/Subjective_Question.sh new file mode 100755 index 000000000..0f723e657 --- /dev/null +++ b/definition-scripts/Subjective_Question.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:subjective-question", + "targetObjectType": "Question", + "objectMetadata": { + "config": {}, + "schema": {} + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Teacher_resource.sh b/definition-scripts/Teacher_resource.sh new file mode 100644 index 000000000..cc87aaa09 --- /dev/null +++ b/definition-scripts/Teacher_resource.sh @@ -0,0 +1,39 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:teacher-resource", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + }, + "autoBatch": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + } + }, + "additionalProperties": false + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/Template_Content.sh b/definition-scripts/Template_Content.sh new file mode 100644 index 000000000..d2bb45ca5 --- /dev/null +++ b/definition-scripts/Template_Content.sh @@ -0,0 +1,15 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request":{ + "objectCategoryDefinition":{ + "categoryId": "obj-cat:template", + "targetObjectType": "Content", + "objectMetadata":{ + "config":{}, + "schema":{} + } + } + } +}' + diff --git a/definition-scripts/TextBookUnit.sh b/definition-scripts/TextBookUnit.sh new file mode 100644 index 000000000..4c7d49a00 --- /dev/null +++ b/definition-scripts/TextBookUnit.sh @@ -0,0 +1,30 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request":{ + "objectCategoryDefinition":{ + "categoryId": "obj-cat:textbook-unit", + "targetObjectType": "Collection", + "objectMetadata":{ + "config":{}, + "schema":{ + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": ["Yes","No"], + "default": "No" + } + }, + "additionalProperties": false + } + } + + } + } + } + } +}' + diff --git a/definition-scripts/TextBookUnit_content.sh b/definition-scripts/TextBookUnit_content.sh new file mode 100644 index 000000000..04af2b93c --- /dev/null +++ b/definition-scripts/TextBookUnit_content.sh @@ -0,0 +1,30 @@ +curl --location --request POST '{{host}}/object/category/definition/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request":{ + "objectCategoryDefinition":{ + "categoryId": "obj-cat:textbook-unit", + "targetObjectType": "Content", + "objectMetadata":{ + "config":{}, + "schema":{ + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": ["Yes","No"], + "default": "No" + } + }, + "additionalProperties": false + } + } + + } + } + } + } +}' + diff --git a/definition-scripts/eTextbook.sh b/definition-scripts/eTextbook.sh new file mode 100644 index 000000000..196096617 --- /dev/null +++ b/definition-scripts/eTextbook.sh @@ -0,0 +1,37 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:etextbook", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + } + }, + "additionalCategories": { + "type": "array", + "items": { + "type": "string", + "default": "Textbook" + } + } + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/definition-scripts/master_category_create b/definition-scripts/master_category_create new file mode 100644 index 000000000..93913a504 --- /dev/null +++ b/definition-scripts/master_category_create @@ -0,0 +1,357 @@ +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Textbook Unit", + "description":"Textbook Unit" + } + } +}' + + + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Course Unit", + "description":"Course Unit" + } + } +}' + + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Lesson Plan Unit", + "description":"Lesson Plan Unit" + } + } +}' + + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Plugin", + "description":"Plugin" + } + } +}' + + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Asset", + "description":"Asset" + } + } +}' + + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Template", + "description":"Template" + } + } +}' + + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Content Playlist", + "description":"Content Playlist" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Digital Textbook", + "description":"Digital Textbook" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Course Assessment", + "description":"Course Assessment" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Teacher Resource", + "description":"Teacher Resource" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "eTextbook", + "description":"eTextbook" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Practice Question Set", + "description":"Practice Question Set" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Course", + "description":"Course" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Learning Resource", + "description":"Learning Resource" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Explanation Content", + "description":"Explanation Content" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Certificate Template", + "description":"Certificate Template" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "CertAsset", + "description":"CertAsset" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Classroom Teaching Video", + "description":"Classroom Teaching Video" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Concept Map", + "description":"Concept Map" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Curiosity Question Set", + "description":"Curiosity Question Set" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Experiential Resource", + "description":"Experiential Resource" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Explanation Video", + "description":"Explanation Video" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Focus Spot", + "description":"Focus Spot" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Learning Outcome Definition", + "description":"Learning Outcome Definition" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Lesson Plan", + "description":"Lesson Plan" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Marking Scheme Rubric", + "description":"Marking Scheme Rubric" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Pedagogy Flow", + "description":"Pedagogy Flow" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Previous Board Exam Papers", + "description":"Previous Board Exam Papers" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "TV Lesson", + "description":"TV Lesson" + } + } +}' +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Textbook", + "description":"Textbook" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Multiple Choice Question", + "description":"Multiple Choice Question" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "Subjective Question", + "description":"Subjective Question" + } + } +}' + +curl --location --request POST '{{host}}/object/category/v4/create' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategory": { + "name": "FTB Question", + "description":"FTB Question" + } + } +}' \ No newline at end of file diff --git a/definition-scripts/practice_question_set.sh b/definition-scripts/practice_question_set.sh new file mode 100644 index 000000000..0f530aa35 --- /dev/null +++ b/definition-scripts/practice_question_set.sh @@ -0,0 +1,40 @@ +curl -L -X POST '{{host}}/object/category/definition/v4/create' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "request": { + "objectCategoryDefinition": { + "categoryId": "obj-cat:practice-question-set", + "targetObjectType": "Content", + "objectMetadata": { + "config": {}, + "schema": { + "properties": { + "trackable": { + "type": "object", + "properties": { + "enabled": { + "type": "string", + "enum": [ + "Yes", + "No" + ], + "default": "No" + }, + "autoBatch": { + "type": "string", + "enum": ["Yes","No"], + "default": "No" + } + }, + "default": { + "enabled": "No", + "autoBatch": "No" + }, + "additionalProperties": false + } + } + } + } + } + } +}' \ No newline at end of file diff --git a/Jenkinsfile b/functional-tests/content/Jenkinsfile similarity index 52% rename from Jenkinsfile rename to functional-tests/content/Jenkinsfile index 6621bc098..0923a5ae9 100644 --- a/Jenkinsfile +++ b/functional-tests/content/Jenkinsfile @@ -8,46 +8,29 @@ node('build-slave') { ansiColor('xterm') { stage('Checkout') { - if (!env.hub_org) { - println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) - error 'Please resolve the errors and rerun..' - } else - println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) - } cleanWs() if (params.github_release_tag == "") { checkout scm commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() branch_name = sh(script: 'git name-rev --name-only HEAD | rev | cut -d "/" -f1| rev', returnStdout: true).trim() - build_tag = branch_name + "_" + commit_hash + build_tag = branch_name + "_" + commit_hash + "_" + env.BUILD_NUMBER println(ANSI_BOLD + ANSI_YELLOW + "github_release_tag not specified, using the latest commit hash: " + commit_hash + ANSI_NORMAL) } else { def scmVars = checkout scm checkout scm: [$class: 'GitSCM', branches: [[name: "refs/tags/$params.github_release_tag"]], userRemoteConfigs: [[url: scmVars.GIT_URL]]] - build_tag = params.github_release_tag + build_tag = params.github_release_tag + "_" + env.BUILD_NUMBER println(ANSI_BOLD + ANSI_YELLOW + "github_release_tag specified, building from tag: " + params.github_release_tag + ANSI_NORMAL) } echo "build_tag: " + build_tag - stage('Build') { - env.NODE_ENV = "build" - print "Environment will be : ${env.NODE_ENV}" - sh 'mvn clean install -DskipTests=true ' - - } + stage('Run functional testcases') { + sh ''' + echo "Running content service Functional testcases" + ''' - stage('Package') { - dir('learning-api') { - sh 'mvn play2:dist -pl content-service' - } - sh('chmod 777 ./build.sh') - sh("./build.sh ${build_tag} ${env.NODE_NAME} ${hub_org}") - } - stage('ArchiveArtifacts') { - archiveArtifacts "metadata.json" - currentBuild.description = "${build_tag}" } - } + } + } } catch (err) { currentBuild.result = "FAILURE" diff --git a/learning-api/content-service/app/Module.scala b/learning-api/content-service/app/Module.scala deleted file mode 100644 index 1636bc7e7..000000000 --- a/learning-api/content-service/app/Module.scala +++ /dev/null @@ -1,17 +0,0 @@ - -import com.google.inject.AbstractModule -import org.sunbird.actors.{CollectionActor, ContentActor, HealthActor, LicenseActor} -import play.libs.akka.AkkaGuiceSupport -import utils.ActorNames - -class Module extends AbstractModule with AkkaGuiceSupport { - - override def configure() = { - super.configure() - bindActor(classOf[HealthActor], ActorNames.HEALTH_ACTOR) - bindActor(classOf[ContentActor], ActorNames.CONTENT_ACTOR) - bindActor(classOf[LicenseActor], ActorNames.LICENSE_ACTOR) - bindActor(classOf[CollectionActor], ActorNames.COLLECTION_ACTOR) - println("Initialized application actors...") - } -} diff --git a/learning-api/content-service/app/controllers/HealthController.scala b/learning-api/content-service/app/controllers/HealthController.scala deleted file mode 100644 index 3a31c56a2..000000000 --- a/learning-api/content-service/app/controllers/HealthController.scala +++ /dev/null @@ -1,16 +0,0 @@ -package controllers - -import akka.actor.{ActorRef, ActorSystem} -import javax.inject._ -import play.api.mvc._ -import utils.{ActorNames, ApiId} - -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future, Promise} - -class HealthController @Inject()(@Named(ActorNames.HEALTH_ACTOR) healthActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { - - def health() = Action.async { implicit request => - getResult(ApiId.APPLICATION_HEALTH, healthActor, new org.sunbird.common.dto.Request()) - } -} diff --git a/learning-api/content-service/app/controllers/v3/ContentController.scala b/learning-api/content-service/app/controllers/v3/ContentController.scala deleted file mode 100644 index 47c548e25..000000000 --- a/learning-api/content-service/app/controllers/v3/ContentController.scala +++ /dev/null @@ -1,80 +0,0 @@ -package controllers.v3 - -import akka.actor.{ActorRef, ActorSystem} -import com.google.inject.Singleton -import controllers.BaseController -import javax.inject.{Inject, Named} -import org.sunbird.telemetry.logger.TelemetryManager -import play.api.mvc.ControllerComponents -import utils.{ActorNames, ApiId} - -import scala.collection.JavaConversions._ -import scala.concurrent.ExecutionContext - -@Singleton -class ContentController @Inject()(@Named(ActorNames.CONTENT_ACTOR) contentActor: ActorRef,@Named(ActorNames.COLLECTION_ACTOR) collectionActor: ActorRef, cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController(cc) { - - val objectType = "Content" - val schemaName: String = "content" - val version = "1.0" - - def create() = Action.async { implicit request => - val headers = commonHeaders() - val body = requestBody() - val content = body.getOrElse("content", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; - content.putAll(headers) - val contentRequest = getRequest(content, headers, "createContent") - setRequestContext(contentRequest, version, objectType, schemaName) - getResult(ApiId.CREATE_CONTENT, contentActor, contentRequest) - } - - /** - * This Api end point takes 3 parameters - * Content Identifier the unique identifier of a content - * Mode in which the content can be viewed (default read or edit) - * Fields are metadata that should be returned to visualize - * @param identifier - * @param mode - * @param fields - * @return - */ - def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request => - val headers = commonHeaders() - val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]] - content.putAll(headers) - content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asInstanceOf[Map[String, Object]]) - val readRequest = getRequest(content, headers, "readContent") - setRequestContext(readRequest, version, objectType, schemaName) - getResult(ApiId.READ_CONTENT, contentActor, readRequest) - } - - def update(identifier:String) = Action.async { implicit request => - val headers = commonHeaders() - val body = requestBody() - val content = body.getOrElse("content", new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]; - content.putAll(headers) - val contentRequest = getRequest(content, headers, "updateContent") - setRequestContext(contentRequest, version, objectType, schemaName) - contentRequest.getContext.put("identifier",identifier); - getResult(ApiId.UPDATE_CONTENT, contentActor, contentRequest) - } - - def addHierarchy() = Action.async { implicit request => - val headers = commonHeaders() - val body = requestBody() - body.putAll(headers) - val contentRequest = getRequest(body, headers, "addHierarchy") - setRequestContext(contentRequest, version, objectType, schemaName) - getResult(ApiId.ADD_HIERARCHY, collectionActor, contentRequest) - } - - def removeHierarchy() = Action.async { implicit request => - val headers = commonHeaders() - val body = requestBody() - body.putAll(headers) - val contentRequest = getRequest(body, headers, "removeHierarchy") - setRequestContext(contentRequest, version, objectType, schemaName) - getResult(ApiId.REMOVE_HIERARCHY, collectionActor, contentRequest) - } - -} diff --git a/learning-api/content-service/app/filters/AccessLogFilter.scala b/learning-api/content-service/app/filters/AccessLogFilter.scala deleted file mode 100644 index f28990033..000000000 --- a/learning-api/content-service/app/filters/AccessLogFilter.scala +++ /dev/null @@ -1,57 +0,0 @@ -package filters - -import akka.util.ByteString -import javax.inject.Inject -import org.sunbird.telemetry.util.TelemetryAccessEventUtil -import play.api.Logging -import play.api.libs.streams.Accumulator -import play.api.mvc._ -import play.core.server.akkahttp.AkkaHeadersWrapper - -import scala.concurrent.ExecutionContext -import scala.collection.JavaConverters._ - -class AccessLogFilter @Inject()(implicit ec: ExecutionContext) extends EssentialFilter with Logging { - def apply(nextFilter: EssentialAction) = new EssentialAction { - def apply(requestHeader: RequestHeader) = { - - val startTime = System.currentTimeMillis - - val accumulator: Accumulator[ByteString, Result] = nextFilter(requestHeader) - - accumulator.map { result => - val endTime = System.currentTimeMillis - val requestTime = endTime - startTime - - val path = requestHeader.headers.asInstanceOf[AkkaHeadersWrapper].request.uri.toString(); - if(!path.contains("/health")){ - var data:Map[String, Any] = Map[String, Any]() - data += ("StartTime" -> startTime) - data += ("env" -> "content") - data += ("RemoteAddress" -> requestHeader.remoteAddress) - data += ("ContentLength" -> result.body.contentLength.getOrElse(0)) - data += ("Status" -> result.header.status) - data += ("Protocol" -> requestHeader.headers.asInstanceOf[AkkaHeadersWrapper].request.protocol) - data += ("path" -> path) - data += ("Method" -> requestHeader.method.toString) - val headers = requestHeader.headers.asInstanceOf[AkkaHeadersWrapper].headers.groupBy(_._1).mapValues(_.map(_._2)); - if(None != headers.get("X-Session-ID")) - data += ("X-Session-ID" -> headers.get("X-Session-ID").head.head) - if(None != headers.get("X-Consumer-ID")) - data += ("X-Consumer-ID" -> headers.get("X-Consumer-ID").head.head) - if(None != headers.get("X-Device-ID")) - data += ("X-Device-ID" -> headers.get("X-Device-ID").head.head) - if(None != headers.get("X-App-Id")) - data += ("APP_ID" -> headers.get("X-App-Id").head.head) - if(None != headers.get("X-Authenticated-Userid")) - data += ("X-Authenticated-Userid" -> headers.get("X-Authenticated-Userid").head.head) - if(None != headers.get("X-Channel-Id")) - data += ("X-Channel-Id" -> headers.get("X-Channel-Id").head.head) - - TelemetryAccessEventUtil.writeTelemetryEventLog(data.asInstanceOf[Map[String, AnyRef]].asJava) - } - result.withHeaders("Request-Time" -> requestTime.toString) - } - } - } - } \ No newline at end of file diff --git a/learning-api/content-service/app/utils/ActorNames.scala b/learning-api/content-service/app/utils/ActorNames.scala deleted file mode 100644 index 6e0a22cf6..000000000 --- a/learning-api/content-service/app/utils/ActorNames.scala +++ /dev/null @@ -1,10 +0,0 @@ -package utils - -object ActorNames { - - final val HEALTH_ACTOR = "healthActor" - final val CONTENT_ACTOR = "contentActor" - final val LICENSE_ACTOR = "licenseActor" - final val COLLECTION_ACTOR = "collectionActor" - -} diff --git a/learning-api/content-service/app/utils/ApiId.scala b/learning-api/content-service/app/utils/ApiId.scala deleted file mode 100644 index a8c9d184f..000000000 --- a/learning-api/content-service/app/utils/ApiId.scala +++ /dev/null @@ -1,21 +0,0 @@ -package utils - -object ApiId { - - final val APPLICATION_HEALTH = "api.content-service.health" - - //Content APIs - final val CREATE_CONTENT = "ekstep.learning.content.create" - final val READ_CONTENT = "ekstep.content.find" - final val UPDATE_CONTENT = "ekstep.learning.content.update" - - // Collection APIs - val ADD_HIERARCHY = "api.content.hierarchy.add" - val REMOVE_HIERARCHY = "api.content.hierarchy.remove" - - //LicenseAPIS - final val CREATE_LICENSE = "api.license.create" - final val READ_LICENSE = "api.license.read" - final val UPDATE_LICENSE = "api.license.update" - final val RETIRE_LICENSE = "api.license.retire" -} diff --git a/learning-api/content-service/conf/routes b/learning-api/content-service/conf/routes deleted file mode 100644 index 68ef9c837..000000000 --- a/learning-api/content-service/conf/routes +++ /dev/null @@ -1,17 +0,0 @@ -# Routes -# This file defines all application routes (Higher priority routes first) -# ~~~~ -GET /health controllers.HealthController.health - -POST /content/v3/create controllers.v3.ContentController.create -PATCH /content/v3/update/:identifier controllers.v3.ContentController.update(identifier:String) -GET /content/v3/read/:identifier controllers.v3.ContentController.read(identifier:String, mode:Option[String], fields:Option[String]) -PATCH /content/v3/hierarchy/add controllers.v3.ContentController.addHierarchy -DELETE /content/v3/hierarchy/remove controllers.v3.ContentController.removeHierarchy - -#These are routes for License Creation -POST /license/v3/create controllers.v3.LicenseController.create -GET /license/v3/read/:identifier controllers.v3.LicenseController.read(identifier: String, fields:Option[String]) -PATCH /license/v3/update/:identifier controllers.v3.LicenseController.update(identifier: String) -DELETE /license/v3/retire/:identifier controllers.v3.LicenseController.retire(identifier: String) - diff --git a/learning-api/hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala b/learning-api/hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala deleted file mode 100644 index c7676e49c..000000000 --- a/learning-api/hierarchy-manager/src/main/scala/org/sunbird/managers/HierarchyManager.scala +++ /dev/null @@ -1,267 +0,0 @@ -package org.sunbird.managers - -import java.util -import java.util.concurrent.CompletionException - -import org.apache.commons.lang3.StringUtils -import org.sunbird.common.dto.{Request, Response, ResponseHandler} -import org.sunbird.common.exception.{ClientException, ErrorCodes, ResponseCode} -import org.sunbird.common.{JsonUtils, Platform} -import org.sunbird.graph.dac.model.Node -import org.sunbird.graph.external.ExternalPropsManager -import org.sunbird.graph.nodes.DataNode -import org.sunbird.utils.{NodeUtil, ScalaJsonUtils} - -import scala.collection.JavaConversions._ -import scala.collection.JavaConverters -import scala.concurrent.{ExecutionContext, Future} - -object HierarchyManager { - - val schemaName: String = "collection" - val imgSuffix: String = ".img" - val keyTobeRemoved = { - if(Platform.config.hasPath("content.hierarchy.removed_props_for_leafNodes")) - Platform.config.getStringList("content.hierarchy.removed_props_for_leafNodes") - else - java.util.Arrays.asList("collections","children","usedByContent","item_sets","methods","libraries","editorState") - } - - @throws[Exception] - def addLeafNodesToHierarchy(request:Request)(implicit ec: ExecutionContext): Future[Response] = { - validateRequest(request) - val rootNodeFuture = getRootNode(request) - rootNodeFuture.map(rootNode => { - val unitId = request.get("unitId").asInstanceOf[String] - val rootNodeMap = NodeUtil.serialize(rootNode, java.util.Arrays.asList("childNodes"), schemaName) - if(!rootNodeMap.get("childNodes").asInstanceOf[Array[String]].toList.contains(unitId)) { - Future{ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "unitId " + unitId + " does not exist")} - }else { - val hierarchyFuture = fetchHierarchy(request) - hierarchyFuture.map(hierarchy => { - if(hierarchy.isEmpty){ - Future{ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), "hierarchy is empty")} - } else { - val leafNodesFuture = fetchLeafNodes(request) - leafNodesFuture.map(leafNodes => { - val updateResponse = updateHierarchy(unitId, hierarchy, leafNodes, rootNode, request, "add") - updateResponse.map(response => { - if(!ResponseHandler.checkError(response)) { - updateRootNode(rootNode, request, "add").map(node => { - val resp: Response = ResponseHandler.OK - resp.put("rootId", rootNode.getIdentifier) - resp.put(unitId, request.get("children")) - resp - }) - } else { - Future { response } - } - }).flatMap(f => f) - }).flatMap(f => f) - } - }).flatMap(f => f) - } - }).flatMap(f => f) recoverWith {case e: CompletionException => throw e.getCause} - } - - @throws[Exception] - def removeLeafNodesFromHierarchy(request: Request)(implicit ec: ExecutionContext): Future[Response] = { - validateRequest(request) - val rootNodeFuture = getRootNode(request) - rootNodeFuture.map(rootNode => { - val unitId = request.get("unitId").asInstanceOf[String] - val rootNodeMap = NodeUtil.serialize(rootNode, java.util.Arrays.asList("childNodes"), schemaName) - if(!rootNodeMap.get("childNodes").asInstanceOf[Array[String]].toList.contains(unitId)) { - Future{ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "unitId " + unitId + " does not exist")} - }else { - val hierarchyFuture = fetchHierarchy(request) - hierarchyFuture.map(hierarchy => { - if(hierarchy.isEmpty){ - Future{ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), "hierarchy is empty")} - } else { - val updateResponse = updateHierarchy(unitId, hierarchy, null, rootNode, request, "remove") - updateResponse.map(response => { - if(!ResponseHandler.checkError(response)) { - updateRootNode(rootNode, request, "remove").map(node => { - val resp: Response = ResponseHandler.OK - resp.put("rootId", rootNode.getIdentifier) - resp - }) - } else { - Future { response } - } - }).flatMap(f => f) - } - }).flatMap(f => f) - } - }).flatMap(f => f) recoverWith {case e: CompletionException => throw e.getCause} - } - - - def validateRequest(request: Request)(implicit ec: ExecutionContext) = { - val rootId = request.get("rootId").asInstanceOf[String] - val unitId = request.get("unitId").asInstanceOf[String] - val children = request.get("children").asInstanceOf[java.util.List[String]] - - if(StringUtils.isBlank(rootId)){ - throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "rootId is mandatory") - } - if(StringUtils.isBlank(unitId)){ - throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "unitId is mandatory") - } - if(null == children || children.isEmpty){ - throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "children are mandatory") - } - } - - private def getRootNode(request: Request)(implicit ec: ExecutionContext): Future[Node] = { - val req = new Request(request) - req.put("identifier", request.get("rootId").asInstanceOf[String]) - req.put("mode", "edit") - DataNode.read(req) - } - - def fetchHierarchy(request: Request)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { - val req = new Request(request) - req.put("identifier", request.get("rootId").asInstanceOf[String] + imgSuffix) - val responseFuture = ExternalPropsManager.fetchProps(req, List("hierarchy")) - responseFuture.map(response => { - if(!ResponseHandler.checkError(response)) { - val hierarchyString = response.getResult.toMap.getOrElse("hierarchy", "").asInstanceOf[String] - if(!hierarchyString.isEmpty) - JsonUtils.deserialize(hierarchyString, classOf[java.util.Map[String, AnyRef]]).toMap - else - Map[String, AnyRef]() - } else { - Map[String, AnyRef]() - } - }) - } - - def fetchLeafNodes(request: Request)(implicit ec: ExecutionContext): Future[List[Node]] = { - val leafNodes = request.get("children").asInstanceOf[java.util.List[String]] - val req = new Request(request) - req.put("identifiers", leafNodes) - val nodes = DataNode.list(req).map(nodes => { - if(nodes.size() != leafNodes.size()) { - val filteredList = leafNodes.toList.filter(id => !nodes.contains(id)) - throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Children which are not available are: " + leafNodes) - } - else nodes.toList - }) - nodes - } - - def convertNodeToMap(leafNodes: List[Node]): java.util.List[java.util.Map[String, AnyRef]] = { - leafNodes.map(node => { - val nodeMap:java.util.Map[String,AnyRef] = NodeUtil.serialize(node, null, schemaName) - nodeMap.keySet().removeAll(keyTobeRemoved) - nodeMap - }) - } - - def addChildrenToUnit(children: java.util.List[java.util.Map[String,AnyRef]], unitId:String, leafNodes: java.util.List[java.util.Map[String, AnyRef]], leafNodeIds: java.util.List[String]): Unit = { - val childNodes = children.filter(child => ("Parent".equalsIgnoreCase(child.get("visibility").asInstanceOf[String]) && unitId.equalsIgnoreCase(child.get("identifier").asInstanceOf[String]))).toList - if(null != childNodes && !childNodes.isEmpty){ - val child = childNodes.get(0) - val childList = child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]] - val restructuredChildren: java.util.List[java.util.Map[String,AnyRef]] = restructureUnit(childList, leafNodes, leafNodeIds, (child.get("depth").asInstanceOf[Integer] + 1), unitId) - child.put("children", restructuredChildren) - } else { - for(child <- children) { - if(null !=child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) - addChildrenToUnit(child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]], unitId, leafNodes, leafNodeIds) - } - } - } - - def removeChildrenFromUnit(children: java.util.List[java.util.Map[String, AnyRef]], unitId: String, leafNodeIds: java.util.List[String]):Unit = { - val childNodes = children.filter(child => ("Parent".equalsIgnoreCase(child.get("visibility").asInstanceOf[String]) && unitId.equalsIgnoreCase(child.get("identifier").asInstanceOf[String]))).toList - if(null != childNodes && !childNodes.isEmpty){ - val child = childNodes.get(0) - if(null != child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) { - var filteredLeafNodes = child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].filter(existingLeafNode => { - !leafNodeIds.contains(existingLeafNode.get("identifier").asInstanceOf[String]) - }) - var index: Integer = 1 - filteredLeafNodes.toList.sortBy(x => x.get("index").asInstanceOf[Integer]).foreach(node => { - node.put("index", index) - index += 1 - }) - child.put("children", filteredLeafNodes) - } - } else { - for(child <- children) { - if(null !=child.get("children") && !child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]].isEmpty) - removeChildrenFromUnit(child.get("children").asInstanceOf[java.util.List[java.util.Map[String,AnyRef]]], unitId, leafNodeIds) - } - } - } - - def updateRootNode(rootNode: Node, request: Request, operation: String)(implicit ec: ExecutionContext) = { - val req = new Request(request) - val leafNodes = request.get("children").asInstanceOf[java.util.List[String]] - var childNodes = new java.util.ArrayList[String]() - childNodes.addAll(rootNode.getMetadata.get("childNodes").asInstanceOf[Array[String]].toList) - if(operation.equalsIgnoreCase("add")) - childNodes.addAll(leafNodes) - if(operation.equalsIgnoreCase("remove")) - childNodes.removeAll(leafNodes) - req.put("childNodes", childNodes.distinct.toArray) - req.getContext.put("identifier", rootNode.getIdentifier.replaceAll(imgSuffix, "")) - req.getContext.put("skipValidation", java.lang.Boolean.TRUE) - DataNode.update(req) - } - - def updateHierarchy(unitId: String, hierarchy: java.util.Map[String, AnyRef], leafNodes: List[Node], rootNode: Node, request: Request, operation: String)(implicit ec: ExecutionContext) = { - val children = hierarchy.get("children").asInstanceOf[java.util.List[java.util.Map[String, AnyRef]]] - val leafNodeIds = request.get("children").asInstanceOf[java.util.List[String]] - if("add".equalsIgnoreCase(operation)){ - val leafNodesMap:java.util.List[java.util.Map[String, AnyRef]] = convertNodeToMap(leafNodes) - addChildrenToUnit(children, unitId, leafNodesMap, leafNodeIds) - } - if("remove".equalsIgnoreCase(operation)) { - removeChildrenFromUnit(children,unitId, leafNodeIds) - } - val updatedHierarchy = new java.util.HashMap[String, AnyRef]() - updatedHierarchy.putAll(hierarchy) - updatedHierarchy.put("children", children) - val req = new Request(request) - req.put("hierarchy", ScalaJsonUtils.serialize(updatedHierarchy)) - req.put("identifier", rootNode.getIdentifier.replaceAll(imgSuffix, "") + imgSuffix) - ExternalPropsManager.saveProps(req) - } - - def restructureUnit(childList: java.util.List[java.util.Map[String, AnyRef]], leafNodes: java.util.List[java.util.Map[String, AnyRef]], leafNodeIds: java.util.List[String], depth: Integer, parent: String): java.util.List[java.util.Map[String, AnyRef]] = { - var maxIndex:Integer = 0 - var leafNodeMap: java.util.Map[String, java.util.Map[String, AnyRef]] = new util.HashMap[String, java.util.Map[String, AnyRef]]() - for(leafNode <- leafNodes){ - leafNodeMap.put(leafNode.get("identifier").asInstanceOf[String], JavaConverters.mapAsJavaMapConverter(leafNode).asJava) - } - var filteredLeafNodes: java.util.List[java.util.Map[String, AnyRef]] = new util.ArrayList[java.util.Map[String, AnyRef]]() - if(null != childList && !childList.isEmpty) { - val childMap:Map[String, java.util.Map[String, AnyRef]] = childList.toList.map(f => f.get("identifier").asInstanceOf[String] -> f).toMap - val existingLeafNodes = childMap.filter(p => leafNodeIds.contains(p._1)) - existingLeafNodes.map(en => { - leafNodeMap.get(en._1).put("index", en._2.get("index").asInstanceOf[Integer]) - }) - filteredLeafNodes = bufferAsJavaList(childList.filter(existingLeafNode => { - !leafNodeIds.contains(existingLeafNode.get("identifier").asInstanceOf[String]) - })) - maxIndex = childMap.values.toList.map(child => child.get("index").asInstanceOf[Integer]).toList.max.asInstanceOf[Integer] - } - leafNodeIds.foreach(id => { - var node = leafNodeMap.get(id) - node.put("parent", parent) - node.put("depth", depth) - if( null == node.get("index")) { - val index:Integer = maxIndex + 1 - node.put("index", index) - maxIndex += 1 - } - filteredLeafNodes.add(node) - }) - filteredLeafNodes - } - -} diff --git a/learning-api/hierarchy-manager/src/main/scala/org/sunbird/utils/NodeUtil.scala b/learning-api/hierarchy-manager/src/main/scala/org/sunbird/utils/NodeUtil.scala deleted file mode 100644 index 5a04a5bda..000000000 --- a/learning-api/hierarchy-manager/src/main/scala/org/sunbird/utils/NodeUtil.scala +++ /dev/null @@ -1,92 +0,0 @@ -package org.sunbird.utils - -import java.util -import java.util.Map - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.scala.DefaultScalaModule -import org.apache.commons.collections4.CollectionUtils -import org.sunbird.graph.dac.model.{Node, Relation} -import org.sunbird.graph.schema.DefinitionNode - -import scala.collection.JavaConverters._ - -object NodeUtil { - val mapper: ObjectMapper = new ObjectMapper() - mapper.registerModule(DefaultScalaModule) - - def serialize(node: Node, fields: util.List[String], schemaName: String): util.Map[String, AnyRef] = { - val metadataMap = node.getMetadata - metadataMap.put("identifier", node.getIdentifier) - if (CollectionUtils.isNotEmpty(fields)) - metadataMap.keySet.retainAll(fields) - val jsonProps = DefinitionNode.fetchJsonProps(node.getGraphId, "1.0", schemaName) - val updatedMetadataMap:util.Map[String, AnyRef] = metadataMap.entrySet().asScala.filter(entry => null != entry.getValue).map((entry: util.Map.Entry[String, AnyRef]) => handleKeyNames(entry, fields) -> convertJsonProperties(entry, jsonProps)).toMap.asJava - val definitionMap = DefinitionNode.getRelationDefinitionMap(node.getGraphId, "1.0", schemaName).asJava - val relMap:util.Map[String, util.List[util.Map[String, AnyRef]]] = getRelationMap(node, updatedMetadataMap, definitionMap) - var finalMetadata = new util.HashMap[String, AnyRef]() - finalMetadata.putAll(updatedMetadataMap) - finalMetadata.putAll(relMap) - finalMetadata - } - - def handleKeyNames(entry: Map.Entry[String, AnyRef], fields: util.List[String]) = { - if(CollectionUtils.isEmpty(fields)) { - entry.getKey.substring(0,1) + entry.getKey.substring(1) - } else { - entry.getKey - } - } - - def getRelationMap(node: Node, updatedMetadataMap: util.Map[String, AnyRef], relationMap: util.Map[String, AnyRef]):util.Map[String, util.List[util.Map[String, AnyRef]]] = { - val inRelations:util.List[Relation] = { if (CollectionUtils.isEmpty(node.getInRelations)) new util.ArrayList[Relation] else node.getInRelations } - val outRelations:util.List[Relation] = { if (CollectionUtils.isEmpty(node.getOutRelations)) new util.ArrayList[Relation] else node.getOutRelations } - val relMap = new util.HashMap[String, util.List[util.Map[String, AnyRef]]] - for (rel <- inRelations.asScala) { - if (relMap.containsKey(relationMap.get(rel.getRelationType + "_in_" + rel.getStartNodeObjectType))) relMap.get(relationMap.get(rel.getRelationType + "_in_" + rel.getStartNodeObjectType)).add(populateRelationMaps(rel, "in")) - else { - if(null != relationMap.get(rel.getRelationType + "_in_" + rel.getStartNodeObjectType)) { - relMap.put(relationMap.get(rel.getRelationType + "_in_" + rel.getStartNodeObjectType).asInstanceOf[String], new util.ArrayList[util.Map[String, AnyRef]]() {}) - } - } - } - for (rel <- outRelations.asScala) { - if (relMap.containsKey(relationMap.get(rel.getRelationType + "_out_" + rel.getEndNodeObjectType))) relMap.get(relationMap.get(rel.getRelationType + "_out_" + rel.getEndNodeObjectType)).add(populateRelationMaps(rel, "out")) - else { - if(null != relationMap.get(rel.getRelationType + "_in_" + rel.getStartNodeObjectType)) { - relMap.put(relationMap.get(rel.getRelationType + "_out_" + rel.getEndNodeObjectType).asInstanceOf[String], new util.ArrayList[util.Map[String, AnyRef]]() {}) - } - } - } - relMap - } - - def convertJsonProperties(entry: Map.Entry[String, AnyRef], jsonProps: scala.List[String]) = { - if(jsonProps.contains(entry.getKey)) { - try {mapper.readTree(entry.getValue.toString)} - catch { case e: Exception => entry.getValue } - } - else entry.getValue - } - - def populateRelationMaps(rel: Relation, direction: String): util.Map[String, AnyRef] = { - if("out".equalsIgnoreCase(direction)) - new util.HashMap[String, Object]() {{ - put("identifier", rel.getEndNodeId.replace(".img", "")) - put("name", rel.getEndNodeName) - put("objectType", rel.getEndNodeObjectType.replace("Image", "")) - put("relation", rel.getRelationType) - put("description", rel.getEndNodeMetadata.get("description")) - put("status", rel.getEndNodeMetadata.get("status")) - }} - else - new util.HashMap[String, Object]() {{ - put("identifier", rel.getStartNodeId.replace(".img", "")) - put("name", rel.getStartNodeName) - put("objectType", rel.getStartNodeObjectType.replace("Image", "")) - put("relation", rel.getRelationType) - put("description", rel.getStartNodeMetadata.get("description")) - put("status", rel.getStartNodeMetadata.get("status")) - }} - } -} \ No newline at end of file diff --git a/learning-api/hierarchy-manager/src/main/scala/org/sunbird/utils/ScalaJsonUtils.scala b/learning-api/hierarchy-manager/src/main/scala/org/sunbird/utils/ScalaJsonUtils.scala deleted file mode 100644 index 9f5b283ec..000000000 --- a/learning-api/hierarchy-manager/src/main/scala/org/sunbird/utils/ScalaJsonUtils.scala +++ /dev/null @@ -1,21 +0,0 @@ -package org.sunbird.utils - -import com.fasterxml.jackson.core.`type`.TypeReference -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.scala.DefaultScalaModule - -object ScalaJsonUtils { - @transient val mapper = new ObjectMapper() - mapper.registerModule(DefaultScalaModule) - - @throws(classOf[Exception]) - def serialize(obj: AnyRef): String = { - mapper.writeValueAsString(obj); - } - - @throws(classOf[Exception]) - def deserialize[T: Class](value: String): T = { - mapper.readValue(value, new TypeReference[T]{}) - } - -} diff --git a/learning-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestHierarchy.scala b/learning-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestHierarchy.scala deleted file mode 100644 index 1417edce0..000000000 --- a/learning-api/hierarchy-manager/src/test/scala/org/sunbird/managers/TestHierarchy.scala +++ /dev/null @@ -1,76 +0,0 @@ -package org.sunbird.managers - -import java.util - -import org.sunbird.common.dto.Request - -class TestHierarchy extends BaseSpec { - - private val script_1 = "CREATE KEYSPACE IF NOT EXISTS hierarchy_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" - private val script_2 = "CREATE TABLE IF NOT EXISTS hierarchy_store.content_hierarchy (identifier text, hierarchy text,PRIMARY KEY (identifier));" - private val script_3 = "INSERT INTO hierarchy_store.content_hierarchy(identifier, hierarchy) values ('do_11283193441064550414.img', '{\"identifier\":\"do_11283193441064550414\",\"children\":[{\"parent\":\"do_11283193441064550414\",\"identifier\":\"do_11283193463014195215\",\"copyright\":\"Sunbird\",\"lastStatusChangedOn\":\"2019-08-21T14:37:50.281+0000\",\"code\":\"2e837725-d663-45da-8ace-9577ab111982\",\"visibility\":\"Parent\",\"index\":1,\"mimeType\":\"application/vnd.ekstep.content-collection\",\"createdOn\":\"2019-08-21T14:37:50.281+0000\",\"versionKey\":\"1566398270281\",\"framework\":\"tpd\",\"depth\":1,\"children\":[],\"name\":\"U1\",\"lastUpdatedOn\":\"2019-08-21T14:37:50.281+0000\",\"contentType\":\"CourseUnit\",\"status\":\"Draft\"}]}');" - - override def beforeAll(): Unit = { - super.beforeAll() - graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"ORG_002\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",channel:\"01246944855007232011\",organisation:[\"ORG_002\"],showNotification:true,language:[\"English\"],mimeType:\"video/mp4\",variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_112831862871203840114/test-resource-cert_1566389714022_do_112831862871203840114_1.0_spine.ecar\\\",\\\"size\\\":35757.0}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_112831862871203840114/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"dev.sunbird.portal\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_112831862871203840114/small.mp4\",contentEncoding:\"identity\",lockKey:\"be6bc445-c75e-471d-b46f-71fefe4a1d2f\",contentType:\"Resource\",lastUpdatedBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",audience:[\"Learner\"],visibility:\"Default\",consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:1,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",lastPublishedOn:\"2019-08-21T12:15:13.652+0000\",size:416488,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Test Resource Cert\",status:\"Live\",code:\"7e6630c7-3818-4319-92ac-4d08c33904d8\",streamingUrl:\"https://sunbirddevmedia-inct.streaming.media.azure.net/25d7a94c-9be3-471c-926b-51eb5d3c4c2c/small.ism/manifest(format=m3u8-aapl-v3)\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T12:11:50.644+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T12:15:13.020+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-08-21T12:30:16.783+0000\",dialcodeRequired:\"No\",creator:\"Pradyumna\",lastStatusChangedOn:\"2019-08-21T12:15:14.384+0000\",createdFor:[\"01246944855007232011\"],os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566389713020\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_112831862871203840114/test-resource-cert_1566389713658_do_112831862871203840114_1.0.ecar\",framework:\"K-12\",createdBy:\"c4cc494f-04c3-49f3-b3d5-7b1a1984abad\",compatibilityLevel:1,IL_UNIQUE_ID:\"do_112831862871203840114\",resourceType:\"Learn\"},{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",certTemplate:\"[{\\\"name\\\":\\\"100PercentCompletionCertificate\\\",\\\"issuer\\\":{\\\"name\\\":\\\"Gujarat Council of Educational Research and Training\\\",\\\"url\\\":\\\"https://gcert.gujarat.gov.in/gcert/\\\",\\\"publicKey\\\":[\\\"1\\\",\\\"2\\\"]},\\\"signatoryList\\\":[{\\\"name\\\":\\\"CEO Gujarat\\\",\\\"id\\\":\\\"CEO\\\",\\\"designation\\\":\\\"CEO\\\",\\\"image\\\":\\\"https://cdn.pixabay.com/photo/2014/11/09/08/06/signature-523237__340.jpg\\\"}],\\\"htmlTemplate\\\":\\\"https://drive.google.com/uc?authuser=1&id=1ryB71i0Oqn2c3aqf9N6Lwvet-MZKytoM&export=download\\\",\\\"notifyTemplate\\\":{\\\"subject\\\":\\\"Course completion certificate\\\",\\\"stateImgUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/orgemailtemplate/img/File-0128212938260643843.png\\\",\\\"regardsperson\\\":\\\"Chairperson\\\",\\\"regards\\\":\\\"Minister of Gujarat\\\",\\\"emailTemplateType\\\":\\\"defaultCertTemp\\\"}}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398314186_do_11283193441064550414_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550414/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Learner\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550414/artifact/do_11283193441064550414_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"Creative Commons Attribution (CC BY)\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Content\",name:\"test prad course cert\",status:\"Live\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11283193441064550414\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\"}] as row CREATE (n:domain) SET n += row") - executeCassandraQuery(script_1, script_2, script_3) - } - - - "addLeafNodesToHierarchy" should "addLeafNodesToHierarchy" in { - val request = new Request() - request.setContext(new util.HashMap[String, AnyRef]() { - { - put("objectType", "Content") - put("graph_id", "domain") - put("version", "1.0") - put("schemaName", "collection") - put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") - } - }) - - request.put("rootId", "do_11283193441064550414") - request.put("unitId", "do_11283193463014195215") - request.put("children", util.Arrays.asList("do_112831862871203840114")) - val future = HierarchyManager.addLeafNodesToHierarchy(request) - future.map(response => { - assert(response.getResponseCode.code() == 200) - assert(response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].containsAll(request.get("children").asInstanceOf[util.List[String]])) - assert(!response.getResult.get("do_11283193463014195215").asInstanceOf[util.List[String]].contains("do_11283193463014195215")) - val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") - .one().getString("hierarchy") - assert(hierarchy.contains("do_112831862871203840114")) - }) - } - - "removeLeafNodesToHierarchy" should "removeLeafNodesToHierarchy" in { - val request = new Request() - request.setContext(new util.HashMap[String, AnyRef]() { - { - put("objectType", "Content") - put("graph_id", "domain") - put("version", "1.0") - put("schemaName", "collection") - put("channel", "b00bc992ef25f1a9a8d63291e20efc8d") - } - }) - - request.put("rootId", "do_11283193441064550414") - request.put("unitId", "do_11283193463014195215") - request.put("children", util.Arrays.asList("do_112831862871203840114")) - val future = HierarchyManager.addLeafNodesToHierarchy(request) - future.map(response => { - assert(response.getResponseCode.code() == 200) - val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") - .one().getString("hierarchy") - assert(hierarchy.contains("do_112831862871203840114")) - val removeFuture = HierarchyManager.removeLeafNodesFromHierarchy(request) - removeFuture.map(resp => { - assert(resp.getResponseCode.code() == 200) - val hierarchy = readFromCassandra("Select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") - .one().getString("hierarchy") - assert(!hierarchy.contains("do_112831862871203840114")) - }) - }).flatMap(f => f) - } -} diff --git a/learning-api/orchestrator/pom.xml b/learning-api/orchestrator/pom.xml deleted file mode 100644 index 9b6ce2214..000000000 --- a/learning-api/orchestrator/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - learning-api - org.sunbird - 1.0-SNAPSHOT - - 4.0.0 - - orchestrator - - - - org.sunbird - actor-core - 1.0-SNAPSHOT - - - org.sunbird - hierarchy-manager - 1.0-SNAPSHOT - - - org.sunbird - graph-engine_2.11 - 1.0-SNAPSHOT - jar - - - - \ No newline at end of file diff --git a/learning-api/orchestrator/src/main/java/org/sunbird/actors/ContentActor.java b/learning-api/orchestrator/src/main/java/org/sunbird/actors/ContentActor.java deleted file mode 100644 index 35c0c4c5a..000000000 --- a/learning-api/orchestrator/src/main/java/org/sunbird/actors/ContentActor.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.sunbird.actors; - -import akka.dispatch.Mapper; -import org.apache.commons.lang3.StringUtils; -import org.sunbird.actor.core.BaseActor; -import org.sunbird.cache.util.RedisCacheUtil; -import org.sunbird.common.ContentParams; -import org.sunbird.common.dto.Request; -import org.sunbird.common.dto.Response; -import org.sunbird.common.dto.ResponseHandler; -import org.sunbird.common.exception.ClientException; -import org.sunbird.common.exception.ResponseCode; -import org.sunbird.graph.dac.model.Node; -import org.sunbird.graph.nodes.DataNode; -import org.sunbird.utils.NodeUtils; -import org.sunbird.utils.RequestUtils; -import scala.concurrent.Future; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class ContentActor extends BaseActor { - - public Future onReceive(Request request) throws Throwable { - String operation = request.getOperation(); - switch(operation) { - case "createContent": return create(request); - case "readContent": return read(request); - case "updateContent": return update(request); - default: return ERROR(operation); - } - } - - private Future create(Request request) throws Exception { - populateDefaultersForCreation(request); - RequestUtils.restrictProperties(request); - return DataNode.create(request, getContext().dispatcher()) - .map(new Mapper() { - @Override - public Response apply(Node node) { - Response response = ResponseHandler.OK(); - response.put("node_id", node.getIdentifier()); - response.put("identifier", node.getIdentifier()); - response.put("versionKey", node.getMetadata().get("versionKey")); - return response; - } - }, getContext().dispatcher()); - } - - private Future update(Request request) throws Exception { - populateDefaultersForUpdation(request); - if(StringUtils.isBlank((String)request.get("versionKey"))) - throw new ClientException("ERR_INVALID_REQUEST", "Please Provide Version Key!"); - RequestUtils.restrictProperties(request); - return DataNode.update(request, getContext().dispatcher()) - .map(new Mapper() { - @Override - public Response apply(Node node) { - Response response = ResponseHandler.OK(); - String identifier = node.getIdentifier().replace(".img",""); - response.put("node_id", identifier); - response.put("identifier", identifier); - response.put("versionKey", node.getMetadata().get("versionKey")); - return response; - } - }, getContext().dispatcher()); - } - - private Future read(Request request) throws Exception { - List fields = Arrays.stream(((String) request.get("fields")).split(",")) - .filter(field -> StringUtils.isNotBlank(field) && !StringUtils.equalsIgnoreCase(field, "null")).collect(Collectors.toList()); - request.getRequest().put("fields", fields); - return DataNode.read(request, getContext().dispatcher()) - .map(new Mapper() { - @Override - public Response apply(Node node) { - Map metadata = NodeUtils.serialize(node, fields, (String) request.getContext().get("schemaName")); - Response response = ResponseHandler.OK(); - response.put("content", metadata); - return response; - } - }, getContext().dispatcher()); - } - - private static void populateDefaultersForCreation(Request request) { - setDefaultsBasedOnMimeType(request, ContentParams.create.name()); - setDefaultLicense(request); - } - - private static void populateDefaultersForUpdation(Request request){ - if(request.getRequest().containsKey(ContentParams.body.name())) - request.put(ContentParams.artifactUrl.name(), null); - } - - private static void setDefaultLicense(Request request) { - if(StringUtils.isEmpty((String)request.getRequest().get("license"))){ - String defaultLicense = RedisCacheUtil.getString("channel_" + (String)request.getRequest().get("channel") + "_license"); - if(StringUtils.isNotEmpty(defaultLicense)) - request.getRequest().put("license", defaultLicense); - else - System.out.println("Default License is not available for channel: " + (String)request.getRequest().get("channel")); - } - } - - private static void setDefaultsBasedOnMimeType(Request request, String operation) { - - String mimeType = (String) request.get(ContentParams.mimeType.name()); - if (StringUtils.isNotBlank(mimeType) && operation.equalsIgnoreCase(ContentParams.create.name())) { - if (StringUtils.equalsIgnoreCase("application/vnd.ekstep.plugin-archive", mimeType)) { - String code = (String) request.get(ContentParams.code.name()); - if (null == code || StringUtils.isBlank(code)) - throw new ClientException("ERR_PLUGIN_CODE_REQUIRED", "Unique code is mandatory for plugins"); - request.put(ContentParams.identifier.name(), request.get(ContentParams.code.name())); - } else { - request.put(ContentParams.osId.name(), "org.ekstep.quiz.app"); - } - - if (mimeType.endsWith("archive") || mimeType.endsWith("vnd.ekstep.content-collection") - || mimeType.endsWith("epub")) - request.put(ContentParams.contentEncoding.name(), ContentParams.gzip.name()); - else - request.put(ContentParams.contentEncoding.name(), ContentParams.identity.name()); - - if (mimeType.endsWith("youtube") || mimeType.endsWith("x-url")) - request.put(ContentParams.contentDisposition.name(), ContentParams.online.name()); - else - request.put(ContentParams.contentDisposition.name(), ContentParams.inline.name()); - } - } - -} diff --git a/learning-api/orchestrator/src/main/java/org/sunbird/actors/HealthActor.java b/learning-api/orchestrator/src/main/java/org/sunbird/actors/HealthActor.java deleted file mode 100644 index e268fcbbf..000000000 --- a/learning-api/orchestrator/src/main/java/org/sunbird/actors/HealthActor.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.sunbird.actors; - -import akka.dispatch.Futures; -import org.sunbird.actor.core.BaseActor; -import org.sunbird.common.dto.Request; -import org.sunbird.common.dto.Response; -import org.sunbird.common.dto.ResponseHandler; -import scala.concurrent.Future; - -public class HealthActor extends BaseActor { - - @Override - public Future onReceive(Request request) throws Throwable { - Response result = ResponseHandler.OK(); - result.put("healthy", true); - return Futures.successful(result); - } -} diff --git a/learning-api/orchestrator/src/main/java/org/sunbird/actors/LicenseActor.java b/learning-api/orchestrator/src/main/java/org/sunbird/actors/LicenseActor.java deleted file mode 100644 index 05a3e4b80..000000000 --- a/learning-api/orchestrator/src/main/java/org/sunbird/actors/LicenseActor.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.sunbird.actors; - -import akka.dispatch.Mapper; -import org.apache.commons.lang3.StringUtils; -import org.sunbird.actor.core.BaseActor; - -import org.sunbird.common.Slug; - -import org.sunbird.common.dto.Request; -import org.sunbird.common.dto.Response; -import org.sunbird.common.dto.ResponseHandler; -import org.sunbird.common.exception.ClientException; -import org.sunbird.common.exception.ResponseCode; -import org.sunbird.graph.dac.model.Node; -import org.sunbird.graph.nodes.DataNode; -import org.sunbird.utils.NodeUtils; -import scala.concurrent.Future; -import org.sunbird.utils.LicenseOperations; -import org.sunbird.utils.RequestUtils; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class LicenseActor extends BaseActor { - - public Future onReceive(Request request) throws Throwable { - String operation = request.getOperation(); - if (LicenseOperations.createLicense.name().equals(operation)) { - return create(request); - } else if (LicenseOperations.readLicense.name().equals(operation)) { - return read(request); - } else if (LicenseOperations.updateLicense.name().equals(operation)) { - return update(request); - } else if (LicenseOperations.retireLicense.name().equals(operation)) { - return retire(request); - } else { - return ERROR(operation); - - } - } - - private Future create(Request request) throws Exception { - RequestUtils.restrictProperties(request); - if (request.getRequest().containsKey("identifier")) { - throw new ClientException("ERR_NAME_SET_AS_IDENTIFIER", "name will be set as identifier"); - } - if (request.getRequest().containsKey("name")) { - request.getRequest().put("identifier", Slug.makeSlug((String) request.getRequest().get("name"))); - } - return DataNode.create(request, getContext().dispatcher()) - .map(new Mapper() { - @Override - public Response apply(Node node) { - Response response = ResponseHandler.OK(); - response.put("node_id", node.getIdentifier()); - return response; - } - }, getContext().dispatcher()); - } - - private Future read(Request request) throws Exception { - List fields = Arrays.stream(((String) request.get("fields")).split(",")) - .filter(field -> StringUtils.isNotBlank(field) && !StringUtils.equalsIgnoreCase(field, "null")).collect(Collectors.toList()); - request.getRequest().put("fields", fields); - return DataNode.read(request, getContext().dispatcher()) - .map(new Mapper() { - @Override - public Response apply(Node node) { - if(NodeUtils.isRetired(node)) - return ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "License not found with identifier: " + node.getIdentifier()); - Map metadata = NodeUtils.serialize(node, fields, (String) request.getContext().get("schemaName")); - Response response = ResponseHandler.OK(); - response.put("license", metadata); - return response; - } - }, getContext().dispatcher()); - } - - private Future update(Request request) throws Exception { - RequestUtils.restrictProperties(request); - request.getRequest().put("status", "Live"); - return DataNode.update(request, getContext().dispatcher()) - .map(new Mapper() { - @Override - public Response apply(Node node) { - Response response = ResponseHandler.OK(); - response.put("node_id", node.getIdentifier()); - return response; - } - }, getContext().dispatcher()); - } - private Future retire(Request request) throws Exception { - request.getRequest().put("status", "Retired"); - return DataNode.update(request, getContext().dispatcher()) - .map(new Mapper() { - @Override - public Response apply(Node node) { - Response response = ResponseHandler.OK(); - response.put("node_id", node.getIdentifier()); - return response; - } - }, getContext().dispatcher()); - } -} diff --git a/learning-api/orchestrator/src/main/java/org/sunbird/utils/LicenseOperations.java b/learning-api/orchestrator/src/main/java/org/sunbird/utils/LicenseOperations.java deleted file mode 100644 index c6f792b32..000000000 --- a/learning-api/orchestrator/src/main/java/org/sunbird/utils/LicenseOperations.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.sunbird.utils; - -public enum LicenseOperations { - createLicense, readLicense, updateLicense, retireLicense -} diff --git a/learning-api/orchestrator/src/main/java/org/sunbird/utils/NodeUtils.java b/learning-api/orchestrator/src/main/java/org/sunbird/utils/NodeUtils.java deleted file mode 100644 index f493bc032..000000000 --- a/learning-api/orchestrator/src/main/java/org/sunbird/utils/NodeUtils.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.sunbird.utils; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.sunbird.common.Platform; -import org.sunbird.graph.dac.model.Node; -import org.sunbird.graph.dac.model.Relation; -import org.sunbird.graph.schema.DefinitionNode; -import scala.collection.JavaConversions; - -import java.util.*; -import java.util.stream.Collectors; - -public class NodeUtils { - private static final ObjectMapper mapper = new ObjectMapper(); - - /** - * This method will convert a Node to map - * @param node - * @param fields - * @return - */ - public static Map serialize(Node node, List fields, String schemaName) { - Map metadataMap = new HashMap<>(); - metadataMap.putAll(node.getMetadata()); - metadataMap.put("languageCode",getLanguageCodes(node)); - if (CollectionUtils.isNotEmpty(fields)) - filterOutFields(metadataMap, fields); - metadataMap.put("identifier", node.getIdentifier().replace(".img","")); - List jsonProps = JavaConversions.seqAsJavaList(DefinitionNode.fetchJsonProps(node.getGraphId(), "1.0", schemaName)); - Map updatedMetadataMap = metadataMap.entrySet().stream().filter(entry -> null != entry.getValue()).collect(Collectors.toMap(entry -> handleKeyNames(entry, fields), entry -> convertJsonProperties(entry, jsonProps))); - Map definitionMap = JavaConversions.mapAsJavaMap(DefinitionNode.getRelationDefinitionMap(node.getGraphId(), "1.0", schemaName)); - if (CollectionUtils.isEmpty(fields) || definitionMap.keySet().stream().anyMatch(key -> fields.contains(key))) { - getRelationMap(node, updatedMetadataMap, definitionMap); - } - return updatedMetadataMap; - } - - private static List getLanguageCodes(Node node) { - List languages = new ArrayList<>(); - Object language = node.getMetadata().get("language"); - if (language instanceof String[] ) - languages.addAll(Arrays.asList( (String[]) language)); - else if(language instanceof List) - languages.addAll((List) language); - return languages.stream().map(lang -> Platform.config.hasPath("languageCode." + lang.toLowerCase()) ? Platform.config.getString("languageCode." + lang.toLowerCase()) : "").collect(Collectors.toList()); - } - - private static void filterOutFields(Map inputMetadata, List fields) { - inputMetadata.keySet().retainAll(fields); - } - - private static Object convertJsonProperties(Map.Entry entry, List jsonProps) { - if (jsonProps.contains(entry.getKey())) - try { - return mapper.readTree(entry.getValue().toString()); - } catch (Exception e) { - return entry.getValue(); - } - else - return entry.getValue(); - } - - private static String handleKeyNames(Map.Entry entry, List fields) { - if (CollectionUtils.isEmpty(fields)) - return entry.getKey().substring(0, 1) + entry.getKey().substring(1); - else - return entry.getKey(); - } - - private static void getRelationMap(Node node, Map metadata, Map relationMap) { - List inRelations = CollectionUtils.isEmpty(node.getInRelations()) ? new ArrayList<>(): node.getInRelations(); - List outRelations = CollectionUtils.isEmpty(node.getOutRelations()) ? new ArrayList<>(): node.getOutRelations(); - Map>> relMap = new HashMap<>(); - for (Relation rel : inRelations) { - if (relMap.containsKey(relationMap.get(rel.getRelationType() + "_in_" + rel.getStartNodeObjectType()))) { - relMap.get(relationMap.get(rel.getRelationType() + "_in_" + rel.getStartNodeObjectType())).add(populateRelationMaps(rel, "in")); - } else { - String relKey = (String) relationMap.get(rel.getRelationType() + "_in_" + rel.getStartNodeObjectType()); - if (StringUtils.isNotBlank(relKey)) { - relMap.put(relKey, - new ArrayList>() {{ - add(populateRelationMaps(rel, "in")); - }}); - } - } - - } - - for (Relation rel : outRelations) { - if (relMap.containsKey(relationMap.get(rel.getRelationType() + "_out_" + rel.getEndNodeObjectType()))) { - relMap.get(relationMap.get(rel.getRelationType() + "_out_" + rel.getEndNodeObjectType())).add(populateRelationMaps(rel, "out")); - } else { - String relKey = (String) relationMap.get(rel.getRelationType() + "_out_" + rel.getEndNodeObjectType()); - if (StringUtils.isNotBlank(relKey)) { - relMap.put(relKey, - new ArrayList>() {{ - add(populateRelationMaps(rel, "out")); - }}); - } - } - - } - metadata.putAll(relMap); - } - - private static Map populateRelationMaps(Relation relation, String direction) { - if (StringUtils.equalsAnyIgnoreCase("out", direction)) { - return new HashMap() {{ - put("identifier", relation.getEndNodeId().replace(".img", "")); - put("name", relation.getEndNodeName()); - put("objectType", relation.getEndNodeObjectType().replace("Image", "")); - put("relation", relation.getRelationType()); - put("description", relation.getEndNodeMetadata().get("description")); - put("status", relation.getEndNodeMetadata().get("status")); - }}; - } else { - return new HashMap() {{ - put("identifier", relation.getStartNodeId().replace(".img", "")); - put("name", relation.getStartNodeName()); - put("objectType", relation.getStartNodeObjectType().replace("Image", "")); - put("relation", relation.getRelationType()); - put("description", relation.getStartNodeMetadata().get("description")); - put("status", relation.getStartNodeMetadata().get("status")); - }}; - } - } - - public static Boolean isRetired(Node node) { - return StringUtils.equalsIgnoreCase((String) node.getMetadata().get("status"), "Retired"); - } -} \ No newline at end of file diff --git a/learning-api/orchestrator/src/main/java/org/sunbird/utils/RequestUtils.java b/learning-api/orchestrator/src/main/java/org/sunbird/utils/RequestUtils.java deleted file mode 100644 index 4b67c8b1d..000000000 --- a/learning-api/orchestrator/src/main/java/org/sunbird/utils/RequestUtils.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.sunbird.utils; - -import org.sunbird.common.dto.Request; -import org.sunbird.common.exception.ClientException; -import org.sunbird.graph.schema.DefinitionNode; -import scala.collection.JavaConversions; - -import java.util.List; - -public class RequestUtils { - - /** - * Method to restrict invalid properties sent in the request - * @param request - */ - public static void restrictProperties(Request request) { - String graphId = (String) request.getContext().get("graph_id"); - String version = (String) request.getContext().get("version"); - String objectType = (String) request.getContext().get("objectType"); - String schemaName = (String) request.getContext().get("schemaName"); - String operation = request.getOperation().toLowerCase().replace(objectType.toLowerCase(), ""); - List restrictedProps = JavaConversions.seqAsJavaList(DefinitionNode.getRestrictedProperties(graphId, version, operation, schemaName)); - if (restrictedProps.stream().anyMatch(prop -> request.getRequest().containsKey(prop))) - throw new ClientException("ERROR_RESTRICTED_PROP", "Properties in list " + restrictedProps + " are not allowed in request"); - } - -} diff --git a/ontology-engine/graph-common/pom.xml b/ontology-engine/graph-common/pom.xml index e932de967..e18eb0beb 100644 --- a/ontology-engine/graph-common/pom.xml +++ b/ontology-engine/graph-common/pom.xml @@ -18,5 +18,42 @@ platform-common 1.0-SNAPSHOT + + org.powermock + powermock-api-mockito + 1.7.4 + test + + + org.powermock + powermock-module-junit4 + 1.7.4 + test + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + \ No newline at end of file diff --git a/ontology-engine/graph-common/src/main/java/org/sunbird/graph/common/enums/GraphDACParams.java b/ontology-engine/graph-common/src/main/java/org/sunbird/graph/common/enums/GraphDACParams.java index e0bedc6e9..230c04005 100644 --- a/ontology-engine/graph-common/src/main/java/org/sunbird/graph/common/enums/GraphDACParams.java +++ b/ontology-engine/graph-common/src/main/java/org/sunbird/graph/common/enums/GraphDACParams.java @@ -8,5 +8,5 @@ public enum GraphDACParams { MERGE, nodes, RETURN, keys, rootNode, nodeId, WHERE, startNodeId, endNodeId, relationType, startNodeIds, endNodeIds, collectionId, collection, indexProperty, taskId, input, getTags, searchCriteria, paramMap, cypherQuery, paramValueMap, queryStatementMap, SYS_INTERNAL_LAST_UPDATED_ON, - CONSUMER_ID, consumerId, CHANNEL_ID, channel, APP_ID, appId; + CONSUMER_ID, consumerId, CHANNEL_ID, channel, APP_ID, appId, Nodes_Count, Relations_Count; } diff --git a/ontology-engine/graph-common/src/test/java/org/sunbird/graph/common/IdentifierTest.java b/ontology-engine/graph-common/src/test/java/org/sunbird/graph/common/IdentifierTest.java new file mode 100644 index 000000000..4d8906a2c --- /dev/null +++ b/ontology-engine/graph-common/src/test/java/org/sunbird/graph/common/IdentifierTest.java @@ -0,0 +1,32 @@ +package org.sunbird.graph.common; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class IdentifierTest { + @BeforeClass + public static void init() { + + } + + @Test + public void getUniqueIdFromNeo4jId() throws Exception { + String id = Identifier.getUniqueIdFromNeo4jId(System.currentTimeMillis()); + Assert.assertTrue(StringUtils.endsWith(id, "1")); + } + + @Test + public void getUniqueIdFromTimestamp() throws Exception { + String id = Identifier.getUniqueIdFromTimestamp(); + Assert.assertTrue(StringUtils.startsWith(id, "1")); + } + + @Test + public void getIdentifier() throws Exception { + String id = Identifier.getIdentifier("domain", "1234"); + Assert.assertTrue(StringUtils.equals(id, "do_1234")); + } + +} diff --git a/learning-api/content-service/conf/application.conf b/ontology-engine/graph-common/src/test/resources/application.conf similarity index 89% rename from learning-api/content-service/conf/application.conf rename to ontology-engine/graph-common/src/test/resources/application.conf index 73585e8fb..dfa892deb 100644 --- a/learning-api/content-service/conf/application.conf +++ b/ontology-engine/graph-common/src/test/resources/application.conf @@ -146,9 +146,6 @@ play.http { } } -play.http.parser.maxDiskBuffer = 10MB -parsers.anyContent.maxLength = 10MB - ## Netty Provider # https://www.playframework.com/documentation/latest/SettingsNetty # ~~~~~ @@ -217,7 +214,7 @@ play.filters { # Enabled filters are run automatically against Play. # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. - enabled = [filters.AccessLogFilter] + enabled = [] # Disabled filters remove elements from the enabled list. # disabled += filters.CSRFFilter @@ -274,39 +271,17 @@ play.filters { } } -play.http.parser.maxMemoryBuffer = 50MB -akka.http.parsing.max-content-length = 50MB - -schema.base_path = "../../schemas/" -content.hierarchy.removed_props_for_leafNodes=["collections","children","usedByContent","item_sets","methods","libraries","editorState"] - -languageCode { - assamese : "as" - bengali : "bn" - english : "en" - gujarati : "gu" - hindi : "hi" - kannada : "ka" - marathi : "mr" - odia : "or" - tamil : "ta" - telugu : "te" -} - -collection.keyspace = "hierarchy_store" -content.keyspace = "content_store" - # Learning-Service Configuration content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit"] # Cassandra Configuration -//content.keyspace.name=content_store -//content.keyspace.table=content_data +content.keyspace.name=content_store +content.keyspace.table=content_data #TODO: Add Configuration for assessment. e.g: question_data orchestrator.keyspace.name=script_store orchestrator.keyspace.table=script_data -cassandra.lp.connection="127.0.0.1:9042" -cassandra.lpa.connection="127.0.0.1:9042" +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" # Redis Configuration redis.host=localhost @@ -432,21 +407,6 @@ telemetry.search.topn=5 installation.id=ekstep -learning.content.copy.invalid_status_list=["Flagged","FlaggedDraft","FraggedReview","Retired", "Processing"] -learning.content.copy.props_to_remove=["downloadUrl", "artifactUrl", "variants", - "createdOn", "collections", "children", "lastUpdatedOn", "SYS_INTERNAL_LAST_UPDATED_ON", - "versionKey", "s3Key", "status", "pkgVersion", "toc_url", "mimeTypesCount", - "contentTypesCount", "leafNodesCount", "childNodes", "prevState", "lastPublishedOn", - "flagReasons", "compatibilityLevel", "size", "publishChecklist", "publishComment", - "LastPublishedBy", "rejectReasons", "rejectComment", "gradeLevel", "subject", - "medium", "board", "topic", "purpose", "subtopic", "contentCredits", - "owner", "collaborators", "creators", "contributors", "badgeAssertions", "dialcodes", - "concepts", "keywords", "reservedDialcodes", "dialcodeRequired", "leafNodes"] - -# Metadata to be added to copied content from origin -learning.content.copy.origin_data=["name", "author", "license", "organisation"] - -learning.content.type.not.copied.list=["Asset"] channel.default="in.ekstep" @@ -456,7 +416,7 @@ dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/sear dialcode.api.authorization=auth_key # Language-Code Configuration -language.graph_ids=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] # Kafka send event to topic enable kafka.topic.send.enable=false @@ -485,8 +445,7 @@ publish.collection.fullecar.disable=true cassandra.lp.consistency.level=QUORUM -content.tagging.backward_enable=false -content.tagging.property="subject,medium" + content.nested.fields="badgeAssertions,targets,badgeAssociations" @@ -501,4 +460,30 @@ framework.cache.read=true # Max size(width/height) of thumbnail in pixels -max.thumbnail.size.pixels=150 \ No newline at end of file +max.thumbnail.size.pixels=150 + +play.http.parser.maxMemoryBuffer = 50MB +akka.http.parsing.max-content-length = 50MB +schema.base_path = "../../schemas/" +//schema.base_path = "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/" + +collection.image.migration.enabled=true +collection.keyspace = "hierarchy_store" +content.keyspace = "content_store" + +languageCode { + assamese : "as" + bengali : "bn" + english : "en" + gujarati : "gu" + hindi : "hi" + kannada : "ka" + marathi : "mr" + odia : "or" + tamil : "ta" + telugu : "te" +} + +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd"] + + diff --git a/ontology-engine/graph-core/.gitignore b/ontology-engine/graph-core/.gitignore deleted file mode 100644 index 4dc009173..000000000 --- a/ontology-engine/graph-core/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -/bin diff --git a/ontology-engine/graph-core/pom.xml b/ontology-engine/graph-core/pom.xml deleted file mode 100644 index 9d5eb3e8e..000000000 --- a/ontology-engine/graph-core/pom.xml +++ /dev/null @@ -1,23 +0,0 @@ - - 4.0.0 - - ontology-engine - org.sunbird - 1.0-SNAPSHOT - - graph-core - - - org.sunbird - graph-dac-api - 1.0-SNAPSHOT - jar - - - org.sunbird - schema-validator - 1.0-SNAPSHOT - - - \ No newline at end of file diff --git a/ontology-engine/graph-core/src/main/java/org/sunbird/graph/exception/GraphEngineErrorCodes.java b/ontology-engine/graph-core/src/main/java/org/sunbird/graph/exception/GraphEngineErrorCodes.java deleted file mode 100644 index abb89391f..000000000 --- a/ontology-engine/graph-core/src/main/java/org/sunbird/graph/exception/GraphEngineErrorCodes.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.graph.exception; - -public enum GraphEngineErrorCodes { - - ERR_INVALID_NODE, - - ERR_GRAPH_PROCESSING_ERROR; -} diff --git a/ontology-engine/graph-core/src/main/java/org/sunbird/graph/exception/GraphRelationErrorCodes.java b/ontology-engine/graph-core/src/main/java/org/sunbird/graph/exception/GraphRelationErrorCodes.java deleted file mode 100644 index ca1ee96a9..000000000 --- a/ontology-engine/graph-core/src/main/java/org/sunbird/graph/exception/GraphRelationErrorCodes.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.graph.exception; - -public enum GraphRelationErrorCodes { - - ERR_RELATION_CREATE, - - ERR_RELATION_VALIDATE; -} diff --git a/ontology-engine/graph-core/src/main/java/org/sunbird/graph/validator/NodeValidator.java b/ontology-engine/graph-core/src/main/java/org/sunbird/graph/validator/NodeValidator.java deleted file mode 100644 index 0f9201497..000000000 --- a/ontology-engine/graph-core/src/main/java/org/sunbird/graph/validator/NodeValidator.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.sunbird.graph.validator; - -import org.sunbird.common.exception.ResourceNotFoundException; -import org.sunbird.common.exception.ServerException; -import org.sunbird.graph.common.enums.SystemProperties; -import org.sunbird.graph.dac.model.Filter; -import org.sunbird.graph.dac.model.MetadataCriterion; -import org.sunbird.graph.dac.model.Node; -import org.sunbird.graph.dac.model.SearchConditions; -import org.sunbird.graph.dac.model.SearchCriteria; -import org.sunbird.graph.exception.GraphEngineErrorCodes; -import org.sunbird.graph.service.operation.SearchAsyncOperations; -import scala.compat.java8.FutureConverters; -import scala.concurrent.Future; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; - -/** - * This class provides utility methods for node validation - * @author Kumar Gauraw - */ -public class NodeValidator { - - /** - * This method validates whether given identifiers exist in the given graph or not. - * @param graphId - * @param identifiers - * @return List - */ - public static Future> validate(String graphId, List identifiers) { - List> result; - Future> nodes = getDataNodes(graphId, identifiers); - CompletionStage> cs = FutureConverters.toJava(nodes).thenApply(dataNodes -> { - Map relationNodes = new HashMap<>(); - if (dataNodes.size() != identifiers.size()) { - List invalidIds = identifiers.stream().filter(id -> dataNodes.stream().noneMatch(node -> node.getIdentifier().equals(id))) - .collect(Collectors.toList()); - throw new ResourceNotFoundException(GraphEngineErrorCodes.ERR_INVALID_NODE.name(), "Node Not Found With Identifier " + invalidIds); - } else { - relationNodes = dataNodes.stream().collect(Collectors.toMap(node -> node.getIdentifier(), node -> node)); - return relationNodes; - } - }); - - return FutureConverters.toScala(cs); - } - - /** - * This method fetch and return list of Node object for given graph & identifiers - * @param graphId - * @param identifiers - * @return List - */ - private static Future> getDataNodes(String graphId, List identifiers) { - SearchCriteria searchCriteria = new SearchCriteria(); - MetadataCriterion mc = null; - if (identifiers.size() == 1) { - mc = MetadataCriterion - .create(Arrays.asList(new Filter(SystemProperties.IL_UNIQUE_ID.name(), SearchConditions.OP_EQUAL, identifiers.get(0)))); - } else { - mc = MetadataCriterion.create(Arrays.asList(new Filter(SystemProperties.IL_UNIQUE_ID.name(), SearchConditions.OP_IN, identifiers), - new Filter("status", SearchConditions.OP_NOT_EQUAL, "Retired"))); - } - searchCriteria.addMetadata(mc); - searchCriteria.setCountQuery(false); - try { - Future> nodes = SearchAsyncOperations.getNodeByUniqueIds(graphId, searchCriteria); - return nodes; - } catch (Exception e) { - throw new ServerException(GraphEngineErrorCodes.ERR_GRAPH_PROCESSING_ERROR.name(), "Unable To Fetch Nodes From Graph. Exception is: " + e.getMessage()); - - } - } -} diff --git a/ontology-engine/graph-core_2.11/pom.xml b/ontology-engine/graph-core_2.11/pom.xml new file mode 100644 index 000000000..9a0f5f9b1 --- /dev/null +++ b/ontology-engine/graph-core_2.11/pom.xml @@ -0,0 +1,146 @@ + + + 4.0.0 + + ontology-engine + org.sunbird + 1.0-SNAPSHOT + + org.sunbird + graph-core_2.11 + 1.0-SNAPSHOT + + + + org.scala-lang + scala-library + ${scala.version} + + + org.sunbird + graph-dac-api + + + org.apache.commons + commons-lang3 + + + 1.0-SNAPSHOT + jar + + + org.sunbird + schema-validator + 1.0-SNAPSHOT + + + org.sunbird + kafka-client + 1.0-SNAPSHOT + + + org.scalatest + scalatest_${scala.maj.version} + 3.0.8 + test + + + org.sunbird + cassandra-connector + 1.0-SNAPSHOT + + + org.neo4j + neo4j-bolt + + + org.apache.commons + commons-lang3 + + + 3.5.0 + test + + + org.neo4j + neo4j-graphdb-api + 3.5.0 + test + + + org.neo4j + neo4j + 3.5.0 + test + + + org.cassandraunit + cassandra-unit + 3.11.2.0 + test + + + org.apache.commons + commons-lang3 + + + + + + + src/main/scala + src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + ${scala.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + + test + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + + diff --git a/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/GraphService.scala b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/GraphService.scala new file mode 100644 index 000000000..f7986ffbc --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/GraphService.scala @@ -0,0 +1,74 @@ +package org.sunbird.graph + +import java.util + +import org.sunbird.common.dto.{Property, Request, Response} +import org.sunbird.graph.dac.model.{Node, SearchCriteria} +import org.sunbird.graph.external.ExternalPropsManager +import org.sunbird.graph.external.store.ExternalStore +import org.sunbird.graph.service.operation.{GraphAsyncOperations, Neo4JBoltSearchOperations, NodeAsyncOperations, SearchAsyncOperations} + +import scala.concurrent.{ExecutionContext, Future} + +class GraphService { + implicit val ec: ExecutionContext = ExecutionContext.global + + def addNode(graphId: String, node: Node): Future[Node] = { + NodeAsyncOperations.addNode(graphId, node) + } + + def upsertNode(graphId: String, node: Node, request: Request): Future[Node] = { + NodeAsyncOperations.upsertNode(graphId, node, request) + } + + def upsertRootNode(graphId: String, request: Request): Future[Node] = { + NodeAsyncOperations.upsertRootNode(graphId, request) + } + + def getNodeByUniqueId(graphId: String, nodeId: String, getTags: Boolean, request: Request): Future[Node] = { + SearchAsyncOperations.getNodeByUniqueId(graphId, nodeId, getTags, request) + } + + def deleteNode(graphId: String, nodeId: String, request: Request): Future[java.lang.Boolean] = { + NodeAsyncOperations.deleteNode(graphId, nodeId, request) + } + + def getNodeProperty(graphId: String, identifier: String, property: String): Future[Property] = { + SearchAsyncOperations.getNodeProperty(graphId, identifier, property) + } + def updateNodes(graphId: String, identifiers:util.List[String], metadata:util.Map[String,AnyRef]):Future[util.Map[String, Node]] = { + NodeAsyncOperations.updateNodes(graphId, identifiers, metadata) + } + + def getNodeByUniqueIds(graphId:String, searchCriteria: SearchCriteria): Future[util.List[Node]] = { + SearchAsyncOperations.getNodeByUniqueIds(graphId, searchCriteria) + } + + def readExternalProps(request: Request, fields: List[String]): Future[Response] = { + ExternalPropsManager.fetchProps(request, fields) + } + + def saveExternalProps(request: Request): Future[Response] = { + ExternalPropsManager.saveProps(request) + } + + def updateExternalProps(request: Request): Future[Response] = { + ExternalPropsManager.update(request) + } + + def deleteExternalProps(request: Request): Future[Response] = { + ExternalPropsManager.deleteProps(request) + } + def checkCyclicLoop(graphId:String, endNodeId: String, startNodeId: String, relationType: String) = { + Neo4JBoltSearchOperations.checkCyclicLoop(graphId, endNodeId, relationType, startNodeId) + } + + def removeRelation(graphId: String, relationMap: util.List[util.Map[String, AnyRef]]) = { + GraphAsyncOperations.removeRelation(graphId, relationMap) + } + + def createRelation(graphId: String, relationMap: util.List[util.Map[String, AnyRef]]) = { + GraphAsyncOperations.createRelation(graphId, relationMap) + } +} + diff --git a/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/OntologyEngineContext.scala b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/OntologyEngineContext.scala new file mode 100644 index 000000000..fb2d92de4 --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/OntologyEngineContext.scala @@ -0,0 +1,27 @@ +package org.sunbird.graph + +import org.sunbird.common.HttpUtil +import org.sunbird.kafka.client.KafkaClient + +class OntologyEngineContext { + + private val graphDB = new GraphService + private val hUtil = new HttpUtil + private val kfClient = new KafkaClient + + def graphService = { + graphDB + } + + def extStoreDB = { + + } + + def redis = { + + } + + def httpUtil = hUtil + + def kafkaClient = kfClient +} diff --git a/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/exception/GraphErrorCodes.scala b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/exception/GraphErrorCodes.scala new file mode 100644 index 000000000..5c87c2437 --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/exception/GraphErrorCodes.scala @@ -0,0 +1,5 @@ +package org.sunbird.graph.exception + +object GraphErrorCodes extends Enumeration { + val ERR_INVALID_NODE, ERR_GRAPH_PROCESSING_ERROR, ERR_RELATION_CREATE, ERR_RELATION_VALIDATE = Value +} diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/ExternalPropsManager.scala b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/ExternalPropsManager.scala similarity index 55% rename from ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/ExternalPropsManager.scala rename to ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/ExternalPropsManager.scala index 573806980..e633d4ab6 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/ExternalPropsManager.scala +++ b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/ExternalPropsManager.scala @@ -7,8 +7,8 @@ import org.sunbird.common.dto.{Request, Response} import org.sunbird.graph.external.store.ExternalStoreFactory import org.sunbird.schema.SchemaValidatorFactory -import scala.concurrent.{ExecutionContext, Future} import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} object ExternalPropsManager { def saveProps(request: Request)(implicit ec: ExecutionContext): Future[Response] = { @@ -25,7 +25,25 @@ object ExternalPropsManager { val version: String = request.getContext.get("version").asInstanceOf[String] val primaryKey: util.List[String] = SchemaValidatorFactory.getExternalPrimaryKey(schemaName, version) val store = ExternalStoreFactory.getExternalStore(SchemaValidatorFactory.getExternalStoreName(schemaName, version), primaryKey) - store.read(request.get("identifier").asInstanceOf[String], fields, getPropsDataType(schemaName, version)) + if (request.get("identifiers") != null) store.read(request.get("identifiers").asInstanceOf[List[String]], fields, getPropsDataType(schemaName, version)) + else store.read(request.get("identifier").asInstanceOf[String], fields, getPropsDataType(schemaName, version)) + } + + def deleteProps(request: Request)(implicit ec: ExecutionContext): Future[Response] = { + val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] + val version: String = request.getContext.get("version").asInstanceOf[String] + val primaryKey: util.List[String] = SchemaValidatorFactory.getExternalPrimaryKey(schemaName, version) + val store = ExternalStoreFactory.getExternalStore(SchemaValidatorFactory.getExternalStoreName(schemaName, version), primaryKey) + store.delete(request.get("identifiers").asInstanceOf[List[String]]) + } + + def update(request: Request)(implicit ec: ExecutionContext): Future[Response] = { + val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] + val version: String = request.getContext.get("version").asInstanceOf[String] + val primaryKey: util.List[String] = SchemaValidatorFactory.getExternalPrimaryKey(schemaName, version) + val store = ExternalStoreFactory.getExternalStore(SchemaValidatorFactory.getExternalStoreName(schemaName, version), primaryKey) + store.update(request.get("identifier").asInstanceOf[String], request.get("fields").asInstanceOf[List[String]], + request.get("values").asInstanceOf[List[java.util.Map[String, AnyRef]]], getPropsDataType(schemaName, version)) } def getPropsDataType(schemaName: String, version: String) = { diff --git a/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStore.scala b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStore.scala new file mode 100644 index 000000000..c9cb9f11e --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStore.scala @@ -0,0 +1,195 @@ +package org.sunbird.graph.external.store + +import java.sql.Timestamp +import java.util +import java.util.Date + +import com.datastax.driver.core.Session +import com.datastax.driver.core.querybuilder.{Clause, Insert, QueryBuilder} +import com.google.common.util.concurrent.{FutureCallback, Futures, ListenableFuture, MoreExecutors} +import org.sunbird.cassandra.{CassandraConnector, CassandraStore} +import org.sunbird.common.JsonUtils +import org.sunbird.common.dto.{Response, ResponseHandler} +import org.sunbird.common.exception.{ErrorCodes, ResponseCode, ServerException} +import org.sunbird.telemetry.logger.TelemetryManager + +import scala.concurrent.{ExecutionContext, Future, Promise} + +class ExternalStore(keySpace: String , table: String , primaryKey: java.util.List[String]) extends CassandraStore(keySpace, table, primaryKey) { + + def insert(request: util.Map[String, AnyRef], propsMapping: Map[String, String])(implicit ec: ExecutionContext): Future[Response] = { + val insertQuery: Insert = QueryBuilder.insertInto(keySpace, table) + val identifier = request.get("identifier") + insertQuery.value(primaryKey.get(0), identifier) + request.remove("identifier") + request.remove("last_updated_on") + if(propsMapping.keySet.contains("last_updated_on")) + insertQuery.value("last_updated_on", new Timestamp(new Date().getTime)) + import scala.collection.JavaConverters._ + for ((key, value) <- request.asScala) { + propsMapping.getOrElse(key, "") match { + case "blob" => insertQuery.value(key, QueryBuilder.fcall("textAsBlob", value)) + case "string" => request.getOrDefault(key, "") match { + case value: String => insertQuery.value(key, value) + case _ => insertQuery.value(key, JsonUtils.serialize(request.getOrDefault(key, ""))) + } + case _ => insertQuery.value(key, value) + } + } + try { + val session: Session = CassandraConnector.getSession + session.executeAsync(insertQuery).asScala.map( resultset => { + ResponseHandler.OK() + }) + } catch { + case e: Exception => + e.printStackTrace() + TelemetryManager.error("Exception Occurred While Saving The Record. | Exception is : " + e.getMessage, e) + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, "Exception Occurred While Saving The Record. Exception is : " + e.getMessage) + } + } + + /** + * Fetching properties which are stored in an external database + * @param identifier + * @param extProps + * @param ec + * @return + */ + def read(identifier: String, extProps: List[String], propsMapping: Map[String, String])(implicit ec: ExecutionContext): Future[Response] = { + val select = QueryBuilder.select() + if(null != extProps && !extProps.isEmpty){ + extProps.foreach(prop => { + if("blob".equalsIgnoreCase(propsMapping.getOrElse(prop, ""))) + select.fcall("blobAsText", QueryBuilder.column(prop)).as(prop) + else + select.column(prop).as(prop) + }) + } + val selectQuery = select.from(keySpace, table) + val clause: Clause = QueryBuilder.eq(primaryKey.get(0), identifier) + selectQuery.where.and(clause) + try { + val session: Session = CassandraConnector.getSession + val futureResult = session.executeAsync(selectQuery) + futureResult.asScala.map(resultSet => { + if (resultSet.iterator().hasNext) { + val row = resultSet.one() + val externalMetadataMap = extProps.map(prop => prop -> row.getObject(prop)).toMap + val response = ResponseHandler.OK() + import scala.collection.JavaConverters._ + response.putAll(externalMetadataMap.asJava) + response + } else { + TelemetryManager.error("Entry is not found in external-store for object with identifier: " + identifier) + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.code().toString, "Entry is not found in external-store for object with identifier: " + identifier) + } + }) + } catch { + case e: Exception => + e.printStackTrace() + TelemetryManager.error("Exception Occurred While Reading The Record. | Exception is : " + e.getMessage, e) + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, "Exception Occurred While Reading The Record. Exception is : " + e.getMessage) + } + } + + def read(identifiers: List[String], extProps: List[String], propsMapping: Map[String, String])(implicit ec: ExecutionContext): Future[Response] = { + val select = QueryBuilder.select() + select.column(primaryKey.get(0)).as(primaryKey.get(0)) + if (null != extProps && !extProps.isEmpty) { + extProps.foreach(prop => { + if ("blob".equalsIgnoreCase(propsMapping.getOrElse(prop, ""))) + select.fcall("blobAsText", QueryBuilder.column(prop)).as(prop) + else + select.column(prop).as(prop) + }) + } + val selectQuery = select.from(keySpace, table) + import scala.collection.JavaConversions._ + val clause: Clause = QueryBuilder.in(primaryKey.get(0), seqAsJavaList(identifiers)) + selectQuery.where.and(clause) + try { + val session: Session = CassandraConnector.getSession + val futureResult = session.executeAsync(selectQuery) + futureResult.asScala.map(resultSet => { + if (resultSet.iterator().hasNext) { + val response = ResponseHandler.OK() + resultSet.iterator().toStream.map(row => { + import scala.collection.JavaConverters._ + val externalMetadataMap = extProps.map(prop => prop -> row.getObject(prop)).toMap.asJava + response.put(row.getString(primaryKey.get(0)), externalMetadataMap) + }).toList + response + } else { + TelemetryManager.error("Entry is not found in external-store for object with identifiers: " + identifiers) + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.code().toString, "Entry is not found in external-store for object with identifiers: " + identifiers) + } + }) + } catch { + case e: Exception => + e.printStackTrace() + TelemetryManager.error("Exception Occurred While Reading The Record. | Exception is : " + e.getMessage, e) + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, "Exception Occurred While Reading The Record. Exception is : " + e.getMessage) + } + } + + def delete(identifiers: List[String])(implicit ec: ExecutionContext): Future[Response] = { + val delete = QueryBuilder.delete() + import scala.collection.JavaConversions._ + val deleteQuery = delete.from(keySpace, table).where(QueryBuilder.in(primaryKey.get(0), seqAsJavaList(identifiers))) + try { + val session: Session = CassandraConnector.getSession + session.executeAsync(deleteQuery).asScala.map(resultSet => { + if (!resultSet.wasApplied()) + TelemetryManager.error("Entry is not found in cassandra for content with identifiers: " + identifiers) + ResponseHandler.OK() + }) + } catch { + case e: Exception => + TelemetryManager.error("Exception Occurred While Deleting The Record. | Exception is : " + e.getMessage, e) + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, "Exception Occurred While Reading The Record. Exception is : " + e.getMessage) + } + } + + def update(identifier: String, columns: List[String], values: List[AnyRef], propsMapping: Map[String, String])(implicit ec: ExecutionContext): Future[Response] = { + val update = QueryBuilder.update(keySpace, table) + val clause: Clause = QueryBuilder.eq(primaryKey.get(0), identifier) + update.where.and(clause) +// if(propsMapping.keySet.contains("last_updated_on")) +// update.`with`(QueryBuilder.add("last_updated_on", new Timestamp(new Date().getTime))) + for ((column, index) <- columns.view.zipWithIndex) { + propsMapping.getOrElse(column, "").toLowerCase match { + case "blob" => update.`with`(QueryBuilder.set(column, QueryBuilder.fcall("textAsBlob", values(index)))) + case "object" => update.`with`(QueryBuilder.putAll(column, values(index).asInstanceOf[java.util.Map[String, AnyRef]])) + case "array" => update.`with`(QueryBuilder.appendAll(column, values(index).asInstanceOf[java.util.List[String]])) + case "string" => values(index) match { + case value: String => update.`with`(QueryBuilder.set(column, values(index))) + case _ => update.`with`(QueryBuilder.set(column, JsonUtils.serialize(values(index)))) + } + case _ => update.`with`(QueryBuilder.set(column, values(index))) + } + } + try { + val session: Session = CassandraConnector.getSession + session.executeAsync(update).asScala.map( resultset => { + ResponseHandler.OK() + }) + } catch { + case e: Exception => + e.printStackTrace() + TelemetryManager.error("Exception Occurred While Saving The Record. | Exception is : " + e.getMessage, e) + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, "Exception Occurred While Saving The Record. Exception is : " + e.getMessage) + } + } + + implicit class RichListenableFuture[T](lf: ListenableFuture[T]) { + def asScala : Future[T] = { + val p = Promise[T]() + Futures.addCallback(lf, new FutureCallback[T] { + def onFailure(t: Throwable): Unit = p failure t + def onSuccess(result: T): Unit = p success result + }, MoreExecutors.directExecutor()) + p.future + } + } +} \ No newline at end of file diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStoreFactory.scala b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStoreFactory.scala similarity index 96% rename from ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStoreFactory.scala rename to ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStoreFactory.scala index 6369a4563..ae06f2392 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStoreFactory.scala +++ b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStoreFactory.scala @@ -1,7 +1,6 @@ package org.sunbird.graph.external.store import java.util -import java.util.{Arrays, List} object ExternalStoreFactory { diff --git a/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/validator/NodeValidator.scala b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/validator/NodeValidator.scala new file mode 100644 index 000000000..5e102a05f --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/main/scala/org/sunbird/graph/validator/NodeValidator.scala @@ -0,0 +1,51 @@ +package org.sunbird.graph.validator + +import java.util +import java.util.concurrent.CompletionException + +import org.sunbird.common.exception.{ResourceNotFoundException, ServerException} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.enums.SystemProperties +import org.sunbird.graph.dac.model.{Filter, MetadataCriterion, Node, SearchConditions, SearchCriteria} +import org.sunbird.graph.exception.GraphErrorCodes +import org.sunbird.graph.service.operation.SearchAsyncOperations + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +object NodeValidator { + + def validate(graphId: String, identifiers: util.List[String])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[util.Map[String, Node]] = { + val nodes = getDataNodes(graphId, identifiers) + + nodes.map(dataNodes => { + if (dataNodes.size != identifiers.size) { + val dbNodeIds = dataNodes.map(node => node.getIdentifier).toList + val invalidIds = identifiers.toList.filter(id => !dbNodeIds.contains(id)) + throw new ResourceNotFoundException(GraphErrorCodes.ERR_INVALID_NODE.toString, "Node Not Found With Identifier " + invalidIds) + } else { + new util.HashMap[String, Node](dataNodes.map(node => node.getIdentifier -> node).toMap.asJava) + } + }) recoverWith { + case e: CompletionException => throw e.getCause + } + } + + private def getDataNodes(graphId: String, identifiers: util.List[String])(implicit ec: ExecutionContext, oec: OntologyEngineContext) = { + val searchCriteria = new SearchCriteria + val mc = if (identifiers.size == 1) + MetadataCriterion.create(util.Arrays.asList(new Filter(SystemProperties.IL_UNIQUE_ID.name, SearchConditions.OP_EQUAL, identifiers.get(0)))) + else + MetadataCriterion.create(util.Arrays.asList(new Filter(SystemProperties.IL_UNIQUE_ID.name, SearchConditions.OP_IN, identifiers), new Filter("status", SearchConditions.OP_NOT_EQUAL, "Retired"))) + searchCriteria.addMetadata(mc) + searchCriteria.setCountQuery(false) + try { + val nodes = oec.graphService.getNodeByUniqueIds(graphId, searchCriteria) + nodes + } catch { + case e: Exception => + throw new ServerException(GraphErrorCodes.ERR_GRAPH_PROCESSING_ERROR.toString, "Unable To Fetch Nodes From Graph. Exception is: " + e.getMessage) + } + } +} diff --git a/ontology-engine/graph-core_2.11/src/test/resources/application.conf b/ontology-engine/graph-core_2.11/src/test/resources/application.conf new file mode 100644 index 000000000..b5fb5b0cb --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/test/resources/application.conf @@ -0,0 +1,488 @@ +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +# ~~~~~ +# Play uses HOCON as its configuration file format. HOCON has a number +# of advantages over other config formats, but there are two things that +# can be used when modifying settings. +# +# You can include other configuration files in this main application.conf file: +#include "extra-config.conf" +# +# You can declare variables and substitute for them: +#mykey = ${some.value} +# +# And if an environment variable exists when there is no other substitution, then +# HOCON will fall back to substituting environment variable: +#mykey = ${JAVA_HOME} + +## Akka +# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration +# https://www.playframework.com/documentation/latest/JavaAkka#Configuration +# ~~~~~ +# Play uses Akka internally and exposes Akka Streams and actors in Websockets and +# other streaming HTTP responses. +akka { + # "akka.log-config-on-start" is extraordinarly useful because it log the complete + # configuration at INFO level, including defaults and overrides, so it s worth + # putting at the very top. + # + # Put the following in your conf/logback.xml file: + # + # + # + # And then uncomment this line to debug the configuration. + # + #log-config-on-start = true +} + +## Secret key +# http://www.playframework.com/documentation/latest/ApplicationSecret +# ~~~~~ +# The secret key is used to sign Play's session cookie. +# This must be changed for production, but we don't recommend you change it in this file. +play.http.secret.key = a-long-secret-to-calm-the-rage-of-the-entropy-gods + +## Modules +# https://www.playframework.com/documentation/latest/Modules +# ~~~~~ +# Control which modules are loaded when Play starts. Note that modules are +# the replacement for "GlobalSettings", which are deprecated in 2.5.x. +# Please see https://www.playframework.com/documentation/latest/GlobalSettings +# for more information. +# +# You can also extend Play functionality by using one of the publically available +# Play modules: https://playframework.com/documentation/latest/ModuleDirectory +play.modules { + # By default, Play will load any class called Module that is defined + # in the root package (the "app" directory), or you can define them + # explicitly below. + # If there are any built-in modules that you want to enable, you can list them here. + #enabled += my.application.Module + + # If there are any built-in modules that you want to disable, you can list them here. + #disabled += "" +} + +## IDE +# https://www.playframework.com/documentation/latest/IDE +# ~~~~~ +# Depending on your IDE, you can add a hyperlink for errors that will jump you +# directly to the code location in the IDE in dev mode. The following line makes +# use of the IntelliJ IDEA REST interface: +#play.editor="http://localhost:63342/api/file/?file=%s&line=%s" + +## Internationalisation +# https://www.playframework.com/documentation/latest/JavaI18N +# https://www.playframework.com/documentation/latest/ScalaI18N +# ~~~~~ +# Play comes with its own i18n settings, which allow the user's preferred language +# to map through to internal messages, or allow the language to be stored in a cookie. +play.i18n { + # The application languages + langs = [ "en" ] + + # Whether the language cookie should be secure or not + #langCookieSecure = true + + # Whether the HTTP only attribute of the cookie should be set to true + #langCookieHttpOnly = true +} + +## Play HTTP settings +# ~~~~~ +play.http { + ## Router + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # Define the Router object to use for this application. + # This router will be looked up first when the application is starting up, + # so make sure this is the entry point. + # Furthermore, it's assumed your route file is named properly. + # So for an application router like `my.application.Router`, + # you may need to define a router file `conf/my.application.routes`. + # Default to Routes in the root package (aka "apps" folder) (and conf/routes) + #router = my.application.Router + + ## Action Creator + # https://www.playframework.com/documentation/latest/JavaActionCreator + # ~~~~~ + #actionCreator = null + + ## ErrorHandler + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # If null, will attempt to load a class called ErrorHandler in the root package, + #errorHandler = null + + ## Session & Flash + # https://www.playframework.com/documentation/latest/JavaSessionFlash + # https://www.playframework.com/documentation/latest/ScalaSessionFlash + # ~~~~~ + session { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + + # Sets the max-age field of the cookie to 5 minutes. + # NOTE: this only sets when the browser will discard the cookie. Play will consider any + # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, + # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. + #maxAge = 300 + + # Sets the domain on the session cookie. + #domain = "example.com" + } + + flash { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + } +} + +## Netty Provider +# https://www.playframework.com/documentation/latest/SettingsNetty +# ~~~~~ +play.server.netty { + # Whether the Netty wire should be logged + log.wire = true + + # If you run Play on Linux, you can use Netty's native socket transport + # for higher performance with less garbage. + transport = "native" +} + +## WS (HTTP Client) +# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS +# ~~~~~ +# The HTTP client primarily used for REST APIs. The default client can be +# configured directly, but you can also create different client instances +# with customized settings. You must enable this by adding to build.sbt: +# +# libraryDependencies += ws // or javaWs if using java +# +play.ws { + # Sets HTTP requests not to follow 302 requests + #followRedirects = false + + # Sets the maximum number of open HTTP connections for the client. + #ahc.maxConnectionsTotal = 50 + + ## WS SSL + # https://www.playframework.com/documentation/latest/WsSSL + # ~~~~~ + ssl { + # Configuring HTTPS with Play WS does not require programming. You can + # set up both trustManager and keyManager for mutual authentication, and + # turn on JSSE debugging in development with a reload. + #debug.handshake = true + #trustManager = { + # stores = [ + # { type = "JKS", path = "exampletrust.jks" } + # ] + #} + } +} + +## Cache +# https://www.playframework.com/documentation/latest/JavaCache +# https://www.playframework.com/documentation/latest/ScalaCache +# ~~~~~ +# Play comes with an integrated cache API that can reduce the operational +# overhead of repeated requests. You must enable this by adding to build.sbt: +# +# libraryDependencies += cache +# +play.cache { + # If you want to bind several caches, you can bind the individually + #bindCaches = ["db-cache", "user-cache", "session-cache"] +} + +## Filter Configuration +# https://www.playframework.com/documentation/latest/Filters +# ~~~~~ +# There are a number of built-in filters that can be enabled and configured +# to give Play greater security. +# +play.filters { + + # Enabled filters are run automatically against Play. + # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. + enabled = [] + + # Disabled filters remove elements from the enabled list. + # disabled += filters.CSRFFilter + + + ## CORS filter configuration + # https://www.playframework.com/documentation/latest/CorsFilter + # ~~~~~ + # CORS is a protocol that allows web applications to make requests from the browser + # across different domains. + # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has + # dependencies on CORS settings. + cors { + # Filter paths by a whitelist of path prefixes + #pathPrefixes = ["/some/path", ...] + + # The allowed origins. If null, all origins are allowed. + #allowedOrigins = ["http://www.example.com"] + + # The allowed HTTP methods. If null, all methods are allowed + #allowedHttpMethods = ["GET", "POST"] + } + + ## Security headers filter configuration + # https://www.playframework.com/documentation/latest/SecurityHeaders + # ~~~~~ + # Defines security headers that prevent XSS attacks. + # If enabled, then all options are set to the below configuration by default: + headers { + # The X-Frame-Options header. If null, the header is not set. + #frameOptions = "DENY" + + # The X-XSS-Protection header. If null, the header is not set. + #xssProtection = "1; mode=block" + + # The X-Content-Type-Options header. If null, the header is not set. + #contentTypeOptions = "nosniff" + + # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. + #permittedCrossDomainPolicies = "master-only" + + # The Content-Security-Policy header. If null, the header is not set. + #contentSecurityPolicy = "default-src 'self'" + } + + ## Allowed hosts filter configuration + # https://www.playframework.com/documentation/latest/AllowedHostsFilter + # ~~~~~ + # Play provides a filter that lets you configure which hosts can access your application. + # This is useful to prevent cache poisoning attacks. + hosts { + # Allow requests to example.com, its subdomains, and localhost:9000. + #allowed = [".example.com", "localhost:9000"] + } +} + +# Learning-Service Configuration +content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit"] + +# Cassandra Configuration +content.keyspace.name=content_store +content.keyspace.table=content_data +#TODO: Add Configuration for assessment. e.g: question_data +orchestrator.keyspace.name=script_store +orchestrator.keyspace.table=script_data +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" + +# Redis Configuration +redis.host=localhost +redis.port=6379 +redis.maxConnections=128 + +#Condition to enable publish locally +content.publish_task.enabled=true + +#directory location where store unzip file +dist.directory=/data/tmp/dist/ +output.zipfile=/data/tmp/story.zip +source.folder=/data/tmp/temp2/ +save.directory=/data/tmp/temp/ + +# Content 2 vec analytics URL +CONTENT_TO_VEC_URL="http://172.31.27.233:9000/content-to-vec" + +# FOR CONTENT WORKFLOW PIPELINE (CWP) + +#--Content Workflow Pipeline Mode +OPERATION_MODE=TEST + +#--Maximum Content Package File Size Limit in Bytes (50 MB) +MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=52428800 + +#--Maximum Asset File Size Limit in Bytes (20 MB) +MAX_ASSET_FILE_SIZE_LIMIT=20971520 + +#--No of Retry While File Download Fails +RETRY_ASSET_DOWNLOAD_COUNT=1 + +#Google-vision-API +google.vision.tagging.enabled = false + +#Orchestrator env properties +env="https://dev.ekstep.in/api/learning" + +#Current environment +cloud_storage.env=dev + + +#Folder configuration +cloud_storage.content.folder=content +cloud_storage.asset.folder=assets +cloud_storage.artefact.folder=artifact +cloud_storage.bundle.folder=bundle +cloud_storage.media.folder=media +cloud_storage.ecar.folder=ecar_files + +# Media download configuration +content.media.base.url="https://dev.open-sunbird.org" +plugin.media.base.url="https://dev.open-sunbird.org" + +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + +# Elasticsearch properties +search.es_conn_info="localhost:9200" +search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","domain","subject","description^10","keywords^25","ageGroup^10","filter^10","theme^10","genre^10","objects^25","contentType^100","language^200","teachingMode^25","skills^10","learningObjective^10","curriculum^100","gradeLevel^100","developer^100","attributions^10","owner^50","text","words","releaseNotes"] +search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] +search.batch.size=500 +search.connection.timeout=30 +platform-api-url="http://localhost:8080/language-service" +MAX_ITERATION_COUNT_FOR_SAMZA_JOB=2 + + +# DIAL Code Configuration +dialcode.keyspace.name="dialcode_store" +dialcode.keyspace.table="dial_code" +dialcode.max_count=1000 + +# System Configuration +system.config.keyspace.name="dialcode_store" +system.config.table="system_config" + +#Publisher Configuration +publisher.keyspace.name="dialcode_store" +publisher.keyspace.table="publisher" + +#DIAL Code Generator Configuration +dialcode.strip.chars="0" +dialcode.length=6.0 +dialcode.large.prime_number=1679979167 + +#DIAL Code ElasticSearch Configuration +dialcode.index=true +dialcode.object_type="DialCode" + +framework.max_term_creation_limit=200 + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + +# Kafka configuration details +kafka.topics.instruction="local.learning.job.request" +kafka.urls="localhost:9092" + +#Youtube Standard Licence Validation +learning.content.youtube.validate.license=true +learning.content.youtube.application.name=fetch-youtube-license +youtube.license.regex.pattern=["\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)","^([A-Za-z0-9\\-\\_]*)"] + +#Top N Config for Search Telemetry +telemetry_env=dev +telemetry.search.topn=5 + +installation.id=ekstep + + +channel.default="in.ekstep" + +# DialCode Link API Config +learning.content.link_dialcode_validation=true +dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/search" +dialcode.api.authorization=auth_key + +# Language-Code Configuration +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] + +# Kafka send event to topic enable +kafka.topic.send.enable=false + +learning.valid_license=["creativeCommon"] +learning.service_provider=["youtube"] + +stream.mime.type=video/mp4 +compositesearch.index.name="compositesearch" + +hierarchy.keyspace.name=hierarchy_store +content.hierarchy.table=content_hierarchy +framework.hierarchy.table=framework_hierarchy + +# Kafka topic for definition update event. +kafka.topic.system.command="dev.system.command" + +learning.reserve_dialcode.content_type=["TextBook"] +# restrict.metadata.objectTypes=["Content", "ContentImage", "AssessmentItem", "Channel", "Framework", "Category", "CategoryInstance", "Term"] + +#restrict.metadata.objectTypes="Content,ContentImage" + +publish.collection.fullecar.disable=true + +# Consistency Level for Multi Node Cassandra cluster +cassandra.lp.consistency.level=QUORUM + + + + +content.nested.fields="badgeAssertions,targets,badgeAssociations" + +content.cache.ttl=86400 +content.cache.enable=true +collection.cache.enable=true +content.discard.status=["Draft","FlagDraft"] + +framework.categories_cached=["subject", "medium", "gradeLevel", "board"] +framework.cache.ttl=86400 +framework.cache.read=true + + +# Max size(width/height) of thumbnail in pixels +max.thumbnail.size.pixels=150 + +play.http.parser.maxMemoryBuffer = 50MB +akka.http.parsing.max-content-length = 50MB +schema.base_path = "../../schemas/" +//schema.base_path = "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/" + +collection.image.migration.enabled=true +collection.keyspace = "hierarchy_store" +content.keyspace = "content_store" + +languageCode { + assamese : "as" + bengali : "bn" + english : "en" + gujarati : "gu" + hindi : "hi" + kannada : "ka" + marathi : "mr" + odia : "or" + tamil : "ta" + telugu : "te" +} + +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd"] +objectcategorydefinition.keyspace=category_store diff --git a/ontology-engine/graph-core_2.11/src/test/resources/cassandra-unit.yaml b/ontology-engine/graph-core_2.11/src/test/resources/cassandra-unit.yaml new file mode 100755 index 000000000..a965a8fe5 --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/test/resources/cassandra-unit.yaml @@ -0,0 +1,590 @@ +# Cassandra storage config YAML + +# NOTE: +# See http://wiki.apache.org/cassandra/StorageConfiguration for +# full explanations of configuration directives +# /NOTE + +# The name of the cluster. This is mainly used to prevent machines in +# one logical cluster from joining another. +cluster_name: 'Test Cluster' + +# You should always specify InitialToken when setting up a production +# cluster for the first time, and often when adding capacity later. +# The principle is that each node should be given an equal slice of +# the token ring; see http://wiki.apache.org/cassandra/Operations +# for more details. +# +# If blank, Cassandra will request a token bisecting the range of +# the heaviest-loaded existing node. If there is no load information +# available, such as is the case with a new cluster, it will pick +# a random token, which will lead to hot spots. +#initial_token: + +# See http://wiki.apache.org/cassandra/HintedHandoff +hinted_handoff_enabled: true +# this defines the maximum amount of time a dead host will have hints +# generated. After it has been dead this long, new hints for it will not be +# created until it has been seen alive and gone down again. +max_hint_window_in_ms: 10800000 # 3 hours +# Maximum throttle in KBs per second, per delivery thread. This will be +# reduced proportionally to the number of nodes in the cluster. (If there +# are two nodes in the cluster, each delivery thread will use the maximum +# rate; if there are three, each will throttle to half of the maximum, +# since we expect two nodes to be delivering hints simultaneously.) +hinted_handoff_throttle_in_kb: 1024 +# Number of threads with which to deliver hints; +# Consider increasing this number when you have multi-dc deployments, since +# cross-dc handoff tends to be slower +max_hints_delivery_threads: 2 + +hints_directory: target/embeddedCassandra/hints + +# The following setting populates the page cache on memtable flush and compaction +# WARNING: Enable this setting only when the whole node's data fits in memory. +# Defaults to: false +# populate_io_cache_on_flush: false + +# Authentication backend, implementing IAuthenticator; used to identify users +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, +# PasswordAuthenticator}. +# +# - AllowAllAuthenticator performs no checks - set it to disable authentication. +# - PasswordAuthenticator relies on username/password pairs to authenticate +# users. It keeps usernames and hashed passwords in system_auth.credentials table. +# Please increase system_auth keyspace replication factor if you use this authenticator. +authenticator: AllowAllAuthenticator + +# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, +# CassandraAuthorizer}. +# +# - AllowAllAuthorizer allows any action to any user - set it to disable authorization. +# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please +# increase system_auth keyspace replication factor if you use this authorizer. +authorizer: AllowAllAuthorizer + +# Validity period for permissions cache (fetching permissions can be an +# expensive operation depending on the authorizer, CassandraAuthorizer is +# one example). Defaults to 2000, set to 0 to disable. +# Will be disabled automatically for AllowAllAuthorizer. +permissions_validity_in_ms: 2000 + + +# The partitioner is responsible for distributing rows (by key) across +# nodes in the cluster. Any IPartitioner may be used, including your +# own as long as it is on the classpath. Out of the box, Cassandra +# provides org.apache.cassandra.dht.{Murmur3Partitioner, RandomPartitioner +# ByteOrderedPartitioner, OrderPreservingPartitioner (deprecated)}. +# +# - RandomPartitioner distributes rows across the cluster evenly by md5. +# This is the default prior to 1.2 and is retained for compatibility. +# - Murmur3Partitioner is similar to RandomPartioner but uses Murmur3_128 +# Hash Function instead of md5. When in doubt, this is the best option. +# - ByteOrderedPartitioner orders rows lexically by key bytes. BOP allows +# scanning rows in key order, but the ordering can generate hot spots +# for sequential insertion workloads. +# - OrderPreservingPartitioner is an obsolete form of BOP, that stores +# - keys in a less-efficient format and only works with keys that are +# UTF8-encoded Strings. +# - CollatingOPP collates according to EN,US rules rather than lexical byte +# ordering. Use this as an example if you need custom collation. +# +# See http://wiki.apache.org/cassandra/Operations for more on +# partitioners and token selection. +partitioner: org.apache.cassandra.dht.Murmur3Partitioner + +# directories where Cassandra should store data on disk. +data_file_directories: + - target/embeddedCassandra/data + +# commit log +commitlog_directory: target/embeddedCassandra/commitlog + +cdc_raw_directory: target/embeddedCassandra/cdc + +# policy for data disk failures: +# stop: shut down gossip and Thrift, leaving the node effectively dead, but +# can still be inspected via JMX. +# best_effort: stop using the failed disk and respond to requests based on +# remaining available sstables. This means you WILL see obsolete +# data at CL.ONE! +# ignore: ignore fatal errors and let requests fail, as in pre-1.2 Cassandra +disk_failure_policy: stop + + +# Maximum size of the key cache in memory. +# +# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the +# minimum, sometimes more. The key cache is fairly tiny for the amount of +# time it saves, so it's worthwhile to use it at large numbers. +# The row cache saves even more time, but must store the whole values of +# its rows, so it is extremely space-intensive. It's best to only use the +# row cache if you have hot rows or static rows. +# +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. +key_cache_size_in_mb: + +# Duration in seconds after which Cassandra should +# safe the keys cache. Caches are saved to saved_caches_directory as +# specified in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 14400 or 4 hours. +key_cache_save_period: 14400 + +# Number of keys from the key cache to save +# Disabled by default, meaning all keys are going to be saved +# key_cache_keys_to_save: 100 + +# Maximum size of the row cache in memory. +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is 0, to disable row caching. +row_cache_size_in_mb: 0 + +# Duration in seconds after which Cassandra should +# safe the row cache. Caches are saved to saved_caches_directory as specified +# in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 0 to disable saving the row cache. +row_cache_save_period: 0 + +# Number of keys from the row cache to save +# Disabled by default, meaning all keys are going to be saved +# row_cache_keys_to_save: 100 + +# saved caches +saved_caches_directory: target/embeddedCassandra/saved_caches + +# commitlog_sync may be either "periodic" or "batch." +# When in batch mode, Cassandra won't ack writes until the commit log +# has been fsynced to disk. It will wait up to +# commitlog_sync_batch_window_in_ms milliseconds for other writes, before +# performing the sync. +# +# commitlog_sync: batch +# commitlog_sync_batch_window_in_ms: 50 +# +# the other option is "periodic" where writes may be acked immediately +# and the CommitLog is simply synced every commitlog_sync_period_in_ms +# milliseconds. +commitlog_sync: periodic +commitlog_sync_period_in_ms: 10000 + +# The size of the individual commitlog file segments. A commitlog +# segment may be archived, deleted, or recycled once all the data +# in it (potentially from each columnfamily in the system) has been +# flushed to sstables. +# +# The default size is 32, which is almost always fine, but if you are +# archiving commitlog segments (see commitlog_archiving.properties), +# then you probably want a finer granularity of archiving; 8 or 16 MB +# is reasonable. +commitlog_segment_size_in_mb: 32 + +# any class that implements the SeedProvider interface and has a +# constructor that takes a Map of parameters will do. +seed_provider: + # Addresses of hosts that are deemed contact points. + # Cassandra nodes use this list of hosts to find each other and learn + # the topology of the ring. You must change this if you are running + # multiple nodes! + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + # seeds is actually a comma-delimited list of addresses. + # Ex: ",," + - seeds: "127.0.0.1" + + +# For workloads with more data than can fit in memory, Cassandra's +# bottleneck will be reads that need to fetch data from +# disk. "concurrent_reads" should be set to (16 * number_of_drives) in +# order to allow the operations to enqueue low enough in the stack +# that the OS and drives can reorder them. +# +# On the other hand, since writes are almost never IO bound, the ideal +# number of "concurrent_writes" is dependent on the number of cores in +# your system; (8 * number_of_cores) is a good rule of thumb. +concurrent_reads: 32 +concurrent_writes: 32 + +# Total memory to use for memtables. Cassandra will flush the largest +# memtable when this much memory is used. +# If omitted, Cassandra will set it to 1/3 of the heap. +# memtable_total_space_in_mb: 2048 + +# Total space to use for commitlogs. +# If space gets above this value (it will round up to the next nearest +# segment multiple), Cassandra will flush every dirty CF in the oldest +# segment and remove it. +# commitlog_total_space_in_mb: 4096 + +# This sets the amount of memtable flush writer threads. These will +# be blocked by disk io, and each one will hold a memtable in memory +# while blocked. If you have a large heap and many data directories, +# you can increase this value for better flush performance. +# By default this will be set to the amount of data directories defined. +#memtable_flush_writers: 1 + +# the number of full memtables to allow pending flush, that is, +# waiting for a writer thread. At a minimum, this should be set to +# the maximum number of secondary indexes created on a single CF. +#memtable_flush_queue_size: 4 + +# Whether to, when doing sequential writing, fsync() at intervals in +# order to force the operating system to flush the dirty +# buffers. Enable this to avoid sudden dirty buffer flushing from +# impacting read latencies. Almost always a good idea on SSD:s; not +# necessarily on platters. +trickle_fsync: false +trickle_fsync_interval_in_kb: 10240 + +# TCP port, for commands and data +storage_port: 0 + +# SSL port, for encrypted communication. Unused unless enabled in +# encryption_options +ssl_storage_port: 7011 + +# Address to bind to and tell other Cassandra nodes to connect to. You +# _must_ change this if you want multiple nodes to be able to +# communicate! +# +# Leaving it blank leaves it up to InetAddress.getLocalHost(). This +# will always do the Right Thing *if* the node is properly configured +# (hostname, name resolution, etc), and the Right Thing is to use the +# address associated with the hostname (it might not be). +# +# Setting this to 0.0.0.0 is always wrong. +listen_address: 127.0.0.1 + +start_native_transport: true +# port for the CQL native transport to listen for clients on +native_transport_port: 9042 + +# Whether to start the thrift rpc server. +start_rpc: true + +# Address to broadcast to other Cassandra nodes +# Leaving this blank will set it to the same value as listen_address +# broadcast_address: 1.2.3.4 + +# The address to bind the Thrift RPC service to -- clients connect +# here. Unlike ListenAddress above, you *can* specify 0.0.0.0 here if +# you want Thrift to listen on all interfaces. +# +# Leaving this blank has the same effect it does for ListenAddress, +# (i.e. it will be based on the configured hostname of the node). +rpc_address: localhost +# port for Thrift to listen for clients on +rpc_port: 0 + +# enable or disable keepalive on rpc connections +rpc_keepalive: true + +# Cassandra provides three options for the RPC Server: +# +# sync -> One connection per thread in the rpc pool (see below). +# For a very large number of clients, memory will be your limiting +# factor; on a 64 bit JVM, 128KB is the minimum stack size per thread. +# Connection pooling is very, very strongly recommended. +# +# async -> Nonblocking server implementation with one thread to serve +# rpc connections. This is not recommended for high throughput use +# cases. Async has been tested to be about 50% slower than sync +# or hsha and is deprecated: it will be removed in the next major release. +# +# hsha -> Stands for "half synchronous, half asynchronous." The rpc thread pool +# (see below) is used to manage requests, but the threads are multiplexed +# across the different clients. +# +# The default is sync because on Windows hsha is about 30% slower. On Linux, +# sync/hsha performance is about the same, with hsha of course using less memory. +rpc_server_type: sync + +# Uncomment rpc_min|max|thread to set request pool size. +# You would primarily set max for the sync server to safeguard against +# misbehaved clients; if you do hit the max, Cassandra will block until one +# disconnects before accepting more. The defaults for sync are min of 16 and max +# unlimited. +# +# For the Hsha server, the min and max both default to quadruple the number of +# CPU cores. +# +# This configuration is ignored by the async server. +# +# rpc_min_threads: 16 +# rpc_max_threads: 2048 + +# uncomment to set socket buffer sizes on rpc connections +# rpc_send_buff_size_in_bytes: +# rpc_recv_buff_size_in_bytes: + +# Frame size for thrift (maximum field length). +# 0 disables TFramedTransport in favor of TSocket. This option +# is deprecated; we strongly recommend using Framed mode. +thrift_framed_transport_size_in_mb: 15 + +# The max length of a thrift message, including all fields and +# internal thrift overhead. +thrift_max_message_length_in_mb: 16 + +# Set to true to have Cassandra create a hard link to each sstable +# flushed or streamed locally in a backups/ subdirectory of the +# Keyspace data. Removing these links is the operator's +# responsibility. +incremental_backups: false + +# Whether or not to take a snapshot before each compaction. Be +# careful using this option, since Cassandra won't clean up the +# snapshots for you. Mostly useful if you're paranoid when there +# is a data format change. +snapshot_before_compaction: false + +# Whether or not a snapshot is taken of the data before keyspace truncation +# or dropping of column families. The STRONGLY advised default of true +# should be used to provide data safety. If you set this flag to false, you will +# lose data on truncation or drop. +auto_snapshot: false + +# Add column indexes to a row after its contents reach this size. +# Increase if your column values are large, or if you have a very large +# number of columns. The competing causes are, Cassandra has to +# deserialize this much of the row to read a single column, so you want +# it to be small - at least if you do many partial-row reads - but all +# the index data is read for each access, so you don't want to generate +# that wastefully either. +column_index_size_in_kb: 64 + +# Size limit for rows being compacted in memory. Larger rows will spill +# over to disk and use a slower two-pass compaction process. A message +# will be logged specifying the row key. +#in_memory_compaction_limit_in_mb: 64 + +# Number of simultaneous compactions to allow, NOT including +# validation "compactions" for anti-entropy repair. Simultaneous +# compactions can help preserve read performance in a mixed read/write +# workload, by mitigating the tendency of small sstables to accumulate +# during a single long running compactions. The default is usually +# fine and if you experience problems with compaction running too +# slowly or too fast, you should look at +# compaction_throughput_mb_per_sec first. +# +# This setting has no effect on LeveledCompactionStrategy. +# +# concurrent_compactors defaults to the number of cores. +# Uncomment to make compaction mono-threaded, the pre-0.8 default. +#concurrent_compactors: 1 + +# Multi-threaded compaction. When enabled, each compaction will use +# up to one thread per core, plus one thread per sstable being merged. +# This is usually only useful for SSD-based hardware: otherwise, +# your concern is usually to get compaction to do LESS i/o (see: +# compaction_throughput_mb_per_sec), not more. +#multithreaded_compaction: false + +# Throttles compaction to the given total throughput across the entire +# system. The faster you insert data, the faster you need to compact in +# order to keep the sstable count down, but in general, setting this to +# 16 to 32 times the rate you are inserting data is more than sufficient. +# Setting this to 0 disables throttling. Note that this account for all types +# of compaction, including validation compaction. +compaction_throughput_mb_per_sec: 16 + +# Track cached row keys during compaction, and re-cache their new +# positions in the compacted sstable. Disable if you use really large +# key caches. +#compaction_preheat_key_cache: true + +# Throttles all outbound streaming file transfers on this node to the +# given total throughput in Mbps. This is necessary because Cassandra does +# mostly sequential IO when streaming data during bootstrap or repair, which +# can lead to saturating the network connection and degrading rpc performance. +# When unset, the default is 200 Mbps or 25 MB/s. +# stream_throughput_outbound_megabits_per_sec: 200 + +# How long the coordinator should wait for read operations to complete +read_request_timeout_in_ms: 5000 +# How long the coordinator should wait for seq or index scans to complete +range_request_timeout_in_ms: 10000 +# How long the coordinator should wait for writes to complete +write_request_timeout_in_ms: 2000 +# How long a coordinator should continue to retry a CAS operation +# that contends with other proposals for the same row +cas_contention_timeout_in_ms: 1000 +# How long the coordinator should wait for truncates to complete +# (This can be much longer, because unless auto_snapshot is disabled +# we need to flush first so we can snapshot before removing the data.) +truncate_request_timeout_in_ms: 60000 +# The default timeout for other, miscellaneous operations +request_timeout_in_ms: 10000 + +# Enable operation timeout information exchange between nodes to accurately +# measure request timeouts. If disabled, replicas will assume that requests +# were forwarded to them instantly by the coordinator, which means that +# under overload conditions we will waste that much extra time processing +# already-timed-out requests. +# +# Warning: before enabling this property make sure to ntp is installed +# and the times are synchronized between the nodes. +cross_node_timeout: false + +# Enable socket timeout for streaming operation. +# When a timeout occurs during streaming, streaming is retried from the start +# of the current file. This _can_ involve re-streaming an important amount of +# data, so you should avoid setting the value too low. +# Default value is 0, which never timeout streams. +# streaming_socket_timeout_in_ms: 0 + +# phi value that must be reached for a host to be marked down. +# most users should never need to adjust this. +# phi_convict_threshold: 8 + +# endpoint_snitch -- Set this to a class that implements +# IEndpointSnitch. The snitch has two functions: +# - it teaches Cassandra enough about your network topology to route +# requests efficiently +# - it allows Cassandra to spread replicas around your cluster to avoid +# correlated failures. It does this by grouping machines into +# "datacenters" and "racks." Cassandra will do its best not to have +# more than one replica on the same "rack" (which may not actually +# be a physical location) +# +# IF YOU CHANGE THE SNITCH AFTER DATA IS INSERTED INTO THE CLUSTER, +# YOU MUST RUN A FULL REPAIR, SINCE THE SNITCH AFFECTS WHERE REPLICAS +# ARE PLACED. +# +# Out of the box, Cassandra provides +# - SimpleSnitch: +# Treats Strategy order as proximity. This improves cache locality +# when disabling read repair, which can further improve throughput. +# Only appropriate for single-datacenter deployments. +# - PropertyFileSnitch: +# Proximity is determined by rack and data center, which are +# explicitly configured in cassandra-topology.properties. +# - RackInferringSnitch: +# Proximity is determined by rack and data center, which are +# assumed to correspond to the 3rd and 2nd octet of each node's +# IP address, respectively. Unless this happens to match your +# deployment conventions (as it did Facebook's), this is best used +# as an example of writing a custom Snitch class. +# - Ec2Snitch: +# Appropriate for EC2 deployments in a single Region. Loads Region +# and Availability Zone information from the EC2 API. The Region is +# treated as the Datacenter, and the Availability Zone as the rack. +# Only private IPs are used, so this will not work across multiple +# Regions. +# - Ec2MultiRegionSnitch: +# Uses public IPs as broadcast_address to allow cross-region +# connectivity. (Thus, you should set seed addresses to the public +# IP as well.) You will need to open the storage_port or +# ssl_storage_port on the public IP firewall. (For intra-Region +# traffic, Cassandra will switch to the private IP after +# establishing a connection.) +# +# You can use a custom Snitch by setting this to the full class name +# of the snitch, which will be assumed to be on your classpath. +endpoint_snitch: SimpleSnitch + +# controls how often to perform the more expensive part of host score +# calculation +dynamic_snitch_update_interval_in_ms: 100 +# controls how often to reset all host scores, allowing a bad host to +# possibly recover +dynamic_snitch_reset_interval_in_ms: 600000 +# if set greater than zero and read_repair_chance is < 1.0, this will allow +# 'pinning' of replicas to hosts in order to increase cache capacity. +# The badness threshold will control how much worse the pinned host has to be +# before the dynamic snitch will prefer other replicas over it. This is +# expressed as a double which represents a percentage. Thus, a value of +# 0.2 means Cassandra would continue to prefer the static snitch values +# until the pinned host was 20% worse than the fastest. +dynamic_snitch_badness_threshold: 0.1 + +# request_scheduler -- Set this to a class that implements +# RequestScheduler, which will schedule incoming client requests +# according to the specific policy. This is useful for multi-tenancy +# with a single Cassandra cluster. +# NOTE: This is specifically for requests from the client and does +# not affect inter node communication. +# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place +# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of +# client requests to a node with a separate queue for each +# request_scheduler_id. The scheduler is further customized by +# request_scheduler_options as described below. +request_scheduler: org.apache.cassandra.scheduler.NoScheduler + +# Scheduler Options vary based on the type of scheduler +# NoScheduler - Has no options +# RoundRobin +# - throttle_limit -- The throttle_limit is the number of in-flight +# requests per client. Requests beyond +# that limit are queued up until +# running requests can complete. +# The value of 80 here is twice the number of +# concurrent_reads + concurrent_writes. +# - default_weight -- default_weight is optional and allows for +# overriding the default which is 1. +# - weights -- Weights are optional and will default to 1 or the +# overridden default_weight. The weight translates into how +# many requests are handled during each turn of the +# RoundRobin, based on the scheduler id. +# +# request_scheduler_options: +# throttle_limit: 80 +# default_weight: 5 +# weights: +# Keyspace1: 1 +# Keyspace2: 5 + +# request_scheduler_id -- An identifer based on which to perform +# the request scheduling. Currently the only valid option is keyspace. +# request_scheduler_id: keyspace + +# index_interval controls the sampling of entries from the primrary +# row index in terms of space versus time. The larger the interval, +# the smaller and less effective the sampling will be. In technicial +# terms, the interval coresponds to the number of index entries that +# are skipped between taking each sample. All the sampled entries +# must fit in memory. Generally, a value between 128 and 512 here +# coupled with a large key cache size on CFs results in the best trade +# offs. This value is not often changed, however if you have many +# very small rows (many to an OS page), then increasing this will +# often lower memory usage without a impact on performance. +index_interval: 128 + +# Enable or disable inter-node encryption +# Default settings are TLS v1, RSA 1024-bit keys (it is imperative that +# users generate their own keys) TLS_RSA_WITH_AES_128_CBC_SHA as the cipher +# suite for authentication, key exchange and encryption of the actual data transfers. +# NOTE: No custom encryption options are enabled at the moment +# The available internode options are : all, none, dc, rack +# +# If set to dc cassandra will encrypt the traffic between the DCs +# If set to rack cassandra will encrypt the traffic between the racks +# +# The passwords used in these options must match the passwords used when generating +# the keystore and truststore. For instructions on generating these files, see: +# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore +# +encryption_options: + internode_encryption: none + keystore: conf/.keystore + keystore_password: cassandra + truststore: conf/.truststore + truststore_password: cassandra + # More advanced defaults below: + # protocol: TLS + # algorithm: SunX509 + # store_type: JKS + # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA] \ No newline at end of file diff --git a/ontology-engine/graph-core_2.11/src/test/resources/logback.xml b/ontology-engine/graph-core_2.11/src/test/resources/logback.xml new file mode 100644 index 000000000..73529d622 --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/test/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + + + + + + %d %msg%n + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ontology-engine/graph-core_2.11/src/test/scala/org/sunbird/graph/BaseSpec.scala b/ontology-engine/graph-core_2.11/src/test/scala/org/sunbird/graph/BaseSpec.scala new file mode 100644 index 000000000..36c38f6dd --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/test/scala/org/sunbird/graph/BaseSpec.scala @@ -0,0 +1,122 @@ +package org.sunbird.graph + +import java.io.File + +import com.datastax.driver.core.Session +import org.apache.commons.io.FileUtils +import org.cassandraunit.utils.EmbeddedCassandraServerHelper +import org.neo4j.graphdb.GraphDatabaseService +import org.neo4j.graphdb.factory.GraphDatabaseFactory +import org.neo4j.graphdb.factory.GraphDatabaseSettings.Connector.ConnectorType +import org.neo4j.kernel.configuration.BoltConnector +import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll, Matchers} +import org.sunbird.cassandra.CassandraConnector +import org.sunbird.common.Platform + +class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { + + var graphDb: GraphDatabaseService = null + var session: Session = null + implicit val oec: OntologyEngineContext = new OntologyEngineContext + + private val script_1 = "CREATE KEYSPACE IF NOT EXISTS content_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_2 = "CREATE TABLE IF NOT EXISTS content_store.content_data (content_id text, last_updated_on timestamp,body blob,oldBody blob,screenshots blob,stageIcons blob,externallink text,PRIMARY KEY (content_id));" + private val script_3 = "CREATE KEYSPACE IF NOT EXISTS hierarchy_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_4 = "CREATE TABLE IF NOT EXISTS hierarchy_store.content_hierarchy (identifier text, hierarchy text,PRIMARY KEY (identifier));" + private val script_5 = "CREATE KEYSPACE IF NOT EXISTS category_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_6 = "CREATE TABLE IF NOT EXISTS category_store.category_definition_data (identifier text, objectmetadata map, forms map ,PRIMARY KEY (identifier));" + private val script_7 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_content_all', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_8 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:course_collection_all', {'config': '{}', 'schema': '{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"}},\"default\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"additionalProperties\":false},\"additionalCategories\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"default\":\"Textbook\"}},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}'});" + private val script_9 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:course_content_all',{'config': '{}', 'schema': '{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"}},\"default\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"additionalProperties\":false},\"additionalCategories\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"default\":\"Textbook\"}},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}'});" + private val script_10 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_collection_all', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_11 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_content_in.ekstep', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_12 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_collection_in.ekstep', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + + + def setUpEmbeddedNeo4j(): Unit = { + if(null == graphDb) { + val bolt: BoltConnector = new BoltConnector("0") + println("GraphDB : " + Platform.config.getString("graph.dir")) + graphDb = new GraphDatabaseFactory() + .newEmbeddedDatabaseBuilder(new File(Platform.config.getString("graph.dir"))) + .setConfig(bolt.`type`, ConnectorType.BOLT.name()) + .setConfig(bolt.enabled, "true").setConfig(bolt.listen_address, "localhost:7687").newGraphDatabase + registerShutdownHook(graphDb) + } + } + + private def registerShutdownHook(graphDb: GraphDatabaseService): Unit = { + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = { + try { + tearEmbeddedNeo4JSetup + System.out.println("cleanup Done!!") + } catch { + case e: Exception => + e.printStackTrace() + } + } + }) + } + + + @throws[Exception] + private def tearEmbeddedNeo4JSetup(): Unit = { + if (null != graphDb) graphDb.shutdown + Thread.sleep(2000) + deleteEmbeddedNeo4j(new File(Platform.config.getString("graph.dir"))) + } + + private def deleteEmbeddedNeo4j(emDb: File): Unit = { + try{ + if(emDb.exists() && emDb.isDirectory) + FileUtils.deleteDirectory(emDb) + }catch{ + case e: Exception => + e.printStackTrace() + } + } + + + def setUpEmbeddedCassandra(): Unit = { + System.setProperty("cassandra.unsafesystem", "true") + EmbeddedCassandraServerHelper.startEmbeddedCassandra("/cassandra-unit.yaml", 100000L) + } + + override def beforeAll(): Unit = { + tearEmbeddedNeo4JSetup() + setUpEmbeddedNeo4j() + setUpEmbeddedCassandra() + executeNeo4jQuery("UNWIND [{identifier:\"obj-cat:course_collection_all\",name:\"LearningResource\",description:\"Learning resource\",categoryId:\"obj-cat:course\",targetObjectType:\"Collection\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"default\\\":{\\\"enabled\\\":\\\"Yes\\\",\\\"autoBatch\\\":\\\"Yes\\\"},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:course_collection_all\"},{identifier:\"obj-cat:learning-resource_content_all\",name:\"LearningResource\",description:\"Learning resource\",categoryId:\"obj-cat:learningresource\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"default\\\":{\\\"enabled\\\":\\\"Yes\\\",\\\"autoBatch\\\":\\\"Yes\\\"},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:learning-resource_content_all\"},{identifier:\"obj-cat:learning-resource_content_all\",name:\"LearningResource\",description:\"Learning resource\",categoryId:\"obj-cat:learningresource\",targetObjectType:\"Collection\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"default\\\":{\\\"enabled\\\":\\\"Yes\\\",\\\"autoBatch\\\":\\\"Yes\\\"},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:learning-resource_collection_all\"}] as row CREATE (n:domain) SET n += row;") + executeCassandraQuery(script_1, script_2, script_3, script_4, script_5, script_6, script_7, script_8, script_9, script_10, script_11, script_12) + } + + override def afterAll(): Unit = { + tearEmbeddedNeo4JSetup() + if(null != session && !session.isClosed) + session.close() + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() + } + + + def executeCassandraQuery(queries: String*): Unit = { + if(null == session || session.isClosed){ + session = CassandraConnector.getSession + } + for(query <- queries) { + session.execute(query) + } + } + + def createRelationData(): Unit = { + graphDb.execute("UNWIND [{identifier:\"Num:C3:SC2\",code:\"Num:C3:SC2\",keywords:[\"Subconcept\",\"Class 3\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",subject:\"numeracy\",channel:\"in.ekstep\",description:\"Multiplication\",versionKey:\"1484389136575\",gradeLevel:[\"Grade 3\",\"Grade 4\"],IL_FUNC_OBJECT_TYPE:\"Concept\",name:\"Multiplication\",lastUpdatedOn:\"2016-06-15T17:15:45.951+0000\",IL_UNIQUE_ID:\"Num:C3:SC2\",status:\"Live\"}, {code:\"31d521da-61de-4220-9277-21ca7ce8335c\",previewUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",downloadUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",channel:\"in.ekstep\",language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790848197_do_11232724509261824014_2.0_spine.ecar\\\",\\\"size\\\":890.0}}\",mimeType:\"application/pdf\",streamingUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",idealScreenSize:\"normal\",createdOn:\"2017-09-07T13:24:20.720+0000\",contentDisposition:\"inline\",artifactUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",contentEncoding:\"identity\",lastUpdatedOn:\"2017-09-07T13:25:53.595+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2017-09-07T13:27:28.417+0000\",contentType:\"Resource\",lastUpdatedBy:\"Ekstep\",audience:[\"Student\"],visibility:\"Default\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",consumerId:\"e84015d2-a541-4c07-a53f-e31d4553312b\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",pkgVersion:2,versionKey:\"1504790848417\",license:\"Creative Commons Attribution (CC BY)\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",size:4864851,lastPublishedOn:\"2017-09-07T13:27:27.410+0000\",createdBy:\"390\",compatibilityLevel:4,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Untitled Content\",publisher:\"EkStep\",IL_UNIQUE_ID:\"do_11232724509261824014\",status:\"Live\",resourceType:[\"Study material\"]}] as row CREATE (n:domain) SET n += row") + } + + def createBulkNodes(): Unit ={ + graphDb.execute("UNWIND [{nodeId:'do_0000123'},{nodeId:'do_0000234'},{nodeId:'do_0000345'}] as row with row.nodeId as Id CREATE (n:domain{IL_UNIQUE_ID:Id});") + } + + def executeNeo4jQuery(query: String): Unit = { + graphDb.execute(query) + } +} diff --git a/ontology-engine/graph-core_2.11/src/test/scala/org/sunbird/graph/external/ExternalPropsManagerTest.scala b/ontology-engine/graph-core_2.11/src/test/scala/org/sunbird/graph/external/ExternalPropsManagerTest.scala new file mode 100644 index 000000000..723289473 --- /dev/null +++ b/ontology-engine/graph-core_2.11/src/test/scala/org/sunbird/graph/external/ExternalPropsManagerTest.scala @@ -0,0 +1,141 @@ +package org.sunbird.graph.external + +import java.util +import org.sunbird.graph.BaseSpec +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.dto.{Request, Response} +import org.sunbird.common.exception.{ ResponseCode} + +import scala.concurrent.Future + +class ExternalPropsManagerTest extends BaseSpec { + + def getContextMap(): java.util.Map[String, AnyRef] = { + new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "ObjectCategoryDefinition") + put("schemaName", "objectcategorydefinition") + } + } + } + + "saveProps" should "create a cassandra record successfully" in { + val request = new Request() + request.setObjectType("ObjectCategoryDefinition") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "ObjectCategoryDefinition") + put("schemaName", "objectcategorydefinition") + } + }) + request.put("identifier", "obj-cat:test_content_all") + request.put("objectMetadata", new util.HashMap[String, AnyRef]() { + { + put("schema", "{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}},\"default\":{\"enabled\":\"Yes\",\"autoBatch\":\"Yes\"},\"additionalProperties\":false},\"monitorable\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"progress-report\",\"score-report\"]}},\"credentials\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}},\"default\":{\"enabled\":\"Yes\"},\"additionalProperties\":false},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}") + put("config", "{}") + } + }) + + val future: Future[Response] = ExternalPropsManager.saveProps(request) + future map { response => { + assert(null != response) + assert(response.getResponseCode == ResponseCode.OK) + } + } + } + + "fetchProps" should "read a cassandra record successfully" in { + val request = new Request() + request.setObjectType("ObjectCategoryDefinition") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "ObjectCategoryDefinition") + put("schemaName", "objectcategorydefinition") + } + }) + request.put("identifier", "obj-cat:course_collection_all") + val future: Future[Response] = ExternalPropsManager.fetchProps(request, List("objectMetadata")) + future map { response => { + assert(null != response) + assert(response.getResponseCode == ResponseCode.OK) + assert(StringUtils.isNotBlank(response.getResult.get("objectMetadata").asInstanceOf[util.Map[String, AnyRef]].get("config").asInstanceOf[String])) + } + } + } + + "fetchProps" should "read list cassandra record successfully" in { + val request = new Request() + request.setObjectType("ObjectCategoryDefinition") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "ObjectCategoryDefinition") + put("schemaName", "objectcategorydefinition") + } + }) + request.put("identifiers", List("obj-cat:course_collection_all")) + val future: Future[Response] = ExternalPropsManager.fetchProps(request, List("objectMetadata")) + future map { response => { + assert(null != response) + assert(response.getResponseCode == ResponseCode.OK) + assert(StringUtils.isNotBlank(response.getResult.get("obj-cat:course_collection_all").asInstanceOf[util.Map[String, AnyRef]].get("objectMetadata").asInstanceOf[util.Map[String, AnyRef]].get("config").asInstanceOf[String])) + } + } + } + + "deleteProps" should "delete a cassandra record successfully" in { + val request = new Request() + request.setObjectType("ObjectCategoryDefinition") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "ObjectCategoryDefinition") + put("schemaName", "objectcategorydefinition") + } + }) + request.put("identifiers", List("obj-cat:course_content_all")) + val future: Future[Response] = ExternalPropsManager.deleteProps(request) + future map { response => { + assert(null != response) + assert(response.getResponseCode == ResponseCode.OK) + } + } + } + + "update" should "update a cassandra record successfully" in { + val request = new Request() + request.setObjectType("ObjectCategoryDefinition") + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "ObjectCategoryDefinition") + put("schemaName", "objectcategorydefinition") + } + }) + val objectMetadata = new util.HashMap[String, AnyRef]() {{ + put("schema", "{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}},\"default\":{\"enabled\":\"Yes\",\"autoBatch\":\"Yes\"},\"additionalProperties\":false},\"monitorable\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"progress-report\",\"score-report\"]}},\"credentials\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}},\"default\":{\"enabled\":\"Yes\"},\"additionalProperties\":false},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}") + put("config", "{}") + }} + request.put("identifier", "obj-cat:course_collection_all") + request.put("fields", List("objectMetadata")) + request.put("values", List(objectMetadata)) + val future: Future[Response] = ExternalPropsManager.update(request) + future map { response => { + assert(null != response) + assert(response.getResponseCode == ResponseCode.OK) + } + } + } + +} + + diff --git a/ontology-engine/graph-dac-api/pom.xml b/ontology-engine/graph-dac-api/pom.xml index 22ae6130e..62669e72e 100644 --- a/ontology-engine/graph-dac-api/pom.xml +++ b/ontology-engine/graph-dac-api/pom.xml @@ -26,14 +26,20 @@ org.scala-lang.modules - scala-java8-compat_2.11 + scala-java8-compat_${scala.maj.version} 0.9.0 org.neo4j neo4j-graphdb-api - 3.0.3 + 3.5.0 + + org.neo4j + neo4j + 3.5.0 + test + org.powermock powermock-api-mockito @@ -46,6 +52,43 @@ 1.7.4 test - + + org.neo4j + neo4j-bolt + 3.5.0 + test + + + org.neo4j + neo4j-cypher + 3.5.0 + test + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + + \ No newline at end of file diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/enums/SystemNodeTypes.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/enums/SystemNodeTypes.java index ccdfec278..fe75518f8 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/enums/SystemNodeTypes.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/enums/SystemNodeTypes.java @@ -2,5 +2,5 @@ public enum SystemNodeTypes { - DATA_NODE, DEFINITION_NODE, SET; + DATA_NODE, DEFINITION_NODE, SET, ROOT_NODE; } diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Node.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Node.java index f9d2862f2..2be21b5bf 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Node.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Node.java @@ -1,6 +1,7 @@ package org.sunbird.graph.dac.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.sunbird.graph.common.enums.SystemProperties; @@ -131,7 +132,9 @@ public List getAddedRelations() { } public void setAddedRelations(List addedRelations) { - this.addedRelations.addAll(addedRelations); + if(CollectionUtils.isEmpty(this.addedRelations)) + this.addedRelations = new ArrayList<>(); + this.addedRelations.addAll(addedRelations); } public List getDeletedRelations() { diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Relation.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Relation.java index 7456a3a20..097ee969b 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Relation.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/dac/model/Relation.java @@ -1,6 +1,7 @@ package org.sunbird.graph.dac.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.neo4j.driver.v1.Value; import org.neo4j.graphdb.Node; @@ -230,8 +231,18 @@ public void setEndNodeId(String endNodeId) { this.endNodeId = endNodeId; } + // TODO: In 3.0 if metadata is empty set it with new HashMap and return (to handle NPE. +// public Map getMetadata() { +// if (MapUtils.isEmpty(metadata)) +// metadata = new HashMap(); +// return metadata; +// } + public Map getMetadata() { - return metadata; + if (!MapUtils.isEmpty(metadata)) + return metadata; + else + return new HashMap(); } public void setMetadata(Map metadata) { diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/GraphAsyncOperations.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/GraphAsyncOperations.java index 7f45a465e..8ec0f6beb 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/GraphAsyncOperations.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/GraphAsyncOperations.java @@ -45,6 +45,7 @@ public static Future createRelation(String graphId, List fn.singleAsync()).thenApply(record->{ return ResponseHandler.OK(); }).exceptionally(error -> { + error.printStackTrace(); throw new ServerException(DACErrorCodeConstants.SERVER_ERROR.name(), "Error! Something went wrong while creating node object. ", error.getCause()); }); @@ -71,7 +72,6 @@ public static Future removeRelation(String graphId, List dataMap = new HashMap(){{ put("data",relationData); }}; @@ -80,6 +80,7 @@ public static Future removeRelation(String graphId, List fn.singleAsync()).thenApply(record->{ return ResponseHandler.OK(); }).exceptionally(error -> { + error.printStackTrace(); throw new ServerException(DACErrorCodeConstants.SERVER_ERROR.name(), "Error! Something went wrong while creating node object. ", error.getCause()); }); diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/Neo4JBoltSearchOperations.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/Neo4JBoltSearchOperations.java index 576c190ea..57b531246 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/Neo4JBoltSearchOperations.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/Neo4JBoltSearchOperations.java @@ -2,20 +2,10 @@ import org.apache.commons.lang3.StringUtils; import org.neo4j.driver.v1.Driver; -import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; -import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.ClientException; -import org.sunbird.common.dto.Property; -import org.sunbird.common.dto.Request; -import org.sunbird.common.exception.ResourceNotFoundException; import org.sunbird.graph.common.enums.GraphDACParams; -import org.sunbird.graph.dac.model.Node; -import org.sunbird.graph.dac.model.Relation; -import org.sunbird.graph.dac.model.SearchCriteria; -import org.sunbird.graph.dac.util.Neo4jNodeUtil; -import org.sunbird.graph.service.common.CypherQueryConfigurationConstants; import org.sunbird.graph.service.common.DACErrorCodeConstants; import org.sunbird.graph.service.common.DACErrorMessageConstants; import org.sunbird.graph.service.common.GraphOperation; @@ -23,563 +13,12 @@ import org.sunbird.graph.service.util.SearchQueryGenerationUtil; import org.sunbird.telemetry.logger.TelemetryManager; -import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; public class Neo4JBoltSearchOperations { - /** - * Gets the node by id. - * - * @param graphId - * the graph id - * @param nodeId - * the node id - * @param getTags - * the get tags - * @param request - * the request - * @return the node by id - */ - public static Node getNodeById(String graphId, Long nodeId, Boolean getTags, Request request) { - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get Node By Id' Operation Failed.]"); - - if (nodeId == 0) - throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), - DACErrorMessageConstants.INVALID_NODE_ID + " | ['Get Node By Id' Operation Failed.]"); - - Node node = new Node(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.nodeId.name(), nodeId); - parameterMap.put(GraphDACParams.getTags.name(), getTags); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session - .run(SearchQueryGenerationUtil.generateGetNodeByIdCypherQuery(parameterMap)); - if (null == result || !result.hasNext()) - throw new ResourceNotFoundException(DACErrorCodeConstants.NOT_FOUND.name(), - DACErrorMessageConstants.NODE_NOT_FOUND + " | [Invalid Node Id.]"); - - Map nodeMap = new HashMap(); - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - for (Record record : result.list()) { - TelemetryManager.log("'Get Node By Id' Operation Finished.", record.asMap()); - if (null != record) - getRecordValues(record, nodeMap, relationMap, startNodeMap, endNodeMap); - } - - if (!nodeMap.isEmpty()) { - for (Entry entry : nodeMap.entrySet()) -// node = new Node(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, -// startNodeMap, endNodeMap); - node= Neo4jNodeUtil.getNode(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, - startNodeMap, endNodeMap); - } - } - TelemetryManager.log("Returning Node By Id: ", node.getMetadata()); - return node; - } - - /** - * Gets the node by unique id. - * - * @param graphId - * the graph id - * @param nodeId - * the node id - * @param getTags - * the get tags - * @param request - * the request - * @return the node by unique id - */ - public static Node getNodeByUniqueId(String graphId, String nodeId, Boolean getTags, Request request) { - TelemetryManager.log("Graph Id: " + graphId + "\nNode Id: " + nodeId + "\nGet Tags:" + getTags); - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get Node By Unique Id' Operation Failed.]"); - - if (StringUtils.isBlank(nodeId)) - throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), - DACErrorMessageConstants.INVALID_IDENTIFIER + " | ['Get Node By Unique Id' Operation Failed.]"); - - - Node node = null; //(Node) NodeCacheManager.getDataNode(graphId, nodeId); - if (null != node) { - TelemetryManager.info("Fetched node from in-memory cache: "+node.getIdentifier()); - return node; - } else { - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.nodeId.name(), nodeId); - parameterMap.put(GraphDACParams.getTags.name(), getTags); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session - .run(SearchQueryGenerationUtil.generateGetNodeByUniqueIdCypherQuery(parameterMap)); - if (null == result || !result.hasNext()) - throw new ResourceNotFoundException(DACErrorCodeConstants.NOT_FOUND.name(), - DACErrorMessageConstants.NODE_NOT_FOUND + " | [Invalid Node Id.]: " + nodeId, nodeId); - - Map nodeMap = new HashMap(); - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - for (Record record : result.list()) { - TelemetryManager.log("'Get Node By Unique Id' Operation Finished.", record.asMap()); - if (null != record) - getRecordValues(record, nodeMap, relationMap, startNodeMap, endNodeMap); - } - - if (!nodeMap.isEmpty()) { - for (Entry entry : nodeMap.entrySet()) -// node = new Node(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, -// startNodeMap, endNodeMap); - node= Neo4jNodeUtil.getNode(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, - startNodeMap, endNodeMap); - } - if (StringUtils.equalsIgnoreCase("Concept", node.getObjectType())) { - TelemetryManager.info("Saving concept to in-memory cache: "+node.getIdentifier()); -// NodeCacheManager.saveDataNode(graphId, node.getIdentifier(), node); - } - } - return node; - } - } - - /** - * Gets the nodes by property. - * - * @param graphId - * the graph id - * @param property - * the property - * @param getTags - * the get tags - * @param request - * the request - * @return the nodes by property - */ - public static List getNodesByProperty(String graphId, Property property, Boolean getTags, Request request) { - TelemetryManager.log("Graph Id: " + graphId + "\nProperty: " + property + "\nGet Tags:" + getTags); - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get Nodes By Property' Operation Failed.]"); - - if (null == property) - throw new ClientException(DACErrorCodeConstants.INVALID_PROPERTY.name(), - DACErrorMessageConstants.INVALID_PROPERTY + " | ['Get Nodes By Property' Operation Failed.]"); - - List nodes = new ArrayList(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.property.name(), property); - parameterMap.put(GraphDACParams.getTags.name(), getTags); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session - .run(SearchQueryGenerationUtil.generateGetNodesByPropertyCypherQuery(parameterMap)); - Map nodeMap = new HashMap(); - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - if (null != result) { - for (Record record : result.list()) { - TelemetryManager.log("'Get Nodes By Property Id' Operation Finished.", record.asMap()); - if (null != record) - getRecordValues(record, nodeMap, relationMap, startNodeMap, endNodeMap); - } - } - - if (!nodeMap.isEmpty()) { - for (Entry entry : nodeMap.entrySet()) { -// nodes.add(new Node(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, -// startNodeMap, endNodeMap)); - nodes.add(Neo4jNodeUtil.getNode(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, - startNodeMap, endNodeMap)); - } - - } - } - TelemetryManager.log("Returning Node By Property: " + nodes.size()); - return nodes; - } - - public static List getNodeByUniqueIds(String graphId, SearchCriteria searchCriteria) { - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID - + " | ['Get Nodes By Search Criteria' Operation Failed.]"); - - if (null == searchCriteria) - throw new ClientException(DACErrorCodeConstants.INVALID_CRITERIA.name(), - DACErrorMessageConstants.INVALID_SEARCH_CRITERIA - + " | ['Get Nodes By Search Criteria' Operation Failed.]"); - - List nodes = new ArrayList(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.searchCriteria.name(), searchCriteria); - Map nodeMap = new HashMap(); - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - String query = SearchQueryGenerationUtil.generateGetNodeByUniqueIdsCypherQuery(parameterMap); - Map params = searchCriteria.getParams(); - StatementResult result = session.run(query, params); - - if (null != result) { - for (Record record : result.list()) { - TelemetryManager.log("'Get Nodes By Search Criteria' Operation Finished.", record.asMap()); - if (null != record) - getRecordValues(record, nodeMap, relationMap, startNodeMap, endNodeMap); - } - } - - if (!nodeMap.isEmpty()) { - for (Entry entry : nodeMap.entrySet()) { -// nodes.add(new Node(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, -// startNodeMap, endNodeMap)); - nodes.add(Neo4jNodeUtil.getNode(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, - startNodeMap, endNodeMap)); - } - } - } - TelemetryManager.log("Returning Node By Search Criteria: " + nodes.size()); - return nodes; - } - - public static List> executeQueryForProps(String graphId, String query, List propKeys) { - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Execute Query For Nodes' Operation Failed.]"); - List> propsList = new ArrayList>(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - - StatementResult result = session.run(query); - if (null != result) { - for (Record record : result.list()) { - if (null != record) { - Map row = new HashMap(); - for (int i = 0; i < propKeys.size(); i++) { - String key = propKeys.get(i); - Value value = record.get(key); - if (null != value) - row.put(key, value.asObject()); - } - if (!row.isEmpty()) - propsList.add(row); - } - } - } - } - return propsList; - } - - /** - * Gets the all nodes. - * - * @param graphId - * the graph id - * @param request - * the request - * @return the all nodes - */ - public static List getAllNodes(String graphId, Request request) { - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get All Nodes' Operation Failed.]"); - - List nodes = new ArrayList(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session - .run(SearchQueryGenerationUtil.generateGetAllNodesCypherQuery(parameterMap)); - Map nodeMap = new HashMap(); - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - if (null != result) { - for (Record record : result.list()) { - TelemetryManager.log("'Get All Nodes' Operation Finished.", record.asMap()); - if (null != record) - getRecordValues(record, nodeMap, relationMap, startNodeMap, endNodeMap); - } - } - - if (!nodeMap.isEmpty()) { - for (Entry entry : nodeMap.entrySet()) { -// nodes.add(new Node(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, -// startNodeMap, endNodeMap)); - nodes.add(Neo4jNodeUtil.getNode(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, - startNodeMap, endNodeMap)); - } - } - } - TelemetryManager.log("Returning All Nodes: " + nodes.size()); - return nodes; - } - - /** - * Gets the all relations. - * - * @param graphId - * the graph id - * @param request - * the request - * @return the all relations - */ - public static List getAllRelations(String graphId, Request request) { - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get All Relations' Operation Failed.]"); - - List relations = new ArrayList(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session - .run(SearchQueryGenerationUtil.generateGetAllRelationsCypherQuery(parameterMap)); - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - if (null != result) { - for (Record record : result.list()) { - TelemetryManager.log("'Get All Relations' Operation Finished.", record.asMap()); - if (null != record) - getRecordValues(record, null, relationMap, startNodeMap, endNodeMap); - } - } - TelemetryManager.log("Relation Map: " + relationMap + "\nStart Node Map: " + startNodeMap + "\nEnd Node Map: " - + endNodeMap); - - if (!relationMap.isEmpty()) { - for (Entry entry : relationMap.entrySet()) - relations.add(new Relation(graphId, (org.neo4j.driver.v1.types.Relationship) entry.getValue(), - startNodeMap, endNodeMap)); - } - } - TelemetryManager.log("Returning All Relations: " + relations.size()); - return relations; - } - - /** - * Gets the relation property. - * - * @param graphId - * the graph id - * - * @param request - * the request - * @return the relation property - */ - public static Property getRelationProperty(String graphId, String startNodeId, String relationType, - String endNodeId, - String key, Request request) { - TelemetryManager.log("Graph Id: " + graphId + "\nStart Node Id: " + startNodeId + "\nRelation Type: " - + relationType + "\nEnd Node Id: " + endNodeId + "\nProperty (Key): " + key); - - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get Relation Property' Operation Failed.]"); - - if (StringUtils.isBlank(startNodeId)) - throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), - DACErrorMessageConstants.INVALID_START_NODE_ID + " | ['Get Relation Property' Operation Failed.]"); - - if (StringUtils.isBlank(relationType)) - throw new ClientException(DACErrorCodeConstants.INVALID_RELATION.name(), - DACErrorMessageConstants.INVALID_RELATION_TYPE + " | ['Get Relation Property' Operation Failed.]"); - - if (StringUtils.isBlank(endNodeId)) - throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), - DACErrorMessageConstants.INVALID_END_NODE_ID + " | ['Get Relation Property' Operation Failed.]"); - - if (StringUtils.isBlank(key)) - throw new ClientException(DACErrorCodeConstants.INVALID_PROPERTY.name(), - DACErrorMessageConstants.INVALID_PROPERTY_KEY + " | ['Get Relation Property' Operation Failed.]"); - - Property property = new Property(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.startNodeId.name(), startNodeId); - parameterMap.put(GraphDACParams.relationType.name(), relationType); - parameterMap.put(GraphDACParams.endNodeId.name(), endNodeId); - parameterMap.put(GraphDACParams.key.name(), key); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session - .run(SearchQueryGenerationUtil.generateGetRelationPropertyCypherQuery(parameterMap)); - if (null != result) { - for (Record record : result.list()) { - TelemetryManager.log("'Get Relation Property' Operation Finished.", record.asMap()); - if (null != record && null != record.get(key)) { - property.setPropertyName(key); - property.setPropertyValue(record.get(key)); - } - } - } - } - return property; - } - - public static Relation getRelationById(String graphId, Long relationId, Request request) { - TelemetryManager.log("Graph Id: " + graphId + "\nRelation Id: " + relationId); - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get Relation By Id' Operation Failed.]"); - - if (null == relationId || relationId < 0) - throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), - DACErrorMessageConstants.INVALID_IDENTIFIER + " | ['Get Relation' Operation Failed.]"); - - Relation relation = new Relation(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.identifier.name(), relationId); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session - .run(SearchQueryGenerationUtil.generateGetRelationByIdCypherQuery(parameterMap)); - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - if (null != result) { - for (Record record : result.list()) { - TelemetryManager.log("'Get Relation' Operation Finished.", record.asMap()); - if (null != record) - getRecordValues(record, null, relationMap, startNodeMap, endNodeMap); - } - } - TelemetryManager.log("Relation Map: " + relationMap + "\nStart Node Map: " + startNodeMap + "\nEnd Node Map: " - + endNodeMap); - - if (!relationMap.isEmpty()) { - for (Entry entry : relationMap.entrySet()) - relation = new Relation(graphId, (org.neo4j.driver.v1.types.Relationship) entry.getValue(), - startNodeMap, endNodeMap); - } - } - return relation; - } - - /** - * Gets the relation. - * - * @param graphId - * the graph id - * @param startNodeId - * the start node id - * @param relationType - * the relation type - * @param endNodeId - * the end node id - * @param request - * the request - * @return the relation - */ - public static Relation getRelation(String graphId, String startNodeId, String relationType, String endNodeId, - Request request) { - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get Relation' Operation Failed.]"); - - if (StringUtils.isBlank(startNodeId)) - throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), - DACErrorMessageConstants.INVALID_START_NODE_ID + " | ['Get Relation' Operation Failed.]"); - - if (StringUtils.isBlank(relationType)) - throw new ClientException(DACErrorCodeConstants.INVALID_RELATION.name(), - DACErrorMessageConstants.INVALID_RELATION_TYPE + " | ['Get Relation' Operation Failed.]"); - - if (StringUtils.isBlank(endNodeId)) - throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), - DACErrorMessageConstants.INVALID_END_NODE_ID + " | ['Get Relation' Operation Failed.]"); - - Relation relation = new Relation(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.startNodeId.name(), startNodeId); - parameterMap.put(GraphDACParams.relationType.name(), relationType); - parameterMap.put(GraphDACParams.endNodeId.name(), endNodeId); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session - .run(SearchQueryGenerationUtil.generateGetRelationCypherQuery(parameterMap)); - if (null == result || !result.hasNext()) - throw new ResourceNotFoundException(DACErrorCodeConstants.NOT_FOUND.name(), - DACErrorMessageConstants.NODE_NOT_FOUND + " | [No Relation found.]"); - - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - for (Record record : result.list()) { - TelemetryManager.log("'Get Relation' Operation Finished.", record.asMap()); - if (null != record) - getRecordValues(record, null, relationMap, startNodeMap, endNodeMap); - } - TelemetryManager.log("Relation Map: " + relationMap + "\nStart Node Map: " + startNodeMap + "\nEnd Node Map: " - + endNodeMap); - - if (!relationMap.isEmpty()) { - for (Entry entry : relationMap.entrySet()) - relation = new Relation(graphId, (org.neo4j.driver.v1.types.Relationship) entry.getValue(), - startNodeMap, endNodeMap); - } - } - return relation; - } - /** * Check cyclic loop. * @@ -640,225 +79,4 @@ public static Map checkCyclicLoop(String graphId, String startNo return cyclicLoopMap; } - /** - * Execute query. - * - * @param graphId - * the graph id - * @param query - * the query - * @param paramMap - * the param map - * @param request - * the request - * @return the list - */ - public static List> executeQuery(String graphId, String query, Map paramMap, - Request request) { - TelemetryManager.log("Graph Id: " + graphId + "\nQuery: " + query + "\nParam Map: ", paramMap); - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Execute Query' Operation Failed.]"); - - if (StringUtils.isBlank(query)) - throw new ClientException(DACErrorCodeConstants.INVALID_QUERY.name(), - DACErrorMessageConstants.INVALID_QUERY + " | ['Execute Query' Operation Failed.]"); - - if (null == paramMap || paramMap.isEmpty()) - throw new ClientException(DACErrorCodeConstants.INVALID_PARAMETER.name(), - DACErrorMessageConstants.INVALID_PARAM_MAP + " | ['Execute Query' Operation Failed.]"); - - List> resultList = new ArrayList>(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - TelemetryManager.log("Session Initialised. | [Graph Id: " + graphId + "]"); - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.cypherQuery.name(), query); - parameterMap.put(GraphDACParams.paramMap.name(), paramMap); - parameterMap.put(GraphDACParams.request.name(), request); - - StatementResult result = session.run(SearchQueryGenerationUtil.generateExecuteQueryCypherQuery(parameterMap), - paramMap); - for (Record record : result.list()) { - TelemetryManager.log("'Execute Query' Operation Finished.", record.asMap()); - Map recordMap = record.asMap(); - Map map = new HashMap(); - if (null != recordMap && !recordMap.isEmpty()) { - for (Entry entry : recordMap.entrySet()) { - map.put(entry.getKey(), entry.getValue()); - } - resultList.add(map); - } - } - } - TelemetryManager.log("Returning Execute Query Result: "+ resultList.size()); - return resultList; - } - - /** - * Search nodes. - * - * @param graphId - * the graph id - * @param searchCriteria - * the search criteria - * @param getTags - * the get tags - * @param request - * the request - * @return the list - */ - public static List searchNodes(String graphId, SearchCriteria searchCriteria, Boolean getTags, - Request request) { - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Search Nodes' Operation Failed.]"); - - if (null == searchCriteria) - throw new ClientException(DACErrorCodeConstants.INVALID_CRITERIA.name(), - DACErrorMessageConstants.INVALID_SEARCH_CRITERIA + " | ['Search Nodes' Operation Failed.]"); - - List nodes = new ArrayList(); - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - TelemetryManager.log("Session Initialised. | [Graph Id: " + graphId + "]"); - List fields = searchCriteria.getFields(); - boolean returnNode = true; - if (null != fields && !fields.isEmpty()) - returnNode = false; - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.searchCriteria.name(), searchCriteria); - parameterMap.put(GraphDACParams.getTags.name(), getTags); - parameterMap.put(GraphDACParams.request.name(), request); - - String query = SearchQueryGenerationUtil.generateSearchNodesCypherQuery(parameterMap); - TelemetryManager.log("Search Query: " + query); - Map params = searchCriteria.getParams(); - TelemetryManager.log("Search Params: " + params); - StatementResult result = session.run(query, params); - Map nodeMap = new LinkedHashMap(); - Map relationMap = new HashMap(); - Map startNodeMap = new HashMap(); - Map endNodeMap = new HashMap(); - if (null != result) { - TelemetryManager.log("'Search Nodes' result: " + result); - for (Record record : result.list()) { - TelemetryManager.log("'Search Nodes' Operation Finished.", record.asMap()); - if (null != record) { - if (returnNode) - getRecordValues(record, nodeMap, relationMap, startNodeMap, endNodeMap); - else { - Node node = new Node(graphId, record.asMap()); - nodes.add(node); - } - } - } - } - TelemetryManager.log("Node Map: " + nodeMap + "\nRelation Map: " + relationMap + "\nStart Node Map: " - + startNodeMap + "\nEnd Node Map: " + endNodeMap); - - if (!nodeMap.isEmpty()) { - for (Entry entry : nodeMap.entrySet()) { -// nodes.add(new Node(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, -// startNodeMap, endNodeMap)); - nodes.add(Neo4jNodeUtil.getNode(graphId, (org.neo4j.driver.v1.types.Node) entry.getValue(), relationMap, - startNodeMap, endNodeMap)); - } - } - } - TelemetryManager.log("Returning Search Nodes: " + nodes); - return nodes; - } - - /** - * Gets the nodes count. - * - * @param graphId - * the graph id - * @param searchCriteria - * the search criteria - * @param request - * the request - * @return the nodes count - */ - public static Long getNodesCount(String graphId, SearchCriteria searchCriteria, Request request) { - - if (StringUtils.isBlank(graphId)) - throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), - DACErrorMessageConstants.INVALID_GRAPH_ID + " | ['Get Nodes Count' Operation Failed.]"); - - if (null == searchCriteria) - throw new ClientException(DACErrorCodeConstants.INVALID_CRITERIA.name(), - DACErrorMessageConstants.INVALID_SEARCH_CRITERIA + " | ['Get Nodes Count' Operation Failed.]"); - - Long count = (long) 0; - Driver driver = DriverUtil.getDriver(graphId, GraphOperation.READ); - TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); - try (Session session = driver.session()) { - TelemetryManager.log("Session Initialised. | [Graph Id: " + graphId + "]"); - - searchCriteria.setCountQuery(true); - Map parameterMap = new HashMap(); - parameterMap.put(GraphDACParams.graphId.name(), graphId); - parameterMap.put(GraphDACParams.searchCriteria.name(), searchCriteria); - parameterMap.put(GraphDACParams.request.name(), request); - - String query = SearchQueryGenerationUtil.generateGetNodesCountCypherQuery(parameterMap); - Map params = searchCriteria.getParams(); - StatementResult result = session.run(query, params); - if (null != result) { - for (Record record : result.list()) { - TelemetryManager.log("'Get Nodes Count' Operation Finished.", record.asMap()); - if (null != record && null != record.get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_COUNT_OBJECT)) - count = record.get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_COUNT_OBJECT).asLong(); - } - } - } - TelemetryManager.log("Returning Nodes Count: " + count); - return count; - } - - - private static void getRecordValues(Record record, Map nodeMap, Map relationMap, - Map startNodeMap, Map endNodeMap) { - if (null != nodeMap) { - Value nodeValue = record.get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_NODE_OBJECT); - if (null != nodeValue && StringUtils.equalsIgnoreCase("NODE", nodeValue.type().name())) { - org.neo4j.driver.v1.types.Node neo4jBoltNode = record - .get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_NODE_OBJECT).asNode(); - nodeMap.put(neo4jBoltNode.id(), neo4jBoltNode); - } - } - if (null != relationMap) { - Value relValue = record.get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_RELATION_OBJECT); - if (null != relValue && StringUtils.equalsIgnoreCase("RELATIONSHIP", relValue.type().name())) { - org.neo4j.driver.v1.types.Relationship relationship = record - .get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_RELATION_OBJECT).asRelationship(); - relationMap.put(relationship.id(), relationship); - } - } - if (null != startNodeMap) { - Value startNodeValue = record.get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_START_NODE_OBJECT); - if (null != startNodeValue && StringUtils.equalsIgnoreCase("NODE", startNodeValue.type().name())) { - org.neo4j.driver.v1.types.Node startNode = record - .get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_START_NODE_OBJECT).asNode(); - startNodeMap.put(startNode.id(), startNode); - } - } - if (null != endNodeMap) { - Value endNodeValue = record.get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_END_NODE_OBJECT); - if (null != endNodeValue && StringUtils.equalsIgnoreCase("NODE", endNodeValue.type().name())) { - org.neo4j.driver.v1.types.Node endNode = record - .get(CypherQueryConfigurationConstants.DEFAULT_CYPHER_END_NODE_OBJECT).asNode(); - endNodeMap.put(endNode.id(), endNode); - } - } - } - } diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/NodeAsyncOperations.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/NodeAsyncOperations.java index 9c0e84793..82e0cb0d5 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/NodeAsyncOperations.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/NodeAsyncOperations.java @@ -1,17 +1,28 @@ package org.sunbird.graph.service.operation; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.exceptions.NoSuchRecordException; +import org.sunbird.common.DateUtils; import org.sunbird.common.JsonUtils; import org.sunbird.common.dto.Request; import org.sunbird.common.exception.ClientException; import org.sunbird.common.exception.MiddlewareException; +import org.sunbird.common.exception.ResourceNotFoundException; import org.sunbird.common.exception.ServerException; +import org.sunbird.graph.common.Identifier; +import org.sunbird.graph.common.enums.AuditProperties; import org.sunbird.graph.common.enums.GraphDACParams; import org.sunbird.graph.common.enums.SystemProperties; +import org.sunbird.graph.dac.enums.SystemNodeTypes; import org.sunbird.graph.dac.model.Node; +import org.sunbird.graph.dac.util.Neo4jNodeUtil; import org.sunbird.graph.service.common.CypherQueryConfigurationConstants; import org.sunbird.graph.service.common.DACErrorCodeConstants; import org.sunbird.graph.service.common.DACErrorMessageConstants; @@ -26,7 +37,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; public class NodeAsyncOperations { @@ -34,7 +44,6 @@ public class NodeAsyncOperations { public static Future addNode(String graphId, Node node) { - if (StringUtils.isBlank(graphId)) throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), DACErrorMessageConstants.INVALID_GRAPH_ID + " | [Create Node Operation Failed.]"); @@ -56,6 +65,7 @@ public static Future addNode(String graphId, Node node) { try (Session session = driver.session()) { String statementTemplate = StringUtils.removeEnd((String) entry.get(GraphDACParams.query.name()), CypherQueryConfigurationConstants.COMMA); Map statementParameters = (Map) entry.get(GraphDACParams.paramValueMap.name()); + CompletionStage cs = session.runAsync(statementTemplate, statementParameters) .thenCompose(fn -> fn.singleAsync()) .thenApply(record -> { @@ -68,6 +78,7 @@ public static Future addNode(String graphId, Node node) { node.getMetadata().put(GraphDACParams.versionKey.name(), versionKey); return node; }).exceptionally(error -> { + error.printStackTrace(); if (error.getCause() instanceof org.neo4j.driver.v1.exceptions.ClientException) throw new ClientException(DACErrorCodeConstants.CONSTRAINT_VALIDATION_FAILED.name(), DACErrorMessageConstants.CONSTRAINT_VALIDATION_FAILED + node.getIdentifier()); else @@ -111,6 +122,7 @@ public static Future upsertNode(String graphId, Node node, Request request try(Session session = driver.session()) { String statement = StringUtils.removeEnd((String) entry.get(GraphDACParams.query.name()), CypherQueryConfigurationConstants.COMMA); Map statementParams = (Map) entry.get(GraphDACParams.paramValueMap.name()); + CompletionStage cs = session.runAsync(statement, statementParams).thenCompose(fn -> fn.singleAsync()) .thenApply(record -> { org.neo4j.driver.v1.types.Node neo4JNode = record.get(DEFAULT_CYPHER_NODE_OBJECT).asNode(); @@ -122,6 +134,7 @@ public static Future upsertNode(String graphId, Node node, Request request node.getMetadata().put(GraphDACParams.versionKey.name(), versionKey); return node; }).exceptionally(error -> { + error.printStackTrace(); throw new ServerException(DACErrorCodeConstants.SERVER_ERROR.name(), "Error! Something went wrong while creating node object. ", error.getCause()); }); @@ -136,6 +149,137 @@ public static Future upsertNode(String graphId, Node node, Request request } } + public static Future> updateNodes(String graphId, List identifiers, Map data) { + if (StringUtils.isBlank(graphId)) + throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), + DACErrorMessageConstants.INVALID_GRAPH_ID + " | [Invalid or 'null' Graph Id.]"); + if (CollectionUtils.isEmpty(identifiers)) + throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), + DACErrorMessageConstants.INVALID_IDENTIFIER + " | [Please Provide Node Identifier.]"); + if (MapUtils.isEmpty(data)) + throw new ClientException(DACErrorCodeConstants.INVALID_METADATA.name(), + DACErrorMessageConstants.INVALID_METADATA + " | [Please Provide Valid Node Metadata]"); + + Driver driver = DriverUtil.getDriver(graphId, GraphOperation.WRITE); + TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); + Map parameterMap = new HashMap<>(); + Map output = new HashMap<>(); + String query = NodeQueryGenerationUtil.generateUpdateNodesQuery(graphId, identifiers, setPrimitiveData(data), parameterMap); + try (Session session = driver.session()) { + CompletionStage> cs = session.runAsync(query, parameterMap).thenCompose(fn -> fn.listAsync()) + .thenApply(result -> { + if (null != result) { + for (Record record : result) { + if (null != record) { + org.neo4j.driver.v1.types.Node neo4JNode = record.get(DEFAULT_CYPHER_NODE_OBJECT).asNode(); + String identifier = neo4JNode.get(SystemProperties.IL_UNIQUE_ID.name()).asString(); + Node node = Neo4jNodeUtil.getNode(graphId, neo4JNode, null, null, null); + output.put(identifier, node); + } + } + } + return output; + }).exceptionally(error -> { + throw new ServerException(DACErrorCodeConstants.SERVER_ERROR.name(), "Error! Something went wrong while performing bulk update operations. ", error.getCause()); + }); + return FutureConverters.toScala(cs); + } + } + + + public static Future upsertRootNode(String graphId, Request request) throws Exception { + if (StringUtils.isBlank(graphId)) + throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), + DACErrorMessageConstants.INVALID_GRAPH_ID + " | [Upsert Root Node Operation Failed.]"); + + Node node = new Node(); + node.setMetadata(new HashMap()); + Driver driver = DriverUtil.getDriver(graphId, GraphOperation.WRITE); + TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); + try (Session session = driver.session()) { + TelemetryManager.log("Session Initialised. | [Graph Id: " + graphId + "]"); + + // Generating Root Node Id + String rootNodeUniqueId = Identifier.getIdentifier(graphId, SystemNodeTypes.ROOT_NODE.name()); + TelemetryManager.log("Generated Root Node Id: " + rootNodeUniqueId); + + node.setGraphId(graphId); + node.setNodeType(SystemNodeTypes.ROOT_NODE.name()); + node.setIdentifier(rootNodeUniqueId); + node.getMetadata().put(SystemProperties.IL_UNIQUE_ID.name(), rootNodeUniqueId); + node.getMetadata().put(SystemProperties.IL_SYS_NODE_TYPE.name(), SystemNodeTypes.ROOT_NODE.name()); + node.getMetadata().put(AuditProperties.createdOn.name(), DateUtils.formatCurrentDate()); + node.getMetadata().put(GraphDACParams.Nodes_Count.name(), 0); + node.getMetadata().put(GraphDACParams.Relations_Count.name(), 0); + + Map parameterMap = new HashMap(); + parameterMap.put(GraphDACParams.graphId.name(), graphId); + parameterMap.put(GraphDACParams.rootNode.name(), node); + parameterMap.put(GraphDACParams.request.name(), request); + String query = NodeQueryGenerationUtil.generateUpsertRootNodeCypherQuery(parameterMap); + CompletionStage cs = session.runAsync(query) + .thenCompose(fn -> fn.singleAsync()) + .thenApply(record -> { + org.neo4j.driver.v1.types.Node neo4JNode = record.get(DEFAULT_CYPHER_NODE_OBJECT).asNode(); + String versionKey = (String) neo4JNode.get(GraphDACParams.versionKey.name()).asString(); + String identifier = (String) neo4JNode.get(SystemProperties.IL_UNIQUE_ID.name()).asString(); + node.setGraphId(graphId); + node.setIdentifier(identifier); + if (StringUtils.isNotBlank(versionKey)) + node.getMetadata().put(GraphDACParams.versionKey.name(), versionKey); + return node; + }).exceptionally(error -> { + error.printStackTrace(); + if (error.getCause() instanceof org.neo4j.driver.v1.exceptions.ServiceUnavailableException) + throw new ServerException(DACErrorCodeConstants.CONNECTION_PROBLEM.name(), + DACErrorMessageConstants.CONNECTION_PROBLEM + " | " + error.getMessage(), error.getCause()); + else + throw new ServerException(DACErrorCodeConstants.SERVER_ERROR.name(), + "Error! Something went wrong while creating node object. ", error.getCause()); + }); + return FutureConverters.toScala(cs); + } catch (Exception e) { + throw new ServerException(DACErrorCodeConstants.CONNECTION_PROBLEM.name(), + DACErrorMessageConstants.CONNECTION_PROBLEM + " | " + e.getMessage(), e); + } + } + + public static Future deleteNode(String graphId, String nodeId, Request request) { + + if (StringUtils.isBlank(graphId)) + throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), + DACErrorMessageConstants.INVALID_GRAPH_ID + " | [Remove Property Values Operation Failed.]"); + + if (StringUtils.isBlank(nodeId)) + throw new ClientException(DACErrorCodeConstants.INVALID_IDENTIFIER.name(), + DACErrorMessageConstants.INVALID_IDENTIFIER + " | [Remove Property Values Operation Failed.]"); + + Driver driver = DriverUtil.getDriver(graphId, GraphOperation.WRITE); + TelemetryManager.log("Driver Initialised. | [Graph Id: " + graphId + "]"); + try (Session session = driver.session()) { + Map parameterMap = new HashMap(); + parameterMap.put(GraphDACParams.graphId.name(), graphId); + parameterMap.put(GraphDACParams.nodeId.name(), nodeId); + parameterMap.put(GraphDACParams.request.name(), request); + + CompletionStage cs = session.runAsync(NodeQueryGenerationUtil.generateDeleteNodeCypherQuery(parameterMap)) + .thenCompose(fn -> fn.singleAsync()) + .thenApply(record -> true) + .exceptionally(error -> { + if(error.getCause() instanceof NoSuchRecordException || error.getCause() instanceof ResourceNotFoundException) + throw new ResourceNotFoundException(DACErrorCodeConstants.NOT_FOUND.name(), + DACErrorMessageConstants.NODE_NOT_FOUND + " | [Invalid Node Id.]: " + nodeId, nodeId); + else + throw new ServerException(DACErrorCodeConstants.SERVER_ERROR.name(), + "Error! Something went wrong while deleting node object. ", error.getCause()); }); + // TODO: Implement Redis Delete + return FutureConverters.toScala(cs); + } catch (Exception e) { + throw new ServerException(DACErrorCodeConstants.CONNECTION_PROBLEM.name(), + DACErrorMessageConstants.CONNECTION_PROBLEM + " | " + e.getMessage()); + } + } + private static Node setPrimitiveData(Node node) { Map metadata = node.getMetadata(); metadata.entrySet().stream() @@ -161,6 +305,30 @@ private static Node setPrimitiveData(Node node) { return node; } + private static Map setPrimitiveData(Map metadata) { + metadata.entrySet().stream() + .map(entry -> { + Object value = entry.getValue(); + try { + if (value instanceof Map) { + value = JsonUtils.serialize(value); + } else if (value instanceof List) { + List listValue = (List) value; + if (CollectionUtils.isNotEmpty(listValue) && listValue.get(0) instanceof Map) { + value = JsonUtils.serialize(value); + } + } + entry.setValue(value); + } catch (Exception e) { + TelemetryManager.error("Exception Occurred While Processing Primitive Data Types | Exception is : " + e.getMessage(), e); + } + + return entry; + }) + .collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll); + return metadata; + } + private static void setRequestContextToNode(Node node, Request request) { if (null != request && null != request.getContext()) { diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/SearchAsyncOperations.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/SearchAsyncOperations.java index a65dce164..6e4b5d4db 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/SearchAsyncOperations.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/operation/SearchAsyncOperations.java @@ -45,7 +45,7 @@ public class SearchAsyncOperations { * the search criteria * @return the node by unique ids */ - public static Future> getNodeByUniqueIds(String graphId, SearchCriteria searchCriteria) { + public static Future> getNodeByUniqueIds(String graphId, SearchCriteria searchCriteria) throws Exception{ if (StringUtils.isBlank(graphId)) throw new ClientException(DACErrorCodeConstants.INVALID_GRAPH.name(), @@ -87,6 +87,7 @@ public static Future> getNodeByUniqueIds(String graphId, SearchCriter } return nodes; }).exceptionally(error -> { + error.printStackTrace(); throw new ServerException(DACErrorCodeConstants.SERVER_ERROR.name(), "Error! Something went wrong while creating node object. ", error.getCause()); }); diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/BaseQueryGenerationUtil.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/BaseQueryGenerationUtil.java index ee2da5f85..df732461d 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/BaseQueryGenerationUtil.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/BaseQueryGenerationUtil.java @@ -490,17 +490,19 @@ protected static Map getOnMatchSetQueryMap(String objectVariable query.append(GraphDACParams.SET.name()).append(CypherQueryConfigurationConstants.BLANK_SPACE); // Adding Metadata - for (Entry entry : node.getMetadata().entrySet()) { - if (!StringUtils.equalsIgnoreCase(entry.getKey(), GraphDACParams.versionKey.name())) { - query.append(objectVariableName + CypherQueryConfigurationConstants.DOT + entry.getKey() - + " = { MD_" - + entry.getKey() + " }, "); - - TelemetryManager.log("Adding Entry: " + entry.getKey() + "Value: "+ entry.getValue()); - - // Populating Param Map - paramValuesMap.put("MD_" + entry.getKey(), entry.getValue()); - TelemetryManager.log("Populating ParamMap:", paramValuesMap); + if (null != node.getMetadata()) { + for (Entry entry : node.getMetadata().entrySet()) { + if (!StringUtils.equalsIgnoreCase(entry.getKey(), GraphDACParams.versionKey.name())) { + query.append(objectVariableName + CypherQueryConfigurationConstants.DOT + entry.getKey() + + " = { MD_" + + entry.getKey() + " }, "); + + TelemetryManager.log("Adding Entry: " + entry.getKey() + "Value: " + entry.getValue()); + + // Populating Param Map + paramValuesMap.put("MD_" + entry.getKey(), entry.getValue()); + TelemetryManager.log("Populating ParamMap:", paramValuesMap); + } } } @@ -514,7 +516,7 @@ protected static Map getOnMatchSetQueryMap(String objectVariable paramValuesMap.put("AP_" + AuditProperties.lastUpdatedOn.name(), date); } - if (StringUtils.isBlank((String) node.getMetadata().get(GraphDACParams.versionKey.name()))) { + if (null != node.getMetadata() && StringUtils.isBlank((String) node.getMetadata().get(GraphDACParams.versionKey.name()))) { String versionKey = Long.toString(DateUtils.parse(date).getTime()); query.append(objectVariableName).append(CypherQueryConfigurationConstants.DOT) .append(GraphDACParams.versionKey.name()).append(CypherQueryConfigurationConstants.EQUALS) diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/GraphQueryGenerationUtil.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/GraphQueryGenerationUtil.java index d0dc482e7..b3ae5f546 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/GraphQueryGenerationUtil.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/GraphQueryGenerationUtil.java @@ -814,7 +814,7 @@ public static String generateCreateBulkRelationsCypherQuery(String graphId) { query.append("MATCH(m:"+graphId+" {"+SystemProperties.IL_UNIQUE_ID.name()+":endNode}) \n"); RelationTypes[] types = RelationTypes.values(); for (RelationTypes type : types) { - query.append("FOREACH (_ IN case WHEN relation='"+type.relationName()+"' then [1] else[] end| merge (n)-[r:"+type.relationName()+"]->(m) on create set r += relMetadata)\n"); + query.append("FOREACH (_ IN case WHEN relation='"+type.relationName()+"' then [1] else[] end| merge (n)-[r:"+type.relationName()+"]->(m) set r += relMetadata)\n"); } query.append("RETURN COUNT(*) AS RESULT;"); return query.toString(); diff --git a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/NodeQueryGenerationUtil.java b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/NodeQueryGenerationUtil.java index c82472ed8..26498d27d 100644 --- a/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/NodeQueryGenerationUtil.java +++ b/ontology-engine/graph-dac-api/src/main/java/org/sunbird/graph/service/util/NodeQueryGenerationUtil.java @@ -491,7 +491,7 @@ public static String generateDeleteNodeCypherQuery(Map parameter + " | [Remove Property Values Query Generation Failed.]"); query.append("MATCH (a:" + graphId + " {" + SystemProperties.IL_UNIQUE_ID.name() + ": '" + nodeId - + "'}) DETACH DELETE a"); + + "'}) DETACH DELETE a RETURN a"); } TelemetryManager.log("Returning Create Node Cypher Query: " + query); @@ -528,4 +528,24 @@ private static String getClassicNodeDeleteCypherQuery(String graphId, String nod } return query.toString(); } + + public static String generateUpdateNodesQuery(String graphId, List identifiers, Map metadata, Map params) { + params.put("identifiers", identifiers); + StringBuilder query = new StringBuilder(); + query.append("MATCH(n:" + graphId + ") "); + query.append("WHERE n." + SystemProperties.IL_UNIQUE_ID.name() + " IN {identifiers} SET"); + int i = 0; + int index = 1; + for (Map.Entry entry : metadata.entrySet()) { + query.append(" ").append("n").append(".").append(entry.getKey()).append(" = {").append(index).append("} "); + params.put("" + index, entry.getValue()); + index += 1; + if (i < metadata.size() - 1) { + query.append(", "); + i++; + } + } + query.append(" RETURN n AS ee ;"); + return query.toString(); + } } diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/FilterTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/FilterTest.java new file mode 100644 index 000000000..f49b38d38 --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/FilterTest.java @@ -0,0 +1,121 @@ +package org.sunbird.graph.dac.model; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class FilterTest { + public static Filter filter_1 = null; + public static Filter filter_2 = null; + public static Filter filter_3 = null; + public static SearchCriteria sc = new SearchCriteria() {{ + setGraphId("domain"); + setNodeType("DATA_NODE"); + setObjectType("Content"); + }}; + public static String param = ""; + + @BeforeClass + public static void init() { + filter_1 = new Filter(); + filter_2 = new Filter("prop1", "val1"); + filter_3 = new Filter("prop2", "!=", "val2"); + } + + @Test + public void testFilterModel_1() { + filter_1.setOperator(""); + Assert.assertEquals("=", filter_2.getOperator()); + } + + @Test + public void testFilterModel_2() { + filter_1.setProperty("testProperty"); + filter_1.setValue("testValue"); + filter_1.setOperator("!="); + Assert.assertEquals("testProperty", filter_1.getProperty()); + Assert.assertEquals("testValue", filter_1.getValue()); + Assert.assertEquals("!=", filter_1.getOperator()); + } + + @Test + public void testFilterModel_3() { + filter_1.setProperty("identifier"); + filter_1.setOperator("="); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID = {4} ", query); + } + + @Test + public void testFilterModel_4() { + filter_1.setProperty("identifier"); + filter_1.setOperator("like"); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID =~ {5} ", query); + } + + @Test + public void testFilterModel_5() { + filter_1.setProperty("identifier"); + filter_1.setOperator("startsWith"); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID =~ {6} ", query); + } + + @Test + public void testFilterModel_6() { + filter_1.setProperty("identifier"); + filter_1.setOperator("endsWith"); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID =~ {7} ", query); + } + + @Test + public void testFilterModel_7() { + filter_1.setProperty("identifier"); + filter_1.setOperator(">"); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID > {8} ", query); + } + + @Test + public void testFilterModel_8() { + filter_1.setProperty("identifier"); + filter_1.setOperator(">="); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID >= {9} ", query); + } + + @Test + public void testFilterModel_9() { + filter_1.setProperty("identifier"); + filter_1.setOperator("<"); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID < {10} ", query); + } + + @Test + public void testFilterModel_10() { + filter_1.setProperty("identifier"); + filter_1.setOperator("<="); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID <= {1} ", query); + } + + @Test + public void testFilterModel_11() { + filter_1.setProperty("identifier"); + filter_1.setOperator("!="); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" NOT n.IL_UNIQUE_ID = {2} ", query); + } + + @Test + public void testFilterModel_12() { + filter_1.setProperty("identifier"); + filter_1.setOperator("in"); + String query = filter_1.getCypher(sc, param); + Assert.assertEquals(" n.IL_UNIQUE_ID in {3} ", query); + } + +} diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/NodeTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/NodeTest.java new file mode 100644 index 000000000..324eaf6bb --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/NodeTest.java @@ -0,0 +1,91 @@ +package org.sunbird.graph.dac.model; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.sunbird.graph.common.enums.SystemProperties; + +import java.util.ArrayList; +import java.util.HashMap; + +public class NodeTest { + public static Node node_1 = null; + public static Node node_2 = null; + public static Node node_3 = null; + + @BeforeClass + public static void init() { + node_1 = new Node(); + node_2 = new Node("domain", "DATA_NODE", "Content"); + node_3 = new Node("domain", new HashMap<>() {{ + put(SystemProperties.IL_UNIQUE_ID.name(), "do_1234"); + put(SystemProperties.IL_SYS_NODE_TYPE.name(), "DATA_NODE"); + put(SystemProperties.IL_FUNC_OBJECT_TYPE.name(), "Content"); + }}); + } + + @Test + public void testNodeModel_1() throws Exception { + node_1.setId(30190391); + node_1.setGraphId("domain"); + node_1.setIdentifier("do_1234"); + node_1.setNodeType("DATA_NODE"); + node_1.setObjectType("Content"); + + Assert.assertEquals(30190391, node_1.getId()); + Assert.assertEquals("do_1234", node_1.getIdentifier()); + Assert.assertEquals("domain", node_1.getGraphId()); + Assert.assertEquals("DATA_NODE", node_1.getNodeType()); + Assert.assertEquals("Content", node_1.getObjectType()); + } + + @Test + public void testNodeModel_2() throws Exception { + node_2.setMetadata(new HashMap<>() {{ + put("status", "Live"); + }}); + node_2.setExternalData(new HashMap<>() {{ + put("body", "

I'm a test body

"); + }}); + + Assert.assertNotNull(node_2.getMetadata()); + Assert.assertNotNull(node_2.getExternalData()); + Assert.assertEquals("Live", node_2.getMetadata().get("status")); + Assert.assertEquals("

I'm a test body

", node_2.getExternalData().get("body")); + } + + @Test + public void testNodeModel_3() throws Exception { + node_3.setOutRelations(new ArrayList<>() {{ + add(new Relation("do_1234", "associatedTo", "do_5678")); + }}); + node_3.setInRelations(new ArrayList<>() {{ + add(new Relation("do_1357", "associatedTo", "do_1234")); + }}); + node_3.setAddedRelations(new ArrayList<>() {{ + add(new Relation("do_1234", "associatedTo", "do_5678")); + add(new Relation("do_1357", "associatedTo", "do_1234")); + }}); + node_3.setDeletedRelations(new ArrayList<>() {{ + add(new Relation("do_1234", "associatedTo", "do_2468")); + }}); + node_3.setRelationNodes(new HashMap<>() {{ + put("do_894", node_1); + put("do_47389", node_2); + }}); + + Assert.assertNotNull(node_3.getOutRelations()); + Assert.assertNotNull(node_3.getInRelations()); + Assert.assertNotNull(node_3.getAddedRelations()); + Assert.assertNotNull(node_3.getDeletedRelations()); + Assert.assertNotNull(node_3.getRelationNodes()); + Assert.assertNotNull(node_3.getRelationNode("do_894")); + + Assert.assertEquals("do_5678", node_3.getOutRelations().get(0).getEndNodeId()); + Assert.assertEquals("do_1234", node_3.getInRelations().get(0).getEndNodeId()); + Assert.assertEquals("do_5678", node_3.getAddedRelations().get(0).getEndNodeId()); + Assert.assertEquals("do_2468", node_3.getDeletedRelations().get(0).getEndNodeId()); + Assert.assertEquals("DATA_NODE", node_3.getRelationNode("do_47389").getNodeType()); + + } +} diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/RelationCriterionTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/RelationCriterionTest.java new file mode 100644 index 000000000..6dcc725be --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/RelationCriterionTest.java @@ -0,0 +1,82 @@ +package org.sunbird.graph.dac.model; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class RelationCriterionTest { + + @Test + public void testRelationCriterion_1() { + RelationCriterion relationCriterion = new RelationCriterion("", "Content"); + + List list2 = new ArrayList<>(){{add(new RelationCriterion("",""));}}; + relationCriterion.setName("test_name"); + relationCriterion.setFromDepth(1); + relationCriterion.setObjectType("Content"); + relationCriterion.setRelations(list2); + relationCriterion.setOp("test"); + relationCriterion.setOptional(false); + relationCriterion.setToDepth(1); + + Assert.assertEquals("test_name", relationCriterion.getName()); + Assert.assertEquals("AND", relationCriterion.getOp()); + Assert.assertEquals("Content", relationCriterion.getObjectType()); + Assert.assertEquals(1, relationCriterion.getToDepth()); + Assert.assertEquals(1, relationCriterion.getFromDepth()); + Assert.assertEquals(false, relationCriterion.isOptional()); + Assert.assertTrue(!relationCriterion.getRelations().isEmpty()); + } + + @Test + public void testRelationCriterion_2() { + List list = new ArrayList() {{add(new RelationFilter("testName"));}}; + RelationCriterion relationCriterion_2 = new RelationCriterion(list, "Content"); + relationCriterion_2.setOp("OR"); + relationCriterion_2.setIdentifiers(new ArrayList<>(){{add("do_123");}}); + relationCriterion_2.addRelationCriterion(relationCriterion_2); + + Assert.assertEquals("OR", relationCriterion_2.getOp()); + Assert.assertTrue(!relationCriterion_2.getIdentifiers().isEmpty()); + Assert.assertTrue(!relationCriterion_2.getRelations().isEmpty()); + } + + @Test + public void testRelationCriterion_3() { + List list = new ArrayList() {{add(new RelationFilter("testName"));}}; + RelationCriterion relationCriterion_3 = new RelationCriterion("", "Content"); + relationCriterion_3.setFilters(list); + relationCriterion_3.addIdentifier("do_123"); + relationCriterion_3.setIdentifiers(new ArrayList<>() {{add("do_123");}}); + + MetadataCriterion mc = MetadataCriterion.create(new ArrayList<>() {{add(new Filter("IL_UNIQUE_ID", SearchConditions.OP_EQUAL));}}); + List listMc = new ArrayList<>() {{add(mc);}}; + relationCriterion_3.setMetadata(listMc); + relationCriterion_3.addMetadata(mc); + + SearchCriteria sc = new SearchCriteria(); + relationCriterion_3.getCypher(sc, "prevParam"); + + Assert.assertTrue(!relationCriterion_3.getFilters().isEmpty()); + } + + @Test + public void testRelationCriterion_4() { + RelationCriterion relationCriterion_4 = new RelationCriterion("", "Content"); + relationCriterion_4.setDirection(RelationCriterion.DIRECTION.IN); + relationCriterion_4.addIdentifier("do_123"); + relationCriterion_4.setIdentifiers(new ArrayList<>() {{add("do_123");}}); + + MetadataCriterion mc = MetadataCriterion.create(new ArrayList<>() {{add(new Filter("IL_UNIQUE_ID", SearchConditions.OP_EQUAL));}}); + List listMc = new ArrayList<>() {{add(mc);}}; + relationCriterion_4.setMetadata(listMc); + relationCriterion_4.addMetadata(mc); + + SearchCriteria sc = new SearchCriteria(); + relationCriterion_4.getCypher(sc, "prevParam"); + + Assert.assertEquals(RelationCriterion.DIRECTION.IN, relationCriterion_4.getDirection()); + } +} diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/RelationFilterTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/RelationFilterTest.java new file mode 100644 index 000000000..0af697225 --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/dac/model/RelationFilterTest.java @@ -0,0 +1,23 @@ +package org.sunbird.graph.dac.model; + +import org.junit.Assert; +import org.junit.Test; + +public class RelationFilterTest { + + @Test + public void testRelationFilter() { + RelationFilter relationFilter_1 = new RelationFilter(""); + RelationFilter relationFilter_2 = new RelationFilter("testName", 1); + RelationFilter relationFilter_3 = new RelationFilter("testName", 1, 1); + relationFilter_1.setName("test_name"); + relationFilter_1.setDirection("out"); + relationFilter_1.setFromDepth(1); + relationFilter_1.setToDepth(1); + + Assert.assertEquals("test_name", relationFilter_1.getName()); + Assert.assertEquals("out", relationFilter_1.getDirection()); + Assert.assertEquals(1, relationFilter_1.getToDepth()); + Assert.assertEquals(1, relationFilter_1.getFromDepth()); + } +} diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/operation/NodeAsyncOperationsExceptionTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/operation/NodeAsyncOperationsExceptionTest.java new file mode 100644 index 000000000..9bacce54d --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/operation/NodeAsyncOperationsExceptionTest.java @@ -0,0 +1,95 @@ +package org.sunbird.graph.service.operation; + +import org.apache.commons.io.FileUtils; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.factory.GraphDatabaseFactory; +import org.neo4j.graphdb.factory.GraphDatabaseSettings; +import org.sunbird.common.Platform; +import org.sunbird.common.exception.ServerException; +import org.sunbird.graph.dac.model.Node; +import org.sunbird.graph.service.util.DriverUtil; +import scala.concurrent.Await; +import scala.concurrent.Future; +import scala.concurrent.duration.Duration; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionException; + +public class NodeAsyncOperationsExceptionTest { + + protected static GraphDatabaseService graphDb = null; + + @BeforeClass + public static void setup() throws Exception { + startEmbeddedNeo4jWithReadOnly(); + } + + @AfterClass + public static void finish() throws Exception { + tearEmbeddedNeo4JSetup(); + DriverUtil.closeDrivers(); + } + + @Test(expected = CompletionException.class) + public void testAddNodeExpectServerException() throws Exception { + Node node = new Node("domain", "DATA_NODE", "Content"); + node.setIdentifier("do_00000000113"); + node.setMetadata(new HashMap() {{ + put("status", "Draft"); + }}); + Future resultFuture = NodeAsyncOperations.addNode("domain", node); + Node result = Await.result(resultFuture, Duration.apply("30s")); + } + + @Ignore + @Test(expected = ServerException.class) + public void testUpdateNodesExpectServerException() throws Exception { + List ids = Arrays.asList("do_0000123", "do_0000234"); + Map data = new HashMap() {{ + put("status", "Review"); + }}; + Future> resultFuture = NodeAsyncOperations.updateNodes("domain",ids, data); + Map result = Await.result(resultFuture, Duration.apply("30s")); + } + + + private static void startEmbeddedNeo4jWithReadOnly() { + GraphDatabaseSettings.BoltConnector bolt = GraphDatabaseSettings.boltConnector("0"); + graphDb = new GraphDatabaseFactory() + .newEmbeddedDatabaseBuilder(new File(Platform.config.getString("graph.dir"))) + .setConfig(bolt.type, "BOLT").setConfig(bolt.enabled, "true") + .setConfig(GraphDatabaseSettings.read_only, "true") + .setConfig(bolt.address, "localhost:7687").newGraphDatabase(); + registerShutdownHook(graphDb); + } + + protected static void registerShutdownHook(final GraphDatabaseService graphDb) { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + tearEmbeddedNeo4JSetup(); + System.out.println("cleanup Done!!"); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + private static void tearEmbeddedNeo4JSetup() throws Exception { + if (null != graphDb) + graphDb.shutdown(); + Thread.sleep(2000); + FileUtils.deleteDirectory(new File(Platform.config.getString("graph.dir"))); + } + +} diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/operation/NodeAsyncOperationsTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/operation/NodeAsyncOperationsTest.java new file mode 100644 index 000000000..79335e156 --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/operation/NodeAsyncOperationsTest.java @@ -0,0 +1,197 @@ +package org.sunbird.graph.service.operation; + +import com.mashape.unirest.http.JsonNode; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.sunbird.common.dto.Request; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResourceNotFoundException; +import org.sunbird.common.exception.ServerException; +import org.sunbird.graph.dac.model.Node; +import org.sunbird.test.BaseTest; +import scala.concurrent.Await; +import scala.concurrent.Future; +import scala.concurrent.duration.Duration; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionException; + +public class NodeAsyncOperationsTest extends BaseTest { + + @BeforeClass + public static void setUp() { + graphDb.execute("UNWIND [{nodeId:'do_000000123', name: 'Test Node'}] as row with row.nodeId as Id CREATE (n:domain{IL_UNIQUE_ID:Id});"); + + } + + @Test + public void testSetPrimitiveData() throws Exception { + Method method = NodeAsyncOperations.class.getDeclaredMethod("setPrimitiveData", Map.class); + method.setAccessible(true); + Map metadata = new HashMap() {{ + put("testMap", new HashMap() {{ + put("name", "test"); + put("identifier", "123"); + }}); + put("list", new ArrayList>() {{ + add(new HashMap(){{ + put("identifier","123"); + }}); + add(new HashMap(){{ + put("identifier","234"); + }}); + }}); + }}; + Map result = (Map) method.invoke(NodeAsyncOperations.class, metadata); + Assert.assertNotNull(result); + Assert.assertEquals("{\"identifier\":\"123\",\"name\":\"test\"}", result.get("testMap")); + } + + @Test + public void testUpdateNodes() throws Exception { + createBulkNodes(); + List ids = Arrays.asList("do_0000123", "do_0000234"); + Map data = new HashMap() {{ + put("status", "Review"); + }}; + Future> resultFuture = NodeAsyncOperations.updateNodes("domain",ids, data); + Map result = Await.result(resultFuture, Duration.apply("30s")); + Assert.assertTrue(result.size()==2); + } + + @Test(expected = ClientException.class) + public void testUpdateNodesWithEmptyGraphId() throws Exception { + List ids = Arrays.asList("do_0000123", "do_0000234"); + Map data = new HashMap() {{ + put("status", "Review"); + }}; + Future> resultFuture = NodeAsyncOperations.updateNodes(null,ids, data); + Map result = Await.result(resultFuture, Duration.apply("30s")); + } + + @Test(expected = ClientException.class) + public void testUpdateNodesWithEmptyIdentifiers() throws Exception { + Map data = new HashMap() {{ + put("status", "Review"); + }}; + Future> resultFuture = NodeAsyncOperations.updateNodes("domain", new ArrayList(), data); + Map result = Await.result(resultFuture, Duration.apply("30s")); + } + + @Test(expected = ClientException.class) + public void testUpdateNodesWithEmptyMetadata() throws Exception { + List ids = Arrays.asList("do_0000123", "do_0000234"); + Future> resultFuture = NodeAsyncOperations.updateNodes("domain", ids, new HashMap()); + Map result = Await.result(resultFuture, Duration.apply("30s")); + } + + @Test + public void testAddNode() throws Exception { + Node node = new Node("domain","DATA_NODE","Content"); + node.setIdentifier("do_000000111"); + node.setMetadata(new HashMap(){{put("status","Draft");}}); + Future resultFuture = NodeAsyncOperations.addNode("graphId",node); + Node result = Await.result(resultFuture, Duration.apply("30s")); + Assert.assertTrue(null!=node); + Assert.assertEquals("do_000000111",result.getIdentifier()); + } + + @Test(expected = ClientException.class) + public void testAddNodeWithEmptyGrpahId() throws Exception { + Node node = new Node("domain","DATA_NODE","Content"); + node.setIdentifier("do_000000112"); + node.setMetadata(new HashMap(){{put("status","Draft");}}); + Future resultFuture = NodeAsyncOperations.addNode(null, node); + Node result = Await.result(resultFuture, Duration.apply("30s")); + } + + @Test(expected = ClientException.class) + public void testAddNodeWithNullNode() throws Exception { + Future resultFuture = NodeAsyncOperations.addNode("domain", null); + Node result = Await.result(resultFuture, Duration.apply("30s")); + } + + @Test + public void testUpsertRootNode() throws Exception { + Future resultFuture = NodeAsyncOperations.upsertRootNode("domain", new Request()); + Node result = Await.result(resultFuture, Duration.apply("30s")); + Assert.assertNotNull(result.getIdentifier()); + Assert.assertEquals("do_ROOT_NODE", result.getIdentifier()); + Assert.assertEquals("ROOT_NODE", result.getNodeType()); + } + + @Test + public void testDeleteWithValidID() throws Exception { + Future resultFuture2 = NodeAsyncOperations.deleteNode("domain", "do_000000123", new Request()); + Assert.assertTrue(Await.result(resultFuture2, Duration.apply("30s"))); + } + + @Test + public void testDeleteWithInvalidId() throws Exception { + Future resultFuture2 = NodeAsyncOperations.deleteNode("domain", "do_000000123_invalid", new Request()); + try { + Await.result(resultFuture2, Duration.apply("30s")); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof ResourceNotFoundException); + } + } + + @Test(expected = ClientException.class) + public void testDeleteWithEmptyId() throws Exception { + Future resultFuture2 = NodeAsyncOperations.deleteNode("domain", " ", new Request()); + Await.result(resultFuture2, Duration.apply("30s")); + } + + @Test(expected = ClientException.class) + public void testDeleteWithEmptyGraphId() throws Exception { + Future resultFuture2 = NodeAsyncOperations.deleteNode("", "do_1234 ", new Request()); + Await.result(resultFuture2, Duration.apply("30s")); + } + + @Test(expected = ServerException.class ) + public void testAddNodeNeo4jException() throws Exception { + Node node = getNode(); + node.getMetadata().put("originData", new JsonNode("{\n" + + " \"name\": \"TB-001\",\n" + + " \"copyType\": \"deep\",\n" + + " \"license\": \"CC BY-NC 4.0\",\n" + + " \"author\": \"b00bc992ef25f1a9a8d63291e20efc8d\"\n" + + " }")); + + Future resultFuture = NodeAsyncOperations.addNode("graphId", node); + Await.result(resultFuture, Duration.apply("30s")); + } + + + @Test(expected = ServerException.class ) + public void testUpsertNodeNeo4jException() throws Exception { + Node node = getNode(); + node.getMetadata().put("originData", new JsonNode("{\n" + + " \"name\": \"TB-001\",\n" + + " \"copyType\": \"deep\",\n" + + " \"license\": \"CC BY-NC 4.0\",\n" + + " \"author\": \"b00bc992ef25f1a9a8d63291e20efc8d\"\n" + + " }")); + + Future resultFuture = NodeAsyncOperations.upsertNode("graphId", node, new Request()); + Await.result(resultFuture, Duration.apply("30s")); + } + + private Node getNode() throws Exception { + Node node = new Node("domain", "DATA_NODE", "Content"); + node.setIdentifier("do_000000123"); + node.setMetadata(new HashMap() {{ + put("status", "Draft"); + put("name", "Test Node"); + put("identifier", "do_000000123"); + }}); + return node; + } + +} diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/util/GraphQueryGenerationUtilTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/util/GraphQueryGenerationUtilTest.java new file mode 100644 index 000000000..d157d50ba --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/util/GraphQueryGenerationUtilTest.java @@ -0,0 +1,1279 @@ +package org.sunbird.graph.service.util; + +import org.junit.Assert; +import org.junit.Test; +import org.neo4j.driver.v1.exceptions.ClientException; +import org.sunbird.common.dto.Request; +import org.sunbird.common.exception.ServerException; +import org.sunbird.graph.common.enums.GraphDACParams; +import org.sunbird.graph.common.enums.SystemProperties; +import org.sunbird.graph.dac.enums.RelationTypes; +import org.sunbird.graph.dac.model.Node; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class GraphQueryGenerationUtilTest { + + @Test + public void testgenerateCreateUniqueConstraintCypherQuery_1() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.indexProperty.name(),"identifier"); + }}; + String query = GraphQueryGenerationUtil.generateCreateUniqueConstraintCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("CREATE CONSTRAINT ON (n:domain) ASSERT n.identifier IS UNIQUE ", query); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateUniqueConstraintCypherQuery_2() { + Map params = new HashMap<>() {{ + put(GraphDACParams.indexProperty.name(),"identifier"); + }}; + GraphQueryGenerationUtil.generateCreateUniqueConstraintCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateUniqueConstraintCypherQuery_3() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + }}; + GraphQueryGenerationUtil.generateCreateUniqueConstraintCypherQuery(params); + } + + @Test + public void testgenerateCreateIndexCypherQuery_1() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.indexProperty.name(),"identifier"); + }}; + String query = GraphQueryGenerationUtil.generateCreateIndexCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("CREATE INDEX ON :domain(identifier) ", query); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateIndexCypherQuery_2() { + Map params = new HashMap<>() {{ + put(GraphDACParams.indexProperty.name(),"identifier"); + }}; + GraphQueryGenerationUtil.generateCreateIndexCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateIndexCypherQuery_3() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + }}; + GraphQueryGenerationUtil.generateCreateIndexCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteGraphCypherQuery_1() { + Map params = new HashMap<>() {{ + put(GraphDACParams.indexProperty.name(),"identifier"); + }}; + GraphQueryGenerationUtil.generateDeleteGraphCypherQuery(params); + } + + @Test + public void testgenerateDeleteGraphCypherQuery_2() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.indexProperty.name(),"identifier"); + }}; + String query = GraphQueryGenerationUtil.generateDeleteGraphCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (n) REMOVE n:domain", query); + } + + @Test(expected = ServerException.class) + public void testgenerateCreateRelationCypherQuery_1() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), ""); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateCreateRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("", query); + } + + @Test + public void testgenerateCreateRelationCypherQuery_2() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateCreateRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("", query); + } + + @Test(expected = ServerException.class) + public void testgenerateCreateRelationCypherQuery_3() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateRelationCypherQuery(params); + } + + @Test(expected = ServerException.class) + public void testgenerateCreateRelationCypherQuery_4() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateRelationCypherQuery(params); + } + + @Test(expected = ServerException.class) + public void testgenerateCreateRelationCypherQuery_5() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateRelationCypherQuery(params); + } + + @Test(expected = ServerException.class) + public void testgenerateCreateRelationCypherQuery_6() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateRelationCypherQuery(params); + } + + @Test + public void testgenerateUpdateRelationCypherQuery_1() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), ""); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateUpdateRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("", query); + } + + @Test + public void testgenerateUpdateRelationCypherQuery_2() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateUpdateRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("", query); + } + + @Test(expected = ClientException.class) + public void testgenerateUpdateRelationCypherQuery_3() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateUpdateRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateUpdateRelationCypherQuery_4() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateUpdateRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateUpdateRelationCypherQuery_5() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateUpdateRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateUpdateRelationCypherQuery_6() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateUpdateRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateUpdateRelationCypherQuery_7() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), null); + + }}; + GraphQueryGenerationUtil.generateUpdateRelationCypherQuery(params); + } + + @Test + public void generateDeleteRelationCypherQuery_1() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), ""); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateDeleteRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (a:domain {IL_UNIQUE_ID: 'do_1234'})-[r:hasSequenceMember]->(b:domain {IL_UNIQUE_ID: 'do_3456'}) DELETE r ", query); + } + + @Test + public void testgenerateDeleteRelationCypherQuery_2() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateDeleteRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (a:domain {IL_UNIQUE_ID: 'do_1234'})-[r:associatedTo]->(b:domain {IL_UNIQUE_ID: 'do_3456'}) DELETE r ", query); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteRelationCypherQuery_3() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteRelationCypherQuery_4() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteRelationCypherQuery_5() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteRelationCypherQuery_6() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_1234"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteRelationCypherQuery(params); + } + + @Test + public void generateCreateIncomingRelationCypherQuery_1() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), ""); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeIds.name(), Arrays.asList("do_1234")); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateCreateIncomingRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("", query); + } + + @Test + public void testgenerateCreateIncomingRelationCypherQuery_2() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateCreateIncomingRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (B:domain { IL_UNIQUE_ID: 'do_12345' }),(C:domain { IL_UNIQUE_ID: 'do_3456' }) MERGE (B)<-[r:associatedTo]-(C)ON CREATE SET r.IL_SEQUENCE_INDEX='1234567' ON MATCH SET r.IL_SEQUENCE_INDEX='1234567' ", query); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateIncomingRelationCypherQuery_3() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.startNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateIncomingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateIncomingRelationCypherQuery_4() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateIncomingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateIncomingRelationCypherQuery_5() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateIncomingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateIncomingRelationCypherQuery_6() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateIncomingRelationCypherQuery(params); + } + + @Test + public void testgenerateCreateOutgoingRelationCypherQuery_1() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), ""); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(), "do_1234"); + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_3456")); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateCreateOutgoingRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("", query); + } + + @Test + public void testgenerateCreateOutgoingRelationCypherQuery_2() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateCreateOutgoingRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (B:domain { IL_UNIQUE_ID: 'do_3456' }),(C:domain { IL_UNIQUE_ID: 'do_12345' }) MERGE (B)-[r:associatedTo]->(C)ON CREATE SET r.IL_SEQUENCE_INDEX='1234567' ON MATCH SET r.IL_SEQUENCE_INDEX='1234567' ", query); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateOutgoingRelationCypherQuery_3() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateOutgoingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateOutgoingRelationCypherQuery_4() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateOutgoingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateOutgoingRelationCypherQuery_5() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(), "do_1234"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateOutgoingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateOutgoingRelationCypherQuery_6() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateCreateOutgoingRelationCypherQuery(params); + } + + @Test + public void generateDeleteIncomingRelationCypherQuery_1() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), ""); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeIds.name(), Arrays.asList("do_1234")); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateDeleteIncomingRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("", query); + } + + @Test + public void testgenerateDeleteIncomingRelationCypherQuery_2() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateDeleteIncomingRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (a:domain {IL_UNIQUE_ID: 'do_12345'})<-[r:associatedTo]-(b:domain {IL_UNIQUE_ID: 'do_3456'}) DELETE r ", query); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteIncomingRelationCypherQuery_3() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.startNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteIncomingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteIncomingRelationCypherQuery_4() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteIncomingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteIncomingRelationCypherQuery_5() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteIncomingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteIncomingRelationCypherQuery_6() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteIncomingRelationCypherQuery(params); + } + + @Test + public void testgenerateDeleteOutgoingRelationCypherQuery_1() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), ""); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(), "do_1234"); + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_3456")); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateDeleteOutgoingRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("", query); + } + + @Test + public void testgenerateDeleteOutgoingRelationCypherQuery_2() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + String query = GraphQueryGenerationUtil.generateDeleteOutgoingRelationCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (a:domain {IL_UNIQUE_ID: 'do_3456'})<-[r:associatedTo]-(b:domain {IL_UNIQUE_ID: 'do_12345'}) DELETE r ", query); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteOutgoingRelationCypherQuery_3() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteOutgoingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteOutgoingRelationCypherQuery_4() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteOutgoingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteOutgoingRelationCypherQuery_5() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(), "do_1234"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteOutgoingRelationCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteOutgoingRelationCypherQuery_6() { + Request request = new Request(); + request.setRequest(new HashMap() {{ + put(GraphDACParams.metadata.name(), new HashMap() {{ + put(SystemProperties.IL_SEQUENCE_INDEX.name(), "1234567"); + }}); + }}); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeIds.name(),Arrays.asList("do_1234", "do_12345")); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.request.name(), request); + + }}; + GraphQueryGenerationUtil.generateDeleteOutgoingRelationCypherQuery(params); + } + + @Test + public void testgenerateRemoveRelationMetadataCypherQuery_1() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.key.name(), "key"); + + }}; + String query = GraphQueryGenerationUtil.generateRemoveRelationMetadataCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (a:domain {IL_UNIQUE_ID: 'do_3456'})-[r:hasSequenceMember]->(b:domain {IL_UNIQUE_ID: 'do_3456'}) REMOVE r.key ", query); + } + + @Test + public void testgenerateRemoveRelationMetadataCypherQuery_2() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.key.name(), "key"); + + }}; + String query = GraphQueryGenerationUtil.generateRemoveRelationMetadataCypherQuery(params); + Assert.assertNotNull(query); + Assert.assertEquals("MATCH (a:domain {IL_UNIQUE_ID: 'do_3456'})-[r:associatedTo]->(b:domain {IL_UNIQUE_ID: 'do_3456'}) REMOVE r.key ", query); + } + + @Test(expected = ClientException.class) + public void testgenerateRemoveRelationMetadataCypherQuery_3() { + Map params = new HashMap<>() {{ + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.key.name(), "key"); + + }}; + GraphQueryGenerationUtil.generateRemoveRelationMetadataCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateRemoveRelationMetadataCypherQuery_4() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.key.name(), "key"); + }}; + GraphQueryGenerationUtil.generateRemoveRelationMetadataCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateRemoveRelationMetadataCypherQuery_5() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.ASSOCIATED_TO.relationName()); + put(GraphDACParams.key.name(), "key"); + }}; + GraphQueryGenerationUtil.generateRemoveRelationMetadataCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateRemoveRelationMetadataCypherQuery_6() { + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.startNodeId.name(),"do_3456"); + put(GraphDACParams.endNodeId.name(),"do_3456"); + put(GraphDACParams.key.name(), "key"); + }}; + GraphQueryGenerationUtil.generateRemoveRelationMetadataCypherQuery(params); + } + + @Test + public void testgenerateCreateCollectionCypherQuery_1() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.collectionId.name(),"do_3456"); + put(GraphDACParams.collection.name(), node); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.members.name(), Arrays.asList("keys")); + put(GraphDACParams.indexProperty.name(), "123456"); + }}; + String query = GraphQueryGenerationUtil.generateCreateCollectionCypherQuery(params); + Assert.assertNotNull(query); + } + + + @Test(expected = ClientException.class) + public void testgenerateCreateCollectionCypherQuery_3() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.collectionId.name(),"do_3456"); + put(GraphDACParams.collection.name(), node); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.members.name(), Arrays.asList("keys")); + put(GraphDACParams.indexProperty.name(), "123456"); + }}; + GraphQueryGenerationUtil.generateCreateCollectionCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateCollectionCypherQuery_4() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.collection.name(), node); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.members.name(), Arrays.asList("keys")); + put(GraphDACParams.indexProperty.name(), "123456"); + }}; + GraphQueryGenerationUtil.generateCreateCollectionCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateCollectionCypherQuery_5() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.collectionId.name(),"do_3456"); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.members.name(), Arrays.asList("keys")); + put(GraphDACParams.indexProperty.name(), "123456"); + }}; + GraphQueryGenerationUtil.generateCreateCollectionCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateCollectionCypherQuery_6() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.collectionId.name(),"do_3456"); + put(GraphDACParams.collection.name(), node); + put(GraphDACParams.members.name(), Arrays.asList("keys")); + put(GraphDACParams.indexProperty.name(), "123456"); + }}; + GraphQueryGenerationUtil.generateCreateCollectionCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateCollectionCypherQuery_7() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.collectionId.name(),"do_3456"); + put(GraphDACParams.collection.name(), node); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.indexProperty.name(), "123456"); + }}; + GraphQueryGenerationUtil.generateCreateCollectionCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateCreateCollectionCypherQuery_8() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.collectionId.name(),"do_3456"); + put(GraphDACParams.collection.name(), node); + put(GraphDACParams.relationType.name(), RelationTypes.SEQUENCE_MEMBERSHIP.relationName()); + put(GraphDACParams.members.name(), Arrays.asList("keys")); + }}; + GraphQueryGenerationUtil.generateCreateCollectionCypherQuery(params); + } + + @Test + public void testgenerateDeleteCollectionCypherQuery_1() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.collectionId.name(),"do_3456"); + }}; + String query = GraphQueryGenerationUtil.generateDeleteCollectionCypherQuery(params); + Assert.assertNotNull(query); + } + + + @Test(expected = ClientException.class) + public void testgenerateDeleteCollectionCypherQuery_4() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + }}; + GraphQueryGenerationUtil.generateDeleteCollectionCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateDeleteCollectionCypherQuery_5() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.collectionId.name(),"do_3456"); + }}; + GraphQueryGenerationUtil.generateDeleteCollectionCypherQuery(params); + } + + @Test + public void testgenerateImportGraphCypherQuery_1() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.taskId.name(),"do_3456"); + put(GraphDACParams.input.name(),"input_1332"); + }}; + String query = GraphQueryGenerationUtil.generateImportGraphCypherQuery(params); + Assert.assertNotNull(query); + } + + + @Test(expected = ClientException.class) + public void testgenerateImportGraphCypherQuery_4() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.input.name(),"input_1332"); + }}; + GraphQueryGenerationUtil.generateImportGraphCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateImportGraphCypherQuery_5() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.taskId.name(),"do_3456"); + put(GraphDACParams.input.name(),"input_1332"); + }}; + GraphQueryGenerationUtil.generateImportGraphCypherQuery(params); + } + + @Test(expected = ClientException.class) + public void testgenerateImportGraphCypherQuery_6() { + Node node = new Node(); + node.setMetadata(new HashMap<>() {{ + put("name", "test_collection"); + put(GraphDACParams.SYS_INTERNAL_LAST_UPDATED_ON.name(), "12-10-2019"); + }}); + node.setGraphId("domain"); + node.setObjectType("Content"); + node.setIdentifier("do_3456"); + node.setNodeType("DATA_NODE"); + Map params = new HashMap<>() {{ + put(GraphDACParams.graphId.name(), "domain"); + put(GraphDACParams.taskId.name(),"do_3456"); + }}; + String query = GraphQueryGenerationUtil.generateImportGraphCypherQuery(params); + Assert.assertNotNull(query); + } + + @Test + public void testgenerateCreateBulkRelationsCypherQuery_1() { + String query = GraphQueryGenerationUtil.generateCreateBulkRelationsCypherQuery("domain"); + Assert.assertNotNull(query); + Assert.assertEquals("UNWIND {data} AS ROW WITH ROW.startNodeId AS startNode, ROW.endNodeId AS endNode, ROW.relation AS relation, ROW.relMetadata as relMetadata MATCH(n:domain {IL_UNIQUE_ID:startNode}) MATCH(m:domain {IL_UNIQUE_ID:endNode}) \n" + + "FOREACH (_ IN case WHEN relation='hasSequenceMember' then [1] else[] end| merge (n)-[r:hasSequenceMember]->(m) set r += relMetadata)\n" + + "FOREACH (_ IN case WHEN relation='associatedTo' then [1] else[] end| merge (n)-[r:associatedTo]->(m) set r += relMetadata)\n" + + "RETURN COUNT(*) AS RESULT;",query); + } + + @Test + public void testgenerateDeleteBulkRelationsCypherQuery_2() { + String query = GraphQueryGenerationUtil.generateDeleteBulkRelationsCypherQuery("domain"); + Assert.assertNotNull(query); + Assert.assertEquals("UNWIND {data} AS ROW WITH ROW.startNodeId AS startNode, ROW.endNodeId AS endNode, ROW.relation AS relation, ROW.relMetadata as relMetadata MATCH(n:domain {IL_UNIQUE_ID:startNode})-[r]->(m:domain {IL_UNIQUE_ID:endNode})FOREACH (_ IN case WHEN relation='hasSequenceMember' then [1] else[] end| delete r\n" + + ")\n" + + "FOREACH (_ IN case WHEN relation='associatedTo' then [1] else[] end| delete r\n" + + ")\n" + + "RETURN COUNT(*) AS RESULT;",query); + } + +} diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/util/NodeQueryGenerationUtilTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/util/NodeQueryGenerationUtilTest.java new file mode 100644 index 000000000..5e218bc67 --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/graph/service/util/NodeQueryGenerationUtilTest.java @@ -0,0 +1,355 @@ +package org.sunbird.graph.service.util; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.neo4j.driver.v1.exceptions.ClientException; +import org.sunbird.graph.dac.model.Node; + +import java.lang.reflect.Method; +import java.util.*; + +public class NodeQueryGenerationUtilTest { + + @Test + public void testGenerateUpdateNodesQuery() { + List ids = Arrays.asList("do_123", "do_234"); + Map metadata = new HashMap<>() {{ + put("version", "3"); + put("status", "Review"); + }}; + Map param = new HashMap<>(); + String query = NodeQueryGenerationUtil.generateUpdateNodesQuery("domain", ids, metadata, param); + Assert.assertTrue(StringUtils.isNoneBlank(query)); + Assert.assertTrue(MapUtils.isNotEmpty(param)); + Assert.assertTrue(param.size() == 3); + Assert.assertEquals("MATCH(n:domain) WHERE n.IL_UNIQUE_ID IN {identifiers} SET n.version = {1} , n.status = {2} RETURN n AS ee ;", query); + } + + @Test + public void testGenerateDeleteNodeCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", "do_123"); + put("metadata", new HashMap()); + }}; + String query = NodeQueryGenerationUtil.generateDeleteNodeCypherQuery(parameterMap); + Assert.assertTrue(StringUtils.isNoneBlank(query)); + Assert.assertEquals("MATCH (a:domain {IL_UNIQUE_ID: 'do_123'}) DETACH DELETE a RETURN a", query); + } + + @Test(expected = ClientException.class) + public void testGenerateDeleteNodeCypherQueryInvalidGraph() { + Map parameterMap = new HashMap<>() {{ + put("graphId", ""); + put("nodeId", "do_123"); + }}; + NodeQueryGenerationUtil.generateDeleteNodeCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateDeleteNodeCypherQueryInvalidIdentifier() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", ""); + }}; + NodeQueryGenerationUtil.generateDeleteNodeCypherQuery(parameterMap); + } + + @Test + public void testGenerateUpdateNodeCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("node", getNode()); + }}; + NodeQueryGenerationUtil.generateUpdateNodeCypherQuery(parameterMap); + Assert.assertTrue(MapUtils.isNotEmpty((Map) parameterMap.get("queryStatementMap"))); + } + + @Test(expected = ClientException.class) + public void testGenerateUpdateNodeCypherQueryInvalidNode() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("node", null); + }}; + NodeQueryGenerationUtil.generateUpdateNodeCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateUpdateNodeCypherQueryEmptyGraphId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", ""); + put("node", new Node()); + }}; + NodeQueryGenerationUtil.generateUpdateNodeCypherQuery(parameterMap); + } + + @Test + public void testGenerateUpdateNodeCypherQueryEmptyNode() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("node", new Node()); + }}; + NodeQueryGenerationUtil.generateUpdateNodeCypherQuery(parameterMap); + Assert.assertTrue(MapUtils.isNotEmpty((Map) parameterMap.get("queryStatementMap"))); + } + + @Test + public void testGenerateUpsertRootNodeCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("rootNode", getNode()); + }}; + String query = NodeQueryGenerationUtil.generateUpsertRootNodeCypherQuery(parameterMap); + Assert.assertTrue(StringUtils.isNoneBlank(query)); + } + + @Test(expected = ClientException.class) + public void testGenerateUpsertRootNodeCypherQueryWithoutRootNode() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("rootNode", null); + }}; + NodeQueryGenerationUtil.generateUpsertRootNodeCypherQuery(parameterMap); + } + + @Test + public void testGenerateUpdatePropertyValuesCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", "do_123"); + put("metadata", new HashMap()); + }}; + String query = NodeQueryGenerationUtil.generateUpdatePropertyValuesCypherQuery(parameterMap); + Assert.assertTrue(StringUtils.isNoneBlank(query)); + Assert.assertEquals("MATCH(ee:domain WHERE ee.IL_UNIQUE_ID='do_123' SET ", query); + } + + @Test(expected = ClientException.class) + public void testGenerateUpdatePropertyValuesCypherQueryEmptyGraphId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", ""); + put("nodeId", "do_123"); + put("metadata", new HashMap()); + }}; + NodeQueryGenerationUtil.generateUpdatePropertyValuesCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateUpdatePropertyValuesCypherQueryEmptyNodeId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", ""); + put("metadata", new HashMap()); + }}; + NodeQueryGenerationUtil.generateUpdatePropertyValuesCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateUpdatePropertyValuesCypherQueryWithoutMetadata() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", "do_123"); + }}; + NodeQueryGenerationUtil.generateUpdatePropertyValuesCypherQuery(parameterMap); + } + + @Test + public void testGenerateRemovePropertyValueCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", "do_123"); + put("key", "test_key"); + put("metadata", new HashMap()); + }}; + String query = NodeQueryGenerationUtil.generateRemovePropertyValueCypherQuery(parameterMap); + Assert.assertTrue(StringUtils.isNoneBlank(query)); + Assert.assertEquals("MATCH(ee:domain) WHERE ee.IL_UNIQUE_ID='do_123' SET ee.test_key=null", query); + } + + @Test(expected = ClientException.class) + public void testGenerateRemovePropertyValueCypherQueryEmptyGraphId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", ""); + put("nodeId", "do_123"); + put("key", "test_key"); + put("metadata", new HashMap()); + }}; + NodeQueryGenerationUtil.generateRemovePropertyValueCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateRemovePropertyValueCypherQueryEmptyNodeId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", ""); + put("key", "test_key"); + put("metadata", new HashMap()); + }}; + NodeQueryGenerationUtil.generateRemovePropertyValueCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateRemovePropertyValueCypherQueryWithoutKey() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", "do_123"); + put("metadata", new HashMap()); + }}; + NodeQueryGenerationUtil.generateRemovePropertyValueCypherQuery(parameterMap); + } + + @Test + public void testGenerateUpsertNodeCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("node", getNodeWithoutId()); + }}; + NodeQueryGenerationUtil.generateUpsertNodeCypherQuery(parameterMap); + Assert.assertTrue(MapUtils.isNotEmpty((Map) parameterMap.get("queryStatementMap"))); + } + + @Test(expected = ClientException.class) + public void testGenerateUpsertNodeCypherQueryInvalidNode() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("node", null); + }}; + NodeQueryGenerationUtil.generateUpsertNodeCypherQuery(parameterMap); + } + + @Test + public void testGenerateCreateNodeCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("node", getNode()); + }}; + NodeQueryGenerationUtil.generateCreateNodeCypherQuery(parameterMap); + Assert.assertTrue(StringUtils.isNotEmpty((String) ((Map) ((Map) parameterMap.get("queryStatementMap")).get("do_000000123")).get("query"))); + } + + @Test(expected = ClientException.class) + public void testGenerateCreateNodeCypherQueryEmptyGraphId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", ""); + put("node", getNode()); + }}; + NodeQueryGenerationUtil.generateCreateNodeCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateCreateNodeCypherQueryInvalidNode() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("node", null); + }}; + NodeQueryGenerationUtil.generateCreateNodeCypherQuery(parameterMap); + } + + @Test + public void testGenerateRemovePropertyValuesCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", "do_123"); + put("keys", new ArrayList()); + put("metadata", new HashMap()); + }}; + String query = NodeQueryGenerationUtil.generateRemovePropertyValuesCypherQuery(parameterMap); + Assert.assertTrue(StringUtils.isNoneBlank(query)); + Assert.assertEquals("MATCH(ee:domain) WHERE ee.IL_UNIQUE_ID='do_123' SET ", query); + } + + @Test(expected = ClientException.class) + public void testGenerateRemovePropertyValuesCypherQueryEmptyGraphId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", ""); + put("nodeId", "do_123"); + put("keys", new ArrayList()); + put("metadata", new HashMap()); + }}; + NodeQueryGenerationUtil.generateRemovePropertyValuesCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateRemovePropertyValuesCypherQueryEmptyNodeId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", ""); + put("keys", new ArrayList()); + put("metadata", new HashMap()); + }}; + NodeQueryGenerationUtil.generateRemovePropertyValuesCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateRemovePropertyValuesCypherQueryWithoutKey() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodeId", "do_123"); + put("metadata", new HashMap()); + }}; + NodeQueryGenerationUtil.generateRemovePropertyValuesCypherQuery(parameterMap); + } + + @Test + public void testGetClassicNodeDeleteCypherQuery() throws Exception { + NodeQueryGenerationUtil nodeQueryGenerationUtil = new NodeQueryGenerationUtil(); + Method getClassicNodeDeleteCypherQueryMethod = NodeQueryGenerationUtil.class.getDeclaredMethod("getClassicNodeDeleteCypherQuery", String.class, String.class); + getClassicNodeDeleteCypherQueryMethod.setAccessible(true); + String query = (String) getClassicNodeDeleteCypherQueryMethod.invoke(nodeQueryGenerationUtil, "domain", "do_123"); + Assert.assertTrue(StringUtils.isNoneBlank(query)); + Assert.assertEquals("MATCH(ee:domain)-[r]-() WHERE ee.IL_UNIQUE_ID='do_123' DELETE ee, r", query); + } + + @Test + public void testGenerateImportNodesCypherQuery() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodes", new ArrayList() {{ + add(getNode()); + }}); + }}; + String query = NodeQueryGenerationUtil.generateImportNodesCypherQuery(parameterMap); + Assert.assertTrue(StringUtils.isNoneBlank(query)); + } + + @Test(expected = ClientException.class) + public void testGenerateImportNodesCypherQueryEmptyGraphId() { + Map parameterMap = new HashMap<>() {{ + put("graphId", ""); + put("nodes", new ArrayList() {{ + add(getNode()); + }}); + }}; + NodeQueryGenerationUtil.generateImportNodesCypherQuery(parameterMap); + } + + @Test(expected = ClientException.class) + public void testGenerateImportNodesCypherQueryEmptyNodes() { + Map parameterMap = new HashMap<>() {{ + put("graphId", "domain"); + put("nodes", new ArrayList()); + }}; + NodeQueryGenerationUtil.generateImportNodesCypherQuery(parameterMap); + } + + + private Node getNode() { + Node node = new Node("do_000000123", "DATA_NODE", "Content"); + node.setMetadata(new HashMap<>() {{ + put("status", "Draft"); + put("name", "Test Node"); + put("identifier", "do_000000123"); + }}); + return node; + } + + private Node getNodeWithoutId() { + Node node = new Node("domain", new HashMap<>() {{ + put("status", "Draft"); + put("name", "Test Node"); + }}); + return node; + } +} diff --git a/ontology-engine/graph-dac-api/src/test/java/org/sunbird/test/BaseTest.java b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/test/BaseTest.java new file mode 100644 index 000000000..7e8053717 --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/java/org/sunbird/test/BaseTest.java @@ -0,0 +1,84 @@ +package org.sunbird.test; + +import org.apache.commons.io.FileUtils; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.factory.GraphDatabaseFactory; +import org.neo4j.graphdb.factory.GraphDatabaseSettings; +//import org.neo4j.kernel.configuration.BoltConnector; +import org.sunbird.common.Platform; +import org.sunbird.graph.service.util.DriverUtil; + +import java.io.File; +import java.io.IOException; + + +public class BaseTest { + + protected static GraphDatabaseService graphDb = null; + + private static String NEO4J_SERVER_ADDRESS = "localhost:7687"; + private static String GRAPH_DIRECTORY_PROPERTY_KEY = "graph.dir"; + private static String BOLT_ENABLED = "true"; + + @AfterClass + public static void afterTest() throws Exception { + tearEmbeddedNeo4JSetup(); + DriverUtil.closeDrivers(); + } + + @BeforeClass + public static void before() throws Exception { + setupEmbeddedNeo4J(); + } + + private static void registerShutdownHook(final GraphDatabaseService graphDb) { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + tearEmbeddedNeo4JSetup(); + System.out.println("cleanup Done!!"); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + private static void setupEmbeddedNeo4J() throws Exception { + if (graphDb == null) { + //BoltConnector bolt = new BoltConnector("0"); + GraphDatabaseSettings.BoltConnector bolt = GraphDatabaseSettings.boltConnector("0"); + graphDb = new GraphDatabaseFactory() + .newEmbeddedDatabaseBuilder(new File(Platform.config.getString(GRAPH_DIRECTORY_PROPERTY_KEY))) + .setConfig(bolt.type, "BOLT").setConfig(bolt.enabled, BOLT_ENABLED) + .setConfig(bolt.address, NEO4J_SERVER_ADDRESS).newGraphDatabase(); + registerShutdownHook(graphDb); + } + } + + private static void tearEmbeddedNeo4JSetup() throws Exception { + if (null != graphDb) + graphDb.shutdown(); + Thread.sleep(2000); + deleteEmbeddedNeo4j(new File(Platform.config.getString(GRAPH_DIRECTORY_PROPERTY_KEY))); + } + + private static void deleteEmbeddedNeo4j(final File emDb) throws IOException { + FileUtils.deleteDirectory(emDb); + } + + protected static void delay(long time) { + try { + Thread.sleep(time); + } catch (Exception e) { + e.printStackTrace(); + } + } + + protected void createBulkNodes() { + graphDb.execute("UNWIND [{nodeId:'do_0000123'},{nodeId:'do_0000234'},{nodeId:'do_0000345'}] as row with row.nodeId as Id CREATE (n:domain{IL_UNIQUE_ID:Id});"); + } +} diff --git a/ontology-engine/graph-dac-api/src/test/resources/application.conf b/ontology-engine/graph-dac-api/src/test/resources/application.conf new file mode 100644 index 000000000..c2de3a014 --- /dev/null +++ b/ontology-engine/graph-dac-api/src/test/resources/application.conf @@ -0,0 +1,14 @@ +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" \ No newline at end of file diff --git a/ontology-engine/graph-engine_2.11/pom.xml b/ontology-engine/graph-engine_2.11/pom.xml index 44a51aedc..a85149bf0 100644 --- a/ontology-engine/graph-engine_2.11/pom.xml +++ b/ontology-engine/graph-engine_2.11/pom.xml @@ -14,7 +14,7 @@ org.sunbird - graph-core + graph-core_2.11 1.0-SNAPSHOT @@ -32,22 +32,33 @@ parseq 1.0-SNAPSHOT + + com.twitter + storehaus-cache_${scala.maj.version} + 0.15.0 + org.scalatest - scalatest_2.11 + scalatest_${scala.maj.version} 3.0.8 test org.neo4j neo4j-bolt - 3.3.4 + 3.5.0 test org.neo4j neo4j-graphdb-api - 3.3.4 + 3.5.0 + test + + + org.neo4j + neo4j + 3.5.0 test @@ -62,29 +73,35 @@ src/test/scala - net.alchim31.maven scala-maven-plugin - 3.2.2 + 4.4.0 + + ${scala.version} + false + + scala-compile-first + process-resources + add-source compile + + + + scala-test-compile + process-test-resources + testCompile - - - -dependencyfile - ${project.build.directory}/.scala_dependencies - - org.scalatest scalatest-maven-plugin - 1.0 + 2.0.0 test diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStore.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStore.scala index 9e76896d6..5baf6639a 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStore.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/external/store/ExternalStore.scala @@ -8,12 +8,12 @@ import com.datastax.driver.core.Session import com.datastax.driver.core.querybuilder.{Clause, Insert, QueryBuilder} import com.google.common.util.concurrent.{FutureCallback, Futures, ListenableFuture, MoreExecutors} import org.sunbird.cassandra.{CassandraConnector, CassandraStore} +import org.sunbird.common.JsonUtils import org.sunbird.common.dto.ResponseHandler import org.sunbird.common.dto.Response import org.sunbird.common.exception.{ErrorCodes, ResponseCode, ServerException} import org.sunbird.telemetry.logger.TelemetryManager -import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future, Promise} class ExternalStore(keySpace: String , table: String , primaryKey: java.util.List[String]) extends CassandraStore(keySpace, table, primaryKey) { @@ -26,11 +26,16 @@ class ExternalStore(keySpace: String , table: String , primaryKey: java.util.Lis request.remove("last_updated_on") if(propsMapping.keySet.contains("last_updated_on")) insertQuery.value("last_updated_on", new Timestamp(new Date().getTime)) + import scala.collection.JavaConverters._ for ((key, value) <- request.asScala) { - if("blob".equalsIgnoreCase(propsMapping.getOrElse(key, ""))) - insertQuery.value(key, QueryBuilder.fcall("textAsBlob", value)) - else - insertQuery.value(key, value) + propsMapping.getOrElse(key, "") match { + case "blob" => insertQuery.value(key, QueryBuilder.fcall("textAsBlob", value)) + case "string" => request.getOrDefault(key, "") match { + case value: String => insertQuery.value(key, value) + case _ => insertQuery.value(key, JsonUtils.serialize(request.getOrDefault(key, ""))) + } + case _ => insertQuery.value(key, value) + } } try { val session: Session = CassandraConnector.getSession @@ -72,6 +77,7 @@ class ExternalStore(keySpace: String , table: String , primaryKey: java.util.Lis val row = resultSet.one() val externalMetadataMap = extProps.map(prop => prop -> row.getObject(prop)).toMap val response = ResponseHandler.OK() + import scala.collection.JavaConverters._ response.putAll(externalMetadataMap.asJava) response } else { @@ -87,6 +93,95 @@ class ExternalStore(keySpace: String , table: String , primaryKey: java.util.Lis } } + def delete(identifiers: List[String])(implicit ec: ExecutionContext): Future[Response] = { + val delete = QueryBuilder.delete() + import scala.collection.JavaConversions._ + val deleteQuery = delete.from(keySpace, table).where(QueryBuilder.in(primaryKey.get(0), seqAsJavaList(identifiers))) + try { + val session: Session = CassandraConnector.getSession + session.executeAsync(deleteQuery).asScala.map(resultSet => { + if (!resultSet.wasApplied()) + TelemetryManager.error("Entry is not found in cassandra for content with identifiers: " + identifiers) + ResponseHandler.OK() + }) + } catch { + case e: Exception => + TelemetryManager.error("Exception Occurred While Deleting The Record. | Exception is : " + e.getMessage, e) + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, "Exception Occurred While Reading The Record. Exception is : " + e.getMessage) + } + } + + def read(identifiers: List[String], extProps: List[String], propsMapping: Map[String, String])(implicit ec: ExecutionContext): Future[Response] = { + val select = QueryBuilder.select() + select.column(primaryKey.get(0)).as(primaryKey.get(0)) + if (null != extProps && !extProps.isEmpty) { + extProps.foreach(prop => { + if ("blob".equalsIgnoreCase(propsMapping.getOrElse(prop, ""))) + select.fcall("blobAsText", QueryBuilder.column(prop)).as(prop) + else + select.column(prop).as(prop) + }) + } + val selectQuery = select.from(keySpace, table) + import scala.collection.JavaConversions._ + val clause: Clause = QueryBuilder.in(primaryKey.get(0), seqAsJavaList(identifiers)) + selectQuery.where.and(clause) + try { + val session: Session = CassandraConnector.getSession + val futureResult = session.executeAsync(selectQuery) + futureResult.asScala.map(resultSet => { + if (resultSet.iterator().hasNext) { + val response = ResponseHandler.OK() + resultSet.iterator().toStream.map(row => { + import scala.collection.JavaConverters._ + val externalMetadataMap = extProps.map(prop => prop -> row.getObject(prop)).toMap.asJava + response.put(row.getString(primaryKey.get(0)), externalMetadataMap) + }).toList + response + } else { + TelemetryManager.error("Entry is not found in external-store for object with identifiers: " + identifiers) + ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.code().toString, "Entry is not found in external-store for object with identifiers: " + identifiers) + } + }) + } catch { + case e: Exception => + e.printStackTrace() + TelemetryManager.error("Exception Occurred While Reading The Record. | Exception is : " + e.getMessage, e) + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, "Exception Occurred While Reading The Record. Exception is : " + e.getMessage) + } + } + + def update(identifier: String, columns: List[String], values: List[AnyRef], propsMapping: Map[String, String])(implicit ec: ExecutionContext): Future[Response] = { + val update = QueryBuilder.update(keySpace, table) + val clause: Clause = QueryBuilder.eq(primaryKey.get(0), identifier) + update.where.and(clause) + // if(propsMapping.keySet.contains("last_updated_on")) + // update.`with`(QueryBuilder.add("last_updated_on", new Timestamp(new Date().getTime))) + for ((column, index) <- columns.view.zipWithIndex) { + propsMapping.getOrElse(column, "").toLowerCase match { + case "blob" => update.`with`(QueryBuilder.set(column, QueryBuilder.fcall("textAsBlob", values(index)))) + case "object" => update.`with`(QueryBuilder.putAll(column, values(index).asInstanceOf[java.util.Map[String, AnyRef]])) + case "array" => update.`with`(QueryBuilder.appendAll(column, values(index).asInstanceOf[java.util.List[String]])) + case "string" => values(index) match { + case value: String => update.`with`(QueryBuilder.set(column, values(index))) + case _ => update.`with`(QueryBuilder.set(column, JsonUtils.serialize(values(index)))) + } + case _ => update.`with`(QueryBuilder.set(column, values(index))) + } + } + try { + val session: Session = CassandraConnector.getSession + session.executeAsync(update).asScala.map( resultset => { + ResponseHandler.OK() + }) + } catch { + case e: Exception => + e.printStackTrace() + TelemetryManager.error("Exception Occurred While Saving The Record. | Exception is : " + e.getMessage, e) + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name, "Exception Occurred While Saving The Record. Exception is : " + e.getMessage) + } + } + implicit class RichListenableFuture[T](lf: ListenableFuture[T]) { def asScala : Future[T] = { val p = Promise[T]() diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/health/HealthCheckManager.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/health/HealthCheckManager.scala new file mode 100644 index 000000000..89f4ccc90 --- /dev/null +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/health/HealthCheckManager.scala @@ -0,0 +1,73 @@ +package org.sunbird.graph.health + + +import com.datastax.driver.core.Session +import org.sunbird.cache.util.RedisConnector +import org.sunbird.cassandra.CassandraConnector +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.service.operation.NodeAsyncOperations + +import scala.collection.JavaConverters +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +object HealthCheckManager extends CassandraConnector with RedisConnector { + val CONNECTION_SUCCESS: String = "connection check is Successful" + val CONNECTION_FAILURE: String = "connection check has Failed" + val redisLabel = "redis cache" + val cassandraLabel = "cassandra db" + val graphDBLabel = "graph db" + + def checkAllSystemHealth()(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + + val allChecks: List[Map[String, Any]] = List(checkRedisHealth(), checkGraphHealth(), checkCassandraHealth()) + val overAllHealth = allChecks.map(check => check.getOrElse("healthy",false).asInstanceOf[Boolean]).foldLeft(true)(_ && _) + val response = ResponseHandler.OK() + response.put("checks", allChecks.map(m => JavaConverters.mapAsJavaMapConverter(m).asJava).asJava) + response.put("healthy", overAllHealth) + Future(response) + } + + private def checkGraphHealth()(implicit oec: OntologyEngineContext, ec: ExecutionContext): Map[String, Any] = { + try { + val futureNode = oec.graphService.upsertRootNode("domain", new Request()) + if (futureNode.isCompleted) { + generateCheck(true, graphDBLabel) + } else { + generateCheck(false, graphDBLabel) + } + } catch { + case e: Exception => generateCheck(false, graphDBLabel) + } + } + + private def checkCassandraHealth(): Map[String, Any] = { + var session: Session = null + try { + session = CassandraConnector.getSession + if (null != session && !session.isClosed) { + session.execute("SELECT now() FROM system.local") + generateCheck(true, cassandraLabel) + } else + generateCheck(false, cassandraLabel) + } catch { + case e: Exception => generateCheck(false, cassandraLabel) + } + } + + private def checkRedisHealth(): Map[String, Any] = { + try { + val jedis = getConnection + jedis.close() + generateCheck(true, redisLabel) + } catch { + case e: Exception => generateCheck(false, redisLabel) + } + } + + def generateCheck(healthy: Boolean = false, serviceName: String, err: Option[String] = None, errMsg: Option[String] = None): Map[String, Any] = healthy match { + case true => Map("name" -> serviceName, "healthy" -> healthy) + case false => Map("name" -> serviceName, "healthy" -> healthy, "err" -> err.getOrElse("503"), "errMsg" -> errMsg.getOrElse((serviceName + " service is unavailable"))) + } +} diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/nodes/DataNode.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/nodes/DataNode.scala index 7e2eeb4d7..355a59ed2 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/nodes/DataNode.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/nodes/DataNode.scala @@ -6,26 +6,29 @@ import java.util.concurrent.CompletionException import org.apache.commons.collections4.{CollectionUtils, MapUtils} import org.apache.commons.lang3.StringUtils -import org.sunbird.common.Platform +import org.sunbird.common.DateUtils import org.sunbird.common.dto.{Request, Response} -import org.sunbird.common.exception.{ClientException, ErrorCodes} +import org.sunbird.common.exception.{ClientException, ErrorCodes, ResponseCode} +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.common.enums.SystemProperties import org.sunbird.graph.dac.model.{Filter, MetadataCriterion, Node, Relation, SearchConditions, SearchCriteria} -import org.sunbird.graph.external.ExternalPropsManager -import org.sunbird.graph.schema.DefinitionNode -import org.sunbird.graph.service.operation.{GraphAsyncOperations, NodeAsyncOperations, SearchAsyncOperations} +import org.sunbird.graph.schema.{DefinitionDTO, DefinitionFactory, DefinitionNode} import org.sunbird.parseq.Task import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} object DataNode { + + private val SYSTEM_UPDATE_ALLOWED_CONTENT_STATUS = List("Live", "Unlisted") + @throws[Exception] - def create(request: Request)(implicit ec: ExecutionContext): Future[Node] = { + def create(request: Request, dataModifier: (Node) => Node = defaultDataModifier)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] DefinitionNode.validate(request).map(node => { - val response = NodeAsyncOperations.addNode(graphId, node) + val response = oec.graphService.addNode(graphId, dataModifier(node)) response.map(node => DefinitionNode.postProcessor(request, node)).map(result => { val futureList = Task.parallel[Response]( saveExternalProperties(node.getIdentifier, node.getExternalData, request.getContext, request.getObjectType), @@ -36,42 +39,40 @@ object DataNode { } @throws[Exception] - def update(request: Request)(implicit ec: ExecutionContext): Future[Node] = { + def update(request: Request, dataModifier: (Node) => Node = defaultDataModifier)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] val identifier: String = request.getContext.get("identifier").asInstanceOf[String] DefinitionNode.validate(identifier, request).map(node => { - val response = NodeAsyncOperations.upsertNode(graphId, node, request) + request.getContext().put("schemaName", node.getObjectType.toLowerCase.replace("image", "")) + val response = oec.graphService.upsertNode(graphId, dataModifier(node), request) response.map(node => DefinitionNode.postProcessor(request, node)).map(result => { val futureList = Task.parallel[Response]( - saveExternalProperties(node.getIdentifier, node.getExternalData, request.getContext, request.getObjectType), + updateExternalProperties(node.getIdentifier, node.getExternalData, request.getContext, request.getObjectType, request), updateRelations(graphId, node, request.getContext)) futureList.map(list => result) }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause} - }).flatMap(f => f) + }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause} } @throws[Exception] - def read(request: Request)(implicit ec: ExecutionContext): Future[Node] = { - val resultNode: Future[Node] = DefinitionNode.getNode(request) - val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] - resultNode.map(node => { + def read(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + DefinitionNode.getNode(request).map(node => { + val schema = node.getObjectType.toLowerCase.replace("image", "") + request.getContext().put("schemaName", schema) val fields: List[String] = Optional.ofNullable(request.get("fields").asInstanceOf[util.List[String]]).orElse(new util.ArrayList[String]()).toList - val extPropNameList = DefinitionNode.getExternalProps(request.getContext.get("graph_id").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String], schemaName) - val finalNodeFuture: Future[Node] = if (CollectionUtils.isNotEmpty(extPropNameList) && null != fields && fields.exists(field => extPropNameList.contains(field))) + val extPropNameList = DefinitionNode.getExternalProps(request.getContext.get("graph_id").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String], schema) + if (CollectionUtils.isNotEmpty(extPropNameList) && null != fields && fields.exists(field => extPropNameList.contains(field))) populateExternalProperties(fields, node, request, extPropNameList) else Future(node) - val isBackwardCompatible = if (Platform.config.hasPath("content.tagging.backward_enable")) Platform.config.getBoolean("content.tagging.backward_enable") else false - if(isBackwardCompatible && !StringUtils.equalsIgnoreCase(request.get("mode").asInstanceOf[String], "edit")) - finalNodeFuture.map(node => updateContentTaggedProperty(node)).flatMap(f => f) - else - finalNodeFuture - }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause} + }).flatMap(f => f) recoverWith { + case e: CompletionException => throw e.getCause + } } @throws[Exception] - def list(request: Request)(implicit ec: ExecutionContext): Future[util.List[Node]] = { + def list(request: Request, objectType: Option[String] = None)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[util.List[Node]] = { val identifiers:util.List[String] = request.get("identifiers").asInstanceOf[util.List[String]] if(null == identifiers || identifiers.isEmpty) { @@ -88,64 +89,61 @@ object DataNode { val searchCriteria = new SearchCriteria {{ addMetadata(mc) setCountQuery(false) + if (objectType.nonEmpty) + setObjectType(objectType.get) }} - SearchAsyncOperations.getNodeByUniqueIds(request.getContext.get("graph_id").asInstanceOf[String], searchCriteria) + oec.graphService.getNodeByUniqueIds(request.getContext.get("graph_id").asInstanceOf[String], searchCriteria) } } - private def saveExternalProperties(identifier: String, externalProps: util.Map[String, AnyRef], context: util.Map[String, AnyRef], objectType: String)(implicit ec: ExecutionContext): Future[Response] = { + @throws[Exception] + def bulkUpdate(request: Request)(implicit ec: ExecutionContext,oec: OntologyEngineContext): Future[util.Map[String, Node]] = { + val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] + val identifiers: util.List[String] = request.get("identifiers").asInstanceOf[util.List[String]] + val metadata: util.Map[String, AnyRef] = request.get("metadata").asInstanceOf[util.Map[String, AnyRef]] + oec.graphService.updateNodes(graphId, identifiers, metadata) + } + + @throws[Exception] + def deleteNode(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[java.lang.Boolean] = { + val graphId: String = request.getContext.getOrDefault("graph_id", "").asInstanceOf[String] + val identifier: String = request.getRequest.getOrDefault("identifier", "").asInstanceOf[String] + oec.graphService.deleteNode(graphId, identifier, request) + } + + private def saveExternalProperties(identifier: String, externalProps: util.Map[String, AnyRef], context: util.Map[String, AnyRef], objectType: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { if (MapUtils.isNotEmpty(externalProps)) { externalProps.put("identifier", identifier) val request = new Request(context, externalProps, "", objectType) - ExternalPropsManager.saveProps(request) + oec.graphService.saveExternalProps(request) } else { Future(new Response) } } + + private def updateExternalProperties(identifier: String, externalProps: util.Map[String, AnyRef], context: util.Map[String, AnyRef], objectType: String, request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Response] = { + if (MapUtils.isNotEmpty(externalProps)) { + val req = new Request(request) + req.put("identifier", identifier) + req.put("fields", externalProps.asScala.keys.toList) + req.put("values", externalProps.asScala.values.toList) + oec.graphService.updateExternalProps(req) + } else Future(new Response) + } - private def createRelations(graphId: String, node: Node, context: util.Map[String, AnyRef])(implicit ec: ExecutionContext) : Future[Response] = { + private def createRelations(graphId: String, node: Node, context: util.Map[String, AnyRef])(implicit ec: ExecutionContext, oec: OntologyEngineContext) : Future[Response] = { val relations: util.List[Relation] = node.getAddedRelations if (CollectionUtils.isNotEmpty(relations)) { - GraphAsyncOperations.createRelation(graphId,getRelationMap(relations)) + oec.graphService.createRelation(graphId,getRelationMap(relations)) } else { Future(new Response) } } - /** - * To support backward compatibility to mobile team. - * @param node - * @param ec - * @return - */ - @Deprecated - private def updateContentTaggedProperty(node: Node)(implicit ec:ExecutionContext): Future[Node] = { - val contentTaggedKeys = if(Platform.config.hasPath("content.tagging.property")) - (for (prop <- Platform.config.getString("content.tagging.property").split(",")) yield prop ) (collection.breakOut) - else - List("subject", "medium") - contentTaggedKeys.map(prop => populateContentTaggedProperty(prop, node.getMetadata.getOrDefault(prop, ""), node)) - Future{node} - } - - private def populateContentTaggedProperty(key:String, value: Any, node:Node)(implicit ec: ExecutionContext): Future[Node] = { - val contentValue:String = value match { - case v: String => v.asInstanceOf[String] - case v: List[Any] => v.head.asInstanceOf[String] - case v: util.ArrayList[String] => if(null!= v && !v.isEmpty) v.get(0).asInstanceOf[String] else "" - case v: Array[String] => v.head - } - if(!StringUtils.isAllBlank(contentValue)) - node.getMetadata.put(key, contentValue) - else - node.getMetadata.remove(key) - Future(node) - } - - private def populateExternalProperties(fields: List[String], node: Node, request: Request, externalProps: List[String])(implicit ec: ExecutionContext): Future[Node] = { + private def populateExternalProperties(fields: List[String], node: Node, request: Request, externalProps: List[String])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { if(StringUtils.equalsIgnoreCase(request.get("mode").asInstanceOf[String], "edit")) - request.put("identifier", node.getIdentifier) - val externalPropsResponse = ExternalPropsManager.fetchProps(request, externalProps.filter(prop => fields.contains(prop))) + request.put("identifier", node.getIdentifier) + val externalPropsResponse = oec.graphService.readExternalProps(request, externalProps.filter(prop => fields.contains(prop))) externalPropsResponse.map(response => { node.getMetadata.putAll(response.getResult) Future { @@ -154,7 +152,7 @@ object DataNode { }).flatMap(f => f) } - private def updateRelations(graphId: String, node: Node, context: util.Map[String, AnyRef])(implicit ec: ExecutionContext) : Future[Response] = { + private def updateRelations(graphId: String, node: Node, context: util.Map[String, AnyRef])(implicit ec: ExecutionContext, oec: OntologyEngineContext) : Future[Response] = { val request: Request = new Request request.setContext(context) @@ -162,9 +160,9 @@ object DataNode { Future(new Response) } else { if (CollectionUtils.isNotEmpty(node.getDeletedRelations)) - GraphAsyncOperations.removeRelation(graphId, getRelationMap(node.getDeletedRelations)) + oec.graphService.removeRelation(graphId, getRelationMap(node.getDeletedRelations)) if (CollectionUtils.isNotEmpty(node.getAddedRelations)) - GraphAsyncOperations.createRelation(graphId,getRelationMap(node.getAddedRelations)) + oec.graphService.createRelation(graphId,getRelationMap(node.getAddedRelations)) Future(new Response) } } @@ -186,4 +184,148 @@ object DataNode { } list } + + private def defaultDataModifier(node: Node) = { + node + } + + @throws[Exception] + def systemUpdate(request: Request, nodeList: util.List[Node], hierarchyKey: String, hierarchyFunc: Option[Request => Future[Response]] = None)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val data: util.Map[String, AnyRef] = request.getRequest + // validate nodes + validateNode(nodeList, request) + + // get definition for the object and filter relations + val definition = getDefinition(request) + val metadata = filterRelations(definition, data) + // get status + val status = getStatus(request, nodeList) + // Generate request for new metadata + val newRequest = new Request(request) + newRequest.putAll(metadata) + newRequest.getContext.put("versioning", "disabled") + // Enrich Hierarchy and Update the nodes + nodeList.map(node => { + enrichHierarchyAndUpdate(newRequest, node, status, hierarchyKey, hierarchyFunc) + }).head + } + + @throws[Exception] + private def enrichHierarchyAndUpdate(request: Request, node: Node, status: String, hierarchyKey: String, hierarchyFunc: Option[Request => Future[Response]] = None)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val metadata: util.Map[String, AnyRef] = request.getRequest + val identifier = node.getIdentifier + // Image node cannot be made Live or Unlisted using system call + if (identifier.endsWith(".img") && + SYSTEM_UPDATE_ALLOWED_CONTENT_STATUS.contains(status)) metadata.remove("status") + if (metadata.isEmpty) throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), s"Invalid Request. Cannot update status of Image Node to $status.") + + // Update previous status and status update Timestamp + if (metadata.containsKey("status")) { + metadata.put("prevStatus", node.getMetadata.get("status")) + metadata.put("lastStatusChangedOn", DateUtils.formatCurrentDate) + } + // Generate new request object for Each request + val newRequest = new Request(request) + newRequest.putAll(metadata) + newRequest.getContext.put("identifier", identifier) + // Enrich Hierarchy and Update with the new request + enrichHierarchy(newRequest, metadata, status, hierarchyKey: String, hierarchyFunc) + .flatMap(req => update(req)) recoverWith { case e: CompletionException => throw e.getCause} + } + + private def enrichHierarchy(request: Request, metadata: util.Map[String, AnyRef], status: String, hierarchyKey: String, hierarchyFunc: Option[Request => Future[Response]] = None)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Request] = { + val identifier = request.getContext.get("identifier").asInstanceOf[String] + // Check if hierarchy could be enriched + if (!identifier.endsWith(".img") && SYSTEM_UPDATE_ALLOWED_CONTENT_STATUS.contains(status)) { + hierarchyFunc match { + case Some(hierarchyFunc) => { + // Get current Hierarchy + val hierarchyRequest = new Request(request) + hierarchyRequest.put("rootId", identifier) + hierarchyFunc(hierarchyRequest).map(response => { + // Add metadata to the hierarchy + if (response.get(hierarchyKey) != null) { + val hierarchy = response.get(hierarchyKey).asInstanceOf[util.Map[String, AnyRef]] + val hierarchyMetadata = new util.HashMap[String, AnyRef]() + hierarchyMetadata.putAll(hierarchy) + hierarchyMetadata.putAll(metadata) + // add hierarchy to the request object + request.put("hierarchy", hierarchyMetadata) + request + } else request + }) + } + case _ => Future(request) + } + } else Future(request) + } + + def validateNode(nodes: java.util.List[Node], request: Request): Unit = { + if (nodes.isEmpty) + throw new ClientException(ResponseCode.RESOURCE_NOT_FOUND.name(), s"Error! Node(s) doesn't Exists with identifier : ${request.getContext.get("identifier")}.") + + val objectType = request.getContext.get("objectType").asInstanceOf[String] + nodes.foreach(node => { + if (node.getMetadata == null && !objectType.equalsIgnoreCase(node.getObjectType) && node.getMetadata.get("status").asInstanceOf[String].equalsIgnoreCase("failed")) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), s"Cannot update content with FAILED status for id : ${node.getIdentifier}.") + }) + } + + @throws[Exception] + def search(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[List[Node]] = { + list(request, Some(request.getObjectType)).map(nodeList => { + validateNodeList(request, nodeList) + val fields: List[String] = Optional.ofNullable(request.get("fields").asInstanceOf[util.List[String]]).orElse(new util.ArrayList[String]()).toList + val extPropNameList = DefinitionNode.getExternalProps(request.getContext.get("graph_id").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String], request.getContext().get("schemaName").asInstanceOf[String]) + if (CollectionUtils.isEmpty(fields) && CollectionUtils.isNotEmpty(extPropNameList)) + populateExternalProperties(nodeList.asScala.toList, extPropNameList, request, extPropNameList) + else if (CollectionUtils.isNotEmpty(extPropNameList) && fields.exists(field => extPropNameList.contains(field))) + populateExternalProperties(nodeList.asScala.toList, fields, request, extPropNameList) + else + Future(nodeList.asScala.toList) + }).flatMap(f => f) recoverWith { + case e: CompletionException => throw e.getCause + } + } + + private def populateExternalProperties(nodes: List[Node], fields: List[String], request: Request, externalProps: List[String])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + request.put("identifiers", nodes.map(node => node.getIdentifier)) + val externalPropsResponse = oec.graphService.readExternalProps(request, externalProps.filter(prop => fields.contains(prop))) + externalPropsResponse.map(response => { + nodes.foreach(node => { + val externalData = Optional.ofNullable(response.get(node.getIdentifier).asInstanceOf[util.Map[String, AnyRef]]).orElse(new util.HashMap[String, AnyRef]()) + node.getMetadata.putAll(externalData) + }) + nodes + }) + } + + private def validateNodeList(request: Request, nodeList: util.List[Node]): Unit = { + val requestIdentifiers = request.get("identifier").asInstanceOf[util.List[String]] + if (requestIdentifiers.length != nodeList.length) { + val nodeIdentifiers = nodeList.map(node => node.getIdentifier) + val missingIds = requestIdentifiers.filter(identifier => !nodeIdentifiers.contains(identifier)) + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), s"Request contains invalid identifiers : ${missingIds.mkString("[", ", ", "]")}.") + } + } + + private def getStatus(request: Request, nodeList: util.List[Node]): String = { + val node = nodeList.filter(node => !node.getIdentifier.endsWith(".img")).headOption.getOrElse(nodeList.head) + request.getOrDefault("status", node.getMetadata.get("status")).asInstanceOf[String] + } + + private def getDefinition(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): DefinitionDTO = { + val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] + val graphId = request.getContext.get("graph_id").asInstanceOf[String] + val version = request.getContext.get("version").asInstanceOf[String] + DefinitionFactory.getDefinition(graphId, schemaName, version) + } + + private def filterRelations(definition: DefinitionDTO, data: util.Map[String, AnyRef]): util.Map[String, AnyRef] = { + val relations = definition.getRelationsMap().keySet() + data.filter(item => { + !relations.contains(item._1) + }) + } + } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/AbstractRelation.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/AbstractRelation.scala index b89addce7..4466c0264 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/AbstractRelation.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/AbstractRelation.scala @@ -1,14 +1,17 @@ package org.sunbird.graph.relations import org.apache.commons.lang3.{BooleanUtils, StringUtils} -import org.sunbird.common.dto.{Request} +import org.sunbird.common.dto.Request import org.sunbird.common.exception.{MiddlewareException, ServerException} +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.common.enums.GraphDACParams import org.sunbird.graph.dac.model.Node -import org.sunbird.graph.exception.GraphRelationErrorCodes +import org.sunbird.graph.exception.GraphErrorCodes import org.sunbird.graph.schema.DefinitionFactory import org.sunbird.graph.service.operation.{Neo4JBoltGraphOperations, Neo4JBoltSearchOperations} +import scala.concurrent.ExecutionContext + abstract class AbstractRelation(graphId: String, startNode: Node, endNode: Node, metadata: java.util.Map[String, AnyRef]) extends IRelation { override def createRelation(req: Request): String = { @@ -31,21 +34,21 @@ abstract class AbstractRelation(graphId: String, startNode: Node, endNode: Node, else null } - def validateObjectTypes(startNodeObjectType: String, endNodeObjectType: String, schemaName: String): String = { + def validateObjectTypes(startNodeObjectType: String, endNodeObjectType: String, schemaName: String, schemaVersion: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): String = { if(StringUtils.isNotBlank(startNodeObjectType) && StringUtils.isNotBlank(endNodeObjectType)) { - val objectTypes = DefinitionFactory.getDefinition("domain", schemaName, "1.0").getOutRelationObjectTypes + val objectTypes = DefinitionFactory.getDefinition("domain", schemaName, schemaVersion).getOutRelationObjectTypes if(!objectTypes.contains(getRelationType + ":" + endNodeObjectType)) getRelationType + " is not allowed between " + startNodeObjectType + " and " + endNodeObjectType else null } else null } - def checkCycle(req: Request): String = try { + def checkCycle(req: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): String = try { val request = new Request(req) request.put(GraphDACParams.start_node_id.name, this.endNode.getIdentifier) request.put(GraphDACParams.relation_type.name, getRelationType) request.put(GraphDACParams.end_node_id.name, this.startNode.getIdentifier) - val result = Neo4JBoltSearchOperations.checkCyclicLoop(graphId, this.endNode.getIdentifier, getRelationType(),this.startNode.getIdentifier); + val result = oec.graphService.checkCyclicLoop(graphId, this.endNode.getIdentifier,this.startNode.getIdentifier, getRelationType()) val loop = result.get(GraphDACParams.loop.name).asInstanceOf[Boolean] if (BooleanUtils.isTrue(loop)) { result.get(GraphDACParams.message.name).asInstanceOf[String] @@ -56,8 +59,10 @@ abstract class AbstractRelation(graphId: String, startNode: Node, endNode: Node, } } catch { case ex: MiddlewareException => + ex.printStackTrace() throw ex; case e: Exception => - throw new ServerException(GraphRelationErrorCodes.ERR_RELATION_VALIDATE.name, "Error occurred while validating the relation", e) + e.printStackTrace() + throw new ServerException(GraphErrorCodes.ERR_RELATION_VALIDATE.toString, "Error occurred while validating the relation", e) } } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/AssociationRelation.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/AssociationRelation.scala index 2dc680083..2ce9be319 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/AssociationRelation.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/AssociationRelation.scala @@ -2,21 +2,25 @@ package org.sunbird.graph.relations import org.sunbird.common.dto.{Request, Response, ResponseHandler} import org.sunbird.common.exception.{ErrorCodes, ResponseCode} +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.common.enums.GraphDACParams import org.sunbird.graph.dac.enums.{RelationTypes, SystemNodeTypes} import org.sunbird.graph.dac.model.Node +import scala.concurrent.ExecutionContext + class AssociationRelation(graphId: String, startNode: Node, endNode: Node, metadata: java.util.Map[String, AnyRef]) extends AbstractRelation(graphId, startNode, endNode, metadata) { override def getRelationType(): String = { RelationTypes.ASSOCIATED_TO.relationName() } - override def validate(request: Request): List[String] = { + override def validate(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): List[String] = { val schemaName = request.getContext.get("schemaName").asInstanceOf[String] + val schemaVersion = request.getContext.get("version").asInstanceOf[String] val errList:List[String] = List(validateNodeTypes(startNode, List(SystemNodeTypes.DATA_NODE.name())), validateNodeTypes(endNode, List(SystemNodeTypes.DATA_NODE.name, SystemNodeTypes.SET.name)), - validateObjectTypes(startNode.getObjectType, endNode.getObjectType, schemaName)).filter(err => (null != err && !err.isEmpty)) + validateObjectTypes(startNode.getObjectType, endNode.getObjectType, schemaName, schemaVersion)).filter(err => (null != err && !err.isEmpty)) errList } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/IRelation.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/IRelation.scala index 8bcbdcb9e..d62d0414d 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/IRelation.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/IRelation.scala @@ -1,10 +1,13 @@ package org.sunbird.graph.relations import org.sunbird.common.dto.Request +import org.sunbird.graph.OntologyEngineContext + +import scala.concurrent.ExecutionContext abstract class IRelation { - def validate(request: Request): List[String] + def validate(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): List[String] def createRelation(req: Request):String diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/RelationHandler.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/RelationHandler.scala index 53a7e698b..b4264f251 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/RelationHandler.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/RelationHandler.scala @@ -4,7 +4,7 @@ import org.apache.commons.lang3.StringUtils import org.sunbird.common.exception.ClientException import org.sunbird.graph.dac.enums.RelationTypes import org.sunbird.graph.dac.model.Node -import org.sunbird.graph.exception.GraphRelationErrorCodes +import org.sunbird.graph.exception.GraphErrorCodes object RelationHandler { @@ -15,10 +15,10 @@ object RelationHandler { else if (StringUtils.equals(RelationTypes.SEQUENCE_MEMBERSHIP.relationName, relationType)) new SequenceMembershipRelation(graphId, startNode, endNode, metadata) else { - throw new ClientException(GraphRelationErrorCodes.ERR_RELATION_CREATE.name, "UnSupported Relation: " + relationType) + throw new ClientException(GraphErrorCodes.ERR_RELATION_CREATE.toString, "UnSupported Relation: " + relationType) } } else { - throw new ClientException(GraphRelationErrorCodes.ERR_RELATION_CREATE.name, "UnSupported Relation: " + relationType) + throw new ClientException(GraphErrorCodes.ERR_RELATION_CREATE.toString, "UnSupported Relation: " + relationType) } } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/SequenceMembershipRelation.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/SequenceMembershipRelation.scala index bf0528d18..5fb13bd93 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/SequenceMembershipRelation.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/relations/SequenceMembershipRelation.scala @@ -1,12 +1,14 @@ package org.sunbird.graph.relations import org.apache.commons.lang3.StringUtils -import org.sunbird.common.dto.{Request, Response, ResponseHandler} -import org.sunbird.common.exception.{ErrorCodes, ResponseCode, ServerException} -import org.sunbird.graph.common.enums.GraphDACParams +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.ServerException +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.dac.enums.RelationTypes import org.sunbird.graph.dac.model.Node -import org.sunbird.graph.exception.GraphRelationErrorCodes +import org.sunbird.graph.exception.GraphErrorCodes + +import scala.concurrent.ExecutionContext class SequenceMembershipRelation(graphId: String, startNode: Node, endNode: Node, metadata: java.util.Map[String, AnyRef]) extends AbstractRelation(graphId, startNode, endNode, metadata) { @@ -15,12 +17,12 @@ class SequenceMembershipRelation(graphId: String, startNode: Node, endNode: Node } - override def validate(request: Request): List[String] = try { + override def validate(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): List[String] = try { val errList = List(checkCycle(request)).filter(err => StringUtils.isNotBlank(err)) errList } catch { case e: Exception => - throw new ServerException(GraphRelationErrorCodes.ERR_RELATION_VALIDATE.name, e.getMessage, e) + throw new ServerException(GraphErrorCodes.ERR_RELATION_VALIDATE.toString, e.getMessage, e) } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/CategoryDefinitionValidator.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/CategoryDefinitionValidator.scala new file mode 100644 index 000000000..075e330a3 --- /dev/null +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/CategoryDefinitionValidator.scala @@ -0,0 +1,89 @@ +package org.sunbird.graph.schema + +import java.io.{ByteArrayInputStream, File} +import java.net.URI +import java.util +import java.util.concurrent.CompletionException + +import com.typesafe.config.{Config, ConfigFactory} +import org.apache.commons.lang3.StringUtils +import org.leadpony.justify.api.JsonSchema +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.{ResourceNotFoundException, ResponseCode, ServerException} +import org.sunbird.common.{JsonUtils, Platform} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.external.ExternalPropsManager +import org.sunbird.schema.impl.BaseSchemaValidator +import org.sunbird.telemetry.logger.TelemetryManager + +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext} +import scala.io.Source + +class CategoryDefinitionValidator(schemaName: String, version: String) extends BaseSchemaValidator(schemaName, version){ + private val basePath = {if (Platform.config.hasPath("schema.base_path")) Platform.config.getString("schema.base_path") + else "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/local/"} + File.separator + schemaName.toLowerCase + File.separator + version + File.separator + + override def resolveSchema(id: URI): JsonSchema = { + null + } + + def loadSchema(categoryId: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): CategoryDefinitionValidator = { + if(ObjectCategoryDefinitionMap.containsKey(categoryId) && null != ObjectCategoryDefinitionMap.get(categoryId)){ + this.schema = ObjectCategoryDefinitionMap.get(categoryId).getOrElse("schema", null).asInstanceOf[JsonSchema] + this.config = ObjectCategoryDefinitionMap.get(categoryId).getOrElse("config", null).asInstanceOf[Config] + } + else { + val (schemaMap, configMap) = prepareSchema(categoryId) + this.schema = readSchema(new ByteArrayInputStream(JsonUtils.serialize(schemaMap).getBytes)) + this.config = ConfigFactory.parseMap(configMap) + ObjectCategoryDefinitionMap.put(categoryId, Map("schema" -> schema, "config" -> config)) + } + this + } + + def prepareSchema(categoryId: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): (java.util.Map[String, AnyRef], java.util.Map[String, AnyRef]) = { + val request: Request = new Request() + val context = new util.HashMap[String, AnyRef]() + context.put("schemaName", "objectcategorydefinition") + context.put("version", "1.0") + request.setContext(context) + request.put("identifier", categoryId) + val response: Response = { + val resp = Await.result(oec.graphService.readExternalProps(request, List("objectMetadata")), Duration.apply("30 seconds")) + if (ResponseHandler.checkError(resp)) { + if(StringUtils.equalsAnyIgnoreCase(resp.getResponseCode.name(), ResponseCode.RESOURCE_NOT_FOUND.name())) { + if ("all".equalsIgnoreCase(categoryId.substring(categoryId.lastIndexOf("_") + 1))) + throw new ResourceNotFoundException(resp.getParams.getErr, resp.getParams.getErrmsg + " " + resp.getResult) + else { + val updatedId = categoryId.replace(categoryId.substring(categoryId.lastIndexOf("_") + 1), "all") + request.put("identifier", updatedId) + val channelCatResp = Await.result(oec.graphService.readExternalProps(request, List("objectMetadata")), Duration.apply("30 seconds")) + if(StringUtils.equalsAnyIgnoreCase(channelCatResp.getResponseCode.name(), ResponseCode.RESOURCE_NOT_FOUND.name())) { + throw new ResourceNotFoundException(channelCatResp.getParams.getErr, channelCatResp.getParams.getErrmsg + " " + channelCatResp.getResult) + } else channelCatResp + } + } else throw new ServerException(resp.getParams.getErr, resp.getParams.getErrmsg + " " + resp.getResult) + } else resp + } + populateSchema(response, categoryId) + } + + def populateSchema(response: Response, identifier: String) : (java.util.Map[String, AnyRef], java.util.Map[String, AnyRef]) = { + val jsonString = getFileToString("schema.json") + val schemaMap: java.util.Map[String, AnyRef] = JsonUtils.deserialize(jsonString, classOf[java.util.Map[String, AnyRef]]) + val configMap: java.util.Map[String, AnyRef] = JsonUtils.deserialize(getFileToString("config.json"), classOf[java.util.Map[String, AnyRef]]) + val objectMetadata = response.getResult.getOrDefault("objectMetadata", new util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]] + val nodeSchema = JsonUtils.deserialize(objectMetadata.getOrDefault("schema", "{}").asInstanceOf[String], classOf[java.util.Map[String, AnyRef]]) + schemaMap.getOrDefault("properties", new java.util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]].putAll(nodeSchema.getOrDefault("properties", new java.util.HashMap[String, AnyRef]()).asInstanceOf[java.util.Map[String, AnyRef]]) + schemaMap.getOrDefault("required", new java.util.ArrayList[String]()).asInstanceOf[java.util.List[String]].addAll(nodeSchema.getOrDefault("required", new java.util.ArrayList[String]()).asInstanceOf[java.util.List[String]]) + configMap.putAll(JsonUtils.deserialize(objectMetadata.getOrDefault("config", "{}").asInstanceOf[String], classOf[java.util.Map[String, AnyRef]])) + (schemaMap, configMap) + } + + def getFileToString(fileName: String): String = { + if(basePath startsWith "http") Source.fromURL(basePath + fileName).mkString + else Source.fromFile(basePath + fileName).mkString + } +} diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/CoreDomainObject.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/CoreDomainObject.scala index 79263eff6..17b9ea2d9 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/CoreDomainObject.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/CoreDomainObject.scala @@ -1,5 +1,5 @@ package org.sunbird.graph.schema -abstract class CoreDomainObject(graphId: String, schemaName: String, version: String) { +abstract class CoreDomainObject(graphId: String, schemaName: String, version: String, categoryId: String) { } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionDTO.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionDTO.scala index b29b33c76..0db627ce6 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionDTO.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionDTO.scala @@ -4,14 +4,18 @@ import java.util import org.apache.commons.collections4.MapUtils import org.apache.commons.lang3.StringUtils +import org.sunbird.common.dto.Request +import org.sunbird.common.exception.{ClientException, ResponseCode} +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.common.Identifier import org.sunbird.graph.dac.enums.SystemNodeTypes import org.sunbird.graph.dac.model.Node import org.sunbird.graph.schema.validator._ import scala.collection.JavaConverters._ +import scala.concurrent.ExecutionContext -class DefinitionDTO(graphId: String, schemaName: String, version: String = "1.0") extends BaseDefinitionNode(graphId , schemaName, version) with VersionKeyValidator with VersioningNode with RelationValidator with FrameworkValidator with PropAsEdgeValidator with SchemaValidator { +class DefinitionDTO(graphId: String, schemaName: String, version: String = "1.0", categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext) extends BaseDefinitionNode(graphId, schemaName, version, categoryId) with VersionKeyValidator with VersioningNode with RelationValidator with FrameworkValidator with PropAsEdgeValidator with SchemaValidator { def getOutRelationObjectTypes: List[String] = outRelationObjectTypes @@ -46,9 +50,9 @@ class DefinitionDTO(graphId: String, schemaName: String, version: String = "1.0" def getInRelations(): List[Map[String, AnyRef]] = { if (schemaValidator.getConfig.hasPath("relations")) schemaValidator.getConfig - .getAnyRef("relations").asInstanceOf[java.util.HashMap[String, Object]].asScala - .filter(e => StringUtils.equals(e._2.asInstanceOf[java.util.HashMap[String, Object]].get("direction").asInstanceOf[String], "in")) - .map(e => Map(e._1 -> e._2)).toList + .getAnyRef("relations").asInstanceOf[java.util.HashMap[String, Object]].asScala + .filter(e => StringUtils.equals(e._2.asInstanceOf[java.util.HashMap[String, Object]].get("direction").asInstanceOf[String], "in")) + .map(e => Map(e._1 -> e._2)).toList else List() } @@ -75,7 +79,7 @@ class DefinitionDTO(graphId: String, schemaName: String, version: String = "1.0" def getRestrictPropsConfig(operation: String): List[String] = { if (schemaValidator.getConfig.hasPath("restrictProps")) { val restrictProps = schemaValidator.getConfig.getAnyRef("restrictProps") - .asInstanceOf[java.util.HashMap[String, Object]].get(operation).asInstanceOf[java.util.ArrayList[String]] + .asInstanceOf[java.util.HashMap[String, Object]].getOrDefault(operation, new util.ArrayList[String]()).asInstanceOf[java.util.ArrayList[String]] restrictProps.asScala.toList } else List() @@ -88,10 +92,39 @@ class DefinitionDTO(graphId: String, schemaName: String, version: String = "1.0" } } + def getRelationsMap(): java.util.HashMap[String, AnyRef] = { + schemaValidator.getConfig + .getAnyRef("relations").asInstanceOf[java.util.HashMap[String, AnyRef]] + } + + def getAllCopySchemes(): List[String] = schemaValidator.getConfig.hasPath("copy.scheme") match { + case true => val copySchemeSet = Set.empty ++ schemaValidator.getConfig.getObject("copy.scheme").keySet().asScala + (for (prop <- copySchemeSet) yield prop) (collection.breakOut) + case false => List() + } + + def getCopySchemeMap(request: Request): java.util.HashMap[String, Object] = + (StringUtils.isNotEmpty(request.getContext.getOrDefault("copyScheme", "").asInstanceOf[String]) + && schemaValidator.getConfig.hasPath("copy.scheme" + "." + request.getContext.get("copyScheme"))) match { + case true => schemaValidator.getConfig.getAnyRef("copy.scheme" + "." + request.getContext.get("copyScheme")).asInstanceOf[java.util.HashMap[String, Object]] + case false => new java.util.HashMap[String, Object]() + } private def generateRelationKey(relation: (String, Object)): Map[String, AnyRef] = { val relationMetadata = relation._2.asInstanceOf[java.util.HashMap[String, Object]] val objects = relationMetadata.get("objects").asInstanceOf[java.util.List[String]].asScala objects.flatMap(objectType => Map((relationMetadata.get("type").asInstanceOf[String] + "_" + relationMetadata.get("direction") + "_" + objectType) -> relation._1)).toMap } + + def validateRequest(request: Request) = { + if(schemaValidator.getConfig.hasPath("schema_restrict_api") && schemaValidator.getConfig.getBoolean("schema_restrict_api")){ + val propsList: List[String] = schemaValidator.getAllProps.asScala.toList + //Todo:: Remove this after v4 apis + val invalidProps: List[String] = request.getRequest.keySet().asScala.toList.filterNot(key => propsList.contains(key) || StringUtils.endsWith(key, "batch_count")) + + if(null != invalidProps && !invalidProps.isEmpty) + throw new ClientException(ResponseCode.CLIENT_ERROR.name, "Invalid request", java.util.Arrays.asList("Invalid Props are : " + invalidProps.asJavaCollection)) + } + } + } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionFactory.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionFactory.scala index d6c0af821..0ee62ee23 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionFactory.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionFactory.scala @@ -1,19 +1,23 @@ package org.sunbird.graph.schema +import org.sunbird.graph.OntologyEngineContext + +import scala.concurrent.ExecutionContext + object DefinitionFactory { private var definitions: Map[String, DefinitionDTO] = Map() - def getDefinition(graphId: String, schemaName: String, version: String): DefinitionDTO = { - val key = getKey(graphId, schemaName, version) - val definition = definitions.getOrElse(key, new DefinitionDTO(graphId, schemaName, version)) + def getDefinition(graphId: String, schemaName: String, version: String, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext): DefinitionDTO = { + val key = getKey(graphId, schemaName, version, categoryId) + val definition = definitions.getOrElse(key, new DefinitionDTO(graphId, schemaName, version, categoryId)) if (!definitions.contains(key)) definitions += (key -> definition) definition } - - def getKey(graphId: String, schemaName: String, version: String): String = { - graphId + ":" + schemaName + ":" + version + + def getKey(graphId: String, schemaName: String, version: String, categoryId: String = ""): String = { + List(graphId, schemaName, version, categoryId) filter (value => null!= value && value.nonEmpty) mkString ":" } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionNode.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionNode.scala index f8cd6457d..951e9d1b0 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionNode.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/DefinitionNode.scala @@ -5,9 +5,10 @@ import java.util.concurrent.CompletionException import org.apache.commons.collections4.{CollectionUtils, MapUtils} import org.apache.commons.lang3.StringUtils -import org.sunbird.cache.util.RedisCacheUtil +import org.sunbird.cache.impl.RedisCache import org.sunbird.common.JsonUtils import org.sunbird.common.dto.Request +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.dac.model.{Node, Relation} import scala.collection.JavaConversions._ @@ -15,88 +16,108 @@ import scala.concurrent.{ExecutionContext, Future} object DefinitionNode { - def validate(request: Request)(implicit ec: ExecutionContext): Future[Node] = { + def validate(request: Request, setDefaultValue: Boolean = true)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] val version: String = request.getContext.get("version").asInstanceOf[String] val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] - val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + val categoryId: String = getPrimaryCategory(request.getRequest, schemaName, request.getContext.getOrDefault("channel", "all").asInstanceOf[String]) + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) + definition.validateRequest(request) val inputNode = definition.getNode(request.getRequest) - definition.validate(inputNode, "create") recoverWith { case e: CompletionException => throw e.getCause} + updateRelationMetadata(inputNode) + definition.validate(inputNode, "create", setDefaultValue) recoverWith { case e: CompletionException => throw e.getCause} } - def getExternalProps(graphId: String, version: String, schemaName: String): List[String] = { - val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + def getExternalProps(graphId: String, version: String, schemaName: String, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext): List[String] = { + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) definition.getExternalProps() } - def fetchJsonProps(graphId: String, version: String, schemaName: String): List[String] = { - val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + def fetchJsonProps(graphId: String, version: String, schemaName: String, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext): List[String] = { + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) definition.fetchJsonProps() } - def getInRelations(graphId: String, version: String, schemaName: String): List[Map[String, AnyRef]] = { - val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + def getInRelations(graphId: String, version: String, schemaName: String, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext): List[Map[String, AnyRef]] = { + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) definition.getInRelations() } - def getOutRelations(graphId: String, version: String, schemaName: String): List[Map[String, AnyRef]] = { - val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + def getOutRelations(graphId: String, version: String, schemaName: String, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext): List[Map[String, AnyRef]] = { + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) definition.getOutRelations() } - def getRelationDefinitionMap(graphId: String, version: String, schemaName: String): Map[String, AnyRef] = { - val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + def getRelationDefinitionMap(graphId: String, version: String, schemaName: String, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext): Map[String, AnyRef] = { + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) definition.getRelationDefinitionMap() } - def getRestrictedProperties(graphId: String, version: String, operation: String, schemaName: String): List[String] = { - val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + def getRelationsMap(request: Request, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext): java.util.HashMap[String, AnyRef] = { + val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] + val version: String = request.getContext.get("version").asInstanceOf[String] + val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) + definition.getRelationsMap() + } + + def getRestrictedProperties(graphId: String, version: String, operation: String, schemaName: String, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext): List[String] = { + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) definition.getRestrictPropsConfig(operation) } - def getNode(request: Request)(implicit ec: ExecutionContext): Future[Node] = { + def getNode(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] val definition = DefinitionFactory.getDefinition(request.getContext.get("graph_id").asInstanceOf[String] , schemaName, request.getContext.get("version").asInstanceOf[String]) - definition.getNode(request.get("identifier").asInstanceOf[String], "read", request.get("mode").asInstanceOf[String]) + definition.getNode(request.get("identifier").asInstanceOf[String], "read", if(request.getRequest.containsKey("mode")) request.get("mode").asInstanceOf[String] else "read") } @throws[Exception] - def validate(identifier: String, request: Request)(implicit ec: ExecutionContext): Future[Node] = { + def validate(identifier: String, request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] val version: String = request.getContext.get("version").asInstanceOf[String] val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] + val reqVersioning: String = request.getContext.getOrDefault("versioning", "").asInstanceOf[String] + val versioning = if(StringUtils.isBlank(reqVersioning)) None else Option(reqVersioning) + val req:util.HashMap[String, AnyRef] = new util.HashMap[String, AnyRef](request.getRequest) val skipValidation: Boolean = {if(request.getContext.containsKey("skipValidation")) request.getContext.get("skipValidation").asInstanceOf[Boolean] else false} val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) - val dbNodeFuture = definition.getNode(identifier, "update", null) - val validationResult: Future[Node] = dbNodeFuture.map(dbNode => { - resetJsonProperties(dbNode, graphId, version, schemaName) - val inputNode: Node = definition.getNode(dbNode.getIdentifier, request.getRequest, dbNode.getNodeType) - setRelationship(dbNode,inputNode) - if (dbNode.getIdentifier.endsWith(".img") && StringUtils.equalsAnyIgnoreCase("Yes", dbNode.getMetadata.get("isImageNodeCreated").asInstanceOf[String])) { - inputNode.getMetadata.put("versionKey", dbNode.getMetadata.get("versionKey")) + definition.getNode(identifier, "update", null, versioning).map(dbNode => { + val schema = dbNode.getObjectType.toLowerCase.replace("image", "") + val categoryId: String = getPrimaryCategory(dbNode.getMetadata, schema, request.getContext.getOrDefault("channel", "all").asInstanceOf[String]) + val categoryDefinition = DefinitionFactory.getDefinition(graphId, schema, version, categoryId) + categoryDefinition.validateRequest(request) + resetJsonProperties(dbNode, graphId, version, schema, categoryId) + val inputNode: Node = categoryDefinition.getNode(dbNode.getIdentifier, request.getRequest, dbNode.getNodeType) + val dbRels = getDBRelations(graphId, schema, version, req, dbNode, categoryId) + setRelationship(dbNode, inputNode, dbRels) + if (dbNode.getIdentifier.endsWith(".img") && StringUtils.equalsAnyIgnoreCase("Yes", dbNode.getMetadata.getOrDefault("isImageNodeCreated", "").asInstanceOf[String])) { + inputNode.getMetadata.put("versionKey", dbNode.getMetadata.getOrDefault("versionKey", "")) dbNode.getMetadata.remove("isImageNodeCreated") } dbNode.getMetadata.putAll(inputNode.getMetadata) - if(MapUtils.isNotEmpty(inputNode.getExternalData)){ - if(MapUtils.isNotEmpty(dbNode.getExternalData)) + if (MapUtils.isNotEmpty(inputNode.getExternalData)) { + if (MapUtils.isNotEmpty(dbNode.getExternalData)) dbNode.getExternalData.putAll(inputNode.getExternalData) else dbNode.setExternalData(inputNode.getExternalData) } - if(!skipValidation) - definition.validate(dbNode,"update") - else Future{dbNode} - }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause} - validationResult + + if (!skipValidation) + categoryDefinition.validate(dbNode, "update") + else Future (dbNode) + + }).flatMap(f => f) } - def postProcessor(request: Request, node: Node)(implicit ec: ExecutionContext): Node = { + def postProcessor(request: Request, node: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Node = { val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] val version: String = request.getContext.get("version").asInstanceOf[String] val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] - val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) - val edgeKey = definition.getEdgeKey() + val categoryId: String = getPrimaryCategory(node.getMetadata, schemaName, request.getContext.getOrDefault("channel", "all").asInstanceOf[String]) + val categoryDefinition = DefinitionFactory.getDefinition(graphId, schemaName, version, categoryId) + val edgeKey = categoryDefinition.getEdgeKey() if (null != edgeKey && !edgeKey.isEmpty) { val metadata = node.getMetadata val cacheKey = "edge_" + request.getObjectType.toLowerCase() @@ -106,28 +127,40 @@ object DefinitionNode { } if (!data.isEmpty) { metadata.get("status") match { - case "Live" => RedisCacheUtil.saveToList(cacheKey, data) - case "Retired" => RedisCacheUtil.deleteFromList(cacheKey, data) + case "Live" => RedisCache.addToList(cacheKey, data) + case "Retired" => RedisCache.removeFromList(cacheKey, data) } } } node } - private def setRelationship(dbNode: Node, inputNode: Node): Unit = { - var addRels: util.List[Relation] = new util.ArrayList[Relation]() + private def setRelationship(dbNode: Node, inputNode: Node, dbRels:util.Map[String, util.List[Relation]]): Unit = { + var addRels: util.List[Relation] = new util.ArrayList[Relation]() var delRels: util.List[Relation] = new util.ArrayList[Relation]() val inRel: util.List[Relation] = dbNode.getInRelations val outRel: util.List[Relation] = dbNode.getOutRelations - val inRelReq: util.List[Relation] = inputNode.getInRelations - val outRelReq: util.List[Relation] = inputNode.getOutRelations - if (CollectionUtils.isNotEmpty(inRelReq)) + val inRelReq: util.List[Relation] = if(CollectionUtils.isNotEmpty(inputNode.getInRelations)) new util.ArrayList[Relation](inputNode.getInRelations) else new util.ArrayList[Relation]() + val outRelReq: util.List[Relation] = if(CollectionUtils.isNotEmpty(inputNode.getOutRelations)) new util.ArrayList[Relation](inputNode.getOutRelations) else new util.ArrayList[Relation]() + if (CollectionUtils.isNotEmpty(inRelReq)) { + if(CollectionUtils.isNotEmpty(dbRels.get("in"))){ + inRelReq.addAll(dbRels.get("in")) + inputNode.setInRelations(inRelReq) + } getNewRelationsList(inRel, inRelReq, addRels, delRels) - if (CollectionUtils.isNotEmpty(outRelReq)) + } + if (CollectionUtils.isNotEmpty(outRelReq)) { + if(CollectionUtils.isNotEmpty(dbRels.get("out"))){ + outRelReq.addAll(dbRels.get("out")) + inputNode.setOutRelations(outRelReq) + } getNewRelationsList(outRel, outRelReq, addRels, delRels) - if (CollectionUtils.isNotEmpty(addRels)) + } + if (CollectionUtils.isNotEmpty(addRels)) { dbNode.setAddedRelations(addRels) - if (CollectionUtils.isNotEmpty(delRels)) + updateRelationMetadata(dbNode) + } + if (CollectionUtils.isNotEmpty(delRels)) dbNode.setDeletedRelations(delRels) } @@ -146,16 +179,119 @@ object DefinitionNode { } } - def resetJsonProperties(node: Node, graphId: String, version: String, schemaName: String):Node = { - val jsonPropList = fetchJsonProps(graphId, version, schemaName) + def updateRelationMetadata(node: Node): Unit = { + var relOcr = new util.HashMap[String, Integer]() + val rels = node.getAddedRelations + for (rel <- rels) { + val relKey = rel.getStartNodeObjectType + rel.getRelationType + rel.getEndNodeObjectType + if (relOcr.containsKey(relKey)) + relOcr.put(relKey, relOcr.get(relKey) + 1) + else relOcr.put(relKey, 1) + + if (relKey.contains("hasSequenceMember")) { + rel.setMetadata(new util.HashMap[String, AnyRef]() {{ + put("IL_SEQUENCE_INDEX", relOcr.get(relKey)); + }}) + } else rel.setMetadata(new util.HashMap[String, AnyRef]()) + } + node.setAddedRelations(rels) + } + + def resetJsonProperties(node: Node, graphId: String, version: String, schemaName: String, categoryId: String= "")(implicit ec: ExecutionContext, oec: OntologyEngineContext):Node = { + val jsonPropList = fetchJsonProps(graphId, version, schemaName, categoryId) if(!jsonPropList.isEmpty){ node.getMetadata.entrySet().map(entry => { if(jsonPropList.contains(entry.getKey)){ - entry.setValue(JsonUtils.deserialize(entry.getValue.asInstanceOf[String], classOf[Object])) + entry.getValue match { + case value: String => entry.setValue(JsonUtils.deserialize(value.asInstanceOf[String], classOf[Object])) + case _ => entry + } } }) } node } + + def getDBRelations(graphId:String, schemaName:String, version:String, request: util.Map[String, AnyRef], dbNode: Node, categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext):util.Map[String, util.List[Relation]] = { + val inRelations = new util.ArrayList[Relation]() + val outRelations = new util.ArrayList[Relation]() + val relDefMap = getRelationDefinitionMap(graphId, version, schemaName, categoryId); + if (null != dbNode) { + if (CollectionUtils.isNotEmpty(dbNode.getInRelations)) { + for (inRel <- dbNode.getInRelations()) { + val key = inRel.getRelationType() + "_in_" + inRel.getStartNodeObjectType() + if (relDefMap.containsKey(key)) { + val value = relDefMap.get(key).get + if (!request.containsKey(value)) { + inRelations.add(inRel) + } + } + } + } + if (CollectionUtils.isNotEmpty(dbNode.getOutRelations)) { + for (outRel <- dbNode.getOutRelations()) { + val key = outRel.getRelationType() + "_out_" + outRel.getEndNodeObjectType() + if (relDefMap.containsKey(key)) { + val value = relDefMap.get(key).get + if (!request.containsKey(value)) { + outRelations.add(outRel) + } + } + } + } + } + new util.HashMap[String, util.List[Relation]](){{ + put("in", inRelations) + put("out",outRelations) + }} + } + + def validateContentNodes(nodes: List[Node], graphId: String, schemaName: String, version: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[List[Node]] = { + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + val futures = nodes.map(node => { + println("Node Identifier :: " + node.getIdentifier + " primaryCategory :: " + node.getMetadata.get("primaryCategory")) + definition.validate(node, "update") recoverWith { case e: CompletionException => throw e.getCause } + }) + Future.sequence(futures) + } + def updateJsonPropsInNodes(nodes: List[Node], graphId: String, schemaName: String, version: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext) = { + nodes.map(node => { + val schema = node.getObjectType.toLowerCase.replace("image", "") + val jsonProps = fetchJsonProps(graphId, version, schema) + val metadata = node.getMetadata + metadata.filter(entry => jsonProps.contains(entry._1)).map(entry => node.getMetadata.put(entry._1, convertJsonProperties(entry, jsonProps))) + }) + } + def convertJsonProperties(entry: (String, AnyRef), jsonProps: scala.List[String]) = { + try { + JsonUtils.deserialize(entry._2.asInstanceOf[String], classOf[Object]) + } catch { + case e: Exception => entry._2 + } + } + + def getAllCopyScheme(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): List[String] = { + val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] + val version: String = request.getContext.get("version").asInstanceOf[String] + val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + definition.getAllCopySchemes() + } + + def getCopySchemeContentType(request: Request)(implicit ec: ExecutionContext, oec: OntologyEngineContext): java.util.HashMap[String, Object] = { + val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] + val version: String = request.getContext.get("version").asInstanceOf[String] + val schemaName: String = request.getContext.get("schemaName").asInstanceOf[String] + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + definition.getCopySchemeMap(request) + } + + + def getPrimaryCategory(request: java.util.Map[String, AnyRef], schemaName: String, channel: String = "all"): String = { + if(null != request && request.containsKey("primaryCategory")) { + val categoryName = request.get("primaryCategory").asInstanceOf[String] + ObjectCategoryDefinitionMap.prepareCategoryId(categoryName, schemaName, channel) + } else "" + } } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/IDefinition.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/IDefinition.scala index 4cee42e17..0cce7fa3d 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/IDefinition.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/IDefinition.scala @@ -1,22 +1,28 @@ package org.sunbird.graph.schema +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.dac.model.Node import org.sunbird.schema.{ISchemaValidator, SchemaValidatorFactory} import scala.concurrent.{ExecutionContext, Future} -abstract class IDefinition(graphId: String, schemaName: String, version: String = "1.0") extends CoreDomainObject(graphId, schemaName, version) { +abstract class IDefinition(graphId: String, schemaName: String, version: String = "1.0", categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext) extends CoreDomainObject(graphId, schemaName, version, categoryId) { - var schemaValidator: ISchemaValidator = SchemaValidatorFactory.getInstance(schemaName, version) + var schemaValidator: ISchemaValidator = if(categoryId.isBlank) SchemaValidatorFactory.getInstance(schemaName, version) else new CategoryDefinitionValidator(schemaName, version).loadSchema(categoryId) + def getNode(input: java.util.Map[String, AnyRef]): Node @throws[Exception] - def validate(node: Node, operation: String = "update")(implicit ec: ExecutionContext): Future[Node] + def validate(node: Node, operation: String = "update", setDefaultValue: Boolean = true)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] @throws[Exception] - def getNode(identifier: String, operation: String = "read", mode: String)(implicit ec: ExecutionContext): Future[Node] + def getNode(identifier: String, operation: String = "read", mode: String, versioning: Option[String] = None)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] def getSchemaName(): String ={ schemaName } + + def getSchemaVersion(): String = { + version + } } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/ObjectCategoryDefinitionMap.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/ObjectCategoryDefinitionMap.scala new file mode 100644 index 000000000..91cae048c --- /dev/null +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/ObjectCategoryDefinitionMap.scala @@ -0,0 +1,30 @@ +package org.sunbird.graph.schema + +import com.twitter.storehaus.cache.Cache +import com.twitter.util.Duration +import org.sunbird.common.{Platform, Slug} + +object ObjectCategoryDefinitionMap { + + val ttlMS = Platform.getLong("object.categoryDefinition.cache.ttl", 10000l) + var cache = Cache.ttl[String, Map[String, AnyRef]](Duration.fromMilliseconds(ttlMS)) + + def get(id: String):Map[String, AnyRef] = { + cache.getNonExpired(id).getOrElse(null) + } + + def put(id: String, data: Map[String, AnyRef]): Unit = { + val updated = cache.putClocked(id, data)._2 + cache = updated + } + + def containsKey(id: String): Boolean = { + cache.contains(id) + } + + def prepareCategoryId(categoryName: String, objectType: String, channel: String = "all") = { + if(!categoryName.isBlank) + "obj-cat"+ ":" + Slug.makeSlug(categoryName + "_" + objectType + "_" + channel, true) + else "" + } +} diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/BaseDefinitionNode.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/BaseDefinitionNode.scala index c6ed13b8d..87b209416 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/BaseDefinitionNode.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/BaseDefinitionNode.scala @@ -5,16 +5,16 @@ import java.util import org.apache.commons.collections4.{CollectionUtils, MapUtils} import org.apache.commons.lang3.StringUtils import org.sunbird.common.dto.Request +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.common.Identifier import org.sunbird.graph.dac.enums.SystemNodeTypes import org.sunbird.graph.dac.model.{Node, Relation} import org.sunbird.graph.schema.IDefinition -import org.sunbird.graph.service.operation.SearchAsyncOperations import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} -class BaseDefinitionNode(graphId: String, schemaName: String, version: String = "1.0") extends IDefinition(graphId, schemaName, version) { +class BaseDefinitionNode(graphId: String, schemaName: String, version: String = "1.0", categoryId: String = "")(implicit ec: ExecutionContext, oec: OntologyEngineContext) extends IDefinition(graphId, schemaName, version, categoryId)(ec, oec) { val inRelationsSchema: Map[String, AnyRef] = relationsSchema("in") val outRelationsSchema: Map[String, AnyRef] = relationsSchema("out") @@ -54,13 +54,13 @@ class BaseDefinitionNode(graphId: String, schemaName: String, version: String = } @throws[Exception] - override def validate(node: Node, operation: String)(implicit ec: ExecutionContext): Future[Node] = { + override def validate(node: Node, operation: String, setDefaultValue: Boolean)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { Future{node} } - override def getNode(identifier: String, operation: String, mode: String)(implicit ec: ExecutionContext): Future[Node] = { + override def getNode(identifier: String, operation: String, mode: String, versioning: Option[String] = None)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { val request: Request = new Request() - val node: Future[Node] = SearchAsyncOperations.getNodeByUniqueId(graphId, identifier, false, request) + val node: Future[Node] = oec.graphService.getNodeByUniqueId(graphId, identifier, false, request) node } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/FrameworkValidator.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/FrameworkValidator.scala index 5c289f565..af1da9317 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/FrameworkValidator.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/FrameworkValidator.scala @@ -2,9 +2,13 @@ package org.sunbird.graph.schema.validator import java.util -import org.sunbird.cache.impl.CategoryCache -import org.sunbird.common.exception.ClientException -import org.sunbird.graph.dac.model.Node +import org.apache.commons.collections4.CollectionUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.exception.{ClientException, ResourceNotFoundException, ServerException} +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.enums.SystemProperties +import org.sunbird.graph.dac.model._ import org.sunbird.graph.schema.IDefinition import scala.collection.JavaConverters._ @@ -12,40 +16,127 @@ import scala.collection.Map import scala.concurrent.{ExecutionContext, Future} trait FrameworkValidator extends IDefinition { - val categoryCache: CategoryCache = new CategoryCache() + @throws[Exception] - abstract override def validate(node: Node, operation: String)(implicit ec: ExecutionContext): Future[Node] = { - val fwCategories: List[String] = schemaValidator.getConfig.getStringList("frameworkCategories").asScala.toList - val framework: String = node.getMetadata.getOrDefault("framework", "").asInstanceOf[String] - if (null != fwCategories && fwCategories.nonEmpty && framework.nonEmpty) { - //prepare data for validation - val fwMetadata: Map[String, AnyRef] = node.getMetadata.asScala.filterKeys(key => fwCategories.contains(key)) - //validate data from cache - if (fwMetadata.nonEmpty) { - val errors: util.List[String] = new util.ArrayList[String] - for (cat: String <- fwMetadata.keys) { - val value: AnyRef = fwMetadata.get(cat).get - val list: List[String] = categoryCache.getList(categoryCache.getKey(framework, cat)).asScala.toList - val result: Boolean = value match { - case value: String => list.contains(value) - case value: util.List[String] => list.asJava.containsAll(value) - case value: Array[String] => value.forall(term => list.contains(term)) - case _ => throw new ClientException("CLIENT_ERROR", "Validation Errors.", util.Arrays.asList("Please provide correct value for [" + cat + "]")) - } + abstract override def validate(node: Node, operation: String, setDefaultValue: Boolean)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { + val fwCategories: List[String] = schemaValidator.getConfig.getStringList("frameworkCategories").asScala.toList + val orgFwTerms: List[String] = schemaValidator.getConfig.getStringList("orgFrameworkTerms").asScala.toList + val targetFwTerms: List[String] = schemaValidator.getConfig.getStringList("targetFrameworkTerms").asScala.toList + validateAndSetMultiFrameworks(node, orgFwTerms, targetFwTerms).map(_ => { + val framework: String = node.getMetadata.getOrDefault("framework", "").asInstanceOf[String] + if (null != fwCategories && fwCategories.nonEmpty && framework.nonEmpty) { + //prepare data for validation + val fwMetadata: Map[String, AnyRef] = node.getMetadata.asScala.filterKeys(key => fwCategories.contains(key)) + //validate data from cache + if (fwMetadata.nonEmpty) { + val errors: util.List[String] = new util.ArrayList[String] + for (cat: String <- fwMetadata.keys) { + val value: AnyRef = fwMetadata.get(cat).get + //TODO: Replace Cache Call With FrameworkCache Implementation + val cacheKey = "cat_" + framework + cat + val list: List[String] = RedisCache.getList(cacheKey) + val result: Boolean = value match { + case value: String => list.contains(value) + case value: util.List[String] => list.asJava.containsAll(value) + case value: Array[String] => value.forall(term => list.contains(term)) + case _ => throw new ClientException("CLIENT_ERROR", "Validation Errors.", util.Arrays.asList("Please provide correct value for [" + cat + "]")) + } - if (!result) { + if (!result) { if (list.isEmpty) { - errors.add(cat + " range data is empty from the given framework.") + errors.add(cat + " range data is empty from the given framework.") } else { - errors.add("Metadata " + cat + " should belong from:" + list.asJava) + errors.add("Metadata " + cat + " should belong from:" + list.asJava) } + } } + if (!errors.isEmpty) + throw new ClientException("CLIENT_ERROR", "Validation Errors.", errors) + } + } + super.validate(node, operation) + }).flatMap(f => f) + } + + private def validateAndSetMultiFrameworks(node: Node, orgFwTerms: List[String], targetFwTerms: List[String])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Map[String, AnyRef]] = { + getValidatedTerms(node, orgFwTerms).map(orgTermMap => { + val boards = fetchValidatedList(getList("boardIds", node), orgTermMap) + if (CollectionUtils.isNotEmpty(boards)) node.getMetadata.putIfAbsent("board", boards.get(0)) + val mediums = fetchValidatedList(getList("mediumIds", node), orgTermMap) + if (CollectionUtils.isNotEmpty(mediums)) node.getMetadata.putIfAbsent("medium", mediums) + val subjects = fetchValidatedList(getList("subjectIds", node), orgTermMap) + if (CollectionUtils.isNotEmpty(subjects)) node.getMetadata.putIfAbsent("subject", subjects) + val grades = fetchValidatedList(getList("gradeLevelIds", node), orgTermMap) + if (CollectionUtils.isNotEmpty(grades)) node.getMetadata.putIfAbsent("gradeLevel", grades) + val topics = fetchValidatedList(getList("topicsIds", node), orgTermMap) + if (CollectionUtils.isNotEmpty(topics)) node.getMetadata.putIfAbsent("topics", topics) + getValidatedTerms(node, targetFwTerms) + }).flatMap(f => f) + } + + + private def getList(termName: String, node: Node): util.List[String] = { + node.getMetadata.get(termName) match { + case e: String => List(e).asJava + case e: util.List[String] => e + case e: Array[String] => e.toList.asJava + case _ => List().asJava + } + } + + + private def getValidatedTerms(node: Node, validationList: List[String])(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Map[String, AnyRef]] = { + val ids: List[String] = node.getMetadata.asScala + .filter(entry => validationList.contains(entry._1)) + .flatMap(entry => entry._2 match { + case e: String => List(e) + case e: util.List[String] => e.asScala + case e: Array[String] => e.toList + case _ => List() + }).toList + if (ids.nonEmpty) { + val mc: MetadataCriterion = MetadataCriterion.create(new util.ArrayList[Filter]() { + { + if (ids.size == 1) add(new Filter(SystemProperties.IL_UNIQUE_ID.name(), SearchConditions.OP_EQUAL, ids.asJava.get(0))) + if (ids.size > 1) add(new Filter(SystemProperties.IL_UNIQUE_ID.name(), SearchConditions.OP_IN, ids.asJava)) + new Filter("status", SearchConditions.OP_NOT_EQUAL, "Retired") + } + }) + + val searchCriteria = new SearchCriteria { + { + addMetadata(mc) + setCountQuery(false) } - if (!errors.isEmpty) - throw new ClientException("CLIENT_ERROR", "Validation Errors.", errors) } + oec.graphService.getNodeByUniqueIds(node.getGraphId, searchCriteria).map(nodeList => { + if (CollectionUtils.isEmpty(nodeList)) + throw new ResourceNotFoundException("ERR_VALIDATING_CONTENT_FRAMEWORK", s"Nodes not found for Id's $ids ") + val termMap = nodeList.asScala.map(node => node.getIdentifier -> node.getMetadata.getOrDefault("name", "")).toMap + validateFrameworkRelatedData(node, termMap, validationList) + termMap + }) + } else Future { + Map() } - super.validate(node, operation) + } + + def validateFrameworkRelatedData(node: Node, termMap: Map[String, AnyRef], validationList: List[String]) = { + validationList.foreach(termName => node.getMetadata.get(termName) match { + case termId: String => if (!termMap.contains(termId)) + throw new ResourceNotFoundException("ERR_VALIDATING_CONTENT_FRAMEWORK", s"No nodes found for $termName with ids: ${node.getMetadata.get(termName)}") + case termIds: util.List[String] => if (termIds.asScala.filterNot(id => termMap.contains(id)).nonEmpty) + throw new ResourceNotFoundException("ERR_VALIDATING_CONTENT_FRAMEWORK", s"No nodes found for $termName with ids: ${node.getMetadata.get(termName)}") + case _ => + }) + } + + def fetchValidatedList(itemList: util.List[String], orgTermMap: Map[String, AnyRef]): util.List[String] = { + if (CollectionUtils.isNotEmpty(itemList)) { + itemList.asScala.map(id => orgTermMap.getOrElse(id, "").asInstanceOf[String]) + .filter(medium => medium.nonEmpty) + .toList.asJava + } else List().asJava } } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/PropAsEdgeValidator.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/PropAsEdgeValidator.scala index 3b17d3126..a0ef9353d 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/PropAsEdgeValidator.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/PropAsEdgeValidator.scala @@ -1,12 +1,13 @@ package org.sunbird.graph.schema.validator import org.apache.commons.collections4.CollectionUtils -import org.sunbird.cache.util.RedisCacheUtil +import org.sunbird.cache.impl.RedisCache import org.sunbird.common.exception.ClientException +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.dac.model.Node import org.sunbird.graph.schema.IDefinition -import scala.collection.JavaConversions._ +import scala.collection.JavaConversions._ import scala.concurrent.{ExecutionContext, Future} trait PropAsEdgeValidator extends IDefinition { @@ -15,13 +16,13 @@ trait PropAsEdgeValidator extends IDefinition { val prefix = "edge_" @throws[Exception] - abstract override def validate(node: Node, operation: String)(implicit ec: ExecutionContext): Future[Node] = { + abstract override def validate(node: Node, operation: String, setDefaultValue: Boolean)(implicit ec: ExecutionContext, oec:OntologyEngineContext): Future[Node] = { if (schemaValidator.getConfig.hasPath(edgePropsKey)) { val keys = CollectionUtils.intersection(node.getMetadata.keySet(), schemaValidator.getConfig.getObject(edgePropsKey).keySet()) if (!keys.isEmpty) { keys.toArray().toStream.map(key => { val cacheKey = prefix + schemaValidator.getConfig.getString(edgePropsKey + "." + key).toLowerCase - val list = RedisCacheUtil.getList(cacheKey) + val list = RedisCache.getList(cacheKey) if (CollectionUtils.isNotEmpty(list)) { val value = node.getMetadata.get(key) if (value.isInstanceOf[String]) { diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/RelationValidator.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/RelationValidator.scala index a2411b968..64c113917 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/RelationValidator.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/RelationValidator.scala @@ -8,6 +8,7 @@ import org.apache.commons.collections4.CollectionUtils import org.apache.commons.lang3.StringUtils import org.sunbird.common.dto.Request import org.sunbird.common.exception.{ClientException, ResponseCode} +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.dac.enums.SystemNodeTypes import org.sunbird.graph.dac.model.{Node, Relation} import org.sunbird.graph.relations.{IRelation, RelationHandler} @@ -20,7 +21,7 @@ import scala.concurrent.{ExecutionContext, Future} trait RelationValidator extends IDefinition { @throws[Exception] - abstract override def validate(node: Node, operation: String)(implicit ec: ExecutionContext): Future[Node] = { + abstract override def validate(node: Node, operation: String, setDefaultValue: Boolean)(implicit ec: ExecutionContext, oec:OntologyEngineContext): Future[Node] = { val relations: java.util.List[Relation] = node.getAddedRelations if (CollectionUtils.isNotEmpty(relations)) { val ids = relations.asScala.map(r => List(r.getStartNodeId, r.getEndNodeId)).flatten @@ -37,6 +38,7 @@ trait RelationValidator extends IDefinition { val req = new Request() req.setContext(new util.HashMap[String, AnyRef]() {{ put("schemaName", getSchemaName()) + put("version", getSchemaVersion()) }}) val errList = iRel.validate(req) if (null != errList && !errList.isEmpty) { diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/SchemaValidator.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/SchemaValidator.scala index 679d64d1f..6eab97c08 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/SchemaValidator.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/SchemaValidator.scala @@ -1,5 +1,6 @@ package org.sunbird.graph.schema.validator +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.dac.model.Node import org.sunbird.graph.schema.IDefinition @@ -8,11 +9,14 @@ import scala.concurrent.{ExecutionContext, Future} trait SchemaValidator extends IDefinition { @throws[Exception] - abstract override def validate(node: Node, operation: String)(implicit ec: ExecutionContext): Future[Node] = { - val result = schemaValidator.validate(node.getMetadata) - if(operation.equalsIgnoreCase("create")) { - node.setMetadata(result.getMetadata) + abstract override def validate(node: Node, operation: String, setDefaultValue: Boolean)(implicit ec: ExecutionContext, oec:OntologyEngineContext): Future[Node] = { + if(setDefaultValue){ + val result = schemaValidator.validate(node.getMetadata) + if(setDefaultValue && operation.equalsIgnoreCase("create")) { + node.setMetadata(result.getMetadata) + } } + super.validate(node, operation) } } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/VersionKeyValidator.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/VersionKeyValidator.scala index 43c90923a..479848937 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/VersionKeyValidator.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/VersionKeyValidator.scala @@ -5,6 +5,7 @@ import java.util.Date import org.apache.commons.lang3.StringUtils import org.sunbird.common.exception.{ClientException, ResponseCode} import org.sunbird.common.{DateUtils, Platform} +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.common.enums.GraphDACParams import org.sunbird.graph.dac.enums.SystemNodeTypes import org.sunbird.graph.dac.model.Node @@ -19,7 +20,7 @@ trait VersionKeyValidator extends IDefinition { private val graphPassportKey = Platform.config.getString(DACConfigurationConstants.PASSPORT_KEY_BASE_PROPERTY) @throws[Exception] - abstract override def validate(node: Node, operation: String)(implicit ec: ExecutionContext): Future[Node] = { + abstract override def validate(node: Node, operation: String, setDefaultValue: Boolean)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { if(!(operation.equalsIgnoreCase("create"))){ isValidVersionkey(node).map(isValid => { if(!isValid)throw new ClientException(ResponseCode.CLIENT_ERROR.name, "Invalid version Key") @@ -30,7 +31,7 @@ trait VersionKeyValidator extends IDefinition { } } - def isValidVersionkey(node: Node)(implicit ec: ExecutionContext): Future[Boolean] = { + def isValidVersionkey(node: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Boolean] = { val versionCheckMode = { if(schemaValidator.getConfig.hasPath("versionCheckMode")) schemaValidator.getConfig.getString("versionCheckMode") else NodeUpdateMode.OFF.name @@ -48,7 +49,7 @@ trait VersionKeyValidator extends IDefinition { } } - def validateUpdateOperation(getGraphId: String, node: Node, storedVersionKey: String)(implicit ec: ExecutionContext): Future[Boolean] = { + def validateUpdateOperation(getGraphId: String, node: Node, storedVersionKey: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Boolean] = { val versionKey: String = node.getMetadata.get(GraphDACParams.versionKey.name).asInstanceOf[String] if(StringUtils.isBlank(versionKey)) throw new ClientException("BLANK_VERSION", "Error! Version Key cannot be Blank. | [Node Id: " + node.getIdentifier + "]") @@ -72,8 +73,8 @@ trait VersionKeyValidator extends IDefinition { } - def getVersionKeyFromDB(identifier: String, graphId: String)(implicit ec: ExecutionContext): Future[String] = { - SearchAsyncOperations.getNodeProperty(graphId, identifier, "versionKey").map(property => { + def getVersionKeyFromDB(identifier: String, graphId: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[String] = { + oec.graphService.getNodeProperty(graphId, identifier, "versionKey").map(property => { val versionKey: String = property.getPropertyValue.asInstanceOf[org.neo4j.driver.internal.value.StringValue].asString() if(StringUtils.isNotBlank(versionKey)) versionKey diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/VersioningNode.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/VersioningNode.scala index 20b4e127b..abcd8b5c2 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/VersioningNode.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/schema/validator/VersioningNode.scala @@ -3,91 +3,92 @@ package org.sunbird.graph.schema.validator import java.util import java.util.concurrent.CompletionException -import org.sunbird.cache.util.RedisCacheUtil -import org.sunbird.common.{JsonUtils, Platform} +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.{DateUtils, JsonUtils, Platform} import org.sunbird.common.dto.{Request, ResponseHandler} import org.sunbird.common.exception.ResourceNotFoundException +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.enums.AuditProperties import org.sunbird.graph.dac.model.Node -import org.sunbird.graph.exception.GraphEngineErrorCodes +import org.sunbird.graph.exception.GraphErrorCodes import org.sunbird.graph.external.ExternalPropsManager -import org.sunbird.graph.schema.IDefinition +import org.sunbird.graph.schema.{DefinitionFactory, IDefinition} import org.sunbird.graph.service.operation.{NodeAsyncOperations, SearchAsyncOperations} import org.sunbird.graph.utils.{NodeUtil, ScalaJsonUtils} import org.sunbird.telemetry.logger.TelemetryManager import scala.collection.JavaConversions._ -import scala.collection.JavaConverters import scala.concurrent.{ExecutionContext, Future} trait VersioningNode extends IDefinition { - val statusList = List("Live", "Unlisted") + val statusList = List("Live", "Unlisted", "Flagged") val IMAGE_SUFFIX = ".img" val IMAGE_OBJECT_SUFFIX = "Image" + val COLLECTION_MIME_TYPE = "application/vnd.ekstep.content-collection" - abstract override def getNode(identifier: String, operation: String, mode: String)(implicit ec: ExecutionContext): Future[Node] = { + abstract override def getNode(identifier: String, operation: String, mode: String = "read", versioning: Option[String] = None)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { operation match { - case "update" => getNodeToUpdate(identifier); + case "update" => getNodeToUpdate(identifier, versioning); case "read" => getNodeToRead(identifier, mode) case _ => getNodeToRead(identifier, mode) } } - private def getNodeToUpdate(identifier: String)(implicit ec: ExecutionContext): Future[Node] = { + private def getNodeToUpdate(identifier: String, versioning: Option[String] = None)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { val nodeFuture: Future[Node] = super.getNode(identifier , "update", null) nodeFuture.map(node => { + val versioningEnable = versioning.getOrElse({if(schemaValidator.getConfig.hasPath("version"))schemaValidator.getConfig.getString("version") else "disable"}) if(null == node) - throw new ResourceNotFoundException(GraphEngineErrorCodes.ERR_INVALID_NODE.name, "Node Not Found With Identifier : " + identifier) - if(schemaValidator.getConfig.hasPath("version") && "enable".equalsIgnoreCase(schemaValidator.getConfig.getString("version"))){ + throw new ResourceNotFoundException(GraphErrorCodes.ERR_INVALID_NODE.toString, "Node Not Found With Identifier : " + identifier) + else if("enable".equalsIgnoreCase(versioningEnable)) getEditableNode(identifier, node) - } else { + else Future{node} - } - }).flatMap(f => f) recoverWith { case e: CompletionException => throw e.getCause} + }).flatMap(f => f) } - private def getNodeToRead(identifier: String, mode: String)(implicit ec: ExecutionContext) = { - val node: Future[Node] = { - if("edit".equalsIgnoreCase(mode)){ - val imageNode = super.getNode(identifier + IMAGE_SUFFIX, "read", mode) - imageNode recoverWith { - case e: CompletionException => { - if (e.getCause.isInstanceOf[ResourceNotFoundException]) - super.getNode(identifier, "read", mode) - else - throw e.getCause - } + private def getNodeToRead(identifier: String, mode: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + if ("edit".equalsIgnoreCase(mode)) { + val imageNode = super.getNode(identifier + IMAGE_SUFFIX, "read", mode) + imageNode recoverWith { + case e: CompletionException => { + if (e.getCause.isInstanceOf[ResourceNotFoundException]) + super.getNode(identifier, "read", mode) + else + throw e.getCause } - } else { - val cacheKey = getSchemaName().toLowerCase() + ".cache.enable" - if(Platform.config.hasPath(cacheKey) && Platform.config.getBoolean(cacheKey)) { - val ttl:Integer = if (Platform.config.hasPath(getSchemaName().toLowerCase() + ".cache.ttl")) Platform.config.getInt(getSchemaName().toLowerCase() + ".cache.ttl") else 86400 - getCachedNode(identifier, ttl) - } - else - super.getNode(identifier , "read", mode) } - }.map(dataNode => dataNode) recoverWith { case e: CompletionException => throw e.getCause} - node + } else { + val cacheKey = getSchemaName().toLowerCase() + ".cache.enable" + if (Platform.config.hasPath(cacheKey) && Platform.config.getBoolean(cacheKey)) { + val ttl: Integer = if (Platform.config.hasPath(getSchemaName().toLowerCase() + ".cache.ttl")) Platform.config.getInt(getSchemaName().toLowerCase() + ".cache.ttl") else 86400 + getCachedNode(identifier, ttl) + } + else + super.getNode(identifier, "read", mode) + } } - private def getEditableNode(identifier: String, node: Node)(implicit ec: ExecutionContext): Future[Node] = { + private def getEditableNode(identifier: String, node: Node)(implicit ec: ExecutionContext, oec: OntologyEngineContext): Future[Node] = { val status = node.getMetadata.get("status").asInstanceOf[String] if(statusList.contains(status)) { val imageId = node.getIdentifier + IMAGE_SUFFIX try{ - val imageNode = SearchAsyncOperations.getNodeByUniqueId(node.getGraphId, imageId, false, new Request()) + val imageNode = oec.graphService.getNodeByUniqueId(node.getGraphId, imageId, false, new Request()) imageNode recoverWith { case e: CompletionException => { - TelemetryManager.error("Exception occured while fetching image node, may not be found", e.getCause) + TelemetryManager.error("Exception occurred while fetching image node, may not be found", e.getCause) if (e.getCause.isInstanceOf[ResourceNotFoundException]) { node.setIdentifier(imageId) node.setObjectType(node.getObjectType + IMAGE_OBJECT_SUFFIX) node.getMetadata.put("status", "Draft") - NodeAsyncOperations.addNode(node.getGraphId, node).map(imgNode => { + node.getMetadata.put("prevStatus", status) + node.getMetadata.put(AuditProperties.lastStatusChangedOn.name, DateUtils.formatCurrentDate()) + oec.graphService.addNode(node.getGraphId, node).map(imgNode => { imgNode.getMetadata.put("isImageNodeCreated", "yes"); - copyExternalProps(identifier, node.getGraphId).map(response => { + copyExternalProps(identifier, node.getGraphId, imgNode.getObjectType.toLowerCase().replace("image", "")).map(response => { if(!ResponseHandler.checkError(response)) { if(null != response.getResult && !response.getResult.isEmpty) imgNode.setExternalData(response.getResult) @@ -104,42 +105,51 @@ trait VersioningNode extends IDefinition { Future{node} } - private def copyExternalProps(identifier: String, graphId: String)(implicit ec : ExecutionContext) = { + private def copyExternalProps(identifier: String, graphId: String, schemaName: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext) = { val request = new Request() request.setContext(new util.HashMap[String, AnyRef](){{ - put("schemaName", getSchemaName()) - put("version", "1.0") + put("schemaName", schemaName) + put("version", getSchemaVersion()) put("graph_id", graphId) }}) request.put("identifier", identifier) - ExternalPropsManager.fetchProps(request, getExternalPropsList()) + oec.graphService.readExternalProps(request, getExternalPropsList(graphId, schemaName, getSchemaVersion())) } - private def getExternalPropsList(): List[String] ={ - if(schemaValidator.getConfig.hasPath("external.properties")){ - new util.ArrayList[String](schemaValidator.getConfig.getObject("external.properties").keySet()).toList + private def getExternalPropsList(graphId: String, schemaName: String, version: String)(implicit ec: ExecutionContext, oec: OntologyEngineContext): List[String] ={ + val definition = DefinitionFactory.getDefinition(graphId, schemaName, version) + if(definition.schemaValidator.getConfig.hasPath("external.properties")){ + new util.ArrayList[String](definition.schemaValidator.getConfig.getObject("external.properties").keySet()).toList }else{ List[String]() } } - def getCachedNode(identifier: String, ttl: Integer)(implicit ec: ExecutionContext): Future[Node] = { - val nodeString:String = RedisCacheUtil.getString(identifier) - if(null != nodeString && !nodeString.isEmpty) { - val nodeMap:util.Map[String, AnyRef] = JsonUtils.deserialize(nodeString, classOf[java.util.Map[String, AnyRef]]) - val node:Node = NodeUtil.deserialize(nodeMap, getSchemaName(), schemaValidator.getConfig - .getAnyRef("relations").asInstanceOf[java.util.Map[String, AnyRef]]) - Future{node} - } else { - super.getNode(identifier , "read", null).map(node => { - if(List("Live", "Unlisted").contains(node.getMetadata.get("status").asInstanceOf[String])) { - val nodeMap = NodeUtil.serialize(node, null, getSchemaName()) - RedisCacheUtil.saveString(identifier, ScalaJsonUtils.serialize(nodeMap), ttl) - } - node - }) - } - + def getCachedNode(identifier: String, ttl: Integer)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Node] = { + val nodeStringFuture: Future[String] = RedisCache.getAsync(identifier, nodeCacheAsyncHandler, ttl) + nodeStringFuture.map(nodeString => { + if (null != nodeString && !nodeString.asInstanceOf[String].isEmpty) { + val nodeMap: util.Map[String, AnyRef] = JsonUtils.deserialize(nodeString.asInstanceOf[String], classOf[java.util.Map[String, AnyRef]]) + val node: Node = NodeUtil.deserialize(nodeMap, getSchemaName(), schemaValidator.getConfig + .getAnyRef("relations").asInstanceOf[java.util.Map[String, AnyRef]]) + Future {node} + } else { + super.getNode(identifier, "read", null) + } + }).flatMap(f => f) } + private def nodeCacheAsyncHandler(objKey: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[String] = { + super.getNode(objKey, "read", null).map(node => { + if (List("Live", "Unlisted").contains(node.getMetadata.get("status").asInstanceOf[String])) { + + val nodeMap = NodeUtil.serialize(node, null, node.getObjectType.toLowerCase().replace("image", ""), getSchemaVersion()) + Future(ScalaJsonUtils.serialize(nodeMap)) + } else Future("") + }).flatMap(f => f) + } + + private def getSchemaNameFromMimeType(node: Node) : String = { + node.getObjectType.replaceAll("Image", "").toLowerCase() + } } diff --git a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/utils/NodeUtil.scala b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/utils/NodeUtil.scala index 7791890f5..85f5f3dee 100644 --- a/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/utils/NodeUtil.scala +++ b/ontology-engine/graph-engine_2.11/src/main/scala/org/sunbird/graph/utils/NodeUtil.scala @@ -5,29 +5,35 @@ import java.util import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule import org.apache.commons.collections4.{CollectionUtils, MapUtils} -import org.sunbird.common.Platform +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.{JsonUtils, Platform} +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.common.enums.SystemProperties import org.sunbird.graph.dac.model.{Node, Relation} -import org.sunbird.graph.schema.DefinitionNode +import org.sunbird.graph.schema.{DefinitionNode, ObjectCategoryDefinitionMap} import scala.collection.JavaConverters import scala.collection.JavaConverters._ +import scala.collection.JavaConversions._ +import scala.concurrent.ExecutionContext object NodeUtil { val mapper: ObjectMapper = new ObjectMapper() mapper.registerModule(DefaultScalaModule) - def serialize(node: Node, fields: util.List[String], schemaName: String): util.Map[String, AnyRef] = { + def serialize(node: Node, fields: util.List[String], schemaName: String, schemaVersion: String, withoutRelations: Boolean = false)(implicit oec: OntologyEngineContext, ec: ExecutionContext): util.Map[String, AnyRef] = { val metadataMap = node.getMetadata - metadataMap.put("identifier", node.getIdentifier) - val jsonProps = DefinitionNode.fetchJsonProps(node.getGraphId, "1.0", schemaName) + val categoryDefinitionId = ObjectCategoryDefinitionMap.prepareCategoryId(node.getMetadata.getOrDefault("primaryCategory", "").asInstanceOf[String], node.getObjectType.toLowerCase().replace("image", ""), node.getMetadata.getOrDefault("channel","all").asInstanceOf[String]) + val jsonProps = DefinitionNode.fetchJsonProps(node.getGraphId, schemaVersion, node.getObjectType.toLowerCase().replace("image", ""), categoryDefinitionId) val updatedMetadataMap:util.Map[String, AnyRef] = metadataMap.entrySet().asScala.filter(entry => null != entry.getValue).map((entry: util.Map.Entry[String, AnyRef]) => handleKeyNames(entry, fields) -> convertJsonProperties(entry, jsonProps)).toMap.asJava - val definitionMap = DefinitionNode.getRelationDefinitionMap(node.getGraphId, "1.0", schemaName).asJava - val relMap:util.Map[String, util.List[util.Map[String, AnyRef]]] = getRelationMap(node, updatedMetadataMap, definitionMap) - var finalMetadata = new util.HashMap[String, AnyRef]() + val definitionMap = DefinitionNode.getRelationDefinitionMap(node.getGraphId, schemaVersion, node.getObjectType.toLowerCase().replace("image", ""), categoryDefinitionId).asJava + val finalMetadata = new util.HashMap[String, AnyRef]() finalMetadata.put("objectType",node.getObjectType) finalMetadata.putAll(updatedMetadataMap) - finalMetadata.putAll(relMap) + if(!withoutRelations){ + val relMap:util.Map[String, util.List[util.Map[String, AnyRef]]] = getRelationMap(node, updatedMetadataMap, definitionMap) + finalMetadata.putAll(relMap) + } if (CollectionUtils.isNotEmpty(fields)) finalMetadata.keySet.retainAll(fields) finalMetadata.put("identifier", node.getIdentifier) @@ -66,7 +72,15 @@ object NodeUtil { put("description", relMap.get("description")) put("status", relMap.get("status")) }}) - if(null != relMap.get("index") && 0 < relMap.get("index").asInstanceOf[Integer]){ + val index:Integer = { + if(null != relMap.get("index")) { + if(relMap.get("index").isInstanceOf[String]){ + Integer.parseInt(relMap.get("index").asInstanceOf[String]) + } else relMap.get("index").asInstanceOf[Number].intValue() + } else + null + } + if(null != index && 0 < index){ rel.setMetadata(new util.HashMap[String, AnyRef](){{ put(SystemProperties.IL_SEQUENCE_INDEX.name(), relMap.get("index")) }}) @@ -99,7 +113,7 @@ object NodeUtil { def handleKeyNames(entry: util.Map.Entry[String, AnyRef], fields: util.List[String]) = { if(CollectionUtils.isEmpty(fields)) { - entry.getKey.substring(0,1) + entry.getKey.substring(1) + entry.getKey.substring(0,1).toLowerCase + entry.getKey.substring(1) } else { entry.getKey } @@ -114,7 +128,7 @@ object NodeUtil { if (relMap.containsKey(relationMap.get(relKey))) relMap.get(relationMap.get(relKey)).add(populateRelationMaps(rel, "in")) else { if(null != relationMap.get(relKey)) { - relMap.put(relationMap.get(relKey).asInstanceOf[String], new util.ArrayList[util.Map[String, AnyRef]]() {}) + relMap.put(relationMap.get(relKey).asInstanceOf[String], new util.ArrayList[util.Map[String, AnyRef]]() {add(populateRelationMaps(rel, "in"))}) } } } @@ -123,7 +137,7 @@ object NodeUtil { if (relMap.containsKey(relationMap.get(relKey))) relMap.get(relationMap.get(relKey)).add(populateRelationMaps(rel, "out")) else { if(null != relationMap.get(relKey)) { - relMap.put(relationMap.get(relKey).asInstanceOf[String], new util.ArrayList[util.Map[String, AnyRef]]() {}) + relMap.put(relationMap.get(relKey).asInstanceOf[String], new util.ArrayList[util.Map[String, AnyRef]]() {add(populateRelationMaps(rel, "out"))}) } } } @@ -132,45 +146,50 @@ object NodeUtil { def convertJsonProperties(entry: util.Map.Entry[String, AnyRef], jsonProps: scala.List[String]) = { if(jsonProps.contains(entry.getKey)) { - try {mapper.readTree(entry.getValue.toString)} + try {JsonUtils.deserialize(entry.getValue.asInstanceOf[String], classOf[Object])} //.readTree(entry.getValue.toString)} catch { case e: Exception => entry.getValue } } else entry.getValue } + // TODO: we should get the list from configuration. + private def relationObjectAttributes(objectType: String): List[String] = { + if (StringUtils.equalsAnyIgnoreCase("framework", objectType)) List("description", "status", "type") else List("description", "status") + } + def populateRelationMaps(rel: Relation, direction: String): util.Map[String, AnyRef] = { - if("out".equalsIgnoreCase(direction)) - new util.HashMap[String, Object]() {{ - put("identifier", rel.getEndNodeId.replace(".img", "")) - put("name", rel.getEndNodeName) - put("objectType", rel.getEndNodeObjectType.replace("Image", "")) - put("relation", rel.getRelationType) - put("description", rel.getEndNodeMetadata.get("description")) - put("status", rel.getEndNodeMetadata.get("status")) - }} - else - new util.HashMap[String, Object]() {{ - put("identifier", rel.getStartNodeId.replace(".img", "")) - put("name", rel.getStartNodeName) - put("objectType", rel.getStartNodeObjectType.replace("Image", "")) - put("relation", rel.getRelationType) - put("description", rel.getStartNodeMetadata.get("description")) - put("status", rel.getStartNodeMetadata.get("status")) - }} + if("out".equalsIgnoreCase(direction)) { + val objectType = rel.getEndNodeObjectType.replace("Image", "") + val relData = Map("identifier" -> rel.getEndNodeId.replace(".img", ""), + "name" -> rel.getEndNodeName, + "objectType" -> objectType, + "relation" -> rel.getRelationType) ++ relationObjectAttributes(objectType).map(key => (key -> rel.getEndNodeMetadata.get(key))).toMap + mapAsJavaMap(relData) + } else { + val objectType = rel.getStartNodeObjectType.replace("Image", "") + val relData = Map("identifier" -> rel.getStartNodeId.replace(".img", ""), + "name" -> rel.getStartNodeName, + "objectType" -> objectType, + "relation" -> rel.getRelationType) ++ relationObjectAttributes(objectType).map(key => (key -> rel.getStartNodeMetadata.get(key))).toMap + mapAsJavaMap(relData) + } } def getLanguageCodes(node: Node): util.List[String] = { - val languages:util.List[String] = { - if (node.getMetadata.get("language").isInstanceOf[String]) util.Arrays.asList(node.getMetadata.get("language").asInstanceOf[String]) - else if (node.getMetadata.get("language").isInstanceOf[util.List[String]]) node.getMetadata.get("language").asInstanceOf[util.List[String]] - else new util.ArrayList[String]() + val value = node.getMetadata.get("language") + val languages:util.List[String] = value match { + case value: String => List(value).asJava + case value: util.List[String] => value + case value: Array[String] => value.filter((lng: String) => StringUtils.isNotBlank(lng)).toList.asJava + case _ => new util.ArrayList[String]() } if(CollectionUtils.isNotEmpty(languages)){ - JavaConverters.bufferAsJavaListConverter(languages.asScala.map(lang => if(Platform.config.hasPath("languageCode" + lang.toLowerCase)) Platform.config.getString("languageCode" + lang.toLowerCase) else "")).asJava + JavaConverters.bufferAsJavaListConverter(languages.asScala.map(lang => if(Platform.config.hasPath("languageCode." + lang.toLowerCase)) Platform.config.getString("languageCode." + lang.toLowerCase) else "")).asJava }else{ languages } } + def isRetired(node: Node): Boolean = StringUtils.equalsIgnoreCase(node.getMetadata.get("status").asInstanceOf[String], "Retired") -} \ No newline at end of file +} diff --git a/ontology-engine/graph-engine_2.11/src/test/resources/application.conf b/ontology-engine/graph-engine_2.11/src/test/resources/application.conf index dcc652f45..b5fb5b0cb 100644 --- a/ontology-engine/graph-engine_2.11/src/test/resources/application.conf +++ b/ontology-engine/graph-engine_2.11/src/test/resources/application.conf @@ -407,21 +407,6 @@ telemetry.search.topn=5 installation.id=ekstep -learning.content.copy.invalid_status_list=["Flagged","FlaggedDraft","FraggedReview","Retired", "Processing"] -learning.content.copy.props_to_remove=["downloadUrl", "artifactUrl", "variants", - "createdOn", "collections", "children", "lastUpdatedOn", "SYS_INTERNAL_LAST_UPDATED_ON", - "versionKey", "s3Key", "status", "pkgVersion", "toc_url", "mimeTypesCount", - "contentTypesCount", "leafNodesCount", "childNodes", "prevState", "lastPublishedOn", - "flagReasons", "compatibilityLevel", "size", "publishChecklist", "publishComment", - "LastPublishedBy", "rejectReasons", "rejectComment", "gradeLevel", "subject", - "medium", "board", "topic", "purpose", "subtopic", "contentCredits", - "owner", "collaborators", "creators", "contributors", "badgeAssertions", "dialcodes", - "concepts", "keywords", "reservedDialcodes", "dialcodeRequired", "leafNodes"] - -# Metadata to be added to copied content from origin -learning.content.copy.origin_data=["name", "author", "license", "organisation"] - -learning.content.type.not.copied.list=["Asset"] channel.default="in.ekstep" @@ -431,7 +416,7 @@ dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/sear dialcode.api.authorization=auth_key # Language-Code Configuration -language.graph_ids=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] # Kafka send event to topic enable kafka.topic.send.enable=false @@ -460,8 +445,7 @@ publish.collection.fullecar.disable=true cassandra.lp.consistency.level=QUORUM -content.tagging.backward_enable=true -content.tagging.property="subject,medium" + content.nested.fields="badgeAssertions,targets,badgeAssociations" @@ -483,5 +467,22 @@ akka.http.parsing.max-content-length = 50MB schema.base_path = "../../schemas/" //schema.base_path = "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/" +collection.image.migration.enabled=true collection.keyspace = "hierarchy_store" -content.keyspace = "content_store" \ No newline at end of file +content.keyspace = "content_store" + +languageCode { + assamese : "as" + bengali : "bn" + english : "en" + gujarati : "gu" + hindi : "hi" + kannada : "ka" + marathi : "mr" + odia : "or" + tamil : "ta" + telugu : "te" +} + +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd"] +objectcategorydefinition.keyspace=category_store diff --git a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/BaseSpec.scala b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/BaseSpec.scala index 2cb6646ff..1b3a650cc 100644 --- a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/BaseSpec.scala +++ b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/BaseSpec.scala @@ -17,9 +17,21 @@ class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { var graphDb: GraphDatabaseService = null var session: Session = null + implicit val oec: OntologyEngineContext = new OntologyEngineContext private val script_1 = "CREATE KEYSPACE IF NOT EXISTS content_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" - private val script_2 = "CREATE TABLE IF NOT EXISTS content_store.content_data (content_id text, last_updated_on timestamp,body blob,oldBody blob,screenshots blob,stageIcons blob,PRIMARY KEY (content_id));" + private val script_2 = "CREATE TABLE IF NOT EXISTS content_store.content_data (content_id text, last_updated_on timestamp,body blob,oldBody blob,screenshots blob,stageIcons blob,externallink text,PRIMARY KEY (content_id));" + private val script_3 = "CREATE KEYSPACE IF NOT EXISTS hierarchy_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_4 = "CREATE TABLE IF NOT EXISTS hierarchy_store.content_hierarchy (identifier text, hierarchy text,PRIMARY KEY (identifier));" + private val script_5 = "CREATE KEYSPACE IF NOT EXISTS category_store WITH replication = {'class': 'SimpleStrategy','replication_factor': '1'};" + private val script_6 = "CREATE TABLE IF NOT EXISTS category_store.category_definition_data (identifier text, objectmetadata map, forms map ,PRIMARY KEY (identifier));" + private val script_7 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_content_all', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_8 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:course_collection_all', {'config': '{}', 'schema': '{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"}},\"default\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"additionalProperties\":false},\"additionalCategories\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"default\":\"Textbook\"}},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}'});" + private val script_9 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:course_content_all',{'config': '{}', 'schema': '{\"properties\":{\"trackable\":{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"},\"autoBatch\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"No\"}},\"default\":{\"enabled\":\"No\",\"autoBatch\":\"No\"},\"additionalProperties\":false},\"additionalCategories\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"default\":\"Textbook\"}},\"userConsent\":{\"type\":\"string\",\"enum\":[\"Yes\",\"No\"],\"default\":\"Yes\"}}}'});" + private val script_10 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_collection_all', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_11 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_content_in.ekstep', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_12 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:learning-resource_collection_in.ekstep', {'config': '{}', 'schema': '{\"properties\":{\"audience\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"Student\",\"Teacher\"]},\"default\":[\"Student\"]},\"mimeType\":{\"type\":\"string\",\"enum\":[\"application/vnd.ekstep.ecml-archive\",\"application/vnd.ekstep.html-archive\",\"application/vnd.ekstep.h5p-archive\",\"application/pdf\",\"video/mp4\",\"video/webm\"]}}}'});" + private val script_13 = "INSERT INTO category_store.category_definition_data (identifier, objectmetadata) VALUES ('obj-cat:practice-question_question_all', {'config': '{}', 'schema': '{}'});" def setUpEmbeddedNeo4j(): Unit = { @@ -73,13 +85,21 @@ class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { } override def beforeAll(): Unit = { + tearEmbeddedNeo4JSetup() setUpEmbeddedNeo4j() setUpEmbeddedCassandra() - executeCassandraQuery(script_1, script_2) + executeNeo4jQuery("UNWIND [{identifier:\"obj-cat:course_collection_all\",name:\"LearningResource\",description:\"Learning resource\",categoryId:\"obj-cat:course\",targetObjectType:\"Collection\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"default\\\":{\\\"enabled\\\":\\\"Yes\\\",\\\"autoBatch\\\":\\\"Yes\\\"},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:course_collection_all\"},{identifier:\"obj-cat:learning-resource_content_all\",name:\"LearningResource\",description:\"Learning resource\",categoryId:\"obj-cat:learningresource\",targetObjectType:\"Content\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"default\\\":{\\\"enabled\\\":\\\"Yes\\\",\\\"autoBatch\\\":\\\"Yes\\\"},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:learning-resource_content_all\"},{identifier:\"obj-cat:learning-resource_content_all\",name:\"LearningResource\",description:\"Learning resource\",categoryId:\"obj-cat:learningresource\",targetObjectType:\"Collection\",status:\"Live\",objectMetadata:\"{\\\"config\\\":{},\\\"schema\\\":{\\\"properties\\\":{\\\"trackable\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"enabled\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"},\\\"autoBatch\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"Yes\\\",\\\"No\\\"],\\\"default\\\":\\\"Yes\\\"}},\\\"default\\\":{\\\"enabled\\\":\\\"Yes\\\",\\\"autoBatch\\\":\\\"Yes\\\"},\\\"additionalProperties\\\":false}}}}\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"ObjectCategoryDefinition\",IL_UNIQUE_ID:\"obj-cat:learning-resource_collection_all\"}" + + ",{owner:\"in.ekstep\",code:\"NCF\",IL_SYS_NODE_TYPE:\"DATA_NODE\",apoc_json:\"{\\\"batch\\\": true}\",consumerId:\"9393568c-3a56-47dd-a9a3-34da3c821638\",channel:\"in.ekstep\",description:\"NCF \",type:\"K-12\",createdOn:\"2018-01-23T09:53:50.189+0000\",versionKey:\"1545195552163\",apoc_text:\"APOC\",appId:\"dev.sunbird.portal\",IL_FUNC_OBJECT_TYPE:\"Framework\",name:\"State (Uttar Pradesh)\",lastUpdatedOn:\"2018-12-19T04:59:12.163+0000\",IL_UNIQUE_ID:\"NCF\",status:\"Live\",apoc_num:1}" + + ",{code:\"cbse\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"Term\",name:\"CBSE\",IL_UNIQUE_ID:\"ncf_board_cbse\",status:\"Live\"}" + + ",{code:\"english\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"Term\",name:\"English\",IL_UNIQUE_ID:\"ncf_medium_english\",status:\"Live\"}" + + ",{code:\"english\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"Term\",name:\"English\",IL_UNIQUE_ID:\"ncf_subject_cbse\",status:\"Live\"}" + + ",{code:\"grade1\",IL_SYS_NODE_TYPE:\"DATA_NODE\",IL_FUNC_OBJECT_TYPE:\"Term\",name:\"Class 1\",IL_UNIQUE_ID:\"ncf_gradelevel_grade1\",status:\"Live\"}" + + ",{owner:\"in.ekstep\",code:\"tpd\",IL_SYS_NODE_TYPE:\"DATA_NODE\",apoc_json:\"{\\\"batch\\\": true}\",consumerId:\"9393568c-3a56-47dd-a9a3-34da3c821638\",channel:\"in.ekstep\",description:\"NCF \",type:\"K-12\",createdOn:\"2018-01-23T09:53:50.189+0000\",versionKey:\"1545195552163\",apoc_text:\"APOC\",appId:\"dev.sunbird.portal\",IL_FUNC_OBJECT_TYPE:\"Framework\",name:\"State (Uttar Pradesh)\",lastUpdatedOn:\"2018-12-19T04:59:12.163+0000\",IL_UNIQUE_ID:\"tpd\",status:\"Live\",apoc_num:1}] as row CREATE (n:domain) SET n += row;") + executeCassandraQuery(script_1, script_2, script_3, script_4, script_5, script_6, script_7, script_8, script_9, script_10, script_11, script_12, script_13) } override def afterAll(): Unit = { - deleteEmbeddedNeo4j(new File(Platform.config.getString("graph.dir"))) + tearEmbeddedNeo4JSetup() if(null != session && !session.isClosed) session.close() EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() @@ -96,6 +116,14 @@ class BaseSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { } def createRelationData(): Unit = { - graphDb.execute("UNWIND [{identifier:\"Num:C3:SC2\",code:\"Num:C3:SC2\",keywords:[\"Subconcept\",\"Class 3\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",subject:\"numeracy\",channel:\"in.ekstep\",description:\"Multiplication\",versionKey:\"1484389136575\",gradeLevel:[\"Grade 3\",\"Grade 4\"],IL_FUNC_OBJECT_TYPE:\"Concept\",name:\"Multiplication\",lastUpdatedOn:\"2016-06-15T17:15:45.951+0000\",IL_UNIQUE_ID:\"Num:C3:SC2\",status:\"Live\"}, {code:\"31d521da-61de-4220-9277-21ca7ce8335c\",previewUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",downloadUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",channel:\"in.ekstep\",language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790848197_do_11232724509261824014_2.0_spine.ecar\\\",\\\"size\\\":890.0}}\",mimeType:\"application/pdf\",streamingUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",idealScreenSize:\"normal\",createdOn:\"2017-09-07T13:24:20.720+0000\",contentDisposition:\"inline\",artifactUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",contentEncoding:\"identity\",lastUpdatedOn:\"2017-09-07T13:25:53.595+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2017-09-07T13:27:28.417+0000\",contentType:\"Resource\",lastUpdatedBy:\"Ekstep\",audience:[\"Learner\"],visibility:\"Default\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",consumerId:\"e84015d2-a541-4c07-a53f-e31d4553312b\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",pkgVersion:2,versionKey:\"1504790848417\",license:\"Creative Commons Attribution (CC BY)\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",size:4864851,lastPublishedOn:\"2017-09-07T13:27:27.410+0000\",createdBy:\"390\",compatibilityLevel:4,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Untitled Content\",publisher:\"EkStep\",IL_UNIQUE_ID:\"do_11232724509261824014\",status:\"Live\",resourceType:[\"Study material\"]}] as row CREATE (n:domain) SET n += row") + graphDb.execute("UNWIND [{identifier:\"Num:C3:SC2\",code:\"Num:C3:SC2\",keywords:[\"Subconcept\",\"Class 3\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",subject:\"numeracy\",channel:\"in.ekstep\",description:\"Multiplication\",versionKey:\"1484389136575\",gradeLevel:[\"Grade 3\",\"Grade 4\"],IL_FUNC_OBJECT_TYPE:\"Concept\",name:\"Multiplication\",lastUpdatedOn:\"2016-06-15T17:15:45.951+0000\",IL_UNIQUE_ID:\"Num:C3:SC2\",status:\"Live\"}, {code:\"31d521da-61de-4220-9277-21ca7ce8335c\",previewUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",downloadUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",channel:\"in.ekstep\",language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/ecar_files/do_11232724509261824014/untitled-content_1504790848197_do_11232724509261824014_2.0_spine.ecar\\\",\\\"size\\\":890.0}}\",mimeType:\"application/pdf\",streamingUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",idealScreenSize:\"normal\",createdOn:\"2017-09-07T13:24:20.720+0000\",contentDisposition:\"inline\",artifactUrl:\"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/assets/do_11232724509261824014/object-oriented-javascript.pdf\",contentEncoding:\"identity\",lastUpdatedOn:\"2017-09-07T13:25:53.595+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2017-09-07T13:27:28.417+0000\",contentType:\"Resource\",lastUpdatedBy:\"Ekstep\",audience:[\"Student\"],visibility:\"Default\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",consumerId:\"e84015d2-a541-4c07-a53f-e31d4553312b\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",pkgVersion:2,versionKey:\"1504790848417\",license:\"Creative Commons Attribution (CC BY)\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_11232724509261824014/untitled-content_1504790847410_do_11232724509261824014_2.0.ecar\",size:4864851,lastPublishedOn:\"2017-09-07T13:27:27.410+0000\",createdBy:\"390\",compatibilityLevel:4,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"Untitled Content\",publisher:\"EkStep\",IL_UNIQUE_ID:\"do_11232724509261824014\",status:\"Live\",resourceType:[\"Study material\"]}] as row CREATE (n:domain) SET n += row") + } + + def createBulkNodes(): Unit ={ + graphDb.execute("UNWIND [{nodeId:'do_0000123'},{nodeId:'do_0000234'},{nodeId:'do_0000345'}] as row with row.nodeId as Id CREATE (n:domain{IL_UNIQUE_ID:Id});") + } + + def executeNeo4jQuery(query: String): Unit = { + graphDb.execute(query) } } diff --git a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/health/TestHealthCheckManager.scala b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/health/TestHealthCheckManager.scala new file mode 100644 index 000000000..26e759a8f --- /dev/null +++ b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/health/TestHealthCheckManager.scala @@ -0,0 +1,33 @@ +package org.sunbird.graph.health + +import org.sunbird.common.dto.Response +import org.sunbird.common.exception.ResponseCode +import org.sunbird.graph.BaseSpec + +import scala.concurrent.Future + +class TestHealthCheckManager extends BaseSpec { + + "check health api" should "return true" in { + val future: Future[Response] = HealthCheckManager.checkAllSystemHealth() + future map { response => { + assert(ResponseCode.OK == response.getResponseCode) + assert(response.get("healthy") == true) + } + } + } + + "check generate check with status false" should "return service unavailable map" in { + val check: Map[String, Any] = HealthCheckManager.generateCheck(false, "redis cache") + assert(Some(false) == check.get("healthy")) + assert(Some("redis cache") == check.get("name")) + assert(Some("503") == check.get("err")) + + } + + "check generate check with status true" should "return healthy service map" in { + val check: Map[String, Any] = HealthCheckManager.generateCheck(true, "redis cache") + assert(Some(true) == check.get("healthy")) + assert(Some("redis cache") == check.get("name")) + } +} diff --git a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/nodes/TestDataNode.scala b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/nodes/TestDataNode.scala index 77d1df012..55a7ecacf 100644 --- a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/nodes/TestDataNode.scala +++ b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/nodes/TestDataNode.scala @@ -2,15 +2,16 @@ package org.sunbird.graph.nodes import java.util -import org.sunbird.cache.util.RedisCacheUtil -import org.sunbird.common.dto.Request +import org.neo4j.graphdb.Result +import org.sunbird.cache.impl.RedisCache +import org.sunbird.common.JsonUtils +import org.sunbird.common.dto.{Request, Response, ResponseHandler} import org.sunbird.common.exception.{ClientException, ResourceNotFoundException} -import org.sunbird.graph.BaseSpec +import org.sunbird.graph.{BaseSpec, OntologyEngineContext} import org.sunbird.graph.dac.model.Node import org.sunbird.graph.utils.ScalaJsonUtils -import scala.collection.JavaConversions._ -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.Future class TestDataNode extends BaseSpec { @@ -34,10 +35,14 @@ class TestDataNode extends BaseSpec { request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") - val future: Future[Node] = DataNode.create(request) + request.put("primaryCategory", "Learning Resource") + val future: Future[Node] = DataNode.create(request,dataModifier) future map {node => {assert(null != node) print(node) - assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("testResource"))}} + + assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("testResource")) + assert(node.getMetadata.get("trackable").asInstanceOf[String].contains("{\"enabled\":\"No\",\"autoBatch\":\"No\"}")) + assert(node.getMetadata.get("contentType").asInstanceOf[String].equalsIgnoreCase("Resource"))}} } @@ -52,6 +57,7 @@ class TestDataNode extends BaseSpec { request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") request.put("concepts", new util.ArrayList[util.Map[String, AnyRef]](){{ add(new util.HashMap[String, AnyRef](){{ put("identifier", "Num:C3:SC2") @@ -83,6 +89,7 @@ class TestDataNode extends BaseSpec { request.put("description", "test") request.put("channel", "in.ekstep") request.put("body", "body") + request.put("primaryCategory", "Learning Resource") val future: Future[Node] = DataNode.create(request) future map { node => { assert(null != node) @@ -112,6 +119,7 @@ class TestDataNode extends BaseSpec { request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") request.put("concepts", new util.ArrayList[util.Map[String, AnyRef]](){{ add(new util.HashMap[String, AnyRef](){{ put("identifier", "invalidConcept") @@ -132,6 +140,7 @@ class TestDataNode extends BaseSpec { request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") val future: Future[Node] = DataNode.create(request) future map {node => {assert(null != node) print(node) @@ -149,16 +158,23 @@ class TestDataNode extends BaseSpec { } "update content with valid relation" should "update node with relation" in { + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_content_0000000001',IL_FUNC_OBJECT_TYPE:'Content',status:'Live'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_concept_0000000001',IL_FUNC_OBJECT_TYPE:'Concept',status:'Live'});") val request = new Request() request.setObjectType("Content") request.setContext(getContextMap()) - request.put("code", "test") request.put("name", "testResource") request.put("mimeType", "application/pdf") request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") + request.put("children", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_content_0000000001") + }}) + }}) val future: Future[Node] = DataNode.create(request) future map {node => {assert(null != node) print(node) @@ -168,7 +184,7 @@ class TestDataNode extends BaseSpec { req.put("name", "updated name") req.put("concepts", new util.ArrayList[util.Map[String, AnyRef]](){{ add(new util.HashMap[String, AnyRef](){{ - put("identifier", "Num:C3:SC2") + put("identifier", "rel_concept_0000000001") }}) }}) val updateFuture = DataNode.update(req) @@ -177,7 +193,7 @@ class TestDataNode extends BaseSpec { readRequest.put("identifier", node.getIdentifier) DataNode.read(readRequest).map(node => { assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("updated name")) - assert(node.getOutRelations.get(0).getEndNodeId().equalsIgnoreCase("Num:C3:SC2")) + assert(node.getOutRelations.size() == 2) }) }) flatMap(f => f) } @@ -195,6 +211,7 @@ class TestDataNode extends BaseSpec { request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") val future: Future[Node] = DataNode.create(request) future map { node => { assert(null != node) @@ -220,6 +237,7 @@ class TestDataNode extends BaseSpec { request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") val future: Future[Node] = DataNode.create(request) future map {node => {assert(null != node) print(node) @@ -248,6 +266,7 @@ class TestDataNode extends BaseSpec { request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") val future: Future[Node] = DataNode.create(request) future map {node => {assert(null != node) print(node) @@ -265,7 +284,7 @@ class TestDataNode extends BaseSpec { } "update live node with external props" should "update image node with existing external props in image node" in { - graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\",keywords:[\"Test\"],plugins:\"[{\\\"identifier\\\":\\\"org.ekstep.stage\\\",\\\"semanticVersion\\\":\\\"1.0\\\"},{\\\"identifier\\\":\\\"org.ekstep.shape\\\",\\\"semanticVersion\\\":\\\"1.0\\\"},{\\\"identifier\\\":\\\"org.ekstep.text\\\",\\\"semanticVersion\\\":\\\"1.2\\\"},{\\\"identifier\\\":\\\"org.ekstep.image\\\",\\\"semanticVersion\\\":\\\"1.1\\\"},{\\\"identifier\\\":\\\"org.ekstep.navigation\\\",\\\"semanticVersion\\\":\\\"1.0\\\"}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499420_do_1129067102240194561252_2.0.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499615_do_1129067102240194561252_2.0_spine.ecar\\\",\\\"size\\\":36069.0}}\",mimeType:\"application/vnd.ekstep.ecml-archive\",editorState:\"{\\\"plugin\\\":{\\\"noOfExtPlugins\\\":7,\\\"extPlugins\\\":[{\\\"plugin\\\":\\\"org.ekstep.contenteditorfunctions\\\",\\\"version\\\":\\\"1.2\\\"},{\\\"plugin\\\":\\\"org.ekstep.keyboardshortcuts\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.richtext\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.iterator\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.navigation\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.reviewercomments\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.questionunit.ftb\\\",\\\"version\\\":\\\"1.1\\\"}]},\\\"stage\\\":{\\\"noOfStages\\\":5,\\\"currentStage\\\":\\\"c5ead48c-d574-488b-80d0-6d7db2d60637\\\",\\\"selectedPluginObject\\\":\\\"5b6a5e3d-5e44-4254-8c70-d82d6c13cc2c\\\"},\\\"sidebar\\\":{\\\"selectedMenu\\\":\\\"settings\\\"}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1129067102240194561252/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",assets:[\"do_112835334818643968148\"],appId:\"dev.sunbird.portal\",contentEncoding:\"gzip\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1129067102240194561252/artifact/1575527499196_do_1129067102240194561252.zip\",lockKey:\"b7992ea7-f326-40d0-abdd-1601146bca84\",contentType:\"Resource\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Learner\"],visibility:\"Default\",consumerId:\"b3e90b00-1e9f-4692-9290-d014c20625f2\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",version:2,pragma:[],prevState:\"Review\",license:\"CC BY 4.0\",lastPublishedOn:\"2019-12-05T06:31:39.415+0000\",size:74105,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"TEST-G-KP-2.0-001\",status:\"Live\",totalQuestions:0,code:\"org.sunbird.3qKh9v\",description:\"Test ECML Content\",streamingUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-12-05T06:09:10.490+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-12-05T06:31:37.880+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-12-05T06:31:40.528+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-12-05T06:31:37.869+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",totalScore:0,pkgVersion:2,versionKey:\"1575527498230\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499420_do_1129067102240194561252_2.0.ecar\",lastSubmittedOn:\"2019-12-05T06:22:33.347+0000\",createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",compatibilityLevel:2,IL_UNIQUE_ID:\"do_1129067102240194561252\",resourceType:\"Learn\"}] as row CREATE (n:domain) SET n += row") + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\",keywords:[\"Test\"],plugins:\"[{\\\"identifier\\\":\\\"org.ekstep.stage\\\",\\\"semanticVersion\\\":\\\"1.0\\\"},{\\\"identifier\\\":\\\"org.ekstep.shape\\\",\\\"semanticVersion\\\":\\\"1.0\\\"},{\\\"identifier\\\":\\\"org.ekstep.text\\\",\\\"semanticVersion\\\":\\\"1.2\\\"},{\\\"identifier\\\":\\\"org.ekstep.image\\\",\\\"semanticVersion\\\":\\\"1.1\\\"},{\\\"identifier\\\":\\\"org.ekstep.navigation\\\",\\\"semanticVersion\\\":\\\"1.0\\\"}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499420_do_1129067102240194561252_2.0.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499615_do_1129067102240194561252_2.0_spine.ecar\\\",\\\"size\\\":36069.0}}\",mimeType:\"application/vnd.ekstep.ecml-archive\",editorState:\"{\\\"plugin\\\":{\\\"noOfExtPlugins\\\":7,\\\"extPlugins\\\":[{\\\"plugin\\\":\\\"org.ekstep.contenteditorfunctions\\\",\\\"version\\\":\\\"1.2\\\"},{\\\"plugin\\\":\\\"org.ekstep.keyboardshortcuts\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.richtext\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.iterator\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.navigation\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.reviewercomments\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.questionunit.ftb\\\",\\\"version\\\":\\\"1.1\\\"}]},\\\"stage\\\":{\\\"noOfStages\\\":5,\\\"currentStage\\\":\\\"c5ead48c-d574-488b-80d0-6d7db2d60637\\\",\\\"selectedPluginObject\\\":\\\"5b6a5e3d-5e44-4254-8c70-d82d6c13cc2c\\\"},\\\"sidebar\\\":{\\\"selectedMenu\\\":\\\"settings\\\"}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1129067102240194561252/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",assets:[\"do_112835334818643968148\"],appId:\"dev.sunbird.portal\",contentEncoding:\"gzip\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1129067102240194561252/artifact/1575527499196_do_1129067102240194561252.zip\",lockKey:\"b7992ea7-f326-40d0-abdd-1601146bca84\",contentType:\"Resource\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],visibility:\"Default\",consumerId:\"b3e90b00-1e9f-4692-9290-d014c20625f2\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",version:2,pragma:[],prevState:\"Review\",license:\"CC BY 4.0\",lastPublishedOn:\"2019-12-05T06:31:39.415+0000\",size:74105,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"TEST-G-KP-2.0-001\",status:\"Live\",totalQuestions:0,code:\"org.sunbird.3qKh9v\",description:\"Test ECML Content\",streamingUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-12-05T06:09:10.490+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-12-05T06:31:37.880+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-12-05T06:31:40.528+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-12-05T06:31:37.869+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",totalScore:0,pkgVersion:2,versionKey:\"1575527498230\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499420_do_1129067102240194561252_2.0.ecar\",lastSubmittedOn:\"2019-12-05T06:22:33.347+0000\",createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",compatibilityLevel:2,IL_UNIQUE_ID:\"do_1129067102240194561252\",resourceType:\"Learn\"}] as row CREATE (n:domain) SET n += row") executeCassandraQuery("INSERT into content_store.content_data(content_id, body) values('do_1129067102240194561252', textAsBlob('body'));") val request = new Request() request.setObjectType("Content") @@ -273,11 +292,38 @@ class TestDataNode extends BaseSpec { request.getContext.put("identifier", "do_1129067102240194561252") request.put("name", "updated name") request.put("versionKey", "1575527498230") + request.put("primaryCategory", "Learning Resource") val updateFuture = DataNode.update(request) updateFuture.map(node => { assert(node.getIdentifier.equalsIgnoreCase("do_1129067102240194561252.img")) val resultSet = session.execute("select blobAsText(body) as body from content_store.content_data where content_id='do_1129067102240194561252.img'") assert(resultSet.one().getString("body").equalsIgnoreCase("body")) + val result: Result = graphDb.execute("Match (n:domain{IL_UNIQUE_ID:'do_1129067102240194561252.img'}) return n.status as status, n.prevStatus as prevStatus") + val resMap = result.next() + assert("Draft".contentEquals(resMap.get("status").asInstanceOf[String])) + assert("Live".contentEquals(resMap.get("prevStatus").asInstanceOf[String])) + }) + } + + "update live collection with external props" should "update image node with existing external props in image node for collection" in { + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"online\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398314186_do_11283193441064550414_1.0_online.ecar\\\",\\\"size\\\":4034.0},\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\\\",\\\"size\\\":73256.0}}\",mimeType:\"application/vnd.ekstep.content-collection\",leafNodes:[\"do_112831862871203840114\"],c_sunbird_dev_private_batch_count:0,appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550414/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",appId:\"local.sunbird.portal\",contentEncoding:\"gzip\",lockKey:\"b079cf15-9e45-4865-be56-2edafa432dd3\",mimeTypesCount:\"{\\\"application/vnd.ekstep.content-collection\\\":1,\\\"video/mp4\\\":1}\",totalCompressedSize:416488,contentType:\"Course\",primaryCategory:\"Course\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],toc_url:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11283193441064550414/artifact/do_11283193441064550414_toc.json\",visibility:\"Default\",contentTypesCount:\"{\\\"CourseUnit\\\":1,\\\"Resource\\\":1}\",author:\"b00bc992ef25f1a9a8d63291e20efc8d\",childNodes:[\"do_11283193463014195215\"],consumerId:\"273f3b18-5dda-4a27-984a-060c7cd398d3\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"System\",version:2,license:\"CC BY-NC 4.0\",prevState:\"Draft\",size:73256,lastPublishedOn:\"2019-08-21T14:38:33.816+0000\",IL_FUNC_OBJECT_TYPE:\"Collection\",name:\"test prad course cert\",status:\"Live\",code:\"org.sunbird.SUi47U\",description:\"Enter description for Course\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-08-21T14:37:23.486+0000\",reservedDialcodes:\"{\\\"I1X4R4\\\":0}\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-08-21T14:38:33.212+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-11-13T12:54:08.295+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-08-21T14:38:34.540+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",pkgVersion:1,versionKey:\"1566398313212\",idealScreenDensity:\"hdpi\",dialcodes:[\"I1X4R4\"],s3Key:\"ecar_files/do_11283193441064550414/test-prad-course-cert_1566398313947_do_11283193441064550414_1.0_spine.ecar\",depth:0,framework:\"tpd\",me_averageRating:5,createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",leafNodesCount:1,compatibilityLevel:4,IL_UNIQUE_ID:\"do_11283193441064550414\",c_sunbird_dev_open_batch_count:0,resourceType:\"Course\"}] as row CREATE (n:domain) SET n += row") + executeCassandraQuery("INSERT into hierarchy_store.content_hierarchy(identifier, hierarchy) values('do_11283193441064550414', '{\"identifier\": \"do_11283193441064550414\"}');") + val request = new Request() + request.setObjectType("Collection") + request.setContext(getContextMap()) + request.getContext.put("identifier", "do_11283193441064550414") + request.put("name", "updated name") + request.put("versionKey", "1566389713020") + request.put("primaryCategory", "Course") + val updateFuture = DataNode.update(request) + updateFuture.map(node => { + assert(node.getIdentifier.equalsIgnoreCase("do_11283193441064550414.img")) + val resultSet = session.execute("select hierarchy from hierarchy_store.content_hierarchy where identifier='do_11283193441064550414.img'") + assert(resultSet.one().getString("hierarchy").equalsIgnoreCase("{\"identifier\": \"do_11283193441064550414\"}")) + val result: Result = graphDb.execute("Match (n:domain{IL_UNIQUE_ID:'do_11283193441064550414.img'}) return n.status as status, n.prevStatus as prevStatus") + val resMap = result.next() + assert("Draft".contentEquals(resMap.get("status").asInstanceOf[String])) + assert("Live".contentEquals(resMap.get("prevStatus").asInstanceOf[String])) }) } @@ -292,7 +338,7 @@ class TestDataNode extends BaseSpec { request.put("contentType", "Resource") request.put("description", "test") request.put("channel", "in.ekstep") - + request.put("primaryCategory", "Learning Resource") val contentCredits = new util.ArrayList[AnyRef]() { { add(new util.HashMap[String, AnyRef]() { @@ -323,7 +369,7 @@ class TestDataNode extends BaseSpec { } "read Live node twice one from neo4j and one from cache" should "read node from neo4j and from cache" in { - graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\",keywords:[\"Test\"],plugins:\"[{\\\"identifier\\\":\\\"org.ekstep.stage\\\",\\\"semanticVersion\\\":\\\"1.0\\\"},{\\\"identifier\\\":\\\"org.ekstep.shape\\\",\\\"semanticVersion\\\":\\\"1.0\\\"},{\\\"identifier\\\":\\\"org.ekstep.text\\\",\\\"semanticVersion\\\":\\\"1.2\\\"},{\\\"identifier\\\":\\\"org.ekstep.image\\\",\\\"semanticVersion\\\":\\\"1.1\\\"},{\\\"identifier\\\":\\\"org.ekstep.navigation\\\",\\\"semanticVersion\\\":\\\"1.0\\\"}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499420_do_1129067102240194561252_2.0.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499615_do_1129067102240194561252_2.0_spine.ecar\\\",\\\"size\\\":36069.0}}\",mimeType:\"application/vnd.ekstep.ecml-archive\",editorState:\"{\\\"plugin\\\":{\\\"noOfExtPlugins\\\":7,\\\"extPlugins\\\":[{\\\"plugin\\\":\\\"org.ekstep.contenteditorfunctions\\\",\\\"version\\\":\\\"1.2\\\"},{\\\"plugin\\\":\\\"org.ekstep.keyboardshortcuts\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.richtext\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.iterator\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.navigation\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.reviewercomments\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.questionunit.ftb\\\",\\\"version\\\":\\\"1.1\\\"}]},\\\"stage\\\":{\\\"noOfStages\\\":5,\\\"currentStage\\\":\\\"c5ead48c-d574-488b-80d0-6d7db2d60637\\\",\\\"selectedPluginObject\\\":\\\"5b6a5e3d-5e44-4254-8c70-d82d6c13cc2c\\\"},\\\"sidebar\\\":{\\\"selectedMenu\\\":\\\"settings\\\"}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1129067102240194561252/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",assets:[\"do_112835334818643968148\"],appId:\"dev.sunbird.portal\",contentEncoding:\"gzip\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1129067102240194561252/artifact/1575527499196_do_1129067102240194561252.zip\",lockKey:\"b7992ea7-f326-40d0-abdd-1601146bca84\",contentType:\"Resource\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Learner\"],visibility:\"Default\",consumerId:\"b3e90b00-1e9f-4692-9290-d014c20625f2\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",version:2,pragma:[],prevState:\"Review\",license:\"CC BY 4.0\",lastPublishedOn:\"2019-12-05T06:31:39.415+0000\",size:74105,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"TEST-G-KP-2.0-001\",status:\"Live\",totalQuestions:0,code:\"org.sunbird.3qKh9v\",description:\"Test ECML Content\",streamingUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-12-05T06:09:10.490+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-12-05T06:31:37.880+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-12-05T06:31:40.528+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-12-05T06:31:37.869+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",totalScore:0,pkgVersion:2,versionKey:\"1575527498230\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499420_do_1129067102240194561252_2.0.ecar\",lastSubmittedOn:\"2019-12-05T06:22:33.347+0000\",createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",compatibilityLevel:2,IL_UNIQUE_ID:\"do_1129067102240194561252\",resourceType:\"Learn\", subject: [\"Hindi\"], framework:\"NCF\"}] as row CREATE (n:domain) SET n += row") + graphDb.execute("UNWIND [{ownershipType:[\"createdBy\"],copyright:\"Sunbird\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\",keywords:[\"Test\"],plugins:\"[{\\\"identifier\\\":\\\"org.ekstep.stage\\\",\\\"semanticVersion\\\":\\\"1.0\\\"},{\\\"identifier\\\":\\\"org.ekstep.shape\\\",\\\"semanticVersion\\\":\\\"1.0\\\"},{\\\"identifier\\\":\\\"org.ekstep.text\\\",\\\"semanticVersion\\\":\\\"1.2\\\"},{\\\"identifier\\\":\\\"org.ekstep.image\\\",\\\"semanticVersion\\\":\\\"1.1\\\"},{\\\"identifier\\\":\\\"org.ekstep.navigation\\\",\\\"semanticVersion\\\":\\\"1.0\\\"}]\",downloadUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499420_do_1129067102240194561252_2.0.ecar\",channel:\"b00bc992ef25f1a9a8d63291e20efc8d\",organisation:[\"Sunbird\"],language:[\"English\"],variants:\"{\\\"spine\\\":{\\\"ecarUrl\\\":\\\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499615_do_1129067102240194561252_2.0_spine.ecar\\\",\\\"size\\\":36069.0}}\",mimeType:\"application/vnd.ekstep.ecml-archive\",editorState:\"{\\\"plugin\\\":{\\\"noOfExtPlugins\\\":7,\\\"extPlugins\\\":[{\\\"plugin\\\":\\\"org.ekstep.contenteditorfunctions\\\",\\\"version\\\":\\\"1.2\\\"},{\\\"plugin\\\":\\\"org.ekstep.keyboardshortcuts\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.richtext\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.iterator\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.navigation\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.reviewercomments\\\",\\\"version\\\":\\\"1.0\\\"},{\\\"plugin\\\":\\\"org.ekstep.questionunit.ftb\\\",\\\"version\\\":\\\"1.1\\\"}]},\\\"stage\\\":{\\\"noOfStages\\\":5,\\\"currentStage\\\":\\\"c5ead48c-d574-488b-80d0-6d7db2d60637\\\",\\\"selectedPluginObject\\\":\\\"5b6a5e3d-5e44-4254-8c70-d82d6c13cc2c\\\"},\\\"sidebar\\\":{\\\"selectedMenu\\\":\\\"settings\\\"}}\",appIcon:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1129067102240194561252/artifact/033019_sz_reviews_feat_1564126718632.thumb.jpg\",assets:[\"do_112835334818643968148\"],appId:\"dev.sunbird.portal\",contentEncoding:\"gzip\",artifactUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1129067102240194561252/artifact/1575527499196_do_1129067102240194561252.zip\",lockKey:\"b7992ea7-f326-40d0-abdd-1601146bca84\",contentType:\"Resource\",lastUpdatedBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",audience:[\"Student\"],visibility:\"Default\",consumerId:\"b3e90b00-1e9f-4692-9290-d014c20625f2\",mediaType:\"content\",osId:\"org.ekstep.quiz.app\",lastPublishedBy:\"Ekstep\",version:2,pragma:[],prevState:\"Review\",license:\"CC BY 4.0\",lastPublishedOn:\"2019-12-05T06:31:39.415+0000\",size:74105,IL_FUNC_OBJECT_TYPE:\"Content\",name:\"TEST-G-KP-2.0-001\",status:\"Live\",totalQuestions:0,code:\"org.sunbird.3qKh9v\",description:\"Test ECML Content\",streamingUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\",posterImage:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11281332607717376012/artifact/033019_sz_reviews_feat_1564126718632.jpg\",idealScreenSize:\"normal\",createdOn:\"2019-12-05T06:09:10.490+0000\",contentDisposition:\"inline\",lastUpdatedOn:\"2019-12-05T06:31:37.880+0000\",SYS_INTERNAL_LAST_UPDATED_ON:\"2019-12-05T06:31:40.528+0000\",dialcodeRequired:\"No\",creator:\"Creation\",createdFor:[\"ORG_001\"],lastStatusChangedOn:\"2019-12-05T06:31:37.869+0000\",os:[\"All\"],IL_SYS_NODE_TYPE:\"DATA_NODE\",totalScore:0,pkgVersion:2,versionKey:\"1575527498230\",idealScreenDensity:\"hdpi\",s3Key:\"ecar_files/do_1129067102240194561252/test-g-kp-2.0-001_1575527499420_do_1129067102240194561252_2.0.ecar\",lastSubmittedOn:\"2019-12-05T06:22:33.347+0000\",createdBy:\"874ed8a5-782e-4f6c-8f36-e0288455901e\",compatibilityLevel:2,IL_UNIQUE_ID:\"do_1129067102240194561252\",resourceType:\"Learn\", subject: [\"Hindi\"], framework:\"NCF\"}] as row CREATE (n:domain) SET n += row") createRelationData() graphDb.execute("MATCH (n:domain{IL_UNIQUE_ID:'do_1129067102240194561252'}) match (m:domain{IL_UNIQUE_ID:'Num:C3:SC2'}) CREATE (n)-[r:associatedTo]->(m)") graphDb.execute("MATCH (n:domain{IL_UNIQUE_ID:'do_1129067102240194561252'}) match (m:domain{IL_UNIQUE_ID:'do_11232724509261824014'}) CREATE (m)-[r:associatedTo]->(n)") @@ -331,16 +377,352 @@ class TestDataNode extends BaseSpec { request.setObjectType("Content") request.setContext(getContextMap()) request.put("identifier", "do_1129067102240194561252") - RedisCacheUtil.delete("do_1129067102240194561252") + RedisCache.delete("do_1129067102240194561252") ScalaJsonUtils.deserialize("{\"IL_SYS_NODE_TYPE\":\"ROOT_NODE\",\"consumerId\":\"72e54829-6402-4cf0-888e-9b30733c1b88\",\"appId\":\"ekstep_portal\",\"channel\":\"in.ekstep\",\"lastUpdatedOn\":\"2018-02-28T13:18:01.346+0000\",\"IL_UNIQUE_ID\":\"do_ROOT_NODE\",\"versionKey\":\"1519823881346\"}")(manifest[Map[String, AnyRef]]) val readFuture = DataNode.read(request) readFuture.map(node => { assert(node.getIdentifier.equalsIgnoreCase("do_1129067102240194561252")) - assert(null != RedisCacheUtil.getString("do_1129067102240194561252")) + assert(null != RedisCache.get("do_1129067102240194561252")) val readFromCache = DataNode.read(request) readFromCache.map(node => { assert(node.getIdentifier.equalsIgnoreCase("do_1129067102240194561252")) }) }).flatMap(f => f) } + + "bulkUpdate with multiple node" should "should update all node successfully" in { + createBulkNodes() + val request = new Request() + request.setObjectType("Content") + request.setContext(getContextMap()) + request.put("identifiers", new util.ArrayList[String]() { + { + add("do_0000123"); add("do_0000234"); add("do_0000345") + } + }) + request.put("metadata", new util.HashMap[String, AnyRef]() { + { + put("status", "Live") + put("IL_FUNC_OBJECT_TYPE", "Content") + } + }) + val future: Future[util.Map[String, Node]] = DataNode.bulkUpdate(request) + future map { data => { + assert(null != data) + assert(data.size() == 3) + } + } + } + + "bulkUpdate with single node" should "should update the node successfully" in { + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'do_0000456'});") + val request = new Request() + request.setObjectType("Content") + request.setContext(getContextMap()) + request.put("identifiers", new util.ArrayList[String]() { + { + add("do_0000456"); + } + }) + request.put("metadata", new util.HashMap[String, AnyRef]() { + { + put("status", "Live") + put("IL_FUNC_OBJECT_TYPE", "Content") + } + }) + val future: Future[util.Map[String, Node]] = DataNode.bulkUpdate(request) + future map { data => { + assert(null != data) + assert(data.size() == 1) + } + } + } + + "update content with valid relations having type assosiatedTo and hasSequenceMember" should "update node with relation" in { + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_concept_00000001',IL_FUNC_OBJECT_TYPE:'Concept',status:'Live'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_concept_00000002',IL_FUNC_OBJECT_TYPE:'Concept',status:'Live'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_itemset_00000001',IL_FUNC_OBJECT_TYPE:'ItemSet',status:'Live'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_collections_00000001',IL_FUNC_OBJECT_TYPE:'Content',status:'Live', contentType:'TextBook'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_collections_00000002',IL_FUNC_OBJECT_TYPE:'Content',status:'Live', contentType:'TextBook'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_collections_00000003',IL_FUNC_OBJECT_TYPE:'ContentImage',status:'Live', contentType:'TextBook'});") + val request = new Request() + request.setObjectType("Content") + request.setContext(getContextMap()) + request.put("code", "test") + request.put("name", "testResource") + request.put("mimeType", "application/pdf") + request.put("contentType", "Resource") + request.put("description", "test") + request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") + request.put("concepts", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_concept_00000001") + }}) + }}) + request.put("collections", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_collections_00000001") + }}) + }}) + val future: Future[Node] = DataNode.create(request) + future map {node => {assert(null != node) + print(node) + assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("testResource")) + val req = new Request(request) + req.getContext.put("identifier", node.getIdentifier) + req.put("name", "updated name") + req.put("concepts", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_concept_00000002") + }}) + }}) + req.put("itemSets", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_itemset_00000001") + }}) + }}) + req.put("collections", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_collections_00000002") + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_collections_00000003") + }}) + }}) + val updateFuture = DataNode.update(req) + updateFuture.map(node => { + val readRequest = new Request(request) + readRequest.put("identifier", node.getIdentifier) + DataNode.read(readRequest).map(node => { + assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("updated name")) + assert(node.getOutRelations.size() == 2) + assert(node.getInRelations.size() == 2) + }) + }) flatMap(f => f) + } + } flatMap(f => f) + } + + "update content with valid relations having in direction" should "update node with relation" in { + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_collections_0000000101',IL_FUNC_OBJECT_TYPE:'Content',status:'Live', contentType:'TextBook'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_collections_0000000102',IL_FUNC_OBJECT_TYPE:'Content',status:'Live', contentType:'TextBook'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_usedbycontent_0000000101',IL_FUNC_OBJECT_TYPE:'Content',status:'Live', IL_SYS_NODE_TYPE:'DATA_NODE', contentType:'TextBook'});") + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'rel_usedbycontent_0000000102',IL_FUNC_OBJECT_TYPE:'Content',status:'Live', IL_SYS_NODE_TYPE:'DATA_NODE',contentType:'TextBook'});") + val request = new Request() + request.setObjectType("Content") + request.setContext(getContextMap()) + request.put("code", "test") + request.put("name", "testResource") + request.put("mimeType", "application/pdf") + request.put("contentType", "Resource") + request.put("description", "test") + request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") + request.put("collections", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_collections_0000000101") + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_collections_0000000102") + }}) + }}) + val future: Future[Node] = DataNode.create(request) + future map {node => {assert(null != node) + print(node) + assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("testResource")) + val req = new Request(request) + req.getContext.put("identifier", node.getIdentifier) + req.put("name", "updated name") + req.put("usedByContent", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_usedbycontent_0000000101") + }}) + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "rel_usedbycontent_0000000102") + }}) + }}) + val updateFuture = DataNode.update(req) + updateFuture.map(node => { + val readRequest = new Request(request) + readRequest.put("identifier", node.getIdentifier) + DataNode.read(readRequest).map(node => { + assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("updated name")) + assert(node.getInRelations.size() == 4) + }) + }) flatMap(f => f) + } + } flatMap(f => f) + } + + "createNode with invalid metadata" should "throw client error" in { + val request = new Request() + request.setObjectType("Content") + request.setContext(getContextMap()) + + request.put("code", "test") + request.put("name", "testResource") + request.put("mimeType", "application/pdf") + request.put("contentType", "Resource") + request.put("description", "test") + request.put("channel", "in.ekstep") + //TODO: Uncomment this line when schema_restrict_api is true +// request.put("test", "test") + //TODO: Remove this when schema_restrict_api is true + request.put("ownershipType", "test") + + request.put("primaryCategory", "Learning Resource") + assertThrows[ClientException](DataNode.create(request)) + // recoverToSucceededIf[ClientException](DataNode.create(request)) + } + + "update content with invalid data" should "throw client error" in { + val request = new Request() + request.setObjectType("Content") + request.setContext(getContextMap()) + + request.put("code", "test") + request.put("name", "testResource") + request.put("mimeType", "application/pdf") + request.put("contentType", "Resource") + request.put("description", "test") + request.put("channel", "in.ekstep") + request.put("primaryCategory", "Learning Resource") + val future: Future[Node] = DataNode.create(request) + future map {node => {assert(null != node) + print(node) + assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("testResource")) + val req = new Request(request) + req.getContext.put("identifier", node.getIdentifier) + req.put("name", "updated name") + //TODO: Uncomment this line when schema_restrict_api is true +// req.put("test", "test") +// assertThrows[ClientException](DataNode.update(req)) +// recoverToSucceededIf[ClientException](DataNode.update(req)) + //TODO: Remove this when schema_restrict_api is true + DataNode.update(req).map(node => + assert(node != null) + )} + } flatMap(f => f) + } + + "update content with map external props" should "update node" in { + val request = new Request() + request.setObjectType("ObjectCategoryDefinition") + request.setContext( new util.HashMap[String, AnyRef](){{ + put("graph_id", "domain") + put("version" , "1.0") + put("objectType" , "ObjectCategoryDefinition") + put("schemaName", "objectcategorydefinition") + }}) + request.put("name", "OK") + request.put("categoryId", "obj-cat:ok") + request.put("targetObjectType", "Content") + request.put("objectMetadata", new util.HashMap[String, AnyRef]() {{ + put("config", new util.HashMap[String, AnyRef](){{ + put("key", "value") + }}) + }}) + + val future: Future[Node] = DataNode.create(request) + future map {node => {assert(null != node) + print(node) + assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("OK")) + val req = new Request(request) + req.getContext.put("identifier", node.getIdentifier) + req.put("objectMetadata", new util.HashMap[String, AnyRef]() {{ + put("schema", new util.HashMap[String, AnyRef](){{ + put("key", "value") + }}) + }}) + val updateFuture = DataNode.update(req) + updateFuture map { node => { + assert(node.getExternalData.get("objectMetadata") != null) + } + } + } + } flatMap(f => f) + } + + "systemUpdate content with valid data" should "update node metadata" in { + val request = new Request() + request.setObjectType("question") + val context = new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Question") + put("schemaName", "question") + } + } + request.setContext(context) + request.put("code", "finemanfine") + request.put("showFeedback", "Yes") + request.put("showSolutions", "Yes") + request.put("mimeType", "application/vnd.sunbird.question") + request.put("primaryCategory", "Practice Question") + request.put("name", "Test Question") + request.put("visibility", "Default") + request.put("description", "hey") + + val future: Future[Node] = DataNode.create(request) + future map { node => { + assert(null != node) + print(node) + assert(node.getMetadata.get("name").asInstanceOf[String].equalsIgnoreCase("Test Question")) + val req = new Request(request) + req.getContext.put("identifier", node.getIdentifier) + req.put("name", "updated name") + req.put("description", "Updated Description") + val updateFuture = DataNode.systemUpdate(req, util.Arrays.asList(node), "", Option(getHierarchy)) + updateFuture map { resNode => { + assert(resNode.getIdentifier.equals(node.getIdentifier)) + assert(resNode.getMetadata.get("description").equals("Updated Description")) + } + } + } + } flatMap (f => f) + } + + "search" should "read data for all identifier" in { + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'do_12345',IL_FUNC_OBJECT_TYPE:'Content',status:'Live',ownershipType:[\"createdBy\"],copyright:\"Sunbird\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\"});") + val request = new Request() + request.setObjectType("Content") + request.setContext(getContextMap()) + request.put("identifiers", util.Arrays.asList("do_12345")) + request.put("identifier", util.Arrays.asList("do_12345")) + request.put("fields", util.Arrays.asList()) + val future: Future[List[Node]] = DataNode.search(request) + future map { nodeList => { + assert(nodeList.length == 1) + assert(nodeList.head.getIdentifier.equalsIgnoreCase("do_12345")) + } + } flatMap (f => f) + } + + "search" should "throw Exception for invalid identifier" in { + executeNeo4jQuery("CREATE (n:domain{IL_UNIQUE_ID:'do_62146325',IL_FUNC_OBJECT_TYPE:'Content',status:'Live',ownershipType:[\"createdBy\"],copyright:\"Sunbird\",previewUrl:\"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/ecml/do_1129067102240194561252-latest\"});") + val request = new Request() + request.setObjectType("Content") + request.setContext(getContextMap()) + request.put("identifiers", util.Arrays.asList("do_62146325", "do_123579")) + request.put("identifier", util.Arrays.asList("do_62146325", "do_123579")) + request.put("fields", util.Arrays.asList()) + recoverToSucceededIf[ClientException](DataNode.search(request)) + } + + def getHierarchy(request: Request) : Future[Response] = { + val hierarchyString: String = "'{\"identifier\": \"do_11283193441064550414\"}'" + val rootHierarchy = JsonUtils.deserialize(hierarchyString, classOf[java.util.Map[String, AnyRef]]) + Future(ResponseHandler.OK.put("questionSet", rootHierarchy)) + } + + def dataModifier(node: Node): Node = { + if(node.getMetadata.containsKey("trackable") && + node.getMetadata.getOrDefault("trackable", new java.util.HashMap[String, AnyRef]).asInstanceOf[java.util.Map[String, AnyRef]].containsKey("enabled") && + "Yes".equalsIgnoreCase(node.getMetadata.getOrDefault("trackable", new java.util.HashMap[String, AnyRef]).asInstanceOf[java.util.Map[String, AnyRef]].getOrDefault("enabled", "").asInstanceOf[String])) { + node.getMetadata.put("contentType", "Course") + } + node + } } diff --git a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/schema/TestObjectCategoryDefinitionMap.scala b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/schema/TestObjectCategoryDefinitionMap.scala new file mode 100644 index 000000000..e73483caa --- /dev/null +++ b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/schema/TestObjectCategoryDefinitionMap.scala @@ -0,0 +1,22 @@ +package org.sunbird.graph.schema + +import org.sunbird.graph.BaseSpec + +class TestObjectCategoryDefinitionMap extends BaseSpec { + + "CategoryDefinitionMap" should "store cache for given id and value" in { + ObjectCategoryDefinitionMap.put("test-definition", Map("schema" -> Map(), "config" -> Map())) + ObjectCategoryDefinitionMap.cache.occupancy shouldBe(1) + } + + it should "store cache with default ttl 10 sec" in { + val tempKey = "test-definition" + val tempValue = Map("schema" -> Map(), "config" -> Map()) + ObjectCategoryDefinitionMap.put(tempKey, tempValue) + ObjectCategoryDefinitionMap.cache.occupancy shouldBe(1) + ObjectCategoryDefinitionMap.get(tempKey) shouldBe(tempValue) + Thread.sleep(10000) + ObjectCategoryDefinitionMap.get(tempKey) shouldBe(null) + } + +} diff --git a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/schema/validator/TestSchemaValidator.scala b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/schema/validator/TestSchemaValidator.scala new file mode 100644 index 000000000..d63a3f395 --- /dev/null +++ b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/schema/validator/TestSchemaValidator.scala @@ -0,0 +1,51 @@ +package org.sunbird.graph.schema.validator + +import java.util + +import org.sunbird.graph.BaseSpec +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.schema.DefinitionFactory + +import scala.concurrent.Future + +class TestSchemaValidator extends BaseSpec { + + /*"check health api" should "return true" in { + val future: Future[Response] = HealthCheckManager.checkAllSystemHealth() + future map { response => { + assert(ResponseCode.OK == response.getResponseCode) + assert(response.get("healthy") == true) + } + } + }*/ + + "check schemaValidate api" should "return true" in { + val definition = DefinitionFactory.getDefinition("domain", "collection", "1.0") + + val a = new util.ArrayList[AnyRef](){{ add(new util.HashMap[String, AnyRef](){{ + put("name","abc") + }}) }} + + val metaData = new util.HashMap[String, AnyRef](){{ + put("name","abc") + put("code", "code") + put("contentType", "TextBook") + put("mimeType", "application/vnd.ekstep.content-collection") + put("channel", "in.ekstep") + put("contentCredits", a) + put("primaryCategory", "Learning Resource") + put("boardIds", Array("ncf_board_cbse")) + put("mediumIds", Array("ncf_medium_english")) + put("subjectIds", Array("ncf_subject_cbse")) + put("gradeLevelIds", Array("ncf_gradelevel_grade1")) + }} + + + val node: Node = new Node("abc", "DATA_NODE", "Content"); + node.setGraphId("domain") + node.setMetadata(metaData) + + val future: Future[Node] = definition.validate(node, "create") + future map { node => assert(null != node) } + } +} diff --git a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/utils/NodeUtilTest.scala b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/utils/NodeUtilTest.scala index 2729fd9c8..7a5fb5241 100644 --- a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/utils/NodeUtilTest.scala +++ b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/utils/NodeUtilTest.scala @@ -1,27 +1,71 @@ package org.sunbird.graph.utils import java.util + import org.scalatest.{FlatSpec, Matchers} -import org.sunbird.common.JsonUtils +import org.sunbird.graph.OntologyEngineContext import org.sunbird.graph.dac.model.Node +import scala.concurrent.ExecutionContext + class NodeUtilTest extends FlatSpec with Matchers { + implicit val oec: OntologyEngineContext = new OntologyEngineContext + implicit val ec: ExecutionContext = ExecutionContext.global + + "getLanguageCodes with node having language with string type" should "return language code successfully" in { + val node:Node = new Node("domain","DATA_NODE","Content"); + node.setMetadata(new util.HashMap[String, AnyRef](){{ + put("language","English") + }}) + val result:util.List[String] = NodeUtil.getLanguageCodes(node) + result.contains("en") shouldBe true + } + + "getLanguageCodes with node having language with array type" should "return language code successfully" in { + val node:Node = new Node("domain","DATA_NODE","Content"); + node.setMetadata(new util.HashMap[String, AnyRef](){{ + put("language", Array("English","Hindi")) + }}) + val result:util.List[String] = NodeUtil.getLanguageCodes(node) + result.contains("en") shouldBe true + result.contains("hi") shouldBe true + } + + "getLanguageCodes with node having language with list type" should "return language code successfully" in { + val node:Node = new Node("domain","DATA_NODE","Content"); + node.setMetadata(new util.HashMap[String, AnyRef](){{ + put("language", new util.ArrayList[String](){{ + add("English") + add("Hindi") + }}) + }}) + val result:util.List[String] = NodeUtil.getLanguageCodes(node) + result.contains("en") shouldBe true + result.contains("hi") shouldBe true + } + + "getLanguageCodes with node not having language attribute" should "return empty language code successfully" in { + val node:Node = new Node("domain","DATA_NODE","Content"); + node.setMetadata(new util.HashMap[String, AnyRef]()) + val result:util.List[String] = NodeUtil.getLanguageCodes(node) + result.isEmpty shouldBe true + } "serialize with fields" should "return only node property which are present in fields" in { val node: Node = new Node("do_1234", "DATA_NODE", "Content"); node.setMetadata(new util.HashMap[String, AnyRef]() {{ - put("language", new util.ArrayList[String]() {{ - add("English") + put("language", new util.ArrayList[String]() {{ + add("English") + }}) + put("contentType", "Resource") + put("name", "Test Resource Content") + put("code", "test.res.1") }}) - put("contentType", "Resource") - put("name", "Test Resource Content") - put("code", "test.res.1") - }}) val fields: util.ArrayList[String] = new util.ArrayList[String]() {{ - add("contentType") - add("name") - }} - val result: util.Map[String, AnyRef] = NodeUtil.serialize(node, fields, "content") + add("contentType") + add("name") + }} + val result: util.Map[String, AnyRef] = NodeUtil.serialize(node, fields, "content", "1.0") result.isEmpty shouldBe false result.size() shouldBe 4 result.containsKey("identifier") shouldBe true @@ -29,128 +73,4 @@ class NodeUtilTest extends FlatSpec with Matchers { result.containsKey("name") shouldBe true result.containsKey("languageCode") shouldBe true } - - "deserialize with valid data" should "return a node with all data" in { - val nodeString :String = """{ - | "ownershipType": [ - | "createdBy" - | ], - | "code": "test.res.1", - | "channel": "test", - | "language": [ - | "English" - | ], - | "mimeType": "application/pdf", - | "idealScreenSize": "normal", - | "createdOn": "2020-01-17T16:17:39.931+0530", - | "objectType": "Content", - | "collections": [ - | { - | "identifier": "LP_FT-74320", - | "name": "LP_FT-74320", - | "description": "test desc", - | "objectType": "Content", - | "relation": "hasSequenceMember", - | "status": "Live" - | } - | ], - | "contentDisposition": "inline", - | "lastUpdatedOn": "2020-01-17T16:17:39.931+0530", - | "contentEncoding": "identity", - | "contentType": "Resource", - | "dialcodeRequired": "No", - | "identifier": "do_11293728197296947212", - | "lastStatusChangedOn": "2020-01-17T16:17:39.931+0530", - | "audience": [ - | "Learner" - | ], - | "os": [ - | "All" - | ], - | "visibility": "Default", - | "resources": [ - | "Speaker", - | "Touch" - | ], - | "mediaType": "content", - | "osId": "org.ekstep.quiz.app", - | "languageCode": [ - | "en" - | ], - | "version": 2, - | "versionKey": "1579258059931", - | "license": "CC BY 4.0", - | "idealScreenDensity": "hdpi", - | "framework": "NCF", - | "concepts": [ - | { - | "identifier": "LO1", - | "name": "Word Meaning", - | "description": "Understanding meaning of words", - | "objectType": "Concept", - | "relation": "associatedTo", - | "status": "Live" - | } - | ], - | "compatibilityLevel": 1, - | "name": "Resource Content 1", - | "status": "Live" - |}""".stripMargin - val nodeMap: util.Map[String, AnyRef] = JsonUtils.deserialize(nodeString.asInstanceOf[String], classOf[java.util.Map[String, AnyRef]]) - val relString = """{ - | "concepts": { - | "objects": [ - | "Concept" - | ], - | "type": "associatedTo", - | "direction": "out" - | }, - | "children": { - | "objects": [ - | "Content", - | "ContentImage" - | ], - | "type": "hasSequenceMember", - | "direction": "out" - | }, - | "collections": { - | "objects": [ - | "Content", - | "ContentImage" - | ], - | "type": "hasSequenceMember", - | "direction": "in" - | }, - | "usesContent": { - | "objects": [ - | "Content" - | ], - | "type": "associatedTo", - | "direction": "out" - | }, - | "questions": { - | "objects": [ - | "AssessmentItem" - | ], - | "type": "associatedTo", - | "direction": "out" - | }, - | "usedByContent": { - | "objects": [ - | "Content" - | ], - | "type": "associatedTo", - | "direction": "in" - | } - |}""".stripMargin - val relationMap: util.Map[String, AnyRef] = JsonUtils.deserialize(relString.asInstanceOf[String], classOf[java.util.Map[String, AnyRef]]) - val node = NodeUtil.deserialize(nodeMap, "content", relationMap) - node.getIdentifier shouldBe "do_11293728197296947212" - node.getOutRelations.size() shouldEqual 1 - node.getOutRelations().get(0).getRelationType shouldEqual "associatedTo" - node.getOutRelations.get(0).getEndNodeId shouldEqual "LO1" - node.getInRelations.size() shouldEqual 1 - node.getInRelations().get(0).getRelationType shouldEqual "hasSequenceMember" - node.getInRelations.get(0).getStartNodeId shouldEqual "LP_FT-74320" - } } diff --git a/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/utils/ScalaJsonUtilsTest.scala b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/utils/ScalaJsonUtilsTest.scala new file mode 100644 index 000000000..8cc1ce192 --- /dev/null +++ b/ontology-engine/graph-engine_2.11/src/test/scala/org/sunbird/graph/utils/ScalaJsonUtilsTest.scala @@ -0,0 +1,52 @@ +package org.sunbird.graph.utils + +import java.util + +import com.fasterxml.jackson.databind.exc.{InvalidDefinitionException, MismatchedInputException} +import org.apache.commons.lang3.StringUtils +import org.codehaus.jackson.JsonProcessingException +import org.scalatest.{FlatSpec, Matchers} + +class ScalaJsonUtilsTest extends FlatSpec with Matchers { + + "serializing an empty object" should "Throw InvalidDefinitionException" in { + assertThrows[InvalidDefinitionException] { // Result type: Assertion + ScalaJsonUtils.serialize(new Object) + } + } + + "serializing an empty object" should "Throw JsonProcessingException" ignore { + assertThrows[JsonProcessingException] { // Result type: Assertion + ScalaJsonUtils.serialize(new util.HashMap()) + } + } + + "serializing a valid Map object" should "Should serialize the object" in { + val value: String = ScalaJsonUtils.serialize(Map("identifier" -> "do_1234", "status" -> "Draft")) + assert(StringUtils.equalsIgnoreCase(value, "{\"identifier\":\"do_1234\",\"status\":\"Draft\"}")) + } + + "serializing a valid List object" should "Should serialize the object" in { + val value: String = ScalaJsonUtils.serialize(List("identifier", "do_1234", "status", "Draft")) + assert(StringUtils.equalsIgnoreCase(value, "[\"identifier\",\"do_1234\",\"status\",\"Draft\"]")) + } + + "deserializing a stringified map" should "Should deserialize the string to map" in { + val value: Map[String, AnyRef] = ScalaJsonUtils.deserialize[Map[String, AnyRef]]("{\"identifier\":\"do_1234\",\"status\":\"Draft\"}") + assert(value != null) + assert(value.getOrElse("status", "").asInstanceOf[String] == "Draft") + } + + "deserializing a stringified list to map" should "Should throw Exception" in { + assertThrows[MismatchedInputException] { + ScalaJsonUtils.deserialize[Map[String, AnyRef]]("[\"identifier\",\"do_1234\",\"status\",\"Draft\"]") + } + } + + "deserializing a stringified list" should "Should deserialize the string to list" in { + val value:List[String] = ScalaJsonUtils.deserialize[List[String]]("[\"identifier\",\"do_1234\",\"status\",\"Draft\"]") + assert(value != null) + assert(value.size == 4) + } + +} diff --git a/ontology-engine/parseq/pom.xml b/ontology-engine/parseq/pom.xml index e6dddf9cb..781dd655a 100644 --- a/ontology-engine/parseq/pom.xml +++ b/ontology-engine/parseq/pom.xml @@ -24,22 +24,28 @@ src/test/scala - net.alchim31.maven scala-maven-plugin - 3.2.2 + 4.4.0 + + ${scala.version} + false + + scala-compile-first + process-resources + add-source compile + + + + scala-test-compile + process-test-resources + testCompile - - - -dependencyfile - ${project.build.directory}/.scala_dependencies - - diff --git a/ontology-engine/pom.xml b/ontology-engine/pom.xml index d5ae3d352..b44cb78ef 100644 --- a/ontology-engine/pom.xml +++ b/ontology-engine/pom.xml @@ -1,7 +1,5 @@ - - + + knowledge-platform org.sunbird @@ -15,16 +13,17 @@ graph-common graph-dac-api - graph-core + graph-core_2.11 graph-engine_2.11 parseq - + + maven-assembly-plugin - 2.3 + 3.3.0 src/assembly/bin.xml @@ -44,4 +43,4 @@ - \ No newline at end of file + diff --git a/platform-core/actor-core/pom.xml b/platform-core/actor-core/pom.xml index 3c892f26e..1eee0fb0f 100644 --- a/platform-core/actor-core/pom.xml +++ b/platform-core/actor-core/pom.xml @@ -43,16 +43,15 @@ org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + 3.8.1 - 1.8 - 1.8 + 11 org.apache.maven.plugins maven-surefire-plugin - 2.20 + 3.0.0-M4 **/*Spec.java diff --git a/platform-core/actor-core/src/main/java/org/sunbird/actor/core/BaseActor.java b/platform-core/actor-core/src/main/java/org/sunbird/actor/core/BaseActor.java index 9001308c7..0f76ddd1c 100644 --- a/platform-core/actor-core/src/main/java/org/sunbird/actor/core/BaseActor.java +++ b/platform-core/actor-core/src/main/java/org/sunbird/actor/core/BaseActor.java @@ -3,6 +3,7 @@ import akka.actor.AbstractActor; import akka.actor.ActorRef; import akka.dispatch.Futures; +import akka.dispatch.Recover; import akka.pattern.Patterns; import org.sunbird.common.dto.Request; import org.sunbird.common.dto.Response; @@ -12,15 +13,25 @@ import org.sunbird.common.exception.ResourceNotFoundException; import org.sunbird.common.exception.ResponseCode; import org.sunbird.common.exception.ServerException; +import scala.Function1; import scala.concurrent.Future; +import java.util.Arrays; +import java.util.List; + public abstract class BaseActor extends AbstractActor { + public List preSignedObjTypes = Arrays.asList("assets", "artifact", "hierarchy"); public abstract Future onReceive(Request request) throws Throwable; private Future internalOnReceive(Request request) { try { - return onReceive(request); + return onReceive(request).recoverWith(new Recover>() { + @Override + public Future recover(Throwable failure) { + return ERROR(request.getOperation(), failure); + } + }, getContext().dispatcher()); } catch (Throwable e) { return ERROR(request.getOperation(), e); } diff --git a/platform-core/cassandra-connector/pom.xml b/platform-core/cassandra-connector/pom.xml index 2e2d2235f..fd72ae8d9 100644 --- a/platform-core/cassandra-connector/pom.xml +++ b/platform-core/cassandra-connector/pom.xml @@ -47,7 +47,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.0.2 + 3.2.0 diff --git a/learning-api/hierarchy-manager/pom.xml b/platform-core/kafka-client/pom.xml similarity index 68% rename from learning-api/hierarchy-manager/pom.xml rename to platform-core/kafka-client/pom.xml index 7e79f5113..3bb49cd4b 100644 --- a/learning-api/hierarchy-manager/pom.xml +++ b/platform-core/kafka-client/pom.xml @@ -3,79 +3,79 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - learning-api + platform-core org.sunbird 1.0-SNAPSHOT 4.0.0 + kafka-client - hierarchy-manager + + 1.1.0 + + + org.scala-lang + scala-library + ${scala.version} + org.sunbird - graph-engine_2.11 + platform-common 1.0-SNAPSHOT - jar org.sunbird - platform-common + platform-telemetry 1.0-SNAPSHOT - org.scala-lang - scala-library - ${scala.version} + org.apache.kafka + kafka-clients + ${kafka.client.version} org.scalatest - scalatest_2.11 + scalatest_${scala.maj.version} 3.0.8 test - org.neo4j - neo4j-bolt - 3.3.4 - test - - - org.neo4j - neo4j-graphdb-api - 3.3.4 - test - - - org.cassandraunit - cassandra-unit - 3.11.2.0 + net.manub + scalatest-embedded-kafka_${scala.maj.version} + 1.1.0-kafka1.1-nosr test - src/main/scala src/test/scala - net.alchim31.maven scala-maven-plugin - 3.2.2 + 4.4.0 + + ${scala.version} + false + + scala-compile-first + process-resources + add-source compile + + + + scala-test-compile + process-test-resources + testCompile - - - -dependencyfile - ${project.build.directory}/.scala_dependencies - - diff --git a/platform-core/kafka-client/src/main/scala/org/sunbird/kafka/client/KafkaClient.scala b/platform-core/kafka-client/src/main/scala/org/sunbird/kafka/client/KafkaClient.scala new file mode 100644 index 000000000..60f659c57 --- /dev/null +++ b/platform-core/kafka-client/src/main/scala/org/sunbird/kafka/client/KafkaClient.scala @@ -0,0 +1,59 @@ +package org.sunbird.kafka.client + +import java.util.Properties +import org.apache.kafka.clients.consumer.{Consumer, ConsumerConfig, KafkaConsumer} +import org.apache.kafka.clients.producer.{KafkaProducer, Producer, ProducerConfig, ProducerRecord} +import org.apache.kafka.common.serialization.{LongDeserializer, LongSerializer, StringDeserializer, StringSerializer} +import org.sunbird.common.Platform +import org.sunbird.common.exception.ClientException +import org.sunbird.telemetry.logger.TelemetryManager + + +class KafkaClient { + + private val BOOTSTRAP_SERVERS = Platform.getString("kafka.urls","localhost:9092") + private val producer = createProducer() + private val consumer = createConsumer() + + protected def getProducer: Producer[Long, String] = producer + protected def getConsumer: Consumer[Long, String] = consumer + + @throws[Exception] + def send(event: String, topic: String): Unit = { + if (!Platform.getBoolean("kafka.topic.send.enable",true)) return + if (validate(topic)) + getProducer.send(new ProducerRecord[Long, String](topic, event)) + else { + TelemetryManager.error("Topic with name: " + topic + ", does not exists.") + throw new ClientException("TOPIC_NOT_FOUND_EXCEPTION", "Topic with name: " + topic + ", does not exists.") + } + } + + @throws[Exception] + def validate(topic: String): Boolean = { + val topics = getConsumer.listTopics + topics.keySet.contains(topic) + } + + private def createProducer(): KafkaProducer[Long, String] = { + new KafkaProducer[Long, String](new Properties() { + { + put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS) + put(ProducerConfig.CLIENT_ID_CONFIG, "KafkaClientProducer") + put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[LongSerializer].getName) + put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer].getName) + } + }) + } + + private def createConsumer(): KafkaConsumer[Long, String] = { + new KafkaConsumer[Long, String](new Properties() { + { + put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS) + put(ConsumerConfig.CLIENT_ID_CONFIG, "KafkaClientConsumer") + put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[LongDeserializer].getName) + put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer].getName) + } + }) + } +} diff --git a/platform-core/kafka-client/src/test/scala/org/sunbird/kafka/client/KafkaClientTest.scala b/platform-core/kafka-client/src/test/scala/org/sunbird/kafka/client/KafkaClientTest.scala new file mode 100644 index 000000000..1a496fd74 --- /dev/null +++ b/platform-core/kafka-client/src/test/scala/org/sunbird/kafka/client/KafkaClientTest.scala @@ -0,0 +1,40 @@ +package org.sunbird.kafka.client + +import org.sunbird.common.exception.ClientException +import org.sunbird.kafka.test.BaseTest + +class KafkaClientTest extends BaseTest { + + "validate with valid topic name" should "return true" in { + createTopic("test.topic1") + val client = new KafkaClient + val result = client.validate("test.topic1") + assert(result) + } + + "validate with invalid topic name" should "return false" in { + val client = new KafkaClient + val result = client.validate("test.topic2") + assert(!result) + } + + "send with valid topic name" should "send the message successfully to the topic" in { + val event = "{\"eid\":\"BE_JOB_REQUEST\",\"ets\":1546931576000,\"mid\":\"LP.1546931576000.b3fb188d-d6fe-431e-b528-da3780c710a8\",\"actor\":{\"id\":\"learning-service\",\"type\":\"System\"},\"context\":{\"pdata\":{\"ver\":\"1.0\",\"id\":\"org.ekstep.platform\"},\"channel\":\"in.ekstep\",\"env\":\"dev\"},\"object\":{\"ver\":1.0,\"id\":\"do_1234\"},\"edata\":{\"action\":\"link_dialcode\",\"iteration\":1,\"graphId\":\"domain\",\"contentType\":\"Course\",\"objectType\":\"Content\"}}" + val topic = "test.topic3" + createTopic(topic) + val client = new KafkaClient + client.send(event, topic) + consumeFirstStringMessageFrom(topic) shouldBe event + } + + "send with invalid topic name" should "throw client exception" in { + val event = "{\"eid\":\"BE_JOB_REQUEST\",\"ets\":1546931576000,\"mid\":\"LP.1546931576000.b3fb188d-d6fe-431e-b528-da3780c710a8\",\"actor\":{\"id\":\"learning-service\",\"type\":\"System\"},\"context\":{\"pdata\":{\"ver\":\"1.0\",\"id\":\"org.ekstep.platform\"},\"channel\":\"in.ekstep\",\"env\":\"dev\"},\"object\":{\"ver\":1.0,\"id\":\"do_1234\"},\"edata\":{\"action\":\"link_dialcode\",\"iteration\":1,\"graphId\":\"domain\",\"contentType\":\"Course\",\"objectType\":\"Content\"}}" + val topic = "test.topic4" + val client = new KafkaClient + val exception = intercept[ClientException] { + client.send(event, topic) + } + exception.getMessage shouldEqual "Topic with name: " + topic + ", does not exists." + } + +} diff --git a/platform-core/kafka-client/src/test/scala/org/sunbird/kafka/test/BaseTest.scala b/platform-core/kafka-client/src/test/scala/org/sunbird/kafka/test/BaseTest.scala new file mode 100644 index 000000000..134dfa88d --- /dev/null +++ b/platform-core/kafka-client/src/test/scala/org/sunbird/kafka/test/BaseTest.scala @@ -0,0 +1,31 @@ +package org.sunbird.kafka.test + +import net.manub.embeddedkafka.{EmbeddedKafka, EmbeddedKafkaConfig} +import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} + +class BaseTest extends FlatSpec with Matchers with BeforeAndAfterAll with EmbeddedKafka { + + implicit val config = EmbeddedKafkaConfig(kafkaPort = 9092) + + override def beforeAll(): Unit = { + try { + EmbeddedKafka.start() + System.out.println("Embedded Kafka Started!") + } catch { + case e: Exception => e.printStackTrace() + } + } + + override def afterAll(): Unit = { + try { + EmbeddedKafka.stop() + System.out.println("Embedded Kafka Shutdown Successfully!") + } catch { + case e: Exception => e.printStackTrace() + } + } + + def createTopic(topicName: String): Unit = { + EmbeddedKafka.createCustomTopic(topicName) + } +} diff --git a/platform-core/platform-cache/pom.xml b/platform-core/platform-cache/pom.xml index b87b26225..4238e298c 100644 --- a/platform-core/platform-cache/pom.xml +++ b/platform-core/platform-cache/pom.xml @@ -15,6 +15,11 @@ + + org.scala-lang + scala-library + ${scala.version} + org.sunbird platform-common @@ -30,6 +35,68 @@ jedis ${jedis.version} + + org.scalatest + scalatest_${scala.maj.version} + 3.1.2 + test + + + src/main/scala + src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + ${scala.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + + test + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + \ No newline at end of file diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/common/CacheErrorCode.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/common/CacheErrorCode.java deleted file mode 100644 index ec15d2ca0..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/common/CacheErrorCode.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.sunbird.cache.common; - -/** - * This Enum Holds All Cache Error Codes. - * @author Kumar Gauraw - */ -public enum CacheErrorCode { - ERR_CACHE_CONNECTION_ERROR, - ERR_CACHE_SAVE_PROPERTY_ERROR, - ERR_CACHE_GET_PROPERTY_ERROR, - ERR_CACHE_DELETE_PROPERTY_ERROR, - ERR_CACHE_PUBLISH_CHANNEL_ERROR, - ERR_CACHE_SUBSCRIBE_CHANNEL_ERROR -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/common/CacheHandlerOperation.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/common/CacheHandlerOperation.java deleted file mode 100644 index 7f9656b2d..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/common/CacheHandlerOperation.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.sunbird.cache.common; - -/** - * This Enum Holds All Operation Code for Cache Handler - * @author Kumar Gauraw - */ -public enum CacheHandlerOperation { - SAVE_STRING, READ_STRING, SAVE_LIST, READ_LIST, DELETE_KEY, INCR_VAL, PUBLISH, SUBSCRIBE -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/connection/RedisConnector.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/connection/RedisConnector.java deleted file mode 100644 index f1f1dbab5..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/connection/RedisConnector.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.sunbird.cache.connection; - -import org.sunbird.cache.common.CacheErrorCode; -import org.sunbird.common.Platform; -import org.sunbird.common.exception.ServerException; -import org.sunbird.telemetry.logger.TelemetryManager; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; - -/** - * This Class Provides Methods for Managing Connection With Redis Cache. - * - * @author Kumar Gauraw - */ -public class RedisConnector { - - private static JedisPool jedisPool; - private static final String HOST = Platform.config.hasPath("redis.host") ? Platform.config.getString("redis.host") : "localhost"; - private static final int PORT = Platform.config.hasPath("redis.port") ? Platform.config.getInt("redis.port") : 6379; - private static final int MAX_CONNECTIONS = Platform.config.hasPath("redis.maxConnections") ? Platform.config.getInt("redis.maxConnections") : 128; - private static final int INDEX = Platform.config.hasPath("redis.dbIndex") ? Platform.config.getInt("redis.dbIndex") : 0; - - static { - JedisPoolConfig config = new JedisPoolConfig(); - config.setMaxTotal(MAX_CONNECTIONS); - config.setBlockWhenExhausted(true); - jedisPool = new JedisPool(config, HOST, PORT); - } - - /** - * This Method Returns a connection object from connection pool. - * - * @return Jedis Object - */ - public static Jedis getConnection() { - try { - Jedis jedis = jedisPool.getResource(); - if (INDEX > 0) - jedis.select(INDEX); - return jedis; - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Returning Redis Cache Connection Object to Pool.", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_CONNECTION_ERROR.name(), e.getMessage()); - } - } - - /** - * This Method takes a connection object and put it back to pool. - * - * @param jedis - */ - public static void returnConnection(Jedis jedis) { - try { - if (null != jedis) { - jedisPool.returnResource(jedis); - } - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Returning Redis Cache Connection Object to Pool.", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_CONNECTION_ERROR.name(), e.getMessage()); - } - } -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/handler/ICacheHandler.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/handler/ICacheHandler.java deleted file mode 100644 index 6b515701f..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/handler/ICacheHandler.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.sunbird.cache.handler; - -/** - * Contract for Cache Handler - * - * @author Kumar Gauraw - */ -public interface ICacheHandler { - public abstract Object execute(String operation, String cacheKey, String objectKey); -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/impl/CategoryCache.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/impl/CategoryCache.java deleted file mode 100644 index 335979a12..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/impl/CategoryCache.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.sunbird.cache.impl; - -import org.sunbird.cache.impl.handler.CategoryCacheHandler; -import org.sunbird.cache.mgr.RedisCacheManager; - -import java.util.List; - -public class CategoryCache extends RedisCacheManager { - - public CategoryCache() { - handler = new CategoryCacheHandler(); - } - - @Override - public String getKey(String... params) { - //TODO: Revert to commented return statement during handler implementation. - //return "cat_" + params[0].toLowerCase() + "_" + params[1].toLowerCase(); - return "cat_" + params[0] + params[1]; - } - - @Override - public String getString(String key) { - return null; - } - - @Override - public void setString(String key, String data, int ttl) { - - } - - @Override - public List getList(String key) { - return getListData(key, getObjectKey(key)); - } - - @Override - public void setList(String key, List list, int ttl) { - - } - - @Override - public void increment(String key) { - - } - - @Override - public void delete(String... key) { - - } - - private String getObjectKey(String cacheKey){ - return cacheKey.split("_")[1]; - } -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/impl/handler/CategoryCacheHandler.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/impl/handler/CategoryCacheHandler.java deleted file mode 100644 index 2f92f4e43..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/impl/handler/CategoryCacheHandler.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.sunbird.cache.impl.handler; - -import org.sunbird.cache.handler.ICacheHandler; - -import java.util.Arrays; - -public class CategoryCacheHandler implements ICacheHandler { - @Override - public Object execute(String operation, String cacheKey, String objectKey) { - //TODO: Get the Framework Hierarchy from Cassandra and load it to cache - //TODO: Filter out required category and return the data. - return Arrays.asList(); - } -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/ICacheManager.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/ICacheManager.java deleted file mode 100644 index 3a4247fc0..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/ICacheManager.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.sunbird.cache.mgr; - -import java.util.List; - -/** - * Contract for Cache Management - * - * @author Kumar Gauraw - */ -public interface ICacheManager { - - /** - * This method provides key generation implementation for cache. - * - * @param params - * @return String - */ - public String getKey(String... params); - - /** - * This method provides implementation for read operation with String value - * - * @param key - * @return String - */ - public String getString(String key); - - /** - * This method provides implementation for write/save operation with String value - * - * @param key - * @param data - * @param ttl - */ - public void setString(String key, String data, int ttl); - - /** - * This method provides implementation for read operation with List Value - * - * @param key - * @return List - */ - public List getList(String key); - - /** - * This method provides implementation for write/save operation with List Value - * - * @param key - * @param list - * @param ttl - */ - public void setList(String key, List list, int ttl); - - /** - * This method provides implementation for increment operation for value of given key - * - * @param key - */ - public void increment(String key); - - /** - * This method provides implementation for reset/delete operation for given key/keys - * - * @param key - */ - public void delete(String... key); - - /** - * This method provides implementation for read operation for given key - * - * @param key - * @return Object - */ - public Object getObject(String key); - - /** - * This method provides implementation for write/save operation for given key - * - * @param key - * @param data - */ - public void setObject(String key, Object data); - - /** - * This method provides implementation for publish message operation to Redis Channel. - * - * @param channel - * @param message - */ - public void publish(String channel, String message); - - /** - * This method provides implementation for subscribe operation to Redis Channel. - * - * @param channels - */ - public void subscribe(String... channels); - -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/LocalCacheManager.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/LocalCacheManager.java deleted file mode 100644 index ec467ada8..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/LocalCacheManager.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.sunbird.cache.mgr; - -import org.sunbird.cache.common.CacheErrorCode; -import org.sunbird.cache.handler.ICacheHandler; -import org.sunbird.cache.util.RedisCacheUtil; -import org.sunbird.common.exception.ServerException; -import org.sunbird.telemetry.logger.TelemetryManager; -import redis.clients.jedis.JedisPubSub; - -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public abstract class LocalCacheManager implements ICacheManager { - - ICacheHandler handler; - - protected void init(String... channels){ - subscribe(channels); - } - - @Override - public void publish(String channel, String message) { - RedisCacheUtil.publish(channel, message); - } - - @Override - public void subscribe(String... channels) { - JedisPubSub pubSub = new JedisPubSub() { - @Override - public void onMessage(String channel, String message) { - processSubscription(channel, message); - } - }; - - ExecutorService pool = null; - try { - pool = Executors.newFixedThreadPool(1); - pool.execute(new Runnable() { - public void run() { - RedisCacheUtil.subscribe(pubSub, channels); - } - }); - } catch (Exception e) { - TelemetryManager.error("Exception Occured While Subscribing to channels : " + channels + " | Exception is : " + e); - throw new ServerException(CacheErrorCode.ERR_CACHE_SUBSCRIBE_CHANNEL_ERROR.name(),e.getMessage()); - } finally { - if (null != pool) - pool.shutdown(); - } - } - - protected abstract void processSubscription(String channel, String message); - - - @Override - public String getString(String key) { - return null; - } - - @Override - public void setString(String key, String data, int ttl) { - - } - - @Override - public List getList(String key) { - return null; - } - - @Override - public void setList(String key, List list, int ttl) { - - } - - @Override - public void increment(String key) { - - } - -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/RedisCacheManager.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/RedisCacheManager.java deleted file mode 100644 index a5072de0f..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/mgr/RedisCacheManager.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.sunbird.cache.mgr; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.sunbird.cache.common.CacheHandlerOperation; -import org.sunbird.cache.handler.ICacheHandler; -import org.sunbird.cache.util.RedisCacheUtil; -import org.sunbird.common.exception.ResourceNotFoundException; -import org.sunbird.telemetry.logger.TelemetryManager; - -import java.util.Arrays; -import java.util.List; - - -/** - * Base Class for Redis Based Cache Implementation - * - * @author Kumar Gauraw - */ -public abstract class RedisCacheManager implements ICacheManager { - - protected ICacheHandler handler; - - //this method can be called directly from final implementation class getString(String key) - protected String getStringData(String cacheKey, String objectKey) { - try { - String data = RedisCacheUtil.getString(cacheKey); - if (StringUtils.isBlank(data) && null != handler) { - data = (String) handler.execute(CacheHandlerOperation.READ_STRING.name(), cacheKey, objectKey); - } - return data; - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Fetching Data For Key : " + cacheKey + " | Exception is : ", e); - if (e instanceof ResourceNotFoundException) - throw e; - } - return null; - } - - protected List getListData(String cacheKey, String objectKey) { - try { - List data = RedisCacheUtil.getList(cacheKey); - if (CollectionUtils.isEmpty(data) && null != handler) { - data = (List) handler.execute(CacheHandlerOperation.READ_LIST.name(), cacheKey, objectKey); - } - return data; - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Fetching Data For Key : " + cacheKey + " | Exception is : ", e); - if (e instanceof ResourceNotFoundException) - throw e; - } - return Arrays.asList(); - } - - @Override - public Object getObject(String key) { - return null; - } - - @Override - public void setObject(String key, Object data) { - - } - - @Override - public void publish(String channel, String message) { - - } - - @Override - public void subscribe(String... channels) { - - } -} diff --git a/platform-core/platform-cache/src/main/java/org/sunbird/cache/util/RedisCacheUtil.java b/platform-core/platform-cache/src/main/java/org/sunbird/cache/util/RedisCacheUtil.java deleted file mode 100644 index eca747dd0..000000000 --- a/platform-core/platform-cache/src/main/java/org/sunbird/cache/util/RedisCacheUtil.java +++ /dev/null @@ -1,245 +0,0 @@ -package org.sunbird.cache.util; - -import org.apache.commons.lang3.StringUtils; -import org.sunbird.cache.common.CacheErrorCode; -import org.sunbird.common.exception.ServerException; -import org.sunbird.telemetry.logger.TelemetryManager; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPubSub; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Set; - -import static org.sunbird.cache.connection.RedisConnector.getConnection; -import static org.sunbird.cache.connection.RedisConnector.returnConnection; - -/** - * This Util Class Provide All CRUD Methods for Redis Cache. - * - * @author Kumar Gauraw - */ -public class RedisCacheUtil { - - /** - * This method store string data into cache for given Key - * - * @param key - * @param value - * @param ttl - */ - public static void saveString(String key, String value, int ttl) { - Jedis jedis = getConnection(); - try { - jedis.del(key); - jedis.set(key, value); - if (ttl > 0) - jedis.expire(key, ttl); - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Saving String Data to Redis Cache for Key : " + key + "| Exception is:", e); - //TODO: Suppress this exception. - throw new ServerException(CacheErrorCode.ERR_CACHE_SAVE_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - - /** - * This method read string data from cache for a given key - * - * @param key - * @return String - */ - public static String getString(String key) { - Jedis jedis = getConnection(); - try { - return jedis.get(key); - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Fetching String Data from Redis Cache for Key : " + key + "| Exception is:", e); - //TODO: Suppress this exception. - throw new ServerException(CacheErrorCode.ERR_CACHE_GET_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - - // TODO: always considering object as string. need to change this. - - /** - * This method store/save list data into cache for given Key - * @param key - * @param values - */ - public static void saveList(String key, List values) { - Jedis jedis = getConnection(); - try { - jedis.del(key); - for (String val : values) { - jedis.sadd(key, val); - } - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Saving List Data to Redis Cache for Key : " + key + "| Exception is:", e); - //TODO: Suppress this exception. - throw new ServerException(CacheErrorCode.ERR_CACHE_SAVE_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - - /** - * This method read list data from cache for a given key - * - * @param key - * @return List - */ - public static List getList(String key) { - Jedis jedis = getConnection(); - try { - Set set = jedis.smembers(key); - List list = new ArrayList<>(set); - return list; - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Fetching List Data from Redis Cache for Key : " + key + "| Exception is:", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_GET_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - - /** - * This method delete data from cache for given key/keys - * - * @param keys - */ - public static void delete(String... keys) { - Jedis jedis = getConnection(); - try { - jedis.del(keys); - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Deleting Records From Redis Cache for Identifiers : " + Arrays.asList(keys) + " | Exception is : ", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_DELETE_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - - /** - * This method delete data from cache for all key/keys matched with given pattern - * - * @param pattern - */ - public static void deleteByPattern(String pattern) { - if (StringUtils.isNotBlank(pattern) && !StringUtils.equalsIgnoreCase(pattern, "*")) { - Jedis jedis = getConnection(); - try { - Set keys = jedis.keys(pattern); - if (keys != null && keys.size() > 0) { - List keyList = new ArrayList<>(keys); - jedis.del(keyList.toArray(new String[keyList.size()])); - } - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Deleting Records From Redis Cache for Pattern : " + pattern + " | Exception is : ", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_DELETE_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - } - - /** - * This method increment the value by 1 into cache for given key and returns the new value - * - * @param key - * @return Double - */ - public static Double getIncVal(String key) { - Jedis jedis = getConnection(); - try { - double inc = 1.0; - double value = jedis.incrByFloat(key, inc); - return value; - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Incrementing Value for Key : " + key + " | Exception is : ", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_GET_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - - /** - * This method push given string message to given channel, which can be consumed by all subscribers of that channel. - * - * @param channel - * @param data - */ - public static void publish(String channel, String data) { - if (StringUtils.isNotBlank(channel) && StringUtils.isNotBlank(data)) { - Jedis jedis = getConnection(); - try { - jedis.publish(channel, data); - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Publishing Message to Redis Channel : " + channel + " for data : " + data + " | Exception is : ", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_PUBLISH_CHANNEL_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - } - - /** - * This method subscribe to given channel/channels to receive messages. - * - * @param pubSub - * @param channel - */ - public static void subscribe(JedisPubSub pubSub, String... channel) { - if (null != channel && null != pubSub) { - Jedis jedis = getConnection(); - try { - jedis.subscribe(pubSub, channel); - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Subscribing to Redis Channel : " + channel + " | Exception is : ", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_SUBSCRIBE_CHANNEL_ERROR.name(), e.getMessage()); - } - } - } - - /** - * This Method Save Given Data To Existing List For Given Key - * @param key - * @param values - */ - public static void saveToList(String key, List values) { - Jedis jedis = getConnection(); - try { - for (String val : values) { - jedis.sadd(key, val); - } - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Saving Partial List Data to Redis Cache for Key : " + key + "| Exception is:", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_SAVE_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } - - /** - * This Method Remove Given Data From Existing List For Given Key - * @param key - * @param values - */ - public static void deleteFromList(String key, List values) { - Jedis jedis = getConnection(); - try { - for (String val : values) { - jedis.srem(key, val); - } - } catch (Exception e) { - TelemetryManager.error("Exception Occurred While Deleting Partial Data From Redis Cache for Key : " + key + "| Exception is:", e); - throw new ServerException(CacheErrorCode.ERR_CACHE_DELETE_PROPERTY_ERROR.name(), e.getMessage()); - } finally { - returnConnection(jedis); - } - } -} diff --git a/platform-core/platform-cache/src/main/scala/org/sunbird/cache/impl/RedisCache.scala b/platform-core/platform-cache/src/main/scala/org/sunbird/cache/impl/RedisCache.scala new file mode 100644 index 000000000..7b739952d --- /dev/null +++ b/platform-core/platform-cache/src/main/scala/org/sunbird/cache/impl/RedisCache.scala @@ -0,0 +1,254 @@ +package org.sunbird.cache.impl + +import org.apache.commons.lang3.StringUtils +import org.slf4j.{Logger, LoggerFactory} +import org.sunbird.cache.util.RedisConnector + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +/** + * This Utility Object Provide Methods To Perform CRUD Operation With Redis + */ +object RedisCache extends RedisConnector { + + private val logger: Logger = LoggerFactory.getLogger(RedisCache.getClass.getCanonicalName) + + /** + * This method store string data into cache for given Key + * + * @param key + * @param data + * @param ttl + */ + def set(key: String, data: String, ttl: Int = 0): Unit = { + val jedis = getConnection + try { + jedis.del(key) + jedis.set(key, data) + if (ttl > 0) jedis.expire(key, ttl) + } catch { + case e: Exception => + logger.error("Exception Occurred While Saving String Data to Redis Cache for Key : " + key + "| Exception is:", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This method read string data from cache for a given key + * + * @param key + * @param ttl + * @param handler + * @return + */ + def get(key: String, handler: (String) => String = defaultStringHandler, ttl: Int = 0): String = { + val jedis = getConnection + try { + var data = jedis.get(key) + if (null != handler && (null == data || data.isEmpty)) { + data = handler(key) + if (null != data && !data.isEmpty) + set(key, data, ttl) + } + data + } + catch { + case e: Exception => + logger.error("Exception Occurred While Fetching String Data from Redis Cache for Key : " + key + "| Exception is:", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This Method Returns Future[String] for given key + * + * @param key + * @param asyncHandler + * @param ttl + * @param ec + * @return Future[String] + */ + def getAsync(key: String, asyncHandler: (String) => Future[String], ttl: Int = 0)(implicit ec: ExecutionContext): Future[String] = { + val jedis = getConnection + try { + val data = jedis.get(key) + if (null != asyncHandler && (null == data || data.isEmpty)) { + val dataFuture: Future[String] = asyncHandler(key) + dataFuture.map(value => { + if (null != value && !value.isEmpty) + set(key, value, ttl) + value + }) + } else Future{data} + } + catch { + case e: Exception => + logger.error("Exception Occurred While Fetching String Data from Redis Cache for Key : " + key + "| Exception is:", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This method increment the value by 1 into cache for given key and returns the new value + * + * @param key + * @return Double + */ + def incrementAndGet(key: String): Double = { + val jedis = getConnection + val inc = 1.0 + try jedis.incrByFloat(key, inc) + catch { + case e: Exception => + logger.error("Exception Occurred While Incrementing Value for Key : " + key + " | Exception is : ", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This method store/save list data into cache for given Key + * + * @param key + * @param data + * @param isPartialUpdate + * @param ttl + */ + def saveList(key: String, data: List[String], ttl: Int = 0, isPartialUpdate: Boolean = false): Unit = { + val jedis = getConnection + try { + if (!isPartialUpdate) + jedis.del(key) + data.foreach(entry => jedis.sadd(key, entry)) + if (ttl > 0 && !isPartialUpdate) jedis.expire(key, ttl) + } catch { + case e: Exception => + logger.error("Exception Occurred While Saving List Data to Redis Cache for Key : " + key + "| Exception is:", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This method store/save list data into cache for given Key + * + * @param key + * @param data + */ + def addToList(key: String, data: List[String]): Unit = { + saveList(key, data, 0, true) + } + + /** + * This method returns list data from cache for a given key + * + * @param key + * @param handler + * @param ttl + * @return + */ + def getList(key: String, handler: (String) => List[String] = defaultListHandler, ttl: Int = 0): List[String] = { + val jedis = getConnection + try { + var data = jedis.smembers(key).asScala.toList + if (null != handler && (null == data || data.isEmpty)) { + data = handler(key) + if (null != data && !data.isEmpty) + saveList(key, data, ttl, false) + } + data + } catch { + case e: Exception => + logger.error("Exception Occurred While Fetching List Data from Redis Cache for Key : " + key + "| Exception is:", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This method returns list data from cache for a given key + * + * @param key + * @param asyncHandler + * @param ttl + * @param ec + * @return Future[List[String]] + */ + def getListAsync(key: String, asyncHandler: (String) => Future[List[String]], ttl: Int = 0)(implicit ec: ExecutionContext): Future[List[String]] = { + val jedis = getConnection + try { + val data = jedis.smembers(key).asScala.toList + if (null != asyncHandler && (null == data || data.isEmpty)) { + val dataFuture = asyncHandler(key) + dataFuture.map(value => { + if (null != value && !value.isEmpty) + saveList(key, value, ttl, false) + value + }) + } else Future {data} + } catch { + case e: Exception => + logger.error("Exception Occurred While Fetching List Data from Redis Cache for Key : " + key + "| Exception is:", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This Method Remove Given Data From Existing List For Given Key + * + * @param key + * @param data + */ + def removeFromList(key: String, data: List[String]): Unit = { + val jedis = getConnection + try data.foreach(entry => jedis.srem(key, entry)) + catch { + case e: Exception => + logger.error("Exception Occurred While Deleting Partial Data From Redis Cache for Key : " + key + "| Exception is:", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This method delete data from cache for given key/keys + * + * @param keys + */ + def delete(keys: String*): Unit = { + val jedis = getConnection + try jedis.del(keys.map(_.asInstanceOf[String]): _*) + catch { + case e: Exception => + logger.error("Exception Occurred While Deleting Records From Redis Cache for Identifiers : " + keys.toArray + " | Exception is : ", e) + throw e + } finally returnConnection(jedis) + } + + /** + * This method delete data from cache for all key/keys matched with given pattern + * + * @param pattern + */ + def deleteByPattern(pattern: String): Unit = { + if (StringUtils.isNotBlank(pattern) && !StringUtils.equalsIgnoreCase(pattern, "*")) { + val jedis = getConnection + try { + val keys = jedis.keys(pattern) + if (keys != null && keys.size > 0) + jedis.del(keys.toArray.map(_.asInstanceOf[String]): _*) + } catch { + case e: Exception => + logger.error("Exception Occurred While Deleting Records From Redis Cache for Pattern : " + pattern + " | Exception is : ", e) + throw e + } finally returnConnection(jedis) + } + } + + private def defaultStringHandler(objKey: String): String = { + //Default Implementation Can Be Provided Here + "" + } + + private def defaultListHandler(objKey: String): List[String] = { + //Default Implementation Can Be Provided Here + List() + } +} diff --git a/platform-core/platform-cache/src/main/scala/org/sunbird/cache/util/RedisConnector.scala b/platform-core/platform-cache/src/main/scala/org/sunbird/cache/util/RedisConnector.scala new file mode 100644 index 000000000..9300ab1b7 --- /dev/null +++ b/platform-core/platform-cache/src/main/scala/org/sunbird/cache/util/RedisConnector.scala @@ -0,0 +1,48 @@ +package org.sunbird.cache.util + +import org.sunbird.common.Platform +import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig} + +/** + * This Object Provides Methods To Get And Return Redis Connection Object + */ +trait RedisConnector { + + private val HOST = Platform.getString("redis.host", "localhost") + private val PORT = Platform.getInteger("redis.port", 6379) + private val MAX_CONNECTIONS = Platform.getInteger("redis.maxConnections", 128) + private val INDEX = Platform.getInteger("redis.dbIndex", 0) + private val jedisPool: JedisPool = new JedisPool(getConfig(), HOST, PORT) + + /** + * This Method Returns a connection object from connection pool. + * + * @return Jedis Object + */ + protected def getConnection: Jedis = try { + val jedis = jedisPool.getResource + if (INDEX > 0) jedis.select(INDEX) + jedis + } catch { + case e: Exception => throw e + } + + /** + * This Method takes a connection object and put it back to pool. + * + * @param jedis + */ + protected def returnConnection(jedis: Jedis): Unit = { + try if (null != jedis) jedisPool.returnResource(jedis) + catch { + case e: Exception => throw e + } + } + + private def getConfig(): JedisPoolConfig = { + val config: JedisPoolConfig = new JedisPoolConfig() + config.setMaxTotal(MAX_CONNECTIONS) + config.setBlockWhenExhausted(true); + config + } +} diff --git a/platform-core/platform-cache/src/test/scala/org/sunbird/cache/impl/RedisCacheTest.scala b/platform-core/platform-cache/src/test/scala/org/sunbird/cache/impl/RedisCacheTest.scala new file mode 100644 index 000000000..212a515d4 --- /dev/null +++ b/platform-core/platform-cache/src/test/scala/org/sunbird/cache/impl/RedisCacheTest.scala @@ -0,0 +1,176 @@ +package org.sunbird.cache.impl + + +import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll, Matchers} + +import scala.collection.immutable.Stream.Empty +import scala.concurrent.Future + +class RedisCacheTest extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { + + var cons_message: String = "" + + override def afterAll() { + RedisCache.deleteByPattern("kptest*") + } + + "set without ttl" should "hold the data for forever into cache" in { + RedisCache.set("kptest-101", "kptest-value-01") + val result = RedisCache.get("kptest-101") + result shouldEqual "kptest-value-01" + val resultAfter5Sec = RedisCache.get("kptest-101") + result shouldEqual resultAfter5Sec + } + + "set with ttl" should "hold the data upto given ttl into cache" in { + RedisCache.set("kptest-102", "kptest-value-02", 2) + val result = RedisCache.get("kptest-102") + result shouldEqual "kptest-value-02" + delay(6000) + val resultAfter2Sec = RedisCache.get("kptest-102") + resultAfter2Sec shouldBe "" + } + + "get with valid key" should "return string data for given key" in { + RedisCache.set("kptest-103", "kptest-value-03", 0) + val result = RedisCache.get("kptest-103") + result.isInstanceOf[String] shouldBe true + result shouldEqual "kptest-value-03" + } + + "saveList" should "store list data into cache for given key" in { + val data = List[String]("kp-test-04-list-val-01", "kp-test-04-list-val-02") + RedisCache.saveList("kptest-104", data) + val result = RedisCache.getList("kptest-104") + data.diff(result) shouldBe Empty + } + + "getList with wrong type key" should "throw an exception" in { + RedisCache.set("kptest-105", "kptest-value-05") + val exception = intercept[Exception] { + RedisCache.getList("kptest-105") + } + exception.getMessage shouldEqual "WRONGTYPE Operation against a key holding the wrong kind of value" + } + + "get with wrong type key" should "throw an exception" in { + val data = List[String]("kp-test-06-list-val-01", "kp-test-06-list-val-02") + RedisCache.saveList("kptest-106", data) + val exception = intercept[Exception] { + RedisCache.get("kptest-106") + } + exception.getMessage shouldEqual "WRONGTYPE Operation against a key holding the wrong kind of value" + } + + "delete with key" should "delete the data from cache for given key" in { + RedisCache.set("kptest-107", "kptest-value-07") + RedisCache.set("kptest-108", "kptest-value-08") + RedisCache.delete("kptest-107", "kptest-108") + val result = RedisCache.get("kptest-107") + val res = RedisCache.get("kptest-108") + result shouldBe "" + res shouldBe "" + } + + "deleteByPattern" should "delete data for all the keys matched with pattern" in { + RedisCache.set("kptestp-01", "kptestp-value-01", 0) + RedisCache.set("kptestp-02", "kptestp-value-02", 0) + RedisCache.deleteByPattern("kptestp-*") + val result = RedisCache.get("kptestp-01") + result shouldBe "" + val res = RedisCache.get("kptestp-02") + res shouldBe "" + } + + "incrementAndGet" should "increase the value for given key by one and return" in { + RedisCache.set("kptest-109", "0", 0) + val result: Double = RedisCache.incrementAndGet("kptest-109") + val exp: Double = 1.0 + exp shouldEqual result + val res: Double = RedisCache.incrementAndGet("kptest-109") + val exp2: Double = 2.0 + exp2 shouldEqual res + } + + "removeFromList" should "delete data from list values for given key" in { + val data = List[String]("kp-test-10-list-val-01", "kp-test-10-list-val-02", "kp-test-10-list-val-03") + RedisCache.saveList("kptest-110", data) + val input = List[String]("kp-test-10-list-val-03") + RedisCache.removeFromList("kptest-110", input) + val result = RedisCache.getList("kptest-110") + result.size shouldBe 2 + } + + "addToList" should "update list data into cache for given key" in { + val data = List[String]("kp-test-111-list-val-01", "kp-test-111-list-val-02") + RedisCache.saveList("kptest-111", data) + val result = RedisCache.getList("kptest-111") + data.diff(result) shouldBe Empty + val updateData = List[String]("kp-test-111-list-val-03", "kp-test-111-list-val-04") + RedisCache.addToList("kptest-111", updateData) + val res = RedisCache.getList("kptest-111") + res.size shouldBe 4 + } + + "saveList with ttl" should "store list data into cache for given key upto ttl given" in { + val data = List[String]("kp-test-112-list-val-01", "kp-test-112-list-val-02") + RedisCache.saveList("kptest-112", data, 2) + val result = RedisCache.getList("kptest-112") + data.diff(result) shouldBe Empty + delay(6000) + val res = RedisCache.getList("kptest-112") + res.isEmpty shouldBe true + } + + "getAsync with key not having data in cache" should "return Future[String] from handler" in { + val future: Future[String] = RedisCache.getAsync("kptest-113", (key: String) => Future("sample-data-handler"), 2) + future map { result => { + assert(null != result) + result shouldEqual "sample-data-handler" + } + } + } + + "getAsync with key having data in cache" should "return Future[String] from cache" in { + RedisCache.set("kptest-114", "sample-cache-data") + val future: Future[String] = RedisCache.getAsync("kptest-114", (key: String) => Future("sample-data-handler"), 2) + future map { result => { + assert(null != result) + result shouldEqual "sample-cache-data" + } + } + } + + "getListAsync with key not having data in cache" should "return Future[List[String]] from handler" in { + val handlerData = List[String]("sample-handler-data1", "sample-handler-data2") + val future: Future[List[String]] = RedisCache.getListAsync("kptest-115", (key: String) => Future(handlerData), 2) + future map { result => { + assert(null != result) + assert(result.isInstanceOf[List[String]]) + handlerData.diff(result) shouldBe Empty + } + } + } + + "getListAsync with key having data in cache" should "return Future[List[String]] from cache" in { + val cacheData = List[String]("sample-cache-data1", "sample-cache-data2") + val handlerData = List[String]("sample-handler-data1", "sample-handler-data2") + RedisCache.saveList("kptest-116", cacheData) + val future: Future[List[String]] = RedisCache.getListAsync("kptest-116", (key: String) => Future(handlerData), 2) + future map { result => { + assert(null != result) + assert(result.isInstanceOf[List[String]]) + cacheData.diff(result) shouldBe Empty + } + } + } + + private def delay(time: Long): Unit = { + try Thread.sleep(time) + catch { + case e: Exception => None + } + } + + +} diff --git a/platform-core/platform-common/pom.xml b/platform-core/platform-common/pom.xml index 0bb16703f..fa8696bf7 100644 --- a/platform-core/platform-common/pom.xml +++ b/platform-core/platform-common/pom.xml @@ -57,6 +57,23 @@ jackson-databind ${fasterxml.jackson.version} + + com.mashape.unirest + unirest-java + 1.4.9 + + + org.powermock + powermock-api-mockito + 1.6.4 + test + + + org.powermock + powermock-module-junit4 + 1.6.4 + test + @@ -64,12 +81,31 @@ org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + 3.8.1 - 1.8 - 1.8 + 11 + + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + +
diff --git a/platform-core/platform-common/src/main/java/org/sunbird/common/ContentParams.java b/platform-core/platform-common/src/main/java/org/sunbird/common/ContentParams.java index c37c397a9..9298968b2 100644 --- a/platform-core/platform-common/src/main/java/org/sunbird/common/ContentParams.java +++ b/platform-core/platform-common/src/main/java/org/sunbird/common/ContentParams.java @@ -1,5 +1,6 @@ package org.sunbird.common; public enum ContentParams { - contentEncoding,gzip,identity,contentDisposition,online,inline,identifier,code,osId,mimeType,create,body,artifactUrl + contentEncoding,gzip,identity,contentDisposition,online,inline,identifier,code,osId,mimeType,create,body,artifactUrl,retired,status,contentType, + courseType } diff --git a/platform-core/platform-common/src/main/java/org/sunbird/common/HttpUtil.java b/platform-core/platform-common/src/main/java/org/sunbird/common/HttpUtil.java new file mode 100644 index 000000000..f0ec7d758 --- /dev/null +++ b/platform-core/platform-common/src/main/java/org/sunbird/common/HttpUtil.java @@ -0,0 +1,113 @@ +package org.sunbird.common; + +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.sunbird.common.dto.Response; +import org.sunbird.common.dto.ResponseHandler; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResponseCode; +import org.sunbird.common.exception.ServerException; + +import java.util.HashMap; +import java.util.Map; + +public class HttpUtil { + + private static final String PLATFORM_API_USERID = "System"; + private static final String DEFAULT_CONTENT_TYPE = "application/json"; + + /** + * @param url + * @param requestMap + * @param headerParam + * @return Response + * @throws Exception + */ + public Response post(String url, Map requestMap, Map headerParam) + throws Exception { + validateRequest(url, headerParam); + setDefaultHeader(headerParam); + if (MapUtils.isEmpty(requestMap)) + throw new ServerException("ERR_INVALID_REQUEST_BODY", "Request Body is Missing!"); + try { + HttpResponse response = Unirest.post(url).headers(headerParam).body(JsonUtils.serialize(requestMap)).asString(); + return getResponse(response); + } catch (Exception e) { + throw new ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); + } + } + + /** + * @param url + * @param queryParam + * @param headerParam + * @return Response + * @throws Exception + */ + public Response get(String url, String queryParam, Map headerParam) + throws Exception { + validateRequest(url, headerParam); + setDefaultHeader(headerParam); + String reqUrl = StringUtils.isNotBlank(queryParam) ? url + "?" + queryParam : url; + try { + HttpResponse response = Unirest.get(reqUrl).headers(headerParam).asString(); + return getResponse(response); + } catch (Exception e) { + throw new ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); + } + } + + /** + * This method is to get file related metadata (size and mimeType)from file url, without downloading. + * @param url + * @param headers + * @return + */ + public Map getMetadata(String url, Map headers) { + try { + validateRequest(url, headers); + setDefaultHeader(headers); + Map metadataMap = new HashMap<>(); + HttpResponse response = Unirest.head(url).headers(headers).asString(); + if (response.getStatus() == 200) { + metadataMap.put("Content-Length", ((Number) Double.parseDouble(response.getHeaders().getOrDefault("Content-Length", response.getHeaders().get("content-length")).get(0))).doubleValue()); + metadataMap.put("Content-Type", response.getHeaders().getOrDefault("Content-Type", response.getHeaders().get("content-type")).get(0)); + return metadataMap; + } else { + throw new ClientException("ERR_API_CALL", "Fetching of file related metadata Failed with response code " + response.getStatus() + " and message: " + response.getStatusText()); + } + } catch (ClientException e) { + throw new ClientException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); + } catch (Exception e) { + throw new ServerException("ERR_API_CALL", "Something Went Wrong While Making API Call | Error is: " + e.getMessage()); + } + } + + private void validateRequest(String url, Map headerParam) { + if (StringUtils.isBlank(url)) + throw new ServerException("ERR_INVALID_URL", "Url Parameter is Missing!"); + if (headerParam == null) + throw new ServerException("ERR_INVALID_HEADER_PARAM", "Header Parameter is Missing!"); + } + + private Response getResponse(HttpResponse response) { + if (null != response && StringUtils.isNotBlank(response.getBody())) { + try { + return JsonUtils.deserialize(response.getBody(), Response.class); + } catch (Exception e) { + throw new ServerException("ERR_DATA_PARSER", "Unable to parse data! | Error is: " + e.getMessage()); + } + } else + return ResponseHandler.ERROR(ResponseCode.SERVER_ERROR, ResponseCode.SERVER_ERROR.name(), "Null Response Received While Making Api Call!"); + } + + private void setDefaultHeader(Map headerParam) { + if(!headerParam.containsKey("Content-Type")) + headerParam.put("Content-Type", DEFAULT_CONTENT_TYPE); + if(!headerParam.containsKey("user-id")) + headerParam.put("user-id", PLATFORM_API_USERID); + } + +} diff --git a/platform-core/platform-common/src/main/java/org/sunbird/common/Platform.java b/platform-core/platform-common/src/main/java/org/sunbird/common/Platform.java index 942dc06b1..3d66b3ca9 100644 --- a/platform-core/platform-common/src/main/java/org/sunbird/common/Platform.java +++ b/platform-core/platform-common/src/main/java/org/sunbird/common/Platform.java @@ -52,4 +52,32 @@ private static List getGraphIds(String service) { return graphIds.get(service); } + public static String getString(String key, String defaultVal) { + return config.hasPath(key) ? config.getString(key) : defaultVal; + } + + public static Integer getInteger(String key, Integer defaultVal) { + return config.hasPath(key) ? config.getInt(key) : defaultVal; + } + + public static Boolean getBoolean(String key, Boolean defaultVal) { + return config.hasPath(key) ? config.getBoolean(key) : defaultVal; + } + + public static List getStringList(String key, List defaultVal) { + return config.hasPath(key) ? config.getStringList(key) : defaultVal; + } + + public static Long getLong(String key, Long defaultVal) { + return config.hasPath(key) ? config.getLong(key) : defaultVal; + } + + public static Double getDouble(String key, Double defaultVal) { + return config.hasPath(key) ? config.getDouble(key) : defaultVal; + } + + public static Object getAnyRef(String key, Object defaultVal) { + return config.hasPath(key) ? config.getAnyRef(key) : defaultVal; + } + } diff --git a/platform-core/platform-common/src/main/java/org/sunbird/common/dto/Request.java b/platform-core/platform-common/src/main/java/org/sunbird/common/dto/Request.java index 8f75bed6a..7139c1c22 100644 --- a/platform-core/platform-common/src/main/java/org/sunbird/common/dto/Request.java +++ b/platform-core/platform-common/src/main/java/org/sunbird/common/dto/Request.java @@ -81,11 +81,19 @@ public Object get(String key) { return request.get(key); } + public Object getOrDefault(String key, Object defaultValue) { + Object value = request.getOrDefault(key, defaultValue); + if (value == null) return defaultValue; else return value; + } public void put(String key, Object vo) { request.put(key, vo); } + public void putAll(Map map) { + request.putAll(map); + } + public String getOperation() { return operation; } diff --git a/platform-core/platform-common/src/main/java/org/sunbird/common/dto/Response.java b/platform-core/platform-common/src/main/java/org/sunbird/common/dto/Response.java index ba10b62af..0b97ae81b 100644 --- a/platform-core/platform-common/src/main/java/org/sunbird/common/dto/Response.java +++ b/platform-core/platform-common/src/main/java/org/sunbird/common/dto/Response.java @@ -31,8 +31,9 @@ public String getId() { return id; } - public void setId(String id) { + public Response setId(String id) { this.id = id; + return this; } public String getVer() { @@ -62,12 +63,14 @@ public Object get(String key) { return result.get(key); } - public void put(String key, Object vo) { + public Response put(String key, Object vo) { result.put(key, vo); + return this; } - public void putAll(Map resultMap) { + public Response putAll(Map resultMap) { result.putAll(resultMap); + return this; } public ResponseParams getParams() { diff --git a/platform-core/platform-common/src/main/java/org/sunbird/common/dto/ResponseHandler.java b/platform-core/platform-common/src/main/java/org/sunbird/common/dto/ResponseHandler.java index 3ae4f12e6..8c8289eae 100644 --- a/platform-core/platform-common/src/main/java/org/sunbird/common/dto/ResponseHandler.java +++ b/platform-core/platform-common/src/main/java/org/sunbird/common/dto/ResponseHandler.java @@ -156,6 +156,12 @@ public static Response getErrorResponse(Throwable e) { return response; } + public static Boolean isResponseNotFoundError(Response response) { + ResponseParams params = response.getParams(); + return (null != params && StringUtils.equals(ResponseParams.StatusType.failed.name(), params.getStatus()) + && StringUtils.equals(response.getResponseCode().name(),ResponseCode.RESOURCE_NOT_FOUND.name())); + } + private static String setErrMessage(Throwable e) { if (e instanceof MiddlewareException) { return e.getMessage(); diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/DateUtilsTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/DateUtilsTest.java new file mode 100644 index 000000000..14d3757a8 --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/DateUtilsTest.java @@ -0,0 +1,67 @@ +package org.sunbird.common; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.text.SimpleDateFormat; +import java.util.Date; + + +public class DateUtilsTest { + + @Test + public void testFormatValidDate() throws Exception { + String date = DateUtils.format(new Date()); + System.out.println(date); + Assert.assertNotNull(date); + Assert.assertTrue(StringUtils.containsAny(date, "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z")); + } + + @Test + public void testFormatNullDate() throws Exception { + String date = DateUtils.format(null); + Assert.assertNull(date); + } + + @Test + public void testParseValidString() throws Exception { + Date date = DateUtils.parse("2020-08-12T14:34:18.691+0530"); + Assert.assertNotNull(date); + Assert.assertNotNull(date.getTime()); + } + + @Test + public void testParseInvalidString() throws Exception { + Date date = DateUtils.parse("0000000"); + Assert.assertNull(date); + } + + @Test + public void testGetDateFormat() throws Exception { + SimpleDateFormat sdf = DateUtils.getDateFormat(); + Assert.assertNotNull(sdf); + Assert.assertTrue(StringUtils.containsAny(sdf.get2DigitYearStart().toString(), "\\w+\\s?\\w+\\s?\\d{2}\\s?\\d{2}:\\d{2}:\\d{2}.\\d{3}Z")); + } + + @Test + public void testformatCurrentDate() throws Exception { + String date = DateUtils.formatCurrentDate(); + Assert.assertNotNull(date); + Assert.assertTrue(StringUtils.containsAny(date, "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z")); + } + + @Test + public void testformatCurrentDateWithPattern() throws Exception { + String date = DateUtils.formatCurrentDate("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + Assert.assertNotNull(date); + Assert.assertTrue(StringUtils.containsAny(date, "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z")); + } + + @Test + public void testformatCurrentDateWithInvalidPattern() throws Exception { + String date = DateUtils.formatCurrentDate(""); + Assert.assertTrue(StringUtils.isAllBlank(date)); + } + +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/HttpUtilTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/HttpUtilTest.java new file mode 100644 index 000000000..d49fe210f --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/HttpUtilTest.java @@ -0,0 +1,159 @@ +package org.sunbird.common; + +import com.mashape.unirest.http.HttpClientHelper; +import com.mashape.unirest.http.HttpResponse; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponseFactory; +import org.apache.http.HttpStatus; +import org.apache.http.HttpVersion; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.DefaultHttpResponseFactory; +import org.apache.http.message.BasicStatusLine; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.sunbird.common.dto.Response; +import org.sunbird.common.exception.ServerException; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertTrue; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(HttpClientHelper.class) +@PowerMockIgnore({"jdk.internal.reflect.*", "javax.management.*", "sun.security.ssl.*", "javax.net.ssl.*", "javax.crypto.*"}) +public class HttpUtilTest { + + private static HttpResponse httpResponse = null; + private static HttpUtil httpUtil = new HttpUtil(); + + @Before + public void setup() throws Exception { + String body = "{\"id\":\"api.content.create\",\"ver\":\"3.0\",\"ts\":\"2020-04-19T21:54:12ZZ\",\"params\":{\"resmsgid\":\"47f07524-3246-4731-9eae-17bab692d3a9\",\"msgid\":null,\"err\":null,\"status\":\"successful\",\"errmsg\":null},\"responseCode\":\"OK\",\"result\":{\"identifier\":\"do_411300343400543846413\",\"node_id\":\"do_411300343400543846413\",\"versionKey\":\"1587333252620\"}}"; + mockStatic(HttpClientHelper.class); + HttpResponseFactory factory = new DefaultHttpResponseFactory(); + org.apache.http.HttpResponse response = factory.newHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, null), null); + response.setEntity(new StringEntity(body)); + httpResponse = new HttpResponse(response, String.class); + when(HttpClientHelper.request(Mockito.anyObject(), Mockito.eq(String.class))).thenReturn(httpResponse); + } + + @Test(expected = ServerException.class) + public void testValidateRequestWithEmptyUrl() throws Exception { + Method method = HttpUtil.class.getDeclaredMethod("validateRequest", String.class, Map.class); + method.setAccessible(true); + Map header = new HashMap() {{ + put("x-channel-id", "test-channel"); + }}; + method.invoke(httpUtil, null, header); + } + + @Test(expected = ServerException.class) + public void testValidateRequestWithEmptyHeader() throws Exception { + Method method = HttpUtil.class.getDeclaredMethod("validateRequest", String.class, Map.class); + method.setAccessible(true); + method.invoke(httpUtil, "http://test.com", null); + } + + @Test + public void testSetDefaultHeader() throws Exception { + Method method = HttpUtil.class.getDeclaredMethod("setDefaultHeader", Map.class); + method.setAccessible(true); + Map header = new HashMap(); + method.invoke(httpUtil, header); + assertTrue(header.size() == 2); + assertTrue(header.containsKey("Content-Type")); + } + + @Test + public void testPost() throws Exception { + Map header = new HashMap() {{ + put("x-channel-id", "test-channel"); + }}; + Map req = new HashMap() {{ + put("request", new HashMap() {{ + put("content", new HashMap() {{ + put("name", "Test Name"); + }}); + }}); + }}; + Response response = httpUtil.post("http://test.com/abc", req, header); + validateResponse(response); + } + + @Test(expected = ServerException.class) + public void testPostWithInvalidRequest() throws Exception { + Map header = new HashMap() {{ + put("x-channel-id", "test-channel"); + }}; + httpUtil.post("http://test.com/abc", new HashMap(), header); + } + + @Test + public void testGet() throws Exception { + Map header = new HashMap() {{ + put("x-channel-id", "test-channel"); + }}; + Response response = httpUtil.get("http://test.com/abc", null, header); + validateResponse(response); + } + + @Test + public void testGetWithQueryString() throws Exception { + Map header = new HashMap() {{ + put("x-channel-id", "test-channel"); + }}; + Response response = httpUtil.get("http://test.com/abc", "mode=edit", header); + validateResponse(response); + } + + @Test + public void testGetResponse() throws Exception { + Method method = HttpUtil.class.getDeclaredMethod("getResponse", HttpResponse.class); + method.setAccessible(true); + Response resp = (Response) method.invoke(httpUtil, httpResponse); + validateResponse(resp); + } + + @Test + public void testGetResponseWithEmptyBody() throws Exception { + HttpResponseFactory factory = new DefaultHttpResponseFactory(); + org.apache.http.HttpResponse response = factory.newHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, null), null); + response.setEntity(new StringEntity("")); + HttpResponse httpResp = new HttpResponse(response, String.class); + Method method = HttpUtil.class.getDeclaredMethod("getResponse", HttpResponse.class); + method.setAccessible(true); + Response resp = (Response) method.invoke(httpUtil, httpResp); + assertTrue(null != resp); + assertTrue(StringUtils.equalsIgnoreCase("SERVER_ERROR", resp.getResponseCode().toString())); + } + + @Test(expected = ServerException.class) + public void testGetResponseWithInvalidBody() throws Exception { + HttpResponseFactory factory = new DefaultHttpResponseFactory(); + org.apache.http.HttpResponse response = factory.newHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, null), null); + response.setEntity(new StringEntity("test")); + HttpResponse httpResp = new HttpResponse(response, String.class); + Method method = HttpUtil.class.getDeclaredMethod("getResponse", HttpResponse.class); + method.setAccessible(true); + method.invoke(httpUtil, httpResp); + } + + private void validateResponse(Response resp) { + assertTrue(null != resp); + assertTrue(StringUtils.equalsIgnoreCase("OK", resp.getResponseCode().toString())); + assertTrue(StringUtils.isNotEmpty(resp.getId())); + assertTrue(MapUtils.isNotEmpty(resp.getResult())); + assertTrue(StringUtils.equals("do_411300343400543846413", (String) resp.getResult().get("identifier"))); + + } +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/HttpUtilTests.java b/platform-core/platform-common/src/test/java/org/sunbird/common/HttpUtilTests.java new file mode 100644 index 000000000..052c357af --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/HttpUtilTests.java @@ -0,0 +1,32 @@ +package org.sunbird.common; + +import org.junit.Test; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ServerException; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class HttpUtilTests { + + HttpUtil httpUtil = new HttpUtil(); + + @Test + public void testGetMetadataValidUrl() throws Exception { + Map metadata = httpUtil.getMetadata("https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1130384356456120321307/3point4gb.mp4", new HashMap<>()); + assertNotNull(metadata); + assertNotNull(metadata.get("Content-Type")); + assertNotNull(metadata.get("Content-Length")); + assertTrue(metadata.get("Content-Length") instanceof Number); + assertTrue(metadata.get("Content-Type") instanceof String); + } + + + @Test(expected = ClientException.class) + public void testGetMetadataInvalidUrl() throws Exception { + httpUtil.getMetadata("https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_113038435645612032130743/3point4gb.mp4", new HashMap<>()); + } +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/JsonUtilsTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/JsonUtilsTest.java new file mode 100644 index 000000000..0e644dfc0 --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/JsonUtilsTest.java @@ -0,0 +1,100 @@ +package org.sunbird.common; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import org.junit.Assert; +import org.junit.Test; +import org.sunbird.common.dto.ResponseParams; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JsonUtilsTest { + + @Test + public void testSerializeValidObject() throws Exception { + String serializedMap = JsonUtils.serialize(new HashMap<>() {{ + put("identifier", "do_1234"); + put("versionKey", "1234"); + }}); + Assert.assertNotNull(serializedMap); + Assert.assertEquals("{\"identifier\":\"do_1234\",\"versionKey\":\"1234\"}",serializedMap); + } + + @Test(expected = InvalidDefinitionException.class) + public void testSerializeInvalidObject() throws Exception { + JsonUtils.serialize(new Object()); + } + + @Test + public void testDeserializeValidString() throws Exception { + Map deserialized = JsonUtils.deserialize("{\"identifier\":\"do_1234\",\"versionKey\":\"1234\"}", Map.class); + + Assert.assertNotNull(deserialized); + Assert.assertEquals(deserialized.size(), 2); + Assert.assertEquals("do_1234", deserialized.get("identifier")); + Assert.assertEquals("1234", deserialized.get("versionKey")); + } + + @Test(expected = MismatchedInputException.class) + public void testDeserializeInValidString() throws Exception { + JsonUtils.deserialize("", Map.class); + } + + @Test(expected = JsonParseException.class) + public void testDeserializeCorruptString() throws Exception { + JsonUtils.deserialize("{\"identifier\":\"do_1234\",\"versionKey\":\"1234\",}", Map.class); + } + + @Test + public void testConvertValidData() throws Exception { + Map deserialized = JsonUtils.deserialize( "{\n"+ + " \"id\": \"api.user.courses.list\",\n"+ + " \"ver\": \"v1\",\n"+ + " \"ts\": \"2020-08-12 08:38:50:415+0000\",\n"+ + " \"params\": {\n"+ + " \"resmsgid\": null,\n"+ + " \"msgid\": \"71ca0432-1320-4dc3-a7db-2d72b6e3fc46\",\n"+ + " \"err\": null,\n"+ + " \"status\": \"success\",\n"+ + " \"errmsg\": null\n"+ + " },\n"+ + " \"responseCode\": \"OK\",\n"+ + " \"result\": {" + + "}" + + "}", Map.class); + + Assert.assertNotNull(deserialized); + ResponseParams responseParams = JsonUtils.convert((Map)deserialized.get("params"), ResponseParams.class ); + Assert.assertNotNull(responseParams); + Assert.assertEquals("71ca0432-1320-4dc3-a7db-2d72b6e3fc46", responseParams.getMsgid()); + + } + + @Test + public void testConvertJsonStringValid_1() throws Exception { + Map deserialized = (Map) JsonUtils.convertJSONString("{\"identifier\":\"do_1234\",\"versionKey\":\"1234\"}"); + Assert.assertNotNull(deserialized); + Assert.assertEquals(deserialized.size(), 2); + Assert.assertEquals("do_1234", deserialized.get("identifier")); + Assert.assertEquals("1234", deserialized.get("versionKey")); + } + + @Test + public void testConvertJsonStringValid_2() throws Exception { + List list = (List) JsonUtils.convertJSONString("[\"test1\",\"test2\"]"); + Assert.assertNotNull(list); + Assert.assertEquals(list.size(), 2); + Assert.assertEquals("test1", list.get(0)); + Assert.assertEquals("test2", list.get(1)); + } + + @Test + public void testConvertJsonStringInvalid() throws Exception { + List list = (List) JsonUtils.convertJSONString("\"test1\""); + Assert.assertNull(list); + + } +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/PlatformTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/PlatformTest.java new file mode 100644 index 000000000..cc0adb310 --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/PlatformTest.java @@ -0,0 +1,153 @@ +package org.sunbird.common; + +import com.typesafe.config.ConfigFactory; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PlatformTest { + + @BeforeClass + public static void init() { + Platform.config = ConfigFactory.parseMap(new HashMap() {{ + put("test.str", "strval"); + put("test.int", 100); + put("test.bool", true); + put("test.long", 1380914990); + put("test.double", 900923.0); + put("content.graph_ids", Arrays.asList("es","ko")); + put("test.strlist", new ArrayList() {{ + add("val1"); + add("val2"); + }}); + put("test.map", new HashMap() {{ + put("key1", "val1"); + put("key2","val2"); + }}); + }}).resolve(); + } + + @Test + public void testGetStringWithValidConfig() { + String str = Platform.getString("test.str", "def_str_val"); + Assert.assertEquals("strval", str); + } + + @Test + public void testGetStringWithInvalidConfig() { + String str = Platform.getString("test.str.1", "def_str_val"); + Assert.assertEquals("def_str_val", str); + } + + @Test + public void testGetIntegerWithValidConfig() { + int result = Platform.getInteger("test.int", 0); + Assert.assertEquals(100, result); + } + + @Test + public void testGetIntegerWithInvalidConfig() { + int result = Platform.getInteger("test.int.1", 0); + Assert.assertEquals(0, result); + } + + @Test + public void testGetBooleanWithValidConfig() { + boolean result = Platform.getBoolean("test.bool", false); + Assert.assertEquals(true, result); + } + + @Test + public void testGetBooleanWithInvalidConfig() { + boolean result = Platform.getBoolean("test.bool.1", false); + Assert.assertEquals(false, result); + } + + @Test + public void testGetStringListWithValidConfig() { + List result = Platform.getStringList("test.strlist", new ArrayList()); + Assert.assertTrue(null != result && !result.isEmpty()); + Assert.assertTrue(result.size() == 2); + Assert.assertTrue(result.contains("val1")); + } + + @Test + public void testGetStringListWithInvalidConfig() { + List result = Platform.getStringList("test.strlist.1", new ArrayList()); + Assert.assertTrue(null != result && result.isEmpty()); + Assert.assertTrue(result.size() == 0); + } + + @Test + public void testGetLongWithValidConfig() { + Long result = Platform.getLong("test.long", 0L); + Long expected = 1380914990L; + Assert.assertTrue(null != result ); + Assert.assertEquals(expected, result); + } + + @Test + public void testGetLongWithInvalidConfig() { + Long result = Platform.getLong("test.long.1", 0L); + Long expected = 0L; + Assert.assertTrue(null != result ); + Assert.assertEquals(expected, result); + } + + @Test + public void testGetDoubleWithValidConfig() { + Double result = Platform.getDouble("test.long", 0.0); + Double expected = 1.38091499E9; + Assert.assertTrue(null != result ); + Assert.assertEquals(expected, result); + } + + @Test + public void testGetDoubleWithInvalidConfig() { + Double result = Platform.getDouble("test.long.1", 0.0); + Double expected = 0.0; + Assert.assertTrue(null != result ); + Assert.assertEquals(expected, result); + } + + @Test + public void testGetGraphIds() { + List values = Platform.getGraphIds("content"); + Assert.assertNotNull(values); + Assert.assertEquals(2, values.size()); + } + + @Test + public void testGetGraphIdsInvalidService() { + List values = Platform.getGraphIds("search"); + Assert.assertNotNull(values); + Assert.assertEquals(0, values.size()); + } + + @Test + public void testGetTimeout() { + int timeout = Platform.getTimeout(); + Assert.assertNotNull(timeout); + Assert.assertEquals(30, timeout); + } + + @Test + public void testGetAnyRefWithValidConfig() { + Map result = (Map) Platform.getAnyRef("test.map", new HashMap()); + Assert.assertTrue(null != result ); + Assert.assertEquals("val1", result.get("key1")); + } + + @Test + public void testGetAnyRefWithInvalidConfig() { + Map result = (Map) Platform.getAnyRef("test.map1", new HashMap()); + Assert.assertTrue(null != result ); + Assert.assertEquals(null, result.get("key1")); + } +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/SlugTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/SlugTest.java new file mode 100644 index 000000000..2bfa7ce00 --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/SlugTest.java @@ -0,0 +1,40 @@ +package org.sunbird.common; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; + +public class SlugTest { + + @Test + public void testMakeSlug() throws Exception { + String sluggified = Slug.makeSlug(" -Cov -e*r+I/ αma.ge.png-- "); + Assert.assertEquals("cov-er-i-ma.ge.png", sluggified); + } + + @Test(expected = IllegalArgumentException.class) + public void testMakeSlugException() throws Exception { + Slug.makeSlug(null); + } + + @Test + public void testMakeSlugTransiliterate() throws Exception { + String sluggified = Slug.makeSlug(" Cov -e*r+I/ αma.ge.png ", true); + Assert.assertEquals("cov-er-i-ama.ge.png", sluggified); + } + + @Test + public void testremoveDuplicateCharacters() throws Exception { + String sluggified = Slug.removeDuplicateChars("akssaaklla"); + Assert.assertEquals("aksakla", sluggified); + } + + @Test + public void testcreateSlugFile() throws Exception { + File file = new File("-αimage.jpg"); + File slugFile = Slug.createSlugFile(file); + Assert.assertEquals("aimage.jpg", slugFile.getName()); + } + +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/dto/ResponseHandlerTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/dto/ResponseHandlerTest.java new file mode 100644 index 000000000..5cc3394d4 --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/dto/ResponseHandlerTest.java @@ -0,0 +1,196 @@ +package org.sunbird.common.dto; + +import org.junit.Assert; +import org.junit.Test; +import org.sunbird.common.JsonUtils; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResponseCode; + +import java.util.Arrays; + +public class ResponseHandlerTest { + + @Test + public void handleResponses_1() throws Exception { + Response finalResponse = ResponseHandler.handleResponses(Arrays.asList(getServerErrorResponse(), getPartialSuccessResponse(), getClientErrorResponse(), getSuccessResponse())); + Assert.assertNotNull(finalResponse); + Assert.assertEquals(finalResponse.getResponseCode(), ResponseCode.SERVER_ERROR); + } + + @Test + public void handleResponses_2() throws Exception { + Response finalResponse = ResponseHandler.handleResponses(Arrays.asList(getSuccessResponse(), getPartialSuccessResponse(), getClientErrorResponse(), getSuccessResponse())); + Assert.assertNotNull(finalResponse); + Assert.assertEquals(finalResponse.getResponseCode(), ResponseCode.PARTIAL_SUCCESS); + } + + @Test + public void handleResponses_3() throws Exception { + Response finalResponse = ResponseHandler.handleResponses(Arrays.asList(getSuccessResponse(), getSuccessResponse(), getClientErrorResponse(), getSuccessResponse())); + Assert.assertNotNull(finalResponse); + Assert.assertEquals(finalResponse.getResponseCode(), ResponseCode.CLIENT_ERROR); + } + + + @Test + public void handleResponses_4() throws Exception { + Response finalResponse = ResponseHandler.handleResponses(Arrays.asList(getSuccessResponse(), getSuccessResponse(), getSuccessResponse(), getSuccessResponse())); + Assert.assertNotNull(finalResponse); + Assert.assertEquals(finalResponse.getResponseCode(), ResponseCode.OK); + } + + @Test + public void handleResponses_5() throws Exception { + Response finalResponse = ResponseHandler.handleResponses(Arrays.asList(getSuccessResponse(), getResourceNotFoundResponse(), getSuccessResponse(), ResponseHandler.OK())); + Assert.assertNotNull(finalResponse); + Assert.assertEquals(finalResponse.getResponseCode(), ResponseCode.RESOURCE_NOT_FOUND); + } + + @Test + public void handleResponses_6() throws Exception { + Boolean isError = ResponseHandler.checkError(getServerErrorResponse()); + Assert.assertTrue(isError); + } + + @Test + public void handleResponses_7() throws Exception { + Boolean isError = ResponseHandler.checkError(getSuccessResponse()); + Assert.assertTrue(!isError); + } + + @Test + public void handleResponses_8() throws Exception { + String message = ResponseHandler.getErrorMessage(getServerErrorResponse()); + Assert.assertNotNull(message); + Assert.assertEquals("Something went wrong in server while processing the request", message); + } + + @Test + public void handleResponses_9() throws Exception { + String message = ResponseHandler.getErrorMessage(new Response()); + Assert.assertNull(message); + } + + @Test + public void handleResponses_10() throws Exception { + Boolean flag = ResponseHandler.isResponseNotFoundError(getResourceNotFoundResponse()); + Assert.assertTrue(flag); + } + + @Test + public void handleResponses_11() throws Exception { + Boolean flag = ResponseHandler.isResponseNotFoundError(getClientErrorResponse()); + Assert.assertFalse(flag); + } + + @Test + public void handleResponses_12() throws Exception { + Throwable e = new ClientException("CLIENT_ERROR","Metadata mimeType should be one of: [application/vnd.ekstep.content-collection]"); + Response response = ResponseHandler.getErrorResponse(e); + Assert.assertNotNull(response); + } + + @Test + public void handleResponses_13() throws Exception { + Throwable e = new ClientException("SERVER_ERROR","Metadata mimeType should be one of: [application/vnd.ekstep.content-collection]"); + Response response = ResponseHandler.getErrorResponse(e); + Assert.assertNotNull(response); + } + + private Response getServerErrorResponse() throws Exception { + return JsonUtils.deserialize("{\n" + + " \"id\": \"api.content.create\",\n" + + " \"ver\": \"3.0\",\n" + + " \"ts\": \"2020-09-30T09:22:32ZZ\",\n" + + " \"params\": {\n" + + " \"resmsgid\": \"fc7e0a7b-7add-4a0d-a805-a8932336667e\",\n" + + " \"msgid\": null,\n" + + " \"err\": \"ERR_SYSTEM_EXCEPTION\",\n" + + " \"status\": \"failed\",\n" + + " \"errmsg\": \"Something went wrong in server while processing the request\"\n" + + " },\n" + + " \"responseCode\": \"SERVER_ERROR\",\n" + + " \"result\": {}\n" + + "}", Response.class); + } + + private Response getSuccessResponse() throws Exception { + return JsonUtils.deserialize("{\n" + + " \"id\": \"api.content.create\",\n" + + " \"ver\": \"3.0\",\n" + + " \"ts\": \"2020-09-30T09:21:10ZZ\",\n" + + " \"params\": {\n" + + " \"resmsgid\": \"be55769c-6d31-4667-9b5c-4b35d7b85c23\",\n" + + " \"msgid\": null,\n" + + " \"err\": null,\n" + + " \"status\": \"successful\",\n" + + " \"errmsg\": null\n" + + " },\n" + + " \"responseCode\": \"OK\",\n" + + " \"result\": {\n" + + " \"identifier\": \"do_1131191412394393601663\",\n" + + " \"node_id\": \"do_1131191412394393601663\",\n" + + " \"versionKey\": \"1601457670834\"\n" + + " }\n" + + "}", Response.class); } + + private Response getPartialSuccessResponse() throws Exception { + return JsonUtils.deserialize("{\n" + + " \"id\": \"api.content.create\",\n" + + " \"ver\": \"3.0\",\n" + + " \"ts\": \"2020-09-30T09:25:55ZZ\",\n" + + " \"params\": {\n" + + " \"resmsgid\": \"9ed49194-2f17-4564-afa8-ef7582a34117\",\n" + + " \"msgid\": null,\n" + + " \"err\": \"PARTIAL_SUCCESS\",\n" + + " \"status\": \"failed\",\n" + + " \"errmsg\": \"Validation Errors\"\n" + + " },\n" + + " \"responseCode\": \"PARTIAL_SUCCESS\",\n" + + " \"result\": {\n" + + " \"messages\": [\n" + + " \"Metadata mimeType should be one of: [application/vnd.ekstep.content-collection]\"\n" + + " ]\n" + + " }\n" + + "}", Response.class); + } + + private Response getClientErrorResponse() throws Exception { + return JsonUtils.deserialize("{\n" + + " \"id\": \"api.content.create\",\n" + + " \"ver\": \"3.0\",\n" + + " \"ts\": \"2020-09-30T09:25:55ZZ\",\n" + + " \"params\": {\n" + + " \"resmsgid\": \"9ed49194-2f17-4564-afa8-ef7582a34117\",\n" + + " \"msgid\": null,\n" + + " \"err\": \"CLIENT_ERROR\",\n" + + " \"status\": \"failed\",\n" + + " \"errmsg\": \"Validation Errors\"\n" + + " },\n" + + " \"responseCode\": \"CLIENT_ERROR\",\n" + + " \"result\": {\n" + + " \"messages\": [\n" + + " \"Metadata mimeType should be one of: [application/vnd.ekstep.content-collection]\"\n" + + " ]\n" + + " }\n" + + "}", Response.class); + } + + private Response getResourceNotFoundResponse() throws Exception { + return JsonUtils.deserialize("{\n" + + " \"id\": \"api.content.hierarchy.get\",\n" + + " \"ver\": \"3.0\",\n" + + " \"ts\": \"2020-09-30T09:34:19ZZ\",\n" + + " \"params\": {\n" + + " \"resmsgid\": \"09649a91-28a8-4389-afb4-f6528c5c61be\",\n" + + " \"msgid\": null,\n" + + " \"err\": \"RESOURCE_NOT_FOUND\",\n" + + " \"status\": \"failed\",\n" + + " \"errmsg\": \"rootId do_1131177453355356814 does not exist\"\n" + + " },\n" + + " \"responseCode\": \"RESOURCE_NOT_FOUND\",\n" + + " \"result\": {}\n" + + "}", Response.class); + } + +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ClientExceptionTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ClientExceptionTest.java new file mode 100644 index 000000000..1fa301d87 --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ClientExceptionTest.java @@ -0,0 +1,40 @@ +package org.sunbird.common.exception; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class ClientExceptionTest { + @Test(expected = ClientException.class) + public void testClientException_1() throws Exception { + throw new ClientException(ResponseCode.CLIENT_ERROR.name(), "Please Provide Valid File Name."); + } + + @Test(expected = ClientException.class) + public void testClientException_2() throws Exception { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Please Provide Valid File Name.", Arrays.asList("Message one ", "message 2")); + } + + @Test(expected = ClientException.class) + public void testClientException_3() throws Exception { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Please Provide Valid File Name.", "message1", "message2"); + } + + @Test(expected = ClientException.class) + public void testClientException_4() throws Exception { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Please Provide Valid File Name.", new Throwable("message throwable"), "message1", "message2"); + + } + + @Test(expected = ClientException.class) + public void testClientException_5() throws Exception { + throw new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Please Provide Valid File Name.", new Throwable("message throwable")); + } + + @Test + public void testClientException_6() throws Exception { + ClientException exception = new ClientException(ErrorCodes.ERR_BAD_REQUEST.name(), "Please Provide Valid File Name.", new Throwable("message throwable")); + Assert.assertEquals(ResponseCode.CLIENT_ERROR, exception.getResponseCode()); + } +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/exception/MiddlewareExceptionTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/exception/MiddlewareExceptionTest.java new file mode 100644 index 000000000..aa62ad82f --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/exception/MiddlewareExceptionTest.java @@ -0,0 +1,43 @@ +package org.sunbird.common.exception; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class MiddlewareExceptionTest { + @Test(expected = MiddlewareException.class) + public void testMiddlewareException_1() throws Exception { + throw new MiddlewareException(ResponseCode.OK.name(), "Please Provide Valid File Name."); + } + + @Test(expected = MiddlewareException.class) + public void testMiddlewareException_2() throws Exception { + throw new MiddlewareException(ResponseCode.PARTIAL_SUCCESS.name(), "Please Provide Valid File Name.", Arrays.asList("Message one ", "message 2")); + } + + @Test(expected = MiddlewareException.class) + public void testMiddlewareException_3() throws Exception { + throw new MiddlewareException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Please Provide Valid File Name.", "message1", "message2"); + } + + @Test(expected = MiddlewareException.class) + public void testMiddlewareException_4() throws Exception { + throw new MiddlewareException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Please Provide Valid File Name.", new Throwable("message throwable"), "message1", "message2"); + + } + + @Test(expected = MiddlewareException.class) + public void testMiddlewareException_5() throws Exception { + throw new MiddlewareException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Please Provide Valid File Name.", new Throwable("message throwable")); + } + + @Test + public void testMiddlewareException_6() throws Exception { + MiddlewareException exception = new MiddlewareException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Please Provide Valid File Name.", new Throwable("message throwable")); + Assert.assertEquals(ResponseCode.SERVER_ERROR, exception.getResponseCode()); + Assert.assertEquals("ERR_SYSTEM_EXCEPTION", exception.getErrCode()); + Assert.assertEquals(null, exception.getMessages()); + } + +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ResourceNotFoundExceptionTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ResourceNotFoundExceptionTest.java new file mode 100644 index 000000000..2f02ff219 --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ResourceNotFoundExceptionTest.java @@ -0,0 +1,41 @@ +package org.sunbird.common.exception; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class ResourceNotFoundExceptionTest { + @Test(expected = ResourceNotFoundException.class) + public void testResourceNotFoundException_1() throws Exception { + throw new ResourceNotFoundException(ResponseCode.RESOURCE_NOT_FOUND.name(), "Node not found with identifier: "); + } + + @Test(expected = ResourceNotFoundException.class) + public void testResourceNotFoundException_2() throws Exception { + throw new ResourceNotFoundException(ResponseCode.RESOURCE_NOT_FOUND.name(), "Node not found with identifier: ", Arrays.asList("Message one ", "message 2")); + } + + @Test(expected = ResourceNotFoundException.class) + public void testResourceNotFoundException_3() throws Exception { + throw new ResourceNotFoundException(ResponseCode.RESOURCE_NOT_FOUND.name(), "Node not found with identifier: ", "message1", "message2"); + } + + @Test(expected = ResourceNotFoundException.class) + public void testResourceNotFoundException_4() throws Exception { + throw new ResourceNotFoundException(ResponseCode.RESOURCE_NOT_FOUND.name(), "Node not found with identifier: ", new Throwable("message throwable"), "message1", "message2"); + + } + + @Test(expected = ResourceNotFoundException.class) + public void testResourceNotFoundException_5() throws Exception { + throw new ResourceNotFoundException(ResponseCode.RESOURCE_NOT_FOUND.name(), "Node not found with identifier: ", new Throwable("message throwable")); + } + + @Test + public void testResourceNotFoundException_6() throws Exception { + ResourceNotFoundException exception = new ResourceNotFoundException(ResponseCode.RESOURCE_NOT_FOUND.name(), "Node not found with identifier: ", "do_1234"); + Assert.assertEquals(ResponseCode.RESOURCE_NOT_FOUND, exception.getResponseCode()); + Assert.assertEquals("do_1234", exception.getIdentifier()); + } +} diff --git a/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ServerExceptionTest.java b/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ServerExceptionTest.java new file mode 100644 index 000000000..c80d1a6d5 --- /dev/null +++ b/platform-core/platform-common/src/test/java/org/sunbird/common/exception/ServerExceptionTest.java @@ -0,0 +1,40 @@ +package org.sunbird.common.exception; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class ServerExceptionTest { + @Test(expected = ServerException.class) + public void testServerException_1() throws Exception { + throw new ServerException(ResponseCode.SERVER_ERROR.name(), "Failed to create node object. Node from database is null."); + } + + @Test(expected = ServerException.class) + public void testServerException_2() throws Exception { + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Failed to create node object. Node from database is null.", Arrays.asList("Message one ", "message 2")); + } + + @Test(expected = ServerException.class) + public void testServerException_3() throws Exception { + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Failed to create node object. Node from database is null.", "message1", "message2"); + } + + @Test(expected = ServerException.class) + public void testServerException_4() throws Exception { + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Failed to create node object. Node from database is null.", new Throwable("message throwable"), "message1", "message2"); + + } + + @Test(expected = ServerException.class) + public void testServerException_5() throws Exception { + throw new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Failed to create node object. Node from database is null.", new Throwable("message throwable")); + } + + @Test + public void testServerException_6() throws Exception { + ServerException exception = new ServerException(ErrorCodes.ERR_SYSTEM_EXCEPTION.name(), "Failed to create node object. Node from database is null.", new Throwable("message throwable")); + Assert.assertEquals(ResponseCode.SERVER_ERROR, exception.getResponseCode()); + } +} diff --git a/platform-core/platform-telemetry/pom.xml b/platform-core/platform-telemetry/pom.xml index ffd283ccc..5a0dd9ceb 100644 --- a/platform-core/platform-telemetry/pom.xml +++ b/platform-core/platform-telemetry/pom.xml @@ -37,12 +37,38 @@ org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + 3.8.1 - 1.8 - 1.8 + 11 + + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + report-aggregate + verify + + report-aggregate + + + + diff --git a/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryManager.java b/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryManager.java index 9edf6eb00..211cc2e46 100644 --- a/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryManager.java +++ b/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryManager.java @@ -226,4 +226,10 @@ private static String getContextValue(String key, String defaultValue) { // TODO: refactor this. return defaultValue; } + + public static void logRequestBody(String message) { + Map context = getContext(); + String event = TelemetryGenerator.log(context, "payload", Level.INFO.name(), message, null, null); + telemetryHandler.send(event, Level.INFO, true); + } } diff --git a/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/TelemetryGeneratorTest.java b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/TelemetryGeneratorTest.java new file mode 100644 index 000000000..0d5bc11d6 --- /dev/null +++ b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/TelemetryGeneratorTest.java @@ -0,0 +1,217 @@ +package org.sunbird.telemetry; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.sunbird.common.JsonUtils; +import org.sunbird.telemetry.handler.Level; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TelemetryGeneratorTest { + + @BeforeClass + public static void init() { + + } + + @Test + public void testAccessTelemetry() throws Exception { + String accessLog = TelemetryGenerator.access(getContext(), getParams()); + Assert.assertNotNull(accessLog); + Map accessMap = JsonUtils.deserialize(accessLog, Map.class); + Assert.assertEquals(accessMap.get("eid"), "LOG"); + Assert.assertTrue(accessMap.get("ets") instanceof Long); + Assert.assertEquals(accessMap.get("ver"), "3.0"); + Assert.assertTrue(StringUtils.startsWith((String) accessMap.get("mid"), "LP.")); + Assert.assertEquals(((Map) accessMap.get("actor")).get("id"), "org.sunbird.learning.platform"); + Assert.assertEquals(((Map) accessMap.get("actor")).get("type"), "System"); + Assert.assertEquals(((Map) accessMap.get("context")).get("channel"), "TEST_CHANNEL"); + Assert.assertEquals(((Map) accessMap.get("context")).get("env"), "TEST_ENV"); + Assert.assertEquals(((Map) accessMap.get("context")).get("sid"), "37948134149401"); + Assert.assertEquals(((Map) accessMap.get("context")).get("did"), "mac"); + Assert.assertEquals(((Map) accessMap.get("edata")).get("level"), "INFO"); + Assert.assertEquals(((Map) accessMap.get("edata")).get("type"), "api_access"); + Assert.assertNotNull(accessMap.get("syncts")); + } + + @Test + public void testLog_1() throws Exception { + String event = TelemetryGenerator.log(getContext(), "payload", + Level.INFO.name(), "This is an info log", "1234", + getParams()); + Assert.assertNotNull(event); + Map eventMap = JsonUtils.deserialize(event, Map.class); + Assert.assertEquals(eventMap.get("eid"), "LOG"); + Assert.assertTrue(eventMap.get("ets") instanceof Long); + Assert.assertEquals(eventMap.get("ver"), "3.0"); + Assert.assertTrue(StringUtils.startsWith((String) eventMap.get("mid"), "LP.")); + Assert.assertEquals(((Map) eventMap.get("actor")).get("id"), "org.sunbird.learning.platform"); + Assert.assertEquals(((Map) eventMap.get("actor")).get("type"), "System"); + Assert.assertEquals(((Map) eventMap.get("context")).get("channel"), "TEST_CHANNEL"); + Assert.assertEquals(((Map) eventMap.get("context")).get("env"), "TEST_ENV"); + Assert.assertEquals(((Map) eventMap.get("context")).get("sid"), "37948134149401"); + Assert.assertEquals(((Map) eventMap.get("context")).get("did"), "mac"); + Assert.assertEquals(((Map) eventMap.get("edata")).get("level"), "INFO"); + Assert.assertEquals(((Map) eventMap.get("edata")).get("type"), "payload"); + Assert.assertNotNull(eventMap.get("syncts")); + } + + @Test + public void testLog_2() throws Exception { + String event = TelemetryGenerator.log(getContext(), "payload", + Level.INFO.name(), "This is an info log"); + Assert.assertNotNull(event); + Map eventMap = JsonUtils.deserialize(event, Map.class); + Assert.assertEquals(eventMap.get("eid"), "LOG"); + Assert.assertTrue(eventMap.get("ets") instanceof Long); + Assert.assertEquals(eventMap.get("ver"), "3.0"); + Assert.assertTrue(StringUtils.startsWith((String) eventMap.get("mid"), "LP.")); + Assert.assertEquals(((Map) eventMap.get("actor")).get("id"), "org.sunbird.learning.platform"); + Assert.assertEquals(((Map) eventMap.get("actor")).get("type"), "System"); + Assert.assertEquals(((Map) eventMap.get("context")).get("channel"), "TEST_CHANNEL"); + Assert.assertEquals(((Map) eventMap.get("context")).get("env"), "TEST_ENV"); + Assert.assertEquals(((Map) eventMap.get("context")).get("sid"), "37948134149401"); + Assert.assertEquals(((Map) eventMap.get("context")).get("did"), "mac"); + Assert.assertEquals(((Map) eventMap.get("edata")).get("level"), "INFO"); + Assert.assertEquals(((Map) eventMap.get("edata")).get("type"), "payload"); + Assert.assertNotNull(eventMap.get("syncts")); + } + + @Test + public void testError_1() throws Exception { + String event = TelemetryGenerator.error(getContext(), "ERR_INVALID_DATA", + Level.ERROR.name(), getStacktrace()); + Assert.assertNotNull(event); + Map eventMap = JsonUtils.deserialize(event, Map.class); + Assert.assertEquals(eventMap.get("eid"), "ERROR"); + Assert.assertTrue(eventMap.get("ets") instanceof Long); + Assert.assertEquals(eventMap.get("ver"), "3.0"); + Assert.assertTrue(StringUtils.startsWith((String) eventMap.get("mid"), "LP.")); + Assert.assertEquals(((Map) eventMap.get("actor")).get("id"), "org.sunbird.learning.platform"); + Assert.assertEquals(((Map) eventMap.get("actor")).get("type"), "System"); + Assert.assertEquals(((Map) eventMap.get("context")).get("channel"), "TEST_CHANNEL"); + Assert.assertEquals(((Map) eventMap.get("context")).get("env"), "TEST_ENV"); + Assert.assertEquals(((Map) eventMap.get("context")).get("sid"), "37948134149401"); + Assert.assertEquals(((Map) eventMap.get("context")).get("did"), "mac"); + Assert.assertEquals(((Map) eventMap.get("edata")).get("err"), "ERR_INVALID_DATA"); + Assert.assertEquals(((Map) eventMap.get("edata")).get("errtype"), "ERROR"); + Assert.assertNotNull(eventMap.get("syncts")); + } + + @Test + public void testError_2() throws Exception { + String event = TelemetryGenerator.error(getContext(), "ERR_INVALID_DATA", + Level.ERROR.name(), getStacktrace(), "1234", Arrays.asList("object")); + Assert.assertNotNull(event); + Map eventMap = JsonUtils.deserialize(event, Map.class); + Assert.assertEquals(eventMap.get("eid"), "ERROR"); + Assert.assertTrue(eventMap.get("ets") instanceof Long); + Assert.assertEquals(eventMap.get("ver"), "3.0"); + Assert.assertTrue(StringUtils.startsWith((String) eventMap.get("mid"), "LP.")); + Assert.assertEquals(((Map) eventMap.get("actor")).get("id"), "org.sunbird.learning.platform"); + Assert.assertEquals(((Map) eventMap.get("actor")).get("type"), "System"); + Assert.assertEquals(((Map) eventMap.get("context")).get("channel"), "TEST_CHANNEL"); + Assert.assertEquals(((Map) eventMap.get("context")).get("env"), "TEST_ENV"); + Assert.assertEquals(((Map) eventMap.get("context")).get("sid"), "37948134149401"); + Assert.assertEquals(((Map) eventMap.get("context")).get("did"), "mac"); + Assert.assertEquals(((Map) eventMap.get("edata")).get("err"), "ERR_INVALID_DATA"); + Assert.assertEquals(((Map) eventMap.get("edata")).get("errtype"), "ERROR"); + Assert.assertNotNull(eventMap.get("syncts")); + } + + @Test + public void testAudit_1() throws Exception { + String event = TelemetryGenerator.audit(getContext(), Arrays.asList("identifier", "status"), "Review", "Draft"); + Assert.assertNotNull(event); + Map eventMap = JsonUtils.deserialize(event, Map.class); + Assert.assertEquals(eventMap.get("eid"), "AUDIT"); + Assert.assertTrue(eventMap.get("ets") instanceof Long); + Assert.assertEquals(eventMap.get("ver"), "3.0"); + Assert.assertTrue(StringUtils.startsWith((String) eventMap.get("mid"), "LP.")); + Assert.assertEquals(((Map) eventMap.get("actor")).get("id"), "org.sunbird.learning.platform"); + Assert.assertEquals(((Map) eventMap.get("actor")).get("type"), "System"); + Assert.assertEquals(((Map) eventMap.get("context")).get("channel"), "TEST_CHANNEL"); + Assert.assertEquals(((Map) eventMap.get("context")).get("env"), "TEST_ENV"); + Assert.assertEquals(((Map) eventMap.get("context")).get("sid"), "37948134149401"); + Assert.assertEquals(((Map) eventMap.get("context")).get("did"), "mac"); + Assert.assertNotNull(((Map) eventMap.get("edata")).get("duration")); + Assert.assertNotNull(eventMap.get("syncts")); + } + + @Test + public void testAudit_2() throws Exception { + String event = TelemetryGenerator.audit(getContext(), Arrays.asList("identifier", "status"), "Review", "Draft", getCdata()); + Assert.assertNotNull(event); + Map eventMap = JsonUtils.deserialize(event, Map.class); + Assert.assertEquals(eventMap.get("eid"), "AUDIT"); + Assert.assertTrue(eventMap.get("ets") instanceof Long); + Assert.assertEquals(eventMap.get("ver"), "3.0"); + Assert.assertTrue(StringUtils.startsWith((String) eventMap.get("mid"), "LP.")); + Assert.assertEquals(((Map) eventMap.get("actor")).get("id"), "org.sunbird.learning.platform"); + Assert.assertEquals(((Map) eventMap.get("actor")).get("type"), "System"); + Assert.assertEquals(((Map) eventMap.get("context")).get("channel"), "TEST_CHANNEL"); + Assert.assertEquals(((Map) eventMap.get("context")).get("env"), "TEST_ENV"); + Assert.assertEquals(((Map) eventMap.get("context")).get("sid"), "37948134149401"); + Assert.assertEquals(((Map) eventMap.get("context")).get("did"), "mac"); + Assert.assertNotNull(((Map) eventMap.get("edata")).get("duration")); + Assert.assertNotNull(eventMap.get("syncts")); + } + + + private Map getContext() { + return new HashMap<>() {{ + put(TelemetryParams.ENV.name(), "TEST_ENV"); + put(TelemetryParams.CHANNEL.name(), "TEST_CHANNEL"); + put("sid", "37948134149401"); + put("did", "mac"); + put(TelemetryParams.APP_ID.name(), "mac-app"); + put("duration", "318361274"); + put("objectId", "CONTENT"); + put("objectType", "Content"); + put("pkgVersion", "2"); + }}; + } + + private Map getParams() { + return new HashMap<>() {{ + put("identifier", "do_1234"); + put("status", "Draft"); + put("versionKey", "37948134149401"); + put("code", "mac-9319"); + put("contentType", "Resource"); + }}; + } + + private String getStacktrace() { + return "java.lang.Throwable: A test exception\n" + + " at com.stackify.stacktrace.StackElementExample.methodD(StackElementExample.java:23)\n" + + " at com.stackify.stacktrace.StackElementExample.methodC(StackElementExample.java:15)\n" + + " at com.stackify.stacktrace.StackElementExampleTest\n" + + " .whenElementOneIsReadUsingThrowable_thenMethodCatchingThrowableIsObtained(StackElementExampleTest.java:34)"; + } + + private List> getCdata() { + return new ArrayList>() { + { + add(new HashMap<>() { + { + put("identifier", "do_1234"); + put("status", "Draft"); + put("versionKey", "37948134149401"); + put("code", "mac-9319"); + put("contentType", "Resource"); + } + }); + + } + }; + } + + +} diff --git a/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/logger/TestTelemetryManager.java b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/logger/TestTelemetryManager.java new file mode 100644 index 000000000..638af6b72 --- /dev/null +++ b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/logger/TestTelemetryManager.java @@ -0,0 +1,75 @@ +package org.sunbird.telemetry.logger; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.sunbird.common.exception.MiddlewareException; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TestTelemetryManager { + + @Test + public void testError() { + Throwable e = new MiddlewareException("500", "message"); + Object object = new Object(); + TelemetryManager.error("message", e, object); + } + + @Test + public void testAudit() { + List props = new ArrayList<>(); + TelemetryManager.audit("id", "type", props, "state", "prevState"); + } + + @Test + public void testSearch() { + Map context = new HashMap<>(); + Object filters = new Object(); + Object sort = new Object(); + Object topN = new Object(); + TelemetryManager.search(context, "query", filters, sort, 1, topN, "type"); + } + + @Test + public void testSearchWithContextData() { + Map context = new HashMap<>() {{ + put("DEVICE_ID", "testdid"); + put("objectId", "testobjectId"); + put("objectType", "content"); + }}; + Object filters = new Object(); + Object sort = new Object(); + Object topN = new Object(); + TelemetryManager.search(context, "query", filters, sort, 1, topN, "type"); + } + + @Test + public void testLog() throws Exception { + Method method = TelemetryManager.class.getDeclaredMethod("log", String.class, Map.class, String.class); + method.setAccessible(true); + Map params = new HashMap<>(); + method.invoke(null, "message", params, "INFO"); + } + + @Test + public void testGetContext() throws Exception { + Method method = TelemetryManager.class.getDeclaredMethod("getContext"); + method.setAccessible(true); + Map context = (Map) method.invoke(null); + Assert.assertTrue(MapUtils.isNotEmpty(context)); + Assert.assertTrue(StringUtils.equals((String) context.get("ACTOR"), "org.sunbird.learning.platform")); + Assert.assertTrue(StringUtils.equals((String) context.get("CHANNEL"), "in.ekstep")); + Assert.assertTrue(StringUtils.equals((String) context.get("ENV"), "system")); + } + + @Test + public void testLogRequestBody() { + TelemetryManager.logRequestBody("message"); + } +} diff --git a/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestLogAsyncGraphEvent.java b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestLogAsyncGraphEvent.java new file mode 100644 index 000000000..68087934e --- /dev/null +++ b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestLogAsyncGraphEvent.java @@ -0,0 +1,21 @@ +package org.sunbird.telemetry.util; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TestLogAsyncGraphEvent { + + @Test + public void testPushMessageToLogger() { + List> messages = new ArrayList>() {{ + add(new HashMap<>() {{ + put("message", "test"); + }}); + }}; + LogAsyncGraphEvent.pushMessageToLogger(messages); + } +} diff --git a/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestLogTelemetryEventUtil.java b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestLogTelemetryEventUtil.java new file mode 100644 index 000000000..3f9c33ef6 --- /dev/null +++ b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestLogTelemetryEventUtil.java @@ -0,0 +1,79 @@ +package org.sunbird.telemetry.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.sunbird.common.JsonUtils; +import org.sunbird.common.dto.Request; +import org.sunbird.common.dto.RequestParams; +import org.sunbird.telemetry.dto.TelemetryBEEvent; + +import java.util.HashMap; +import java.util.Map; + +public class TestLogTelemetryEventUtil { + + ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testLogInstructionEvent() throws Exception { + Map actor = new HashMap<>(); + Map context = new HashMap<>(); + Map object = new HashMap<>(); + Map edata = new HashMap<>(); + String jsonMessage = LogTelemetryEventUtil.logInstructionEvent(actor, context, object, edata); + Assert.assertTrue(StringUtils.isNotEmpty(jsonMessage)); + Assert.assertTrue(StringUtils.isNotEmpty((String) mapper.readValue(jsonMessage, Map.class).get("mid"))); + } + + @Test + public void testLogContentSearchEvent() throws Exception { + String query = ""; + Object filters = new HashMap<>(); + Object sort = new HashMap<>(); + String correlationId = ""; + int size = 0; + Request req = getReq(); + String jsonMessage = LogTelemetryEventUtil.logContentSearchEvent(query, filters, sort, correlationId, size, req); + Assert.assertTrue(StringUtils.isNotEmpty(jsonMessage)); + Assert.assertTrue(StringUtils.isNotEmpty((String) mapper.readValue(jsonMessage, Map.class).get("mid"))); + } + + @Test + public void testLogContentSearchEventWithEmptyValue() throws Exception { + String query = ""; + Object filters = new Object(); + Object sort = new Object(); + String correlationId = ""; + int size = 0; + Request req = getReqWithoutDid(); + String jsonMessage = LogTelemetryEventUtil.logContentSearchEvent(query, filters, sort, correlationId, size, req); + Assert.assertTrue(StringUtils.isEmpty(jsonMessage)); + } + + @Test + public void testGetMD5Hash() throws Exception { + TelemetryBEEvent event = new TelemetryBEEvent() {{ + setEid("test"); + setEts(12345L); + }}; + Map data = new HashMap<>() {{ + putAll(JsonUtils.deserialize("{\"id\":\"testid\",\"state\":\"teststate\",\"prevstate\":\"testprevstate\"}", Map.class)); + }}; + String messageId = LogTelemetryEventUtil.getMD5Hash(event, data); + Assert.assertTrue(StringUtils.isNotEmpty(messageId)); + } + + private Request getReq() throws Exception { + return new Request() {{ + setParams(JsonUtils.convert(JsonUtils.deserialize("{\"cid\":\"testcid\",\"uid\":\"testuid\",\"sid\":\"testsid\",\"did\":\"testdid\",\"sid\":\"testsid\"}", Map.class), RequestParams.class)); + }}; + } + + private Request getReqWithoutDid() throws Exception { + return new Request() {{ + setParams(JsonUtils.convert(JsonUtils.deserialize("{\"cid\":\"testcid\",\"uid\":\"testuid\",\"sid\":\"testsid\",\"sid\":\"testsid\"}", Map.class), RequestParams.class)); + }}; + } +} diff --git a/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestTelemetryAccessEventUtil.java b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestTelemetryAccessEventUtil.java new file mode 100644 index 000000000..2369d689a --- /dev/null +++ b/platform-core/platform-telemetry/src/test/java/org/sunbird/telemetry/util/TestTelemetryAccessEventUtil.java @@ -0,0 +1,51 @@ +package org.sunbird.telemetry.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.sunbird.common.JsonUtils; +import org.sunbird.common.dto.Request; +import org.sunbird.common.dto.RequestParams; +import org.sunbird.common.dto.Response; + +import java.util.HashMap; +import java.util.Map; + +public class TestTelemetryAccessEventUtil { + + ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testWriteTelemetryEventLog() throws Exception { + Map data = getDataMap(); + TelemetryAccessEventUtil.writeTelemetryEventLog(data); + } + + @Test + public void testWriteTelemetryEventLogWithHeaders() throws Exception { + Map data = getDataMapWithHeaders(); + TelemetryAccessEventUtil.writeTelemetryEventLog(data); + } + + private Map getDataMap() throws Exception { + Map data = new HashMap() {{ + put("Request", new Request() {{ + setParams(JsonUtils.convert(JsonUtils.deserialize("{\"cid\":\"testcid\",\"uid\":\"testuid\",\"sid\":\"testsid\",\"did\":\"testdid\",\"sid\":\"testsid\"}", Map.class), RequestParams.class)); + }}); + put("Response", new Response() {{ + setId("ekstep.learning.categoryinstance.read"); + }}); + putAll(mapper.readValue("{\"path\":\"/content/v3/create\",\"RemoteAddress\":\"0:0:0:0:0:0:0:1\",\"Method\":\"POST\",\"X-Channel-Id\":\"in.ekstep\",\"Protocol\":\"http\",\"env\":\"content\",\"Status\":500}", Map.class)); + put("ContentLength", 307L); + put("StartTime", 1596620754800L); + put("APP_ID", "app-id"); + }}; + return data; + } + + private Map getDataMapWithHeaders() throws Exception { + Map data = getDataMap(); + data.putAll(mapper.readValue("{\"X-Session-ID\":\"testsid\",\"X-Consumer-ID\":\"testcid\",\"X-Device-ID\":\"testdid\",\"X-Authenticated-Userid\":\"testuid\"}", Map.class)); + ((Response) data.get("Response")).setId("test"); + return data; + } +} diff --git a/platform-core/pom.xml b/platform-core/pom.xml index 8248086f2..915ae188d 100755 --- a/platform-core/pom.xml +++ b/platform-core/pom.xml @@ -17,6 +17,7 @@ schema-validator platform-cache cassandra-connector + kafka-client @@ -25,11 +26,10 @@ commons-lang3 3.9 - junit junit - 4.4 + 4.13.1 test @@ -40,17 +40,25 @@ maven-assembly-plugin - 2.3 + 3.3.0 src/assembly/bin.xml + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + + org.jacoco jacoco-maven-plugin - 0.7.9 + 0.8.5 default-prepare-agent diff --git a/platform-core/schema-validator/pom.xml b/platform-core/schema-validator/pom.xml index e17e4bb24..42ebafdfb 100644 --- a/platform-core/schema-validator/pom.xml +++ b/platform-core/schema-validator/pom.xml @@ -33,6 +33,18 @@ platform-common 1.0-SNAPSHOT + + org.powermock + powermock-api-mockito + 1.7.4 + test + + + org.powermock + powermock-module-junit4 + 1.7.4 + test + @@ -40,12 +52,31 @@ org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + 3.8.1 - 1.8 - 1.8 + 11 + + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + \ No newline at end of file diff --git a/platform-core/schema-validator/src/main/java/org/sunbird/schema/ISchemaValidator.java b/platform-core/schema-validator/src/main/java/org/sunbird/schema/ISchemaValidator.java index 52d08f46a..d3e7ea83e 100644 --- a/platform-core/schema-validator/src/main/java/org/sunbird/schema/ISchemaValidator.java +++ b/platform-core/schema-validator/src/main/java/org/sunbird/schema/ISchemaValidator.java @@ -15,5 +15,7 @@ public interface ISchemaValidator { Config getConfig(); List getJsonProps(); + + List getAllProps(); } diff --git a/platform-core/schema-validator/src/main/java/org/sunbird/schema/formatter/UrlFormatter.java b/platform-core/schema-validator/src/main/java/org/sunbird/schema/formatter/UrlFormatter.java index 9adf92240..ac26afde3 100644 --- a/platform-core/schema-validator/src/main/java/org/sunbird/schema/formatter/UrlFormatter.java +++ b/platform-core/schema-validator/src/main/java/org/sunbird/schema/formatter/UrlFormatter.java @@ -21,7 +21,6 @@ public InstanceType valueType() { @Override public boolean test(JsonValue value) { - System.out.println("Validating Url..."); String str = ((JsonString) value).getString(); try { //TODO: Change it to Head Call. diff --git a/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/BaseSchemaValidator.java b/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/BaseSchemaValidator.java index ae0653da3..79e3ebf5e 100644 --- a/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/BaseSchemaValidator.java +++ b/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/BaseSchemaValidator.java @@ -87,7 +87,6 @@ public ValidationResult getStructuredData(Map input) { } public ValidationResult validate(Map data) throws Exception { - String dataWithDefaults = withDefaultValues(JsonUtils.serialize(data)); Map validationDataWithDefaults = cleanEmptyKeys(JsonUtils.deserialize(dataWithDefaults, Map.class)); @@ -139,7 +138,7 @@ private Map getExternalProps(Map input) { Map externalData = new HashMap<>(); if (config != null && config.hasPath("external.properties")) { Set extProps = config.getObject("external.properties").keySet(); - externalData = input.entrySet().stream().filter(f -> extProps.contains(f.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + externalData = input.entrySet().stream().filter(f -> extProps.contains(f.getKey()) && f.getValue()!=null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); input.keySet().removeAll(extProps); } return externalData; @@ -168,11 +167,33 @@ public List getJsonProps() { return ((Map) (new ObjectMapper().readValue(((BasicJsonSchema) schema).get("properties") .getValueAsJson().asJsonObject().toString(), Map.class))).entrySet().stream().filter(entry -> StringUtils.equalsIgnoreCase("object", (String) ((Map) entry.getValue()).get("type")) || - (null!=((Map) entry.getValue()).get("items") && StringUtils.equalsIgnoreCase("object", (String) ((Map) ((Map) entry.getValue()).get("items")).get("type"))) + (null!=((Map) entry.getValue()).get("items") && StringUtils.equalsIgnoreCase("object", (String) ((Map) ((Map) entry.getValue()).get("items")).get("type")) + || (null!=((Map) entry.getValue()).get("items") && StringUtils.equalsIgnoreCase("string", (String) ((Map) ((Map) entry.getValue()).get("items")).get("type")))) ).map(entry -> entry.getKey()).collect(Collectors.toList()); } catch (IOException e) { e.printStackTrace(); } return new ArrayList<>(); } + + public List getAllProps() { + List propsList = new ArrayList<>(); + try { + propsList.addAll(((Map) (new ObjectMapper().readValue(((BasicJsonSchema) schema).get("properties") + .getValueAsJson().asJsonObject().toString(), Map.class))).keySet()); + + if(null != config && config.hasPath("external.properties")) + propsList.addAll(config.getObject("external.properties").keySet()); + + if(null != config && config.hasPath("relations")) + propsList.addAll(config.getObject("relations").keySet()); + + if(null != config && config.hasPath("edge.properties")) + propsList.addAll(config.getObject("edge.properties").keySet()); + propsList.addAll(Arrays.asList("objectType", "identifier", "languageCode")); + } catch (IOException e) { + e.printStackTrace(); + } + return propsList; + } } diff --git a/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/CustomProblemHandler.java b/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/CustomProblemHandler.java index a96abad9c..ef1c4b741 100644 --- a/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/CustomProblemHandler.java +++ b/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/CustomProblemHandler.java @@ -3,7 +3,6 @@ import org.apache.commons.lang3.StringUtils; import org.leadpony.justify.api.Problem; import org.leadpony.justify.api.ProblemHandler; -import scala.collection.immutable.StringOps; import java.util.ArrayList; import java.util.Arrays; @@ -34,7 +33,8 @@ protected List getProblemMessages() { } private String processMessage(Problem problem) { - switch (problem.getKeyword()) { + String keyword = StringUtils.isNotBlank(problem.getKeyword()) ? problem.getKeyword() : "additionalProp"; + switch (keyword) { case "enum": return ("Metadata " + Arrays.stream(problem.getPointer().split("/")) .filter(StringUtils::isNotBlank) @@ -42,9 +42,14 @@ private String processMessage(Problem problem) { + " should be one of: " + problem.parametersAsMap().get("expected")).replace("\"", ""); case "required": + String param; + if (StringUtils.isNotBlank(problem.getPointer())) { + param = problem.getPointer().replaceAll("/", "") + "." + problem.parametersAsMap().get(problem.getKeyword()); + } else { + param = problem.parametersAsMap().get(problem.getKeyword()).toString(); + } return "Required Metadata " - + problem.parametersAsMap().get(problem.getKeyword()) - .toString().replace("\"", "") + + param.replace("\"", "") + " not set"; case "type": { return ("Metadata " + Arrays.stream(problem.getPointer().split("/")) @@ -54,6 +59,20 @@ private String processMessage(Problem problem) { + StringUtils.capitalize(((Enum) problem.parametersAsMap().get("expected")).name().toLowerCase())).replace("\"", "") + " value"; } + case "additionalProp": { + return ("Metadata " + Arrays.stream(problem.getPointer().split("/")) + .filter(StringUtils::isNotBlank) + .findFirst().get() + + " cannot have new property with name " + + ((String) problem.parametersAsMap().get("name")).replace("\"", "")); + } + case "format": { + return ("Incorrect format for " + Arrays.stream(problem.getPointer().split("/")) + .filter(StringUtils::isNotBlank) + .findFirst().orElse("") + + " : " + + problem.getMessage()); + } default: return ""; } diff --git a/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/JsonSchemaValidator.java b/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/JsonSchemaValidator.java index 9b40c681d..13b7bcc2f 100644 --- a/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/JsonSchemaValidator.java +++ b/platform-core/schema-validator/src/main/java/org/sunbird/schema/impl/JsonSchemaValidator.java @@ -20,7 +20,7 @@ public class JsonSchemaValidator extends BaseSchemaValidator { public JsonSchemaValidator(String name, String version) throws Exception { super(name, version); - basePath = basePath + name.toLowerCase() + "/" + version + "/"; + basePath = basePath + File.separator + name.toLowerCase() + File.separator + version + File.separator; loadSchema(); loadConfig(); } diff --git a/platform-core/schema-validator/src/test/java/org/sunbird/schema/TestSchemaValidatorFactory.java b/platform-core/schema-validator/src/test/java/org/sunbird/schema/TestSchemaValidatorFactory.java new file mode 100644 index 000000000..41edc65e2 --- /dev/null +++ b/platform-core/schema-validator/src/test/java/org/sunbird/schema/TestSchemaValidatorFactory.java @@ -0,0 +1,61 @@ +package org.sunbird.schema; + +import com.typesafe.config.ConfigException; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.sunbird.common.exception.ServerException; + +import javax.json.JsonException; +import java.util.Arrays; +import java.util.List; + +public class TestSchemaValidatorFactory { + + + @BeforeClass + public static void init() { + + } + + @Test + public void testGetInstance() throws Exception { + Assert.assertNotNull(SchemaValidatorFactory.getInstance("content", "1.0")); + } + + @Test (expected = JsonException.class) + public void testGetInstanceInvalidSchema() throws Exception { + SchemaValidatorFactory.getInstance("content", "2.0"); + } + + + @Test (expected = JsonException.class) + public void testGetInstanceInvalidFallbackSchema() throws Exception { + SchemaValidatorFactory.getInstance("content", "3.0", "2.0"); + } + + @Test + public void getExternalStoreName() throws Exception { + String actual = SchemaValidatorFactory.getExternalStoreName("content", "1.0"); + Assert.assertNotNull(actual); + Assert.assertEquals(actual, "content_store.content_data"); + } + + @Test(expected = ServerException.class) + public void getExternalStoreNameInvalid() throws Exception { + SchemaValidatorFactory.getExternalStoreName("category", "1.0"); + } + + @Test + public void getExternalPrimaryKey() throws Exception{ + List actual = SchemaValidatorFactory.getExternalPrimaryKey("content", "1.0"); + Assert.assertNotNull(actual); + Assert.assertEquals(actual, Arrays.asList("content_id")); + } + + @Test(expected = ConfigException.class) + public void getExternalPrimaryKeyInvalid() throws Exception{ + SchemaValidatorFactory.getExternalPrimaryKey("category", "1.0"); + } + +} diff --git a/platform-core/schema-validator/src/test/java/org/sunbird/schema/impl/TestBaseSchemaValidator.java b/platform-core/schema-validator/src/test/java/org/sunbird/schema/impl/TestBaseSchemaValidator.java index a399d2f8a..d72b11025 100644 --- a/platform-core/schema-validator/src/test/java/org/sunbird/schema/impl/TestBaseSchemaValidator.java +++ b/platform-core/schema-validator/src/test/java/org/sunbird/schema/impl/TestBaseSchemaValidator.java @@ -1,17 +1,32 @@ package org.sunbird.schema.impl; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigObject; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.junit.Assert; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.reflect.Whitebox; import org.sunbird.schema.ISchemaValidator; import org.sunbird.schema.SchemaValidatorFactory; +import org.sunbird.schema.dto.ValidationResult; -import java.util.List; +import java.lang.reflect.Method; +import java.util.*; +@PrepareForTest({Config.class}) public class TestBaseSchemaValidator { static ISchemaValidator validator; + ObjectMapper mapper = new ObjectMapper(); + private BaseSchemaValidator baseSchemaValidator; @BeforeClass public static void init(){ @@ -22,6 +37,11 @@ public static void init(){ } } + @Before + public void setup(){ + MockitoAnnotations.initMocks(this); + } + @Test public void testGetJsonProps() { try{ @@ -32,4 +52,53 @@ public void testGetJsonProps() { e.printStackTrace(); } } + + @Test + public void testGetStructuredData() throws Exception { + mockClass(); + Mockito.doCallRealMethod().when(baseSchemaValidator).getStructuredData(getInput()); + ValidationResult result = baseSchemaValidator.getStructuredData(getInput()); + Assert.assertTrue(MapUtils.isNotEmpty(result.getMetadata())); + Assert.assertTrue(StringUtils.equals((String) result.getMetadata().get("name"), "c-12")); + } + + @Test + public void testCleanEmptyKeys() throws Exception { + mockClass(); + Method cleanEmptyKeys = BaseSchemaValidator.class.getDeclaredMethod("cleanEmptyKeys", Map.class); + cleanEmptyKeys.setAccessible(true); + Map inputMap = getInput(); + inputMap.put("emptyString", ""); + inputMap.put("emptyMap", new HashMap<>()); + Map resultMap = (Map) cleanEmptyKeys.invoke(baseSchemaValidator, inputMap); + Assert.assertTrue(MapUtils.isNotEmpty(resultMap)); + Assert.assertTrue(!resultMap.containsKey("emptyString")); + Assert.assertTrue(!resultMap.containsKey("emptyMap")); + } + + @Test + public void testGetAllProps() { + try{ + List jsonProps = validator.getAllProps(); + Assert.assertNotNull(jsonProps); + Assert.assertFalse(jsonProps.isEmpty()); + }catch(Exception e){ + e.printStackTrace(); + } + } + + public Map getInput() throws Exception { + Map input = mapper.readValue("{\"contentType\":\"Resource\",\"name\":\"c-12\",\"code\":\"c-12\",\"mimeType\":\"application/pdf\",\"tags\":[\"colors\",\"games\"],\"subject\":[\"Hindi\",\"English\"],\"medium\":[\"Hindi\",\"English\"],\"channel\":\"in.ekstep\",\"osId\":\"org.ekstep.quiz.app\",\"contentEncoding\":\"identity\",\"contentDisposition\":\"inline\"}", Map.class); + return input; + } + + public void mockClass() { + Config config = Mockito.mock(Config.class); + Mockito.when(config.hasPath("relations")).thenReturn(true); + baseSchemaValidator = Mockito.mock(BaseSchemaValidator.class); + Whitebox.setInternalState(baseSchemaValidator, "name", "version"); + Mockito.when(baseSchemaValidator.getConfig()).thenReturn(config); + ConfigObject obj = Mockito.mock(ConfigObject.class); + Mockito.when(baseSchemaValidator.getConfig().getObject("relations")).thenReturn(obj); + } } diff --git a/platform-core/schema-validator/src/test/resources/application.conf b/platform-core/schema-validator/src/test/resources/application.conf index d90b695df..497ded178 100644 --- a/platform-core/schema-validator/src/test/resources/application.conf +++ b/platform-core/schema-validator/src/test/resources/application.conf @@ -1 +1,489 @@ -schema.base_path = "../../schemas/" \ No newline at end of file +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +# ~~~~~ +# Play uses HOCON as its configuration file format. HOCON has a number +# of advantages over other config formats, but there are two things that +# can be used when modifying settings. +# +# You can include other configuration files in this main application.conf file: +#include "extra-config.conf" +# +# You can declare variables and substitute for them: +#mykey = ${some.value} +# +# And if an environment variable exists when there is no other substitution, then +# HOCON will fall back to substituting environment variable: +#mykey = ${JAVA_HOME} + +## Akka +# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration +# https://www.playframework.com/documentation/latest/JavaAkka#Configuration +# ~~~~~ +# Play uses Akka internally and exposes Akka Streams and actors in Websockets and +# other streaming HTTP responses. +akka { + # "akka.log-config-on-start" is extraordinarly useful because it log the complete + # configuration at INFO level, including defaults and overrides, so it s worth + # putting at the very top. + # + # Put the following in your conf/logback.xml file: + # + # + # + # And then uncomment this line to debug the configuration. + # + #log-config-on-start = true +} + +## Secret key +# http://www.playframework.com/documentation/latest/ApplicationSecret +# ~~~~~ +# The secret key is used to sign Play's session cookie. +# This must be changed for production, but we don't recommend you change it in this file. +play.http.secret.key = a-long-secret-to-calm-the-rage-of-the-entropy-gods + +## Modules +# https://www.playframework.com/documentation/latest/Modules +# ~~~~~ +# Control which modules are loaded when Play starts. Note that modules are +# the replacement for "GlobalSettings", which are deprecated in 2.5.x. +# Please see https://www.playframework.com/documentation/latest/GlobalSettings +# for more information. +# +# You can also extend Play functionality by using one of the publically available +# Play modules: https://playframework.com/documentation/latest/ModuleDirectory +play.modules { + # By default, Play will load any class called Module that is defined + # in the root package (the "app" directory), or you can define them + # explicitly below. + # If there are any built-in modules that you want to enable, you can list them here. + #enabled += my.application.Module + + # If there are any built-in modules that you want to disable, you can list them here. + #disabled += "" +} + +## IDE +# https://www.playframework.com/documentation/latest/IDE +# ~~~~~ +# Depending on your IDE, you can add a hyperlink for errors that will jump you +# directly to the code location in the IDE in dev mode. The following line makes +# use of the IntelliJ IDEA REST interface: +#play.editor="http://localhost:63342/api/file/?file=%s&line=%s" + +## Internationalisation +# https://www.playframework.com/documentation/latest/JavaI18N +# https://www.playframework.com/documentation/latest/ScalaI18N +# ~~~~~ +# Play comes with its own i18n settings, which allow the user's preferred language +# to map through to internal messages, or allow the language to be stored in a cookie. +play.i18n { + # The application languages + langs = [ "en" ] + + # Whether the language cookie should be secure or not + #langCookieSecure = true + + # Whether the HTTP only attribute of the cookie should be set to true + #langCookieHttpOnly = true +} + +## Play HTTP settings +# ~~~~~ +play.http { + ## Router + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # Define the Router object to use for this application. + # This router will be looked up first when the application is starting up, + # so make sure this is the entry point. + # Furthermore, it's assumed your route file is named properly. + # So for an application router like `my.application.Router`, + # you may need to define a router file `conf/my.application.routes`. + # Default to Routes in the root package (aka "apps" folder) (and conf/routes) + #router = my.application.Router + + ## Action Creator + # https://www.playframework.com/documentation/latest/JavaActionCreator + # ~~~~~ + #actionCreator = null + + ## ErrorHandler + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # If null, will attempt to load a class called ErrorHandler in the root package, + #errorHandler = null + + ## Session & Flash + # https://www.playframework.com/documentation/latest/JavaSessionFlash + # https://www.playframework.com/documentation/latest/ScalaSessionFlash + # ~~~~~ + session { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + + # Sets the max-age field of the cookie to 5 minutes. + # NOTE: this only sets when the browser will discard the cookie. Play will consider any + # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, + # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. + #maxAge = 300 + + # Sets the domain on the session cookie. + #domain = "example.com" + } + + flash { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + } +} + +## Netty Provider +# https://www.playframework.com/documentation/latest/SettingsNetty +# ~~~~~ +play.server.netty { + # Whether the Netty wire should be logged + log.wire = true + + # If you run Play on Linux, you can use Netty's native socket transport + # for higher performance with less garbage. + transport = "native" +} + +## WS (HTTP Client) +# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS +# ~~~~~ +# The HTTP client primarily used for REST APIs. The default client can be +# configured directly, but you can also create different client instances +# with customized settings. You must enable this by adding to build.sbt: +# +# libraryDependencies += ws // or javaWs if using java +# +play.ws { + # Sets HTTP requests not to follow 302 requests + #followRedirects = false + + # Sets the maximum number of open HTTP connections for the client. + #ahc.maxConnectionsTotal = 50 + + ## WS SSL + # https://www.playframework.com/documentation/latest/WsSSL + # ~~~~~ + ssl { + # Configuring HTTPS with Play WS does not require programming. You can + # set up both trustManager and keyManager for mutual authentication, and + # turn on JSSE debugging in development with a reload. + #debug.handshake = true + #trustManager = { + # stores = [ + # { type = "JKS", path = "exampletrust.jks" } + # ] + #} + } +} + +## Cache +# https://www.playframework.com/documentation/latest/JavaCache +# https://www.playframework.com/documentation/latest/ScalaCache +# ~~~~~ +# Play comes with an integrated cache API that can reduce the operational +# overhead of repeated requests. You must enable this by adding to build.sbt: +# +# libraryDependencies += cache +# +play.cache { + # If you want to bind several caches, you can bind the individually + #bindCaches = ["db-cache", "user-cache", "session-cache"] +} + +## Filter Configuration +# https://www.playframework.com/documentation/latest/Filters +# ~~~~~ +# There are a number of built-in filters that can be enabled and configured +# to give Play greater security. +# +play.filters { + + # Enabled filters are run automatically against Play. + # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. + enabled = [] + + # Disabled filters remove elements from the enabled list. + # disabled += filters.CSRFFilter + + + ## CORS filter configuration + # https://www.playframework.com/documentation/latest/CorsFilter + # ~~~~~ + # CORS is a protocol that allows web applications to make requests from the browser + # across different domains. + # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has + # dependencies on CORS settings. + cors { + # Filter paths by a whitelist of path prefixes + #pathPrefixes = ["/some/path", ...] + + # The allowed origins. If null, all origins are allowed. + #allowedOrigins = ["http://www.example.com"] + + # The allowed HTTP methods. If null, all methods are allowed + #allowedHttpMethods = ["GET", "POST"] + } + + ## Security headers filter configuration + # https://www.playframework.com/documentation/latest/SecurityHeaders + # ~~~~~ + # Defines security headers that prevent XSS attacks. + # If enabled, then all options are set to the below configuration by default: + headers { + # The X-Frame-Options header. If null, the header is not set. + #frameOptions = "DENY" + + # The X-XSS-Protection header. If null, the header is not set. + #xssProtection = "1; mode=block" + + # The X-Content-Type-Options header. If null, the header is not set. + #contentTypeOptions = "nosniff" + + # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. + #permittedCrossDomainPolicies = "master-only" + + # The Content-Security-Policy header. If null, the header is not set. + #contentSecurityPolicy = "default-src 'self'" + } + + ## Allowed hosts filter configuration + # https://www.playframework.com/documentation/latest/AllowedHostsFilter + # ~~~~~ + # Play provides a filter that lets you configure which hosts can access your application. + # This is useful to prevent cache poisoning attacks. + hosts { + # Allow requests to example.com, its subdomains, and localhost:9000. + #allowed = [".example.com", "localhost:9000"] + } +} + +# Learning-Service Configuration +content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit"] + +# Cassandra Configuration +content.keyspace.name=content_store +content.keyspace.table=content_data +#TODO: Add Configuration for assessment. e.g: question_data +orchestrator.keyspace.name=script_store +orchestrator.keyspace.table=script_data +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" + +# Redis Configuration +redis.host=localhost +redis.port=6379 +redis.maxConnections=128 + +#Condition to enable publish locally +content.publish_task.enabled=true + +#directory location where store unzip file +dist.directory=/data/tmp/dist/ +output.zipfile=/data/tmp/story.zip +source.folder=/data/tmp/temp2/ +save.directory=/data/tmp/temp/ + +# Content 2 vec analytics URL +CONTENT_TO_VEC_URL="http://172.31.27.233:9000/content-to-vec" + +# FOR CONTENT WORKFLOW PIPELINE (CWP) + +#--Content Workflow Pipeline Mode +OPERATION_MODE=TEST + +#--Maximum Content Package File Size Limit in Bytes (50 MB) +MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=52428800 + +#--Maximum Asset File Size Limit in Bytes (20 MB) +MAX_ASSET_FILE_SIZE_LIMIT=20971520 + +#--No of Retry While File Download Fails +RETRY_ASSET_DOWNLOAD_COUNT=1 + +#Google-vision-API +google.vision.tagging.enabled = false + +#Orchestrator env properties +env="https://dev.ekstep.in/api/learning" + +#Current environment +cloud_storage.env=dev + + +#Folder configuration +cloud_storage.content.folder=content +cloud_storage.asset.folder=assets +cloud_storage.artefact.folder=artifact +cloud_storage.bundle.folder=bundle +cloud_storage.media.folder=media +cloud_storage.ecar.folder=ecar_files + +# Media download configuration +content.media.base.url="https://dev.open-sunbird.org" +plugin.media.base.url="https://dev.open-sunbird.org" + +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + +# Elasticsearch properties +search.es_conn_info="localhost:9200" +search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","domain","subject","description^10","keywords^25","ageGroup^10","filter^10","theme^10","genre^10","objects^25","contentType^100","language^200","teachingMode^25","skills^10","learningObjective^10","curriculum^100","gradeLevel^100","developer^100","attributions^10","owner^50","text","words","releaseNotes"] +search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] +search.batch.size=500 +search.connection.timeout=30 +platform-api-url="http://localhost:8080/language-service" +MAX_ITERATION_COUNT_FOR_SAMZA_JOB=2 + + +# DIAL Code Configuration +dialcode.keyspace.name="dialcode_store" +dialcode.keyspace.table="dial_code" +dialcode.max_count=1000 + +# System Configuration +system.config.keyspace.name="dialcode_store" +system.config.table="system_config" + +#Publisher Configuration +publisher.keyspace.name="dialcode_store" +publisher.keyspace.table="publisher" + +#DIAL Code Generator Configuration +dialcode.strip.chars="0" +dialcode.length=6.0 +dialcode.large.prime_number=1679979167 + +#DIAL Code ElasticSearch Configuration +dialcode.index=true +dialcode.object_type="DialCode" + +framework.max_term_creation_limit=200 + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + +# Kafka configuration details +kafka.topics.instruction="local.learning.job.request" +kafka.urls="localhost:9092" + +#Youtube Standard Licence Validation +learning.content.youtube.validate.license=true +learning.content.youtube.application.name=fetch-youtube-license +youtube.license.regex.pattern=["\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)","^([A-Za-z0-9\\-\\_]*)"] + +#Top N Config for Search Telemetry +telemetry_env=dev +telemetry.search.topn=5 + +installation.id=ekstep + + +channel.default="in.ekstep" + +# DialCode Link API Config +learning.content.link_dialcode_validation=true +dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/search" +dialcode.api.authorization=auth_key + +# Language-Code Configuration +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] + +# Kafka send event to topic enable +kafka.topic.send.enable=false + +learning.valid_license=["creativeCommon"] +learning.service_provider=["youtube"] + +stream.mime.type=video/mp4 +compositesearch.index.name="compositesearch" + +hierarchy.keyspace.name=hierarchy_store +content.hierarchy.table=content_hierarchy +framework.hierarchy.table=framework_hierarchy + +# Kafka topic for definition update event. +kafka.topic.system.command="dev.system.command" + +learning.reserve_dialcode.content_type=["TextBook"] +# restrict.metadata.objectTypes=["Content", "ContentImage", "AssessmentItem", "Channel", "Framework", "Category", "CategoryInstance", "Term"] + +#restrict.metadata.objectTypes="Content,ContentImage" + +publish.collection.fullecar.disable=true + +# Consistency Level for Multi Node Cassandra cluster +cassandra.lp.consistency.level=QUORUM + + + + +content.nested.fields="badgeAssertions,targets,badgeAssociations" + +content.cache.ttl=86400 +content.cache.enable=true +collection.cache.enable=true +content.discard.status=["Draft","FlagDraft"] + +framework.categories_cached=["subject", "medium", "gradeLevel", "board"] +framework.cache.ttl=86400 +framework.cache.read=true + + +# Max size(width/height) of thumbnail in pixels +max.thumbnail.size.pixels=150 + +play.http.parser.maxMemoryBuffer = 50MB +akka.http.parsing.max-content-length = 50MB +schema.base_path = "../../schemas/" +//schema.base_path = "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/schemas/" + +collection.image.migration.enabled=true +collection.keyspace = "hierarchy_store" +content.keyspace = "content_store" +category.keyspace="" + +languageCode { + assamese : "as" + bengali : "bn" + english : "en" + gujarati : "gu" + hindi : "hi" + kannada : "ka" + marathi : "mr" + odia : "or" + tamil : "ta" + telugu : "te" +} + +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd"] +objectcategorydefinition.keyspace=category_store diff --git a/platform-modules/import-manager/pom.xml b/platform-modules/import-manager/pom.xml new file mode 100644 index 000000000..7914fcf5c --- /dev/null +++ b/platform-modules/import-manager/pom.xml @@ -0,0 +1,101 @@ + + + + platform-modules + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + import-manager + jar + + + + org.sunbird + platform-common + 1.0-SNAPSHOT + + + org.sunbird + platform-telemetry + 1.0-SNAPSHOT + + + org.sunbird + graph-engine_2.11 + 1.0-SNAPSHOT + jar + + + org.scalatest + scalatest_${scala.maj.version} + 3.0.8 + test + + + org.scalamock + scalamock_${scala.maj.version} + 4.4.0 + test + + + + + src/main/scala + src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + ${scala.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + + test + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + + \ No newline at end of file diff --git a/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/ImportManager.scala b/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/ImportManager.scala new file mode 100644 index 000000000..b59ba051d --- /dev/null +++ b/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/ImportManager.scala @@ -0,0 +1,133 @@ +package org.sunbird.`object`.importer + +import java.util +import java.util.UUID + +import org.apache.commons.collections4.{CollectionUtils, MapUtils} +import org.apache.commons.lang3.StringUtils +import org.sunbird.`object`.importer.constant.ImportConstants +import org.sunbird.`object`.importer.error.ImportErrors +import org.sunbird.common.Platform +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.graph.common.Identifier +import org.sunbird.graph.utils.ScalaJsonUtils +import org.sunbird.telemetry.util.LogTelemetryEventUtil + +import scala.collection.JavaConversions.mapAsJavaMap +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.concurrent.{ExecutionContext, Future} + + +case class ImportConfig(topicName: String, requestLimit: Integer, requiredProps: List[String], validContentStage: List[String], propsToRemove: List[String]) + +class ImportManager(config: ImportConfig) { + + def importObject(request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[Response] = { + val graphId: String = request.getContext.get("graph_id").asInstanceOf[String] + val reqList: util.List[util.Map[String, AnyRef]] = getRequest(request) + if (CollectionUtils.isEmpty(reqList)) + throw new ClientException(ImportErrors.ERR_INVALID_IMPORT_REQUEST, ImportErrors.ERR_INVALID_IMPORT_REQUEST_MSG) + else if (CollectionUtils.isNotEmpty(reqList) && reqList.size > config.requestLimit) + throw new ClientException(ImportErrors.ERR_REQUEST_LIMIT_EXCEED, ImportErrors.ERR_REQUEST_LIMIT_EXCEED_MSG + config.requestLimit) + val reqPid = request.getRequest.getOrDefault(ImportConstants.PROCESS_ID, "").asInstanceOf[String] + val processId: String = if(StringUtils.isNotBlank(reqPid)) reqPid else UUID.randomUUID().toString + val invalidCodes: util.List[String] = new util.ArrayList[String]() + val invalidStage: util.List[String] = new util.ArrayList[String]() + validateAndGetRequest(reqList, processId, invalidCodes, invalidStage, request).map(objects => { + if (CollectionUtils.isNotEmpty(invalidCodes)) { + val msg = if (invalidCodes.asScala.filter(c => StringUtils.isNotBlank(c)).toList.size > 0) " | Required Property's Missing For " + invalidCodes else "" + throw new ClientException(ImportErrors.ERR_REQUIRED_PROPS_VALIDATION, ImportErrors.ERR_REQUIRED_PROPS_VALIDATION_MSG + ScalaJsonUtils.serialize(config.requiredProps) + msg) + } else if (CollectionUtils.isNotEmpty(invalidStage)) throw new ClientException(ImportErrors.ERR_OBJECT_STAGE_VALIDATION, ImportErrors.ERR_OBJECT_STAGE_VALIDATION_MSG + request.getContext.get("VALID_OBJECT_STAGE").asInstanceOf[java.util.List[String]]) + else { + objects.asScala.map(obj => pushInstructionEvent(graphId, obj)) + val response = ResponseHandler.OK() + response.put(ImportConstants.PROCESS_ID, processId) + response + } + }) + + } + + def validateAndGetRequest(objects: util.List[util.Map[String, AnyRef]], processId: String, invalidCodes: util.List[String], invalidStages: util.List[String], request: Request)(implicit oec: OntologyEngineContext, ec: ExecutionContext): Future[util.List[util.Map[String, AnyRef]]] = { + Future { + objects.asScala.map(obj => { + val source: String = obj.getOrDefault(ImportConstants.SOURCE, "").toString + val stage: String = obj.getOrDefault(ImportConstants.STAGE, "").toString + val reqMetadata: util.Map[String, AnyRef] = obj.getOrDefault(ImportConstants.METADATA, new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + val sourceMetadata: util.Map[String, AnyRef] = getMetadata(source, request.getContext.get("objectType").asInstanceOf[String].toLowerCase()) + val finalMetadata: util.Map[String, AnyRef] = if (MapUtils.isNotEmpty(sourceMetadata)) { + sourceMetadata.putAll(reqMetadata) + sourceMetadata.put(ImportConstants.SOURCE, source) + sourceMetadata + } else reqMetadata + val originData = finalMetadata.getOrDefault(ImportConstants.ORIGIN_DATA, new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + finalMetadata.keySet().removeAll(config.propsToRemove.asJava) + finalMetadata.put(ImportConstants.PROCESS_ID, processId) + if (!validateMetadata(finalMetadata, config.requiredProps.asJava)) + invalidCodes.add(finalMetadata.getOrDefault(ImportConstants.CODE, "").asInstanceOf[String]) + if(!validateStage(stage, config.validContentStage.asJava)) invalidStages.add(finalMetadata.getOrDefault(ImportConstants.CODE, "").asInstanceOf[String]) + if(StringUtils.isBlank(finalMetadata.getOrDefault(ImportConstants.OBJECT_TYPE, "").asInstanceOf[String])) + finalMetadata.put(ImportConstants.OBJECT_TYPE, request.getObjectType) + obj.put(ImportConstants.METADATA, finalMetadata) + obj.put(ImportConstants.ORIGIN_DATA, originData) + obj + }).asJava + } + } + + def getRequest(request: Request): util.List[util.Map[String, AnyRef]] = { + val req = request.getRequest.get(request.getObjectType.toLowerCase()) + req match { + case req: util.List[util.Map[String, AnyRef]] => req + case req: util.Map[String, AnyRef] => new util.ArrayList[util.Map[String, AnyRef]]() { + { + add(req) + } + } + case _ => throw new ClientException(ImportErrors.ERR_INVALID_IMPORT_REQUEST, ImportErrors.ERR_INVALID_IMPORT_REQUEST_MSG) + } + } + + def getMetadata(source: String, key: String)(implicit oec: OntologyEngineContext, ec: ExecutionContext): util.Map[String, AnyRef] = { + if (StringUtils.isNotBlank(source)) { + val response: Response = oec.httpUtil.get(source, "", new util.HashMap[String, String]()) + if (null != response && response.getResponseCode.code() == 200) + response.getResult.getOrDefault(key, new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + else throw new ClientException(ImportErrors.ERR_READ_SOURCE, ImportErrors.ERR_READ_SOURCE_MSG + response.getResponseCode) + } else new util.HashMap[String, AnyRef]() + } + + def validateMetadata(metadata: util.Map[String, AnyRef], requiredProps: util.List[String]): Boolean = { + val reqFields = requiredProps.asScala.filter(x => null == metadata.get(x)).toList + reqFields.isEmpty + } + + def validateStage(stage: String, validObjectStage: util.List[String]): Boolean = if(StringUtils.isNotBlank(stage)) validObjectStage.contains(stage) else true + + def getInstructionEvent(identifier: String, source: String, metadata: util.Map[String, AnyRef], collection: util.List[util.Map[String, AnyRef]], stage: String, originData: util.Map[String, AnyRef]): String = { + val actor = mapAsJavaMap[String, AnyRef](Map[String, AnyRef]("id" -> "Auto Creator", "type" -> "System")) + val context = mapAsJavaMap[String, AnyRef](Map[String, AnyRef]("pdata" -> mapAsJavaMap(Map[String, AnyRef]("id" -> "org.sunbird.platform", "ver" -> "1.0", "env" -> Platform.getString("cloud_storage.env", "dev"))), ImportConstants.CHANNEL -> metadata.getOrDefault(ImportConstants.CHANNEL, ""))) + val objectData = mapAsJavaMap[String, AnyRef](Map[String, AnyRef]("id" -> identifier, "ver" -> metadata.get(ImportConstants.VERSION_KEY))) + val edata = mutable.Map[String, AnyRef]("action" -> "auto-create", "iteration" -> 1.asInstanceOf[AnyRef], ImportConstants.OBJECT_TYPE -> metadata.getOrDefault(ImportConstants.OBJECT_TYPE, "").asInstanceOf[String], + if (StringUtils.isNotBlank(source)) ImportConstants.REPOSITORY -> source else ImportConstants.IDENTIFIER -> identifier, ImportConstants.METADATA -> metadata, if (CollectionUtils.isNotEmpty(collection)) ImportConstants.COLLECTION -> collection else ImportConstants.COLLECTION -> List().asJava, + ImportConstants.STAGE -> stage, if(StringUtils.isNotBlank(source) && MapUtils.isNotEmpty(originData)) ImportConstants.ORIGIN_DATA -> originData else ImportConstants.ORIGIN_DATA -> new util.HashMap[String, AnyRef]()).asJava + val kafkaEvent: String = LogTelemetryEventUtil.logInstructionEvent(actor, context, objectData, edata) + if (StringUtils.isBlank(kafkaEvent)) throw new ClientException(ImportErrors.BE_JOB_REQUEST_EXCEPTION, ImportErrors.ERR_INVALID_IMPORT_REQUEST_MSG) + kafkaEvent + } + + def pushInstructionEvent(graphId: String, obj: util.Map[String, AnyRef])(implicit oec: OntologyEngineContext): Unit = { + val stage = obj.getOrDefault(ImportConstants.STAGE, "").toString + val source: String = obj.getOrDefault(ImportConstants.SOURCE, "").toString + //TODO: Enhance identifier extraction logic for handling any query param, if present in source + val identifier = if (StringUtils.isNotBlank(source)) source.substring(source.lastIndexOf('/') + 1) else Identifier.getIdentifier(graphId, Identifier.getUniqueIdFromTimestamp) + val metadata = obj.getOrDefault(ImportConstants.METADATA, new util.HashMap()).asInstanceOf[util.Map[String, AnyRef]] + val collection = obj.getOrDefault(ImportConstants.COLLECTION, new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.ArrayList[util.Map[String, AnyRef]]] + val originData = obj.getOrDefault(ImportConstants.ORIGIN_DATA, new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + val event = getInstructionEvent(identifier, source, metadata, collection, stage, originData) + oec.kafkaClient.send(event, config.topicName) + } +} \ No newline at end of file diff --git a/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/constant/ImportConstants.scala b/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/constant/ImportConstants.scala new file mode 100644 index 000000000..2ee88b7f8 --- /dev/null +++ b/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/constant/ImportConstants.scala @@ -0,0 +1,17 @@ +package org.sunbird.`object`.importer.constant + +object ImportConstants { + + val SOURCE: String = "source" + val METADATA: String = "metadata" + val COLLECTION: String = "collection" + val PROCESS_ID: String = "processId" + val CODE: String = "code" + val CHANNEL: String = "channel" + val VERSION_KEY: String = "versionKey" + val IDENTIFIER: String = "identifier" + val OBJECT_TYPE: String = "objectType" + val REPOSITORY: String = "repository" + val STAGE: String = "stage" + val ORIGIN_DATA = "originData" +} diff --git a/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/error/ImportErrors.scala b/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/error/ImportErrors.scala new file mode 100644 index 000000000..2308f5617 --- /dev/null +++ b/platform-modules/import-manager/src/main/scala/org/sunbird/object/importer/error/ImportErrors.scala @@ -0,0 +1,20 @@ +package org.sunbird.`object`.importer.error + +object ImportErrors { + + //Error Codes + val ERR_INVALID_IMPORT_REQUEST: String = "ERR_INVALID_IMPORT_REQUEST" + val ERR_REQUEST_LIMIT_EXCEED: String = "ERR_REQUEST_LIMIT_EXCEED" + val ERR_READ_SOURCE: String = "ERR_READ_SOURCE" + val ERR_REQUIRED_PROPS_VALIDATION: String = "ERR_REQUIRED_PROPS_VALIDATION" + val BE_JOB_REQUEST_EXCEPTION: String = "BE_JOB_REQUEST_EXCEPTION" + val ERR_OBJECT_STAGE_VALIDATION: String = "ERR_OBJECT_STAGE_VALIDATION" + + //Error Messages + val ERR_INVALID_IMPORT_REQUEST_MSG: String = "Invalid Request! Please Provide Valid Request." + val ERR_REQUEST_LIMIT_EXCEED_MSG: String = "Request Limit Exceeded. Maximum Allowed Objects In Single Request is " + val ERR_READ_SOURCE_MSG: String = "Received Invalid Response While Reading Data From Source. Response Code is : " + val ERR_REQUIRED_PROPS_VALIDATION_MSG: String = "Validation Failed! Mandatory Properties Are " + val BE_JOB_REQUEST_EXCEPTION_MSG: String = "Kafka Event Is Not Generated Properly." + val ERR_OBJECT_STAGE_VALIDATION_MSG: String = "Object Stage Validation Failed! Valid Object Stages Are " +} diff --git a/platform-modules/import-manager/src/test/resources/application.conf b/platform-modules/import-manager/src/test/resources/application.conf new file mode 100644 index 000000000..66d2264a0 --- /dev/null +++ b/platform-modules/import-manager/src/test/resources/application.conf @@ -0,0 +1 @@ +import.output_topic_name = "local.auto.creation.job.request" \ No newline at end of file diff --git a/platform-modules/import-manager/src/test/scala/org/sunbird/object/importer/ImportManagerTest.scala b/platform-modules/import-manager/src/test/scala/org/sunbird/object/importer/ImportManagerTest.scala new file mode 100644 index 000000000..e80245ab1 --- /dev/null +++ b/platform-modules/import-manager/src/test/scala/org/sunbird/object/importer/ImportManagerTest.scala @@ -0,0 +1,255 @@ +package org.sunbird.`object`.importer + +import java.util + +import org.apache.commons.collections4.{CollectionUtils, MapUtils} +import org.apache.commons.lang3.{BooleanUtils, StringUtils} +import org.scalatest.AsyncFlatSpec +import org.scalamock.matchers.Matchers +import org.scalamock.scalatest.AsyncMockFactory +import org.sunbird.common.{HttpUtil, JsonUtils} +import org.sunbird.common.dto.{Request, Response, ResponseHandler} +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.OntologyEngineContext +import org.sunbird.kafka.client.KafkaClient + +import scala.collection.JavaConverters._ + +class ImportManagerTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val REQUEST_LIMIT = 300 + val AUTO_CREATE_TOPIC_NAME = "test.import.request" + val REQUIRED_PROPS = List("name", "code", "mimeType", "contentType", "artifactUrl", "framework") + val VALID_OBJECT_STAGE = List("create", "upload", "review", "publish") + val PROPS_TO_REMOVE = List("downloadUrl","variants","previewUrl","streamingUrl","itemSets") + lazy val importConfig = ImportConfig(AUTO_CREATE_TOPIC_NAME, REQUEST_LIMIT, REQUIRED_PROPS, VALID_OBJECT_STAGE, PROPS_TO_REMOVE) + lazy val importMgr = new ImportManager(importConfig) + + "getRequest with list input" should "return request data as list with java types" in { + val reqMap : java.util.Map[String, AnyRef] = new util.HashMap[String, AnyRef](){{ + put("content", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("source","https://dock.sunbirded.org/api/content/v1/read/do_11307822356267827219477") + put("metadata", new util.HashMap[String, AnyRef](){{ + put("name", "Test Content") + put("description", "Test Content") + }}) + put("collection", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_123") + put("unitId", "do_3456") + }}) + }}) + }}) + add(new util.HashMap[String, AnyRef](){{ + put("source","https://dock.sunbirded.org/api/content/v1/read/do_11307822356267827219477") + put("metadata", new util.HashMap[String, AnyRef](){{ + put("name", "Test Content 2") + put("description", "Test Content 2") + }}) + put("collection", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_123") + put("unitId", "do_4567") + }}) + }}) + }}) + + }}) + }} + val request = new Request() + request.setObjectType("Content") + request.putAll(reqMap) + val result: util.List[util.Map[String, AnyRef]] = importMgr.getRequest(request) + assert(CollectionUtils.isNotEmpty(result)) + assert(result.isInstanceOf[util.List[AnyRef]]) + assert(result.size==2) + assert(MapUtils.isNotEmpty(result.get(0))) + assert(MapUtils.isNotEmpty(result.get(1))) + } + + "getRequest with map input" should "return request data as list with java types" in { + val reqMap : java.util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]() {{ + put("content", new util.HashMap[String, AnyRef](){{ + put("source","https://dock.sunbirded.org/api/content/v1/read/do_11307822356267827219477") + put("metadata", new util.HashMap[String, AnyRef](){{ + put("name", "Test Content 2") + put("description", "Test Content 2") + }}) + put("collection", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_123") + put("unitId", "do_3456") + }}) + }}) + }}) + }} + val request = new Request() + request.putAll(reqMap) + request.setObjectType("Content") + val result: util.List[util.Map[String, AnyRef]] = importMgr.getRequest(request) + assert(CollectionUtils.isNotEmpty(result)) + assert(result.isInstanceOf[util.List[AnyRef]]) + assert(result.size==1) + assert(MapUtils.isNotEmpty(result.get(0))) + } + + "getRequestData with invalid input" should "throw client exception" in { + val exception = intercept[ClientException] { + val req = new Request() + req.setObjectType("Content") + importMgr.getRequest(req) + } + assert(exception.getMessage == "Invalid Request! Please Provide Valid Request.") + } + + + "getInstructionEvent with valid input" should "return kafka event string" in { + val source = "https://dock.sunbirded.org/api/content/v1/read/do_11307822356267827219477" + val metadata = new util.HashMap[String, AnyRef]() {{ + put("source","https://dock.sunbirded.org/api/content/v1/read/do_11307822356267827219477") + put("name", "Test Content 2") + put("code", "test.content.1") + put("mimeType","application/pdf") + put("contentType","Resource") + put("description", "Test Content 2") + put("channel", "in.ekstep") + put("versionKey", "12345") + }} + val collection = new util.ArrayList[util.Map[String, AnyRef]]() {{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_123") + put("unitId", "do_3456") + }}) + }} + val originData = new util.HashMap[String, AnyRef]() {{ + put("identifier", "do_1234") + put("repository", "https://dock.sunbirded.org/api/content/v1/read/do_1234") + }} + val result = importMgr.getInstructionEvent("do_11307822356267827219477", source, metadata, collection, "publish", originData) + assert(StringUtils.isNoneBlank(result)) + val resultMap = JsonUtils.deserialize(result, classOf[util.Map[String, AnyRef]]) + assert(MapUtils.isNotEmpty(resultMap)) + val edata = resultMap.getOrDefault("edata", new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + assert(MapUtils.isNotEmpty(edata)) + assert(StringUtils.equalsIgnoreCase("auto-create", edata.get("action").asInstanceOf[String])) + assert(MapUtils.isNotEmpty(edata.get("originData").asInstanceOf[util.Map[String, AnyRef]])) + } + + "getInstructionEvent with valid input having originData and empty source" should "return edata with empty originData" in { + val source = "" + val metadata = new util.HashMap[String, AnyRef]() {{ + put("source","https://dock.sunbirded.org/api/content/v1/read/do_11307822356267827219477") + put("name", "Test Content 2") + put("code", "test.content.1") + put("mimeType","application/pdf") + put("contentType","Resource") + put("description", "Test Content 2") + put("channel", "in.ekstep") + put("versionKey", "12345") + }} + val collection = new util.ArrayList[util.Map[String, AnyRef]]() {{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_123") + put("unitId", "do_3456") + }}) + }} + val originData = new util.HashMap[String, AnyRef]() {{ + put("identifier", "do_1234") + put("repository", "https://dock.sunbirded.org/api/content/v1/read/do_1234") + }} + val result = importMgr.getInstructionEvent("do_11307822356267827219477", source, metadata, collection, "publish", originData) + assert(StringUtils.isNoneBlank(result)) + val resultMap = JsonUtils.deserialize(result, classOf[util.Map[String, AnyRef]]) + assert(MapUtils.isNotEmpty(resultMap)) + val edata = resultMap.getOrDefault("edata", new util.HashMap[String, AnyRef]()).asInstanceOf[util.Map[String, AnyRef]] + assert(MapUtils.isNotEmpty(edata)) + assert(StringUtils.equalsIgnoreCase("auto-create", edata.get("action").asInstanceOf[String])) + assert(MapUtils.isEmpty(edata.get("originData").asInstanceOf[util.Map[String, AnyRef]])) + } + + "importObject with valid input" should "return the response having processId" in { + val request = getRequest() + request.putAll(new util.HashMap[String, AnyRef](){{ + put("content", new util.HashMap[String, AnyRef](){{ + put("stage", "upload") + put("source","https://dock.sunbirded.org/api/content/v1/read/do_11307822356267827219477") + put("metadata", new util.HashMap[String, AnyRef](){{ + put("name", "Test Content 2") + put("description", "Test Content 2") + }}) + put("collection", new util.ArrayList[util.Map[String, AnyRef]](){{ + add(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_123") + put("unitId", "do_3456") + }}) + }}) + }}) + }}) + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val kfClient = mock[KafkaClient] + val hUtil = mock[HttpUtil] + (oec.httpUtil _).expects().returns(hUtil) + val resp :Response = ResponseHandler.OK() + resp.putAll(new util.HashMap[String, AnyRef](){{ + put("content", new util.HashMap[String, AnyRef](){{ + put("mimeType", "application/pdf") + put("code", "test.res.1") + put("framework", "NCF") + put("contentType", "Resource") + put("artifactUrl", "http://test.com/test.pdf") + put("channel", "test") + put("downloadUrl", "http://test.com/test.ecar") + put("itemSets", "do_123") + }}) + }}) + (hUtil.get(_: String, _: String, _: util.Map[String, String])).expects(*, *, *).returns(resp) + (oec.kafkaClient _).expects().returns(kfClient) + (kfClient.send(_: String, _: String)).expects(*, *).returns(None) + val resFuture = importMgr.importObject(request) + resFuture.map(result => { + assert(null != result) + assert(result.getResponseCode.toString=="OK") + assert(null != result.getResult.get("processId")) + }) + } + + "importObject with invalid input" should "throw client exception" in { + val request = getRequest() + request.putAll(new util.HashMap[String, AnyRef](){{ + put("content", new util.ArrayList[String]()) + }}) + implicit val oec: OntologyEngineContext = mock[OntologyEngineContext] + val exception = intercept[ClientException] { + importMgr.importObject(request) + } + assert(exception.getMessage == "Invalid Request! Please Provide Valid Request.") + } + + "validateStage with invalid input" should "return false" in { + val result = importMgr.validateStage("Flagged", importConfig.validContentStage.asJava) + assert(BooleanUtils.isFalse(result)) + } + + "validateStage with valid input" should "return true" in { + val result = importMgr.validateStage("review", importConfig.validContentStage.asJava) + assert(BooleanUtils.isTrue(result)) + } + + private def getRequest(): Request = { + val request = new Request() + request.setContext(new util.HashMap[String, AnyRef]() { + { + put("graph_id", "domain") + put("version", "1.0") + put("objectType", "Content") + put("schemaName", "content") + put("X-Channel-Id", "in.ekstep") + } + }) + request.setObjectType("Content") + request + } + +} diff --git a/platform-modules/mimetype-manager/pom.xml b/platform-modules/mimetype-manager/pom.xml new file mode 100644 index 000000000..9241f53e7 --- /dev/null +++ b/platform-modules/mimetype-manager/pom.xml @@ -0,0 +1,146 @@ + + + + platform-modules + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + mimetype-manager + + 2.11 + 2.7.2 + + + + + org.sunbird + platform-common + 1.0-SNAPSHOT + + + + org.sunbird + graph-engine_2.11 + 1.0-SNAPSHOT + jar + + + org.sunbird + cloud-store-sdk + 1.2.8 + + + org.scala-lang + scala-library + ${scala.version} + + + commons-validator + commons-validator + 1.6 + + + org.sunbird + url-manager + 1.0-SNAPSHOT + + + com.fasterxml.jackson.core + jackson-core + ${fasterxml.jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${fasterxml.jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${fasterxml.jackson.version} + + + org.scala-lang.modules + scala-xml_${scala.maj.version} + 1.2.0 + + + org.apache.tika + tika-core + 1.22 + + + org.scalatest + scalatest_${scala.maj.version} + 3.0.8 + test + + + org.scalamock + scalamock_${scala.maj.version} + 4.4.0 + test + + + + + src/main/scala + src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.4.0 + + ${scala.version} + false + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + + test + test + + test + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + + + \ No newline at end of file diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/cloudstore/StorageService.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/cloudstore/StorageService.scala new file mode 100644 index 000000000..4c9b69afc --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/cloudstore/StorageService.scala @@ -0,0 +1,81 @@ +package org.sunbird.cloudstore + +import java.io.File + +import org.apache.commons.lang3.StringUtils +import org.sunbird.cloud.storage.BaseStorageService +import org.sunbird.common.Platform +import org.sunbird.cloud.storage.factory.StorageConfig +import org.sunbird.cloud.storage.factory.StorageServiceFactory +import org.sunbird.common.exception.ServerException +import org.sunbird.common.Slug + +import scala.concurrent.ExecutionContext + +class StorageService { + + val storageType: String = if (Platform.config.hasPath("cloud_storage_type")) Platform.config.getString("cloud_storage_type") else "" + var storageService: BaseStorageService = null + + @throws[Exception] + def getService(): BaseStorageService = { + if (null == storageService) { + if (StringUtils.equalsIgnoreCase(storageType, "azure")) { + val storageKey = Platform.config.getString("azure_storage_key") + val storageSecret = Platform.config.getString("azure_storage_secret") + storageService = StorageServiceFactory.getStorageService(new StorageConfig(storageType, storageKey, storageSecret)) + } else if (StringUtils.equalsIgnoreCase(storageType, "aws")) { + val storageKey = Platform.config.getString("aws_storage_key") + val storageSecret = Platform.config.getString("aws_storage_secret") + storageService = StorageServiceFactory.getStorageService(new StorageConfig(storageType, storageKey, storageSecret)) + } else throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Error while initialising cloud storage") + } + storageService + } + + def getContainerName(): String = { + if (StringUtils.equalsIgnoreCase(storageType, "azure")) + Platform.config.getString("azure_storage_container") + else if (StringUtils.equalsIgnoreCase(storageType, "aws")) + Platform.config.getString("aws_storage_container") + else + throw new ServerException("ERR_INVALID_CLOUD_STORAGE", "Container name not configured.") + } + + def uploadFile(folderName: String, file: File, slug: Option[Boolean] = Option(true)): Array[String] = { + val slugFile = if (slug.getOrElse(true)) Slug.createSlugFile(file) else file + val objectKey = folderName + "/" + slugFile.getName + val url = getService.upload(getContainerName, slugFile.getAbsolutePath, objectKey, Option.apply(false), Option.apply(1), Option.apply(5), Option.empty) + Array[String](objectKey, url) + } + + def uploadDirectory(folderName: String, directory: File, slug: Option[Boolean] = Option(true)): Array[String] = { + val slugFile = if (slug.getOrElse(true)) Slug.createSlugFile(directory) else directory + val objectKey = folderName + File.separator + val url = getService.upload(getContainerName(), slugFile.getAbsolutePath, objectKey, Option.apply(true), Option.apply(1), Option.apply(5), Option.empty) + Array[String](objectKey, url) + } + + def uploadDirectoryAsync(folderName: String, directory: File, slug: Option[Boolean] = Option(true))(implicit ec: ExecutionContext) = { + val slugFile = if (slug.getOrElse(true)) Slug.createSlugFile(directory) else directory + val objectKey = folderName + File.separator + getService.uploadFolder(getContainerName, slugFile.getAbsolutePath, objectKey, Option.apply(false), None, None, 1) + } + + def getObjectSize(key: String): Double = { + val blob = getService.getObject(getContainerName, key, Option.apply(false)) + blob.contentLength + } + + def copyObjectsByPrefix(source: String, destination: String) = { + getService.copyObjects(getContainerName, source, getContainerName, destination, Option.apply(true)) + } + + def deleteFile(key: String, isDirectory: Option[Boolean] = Option(false)) = { + getService.deleteObject(getContainerName, key, isDirectory) + } + + def getSignedURL(key: String, ttl: Option[Int], permission: Option[String]): String = { + getService().getSignedURL(getContainerName, key, ttl, permission) + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/ECMLExtractor.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/ECMLExtractor.scala new file mode 100644 index 000000000..b210d9905 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/ECMLExtractor.scala @@ -0,0 +1,7 @@ +package org.sunbird.mimetype.ecml + +import org.sunbird.mimetype.ecml.processor.{AssetsValidatorProcessor, BaseProcessor, EmbedControllerProcessor, GlobalizeAssetProcessor, MissingAssetValidatorProcessor, MissingControllerValidatorProcessor} + +class ECMLExtractor(basePath: String, identifier: String) extends BaseProcessor(basePath, identifier) with EmbedControllerProcessor with GlobalizeAssetProcessor with MissingControllerValidatorProcessor with AssetsValidatorProcessor with MissingAssetValidatorProcessor { + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/AssetsValidatorProcessor.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/AssetsValidatorProcessor.scala new file mode 100644 index 000000000..1fd31c952 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/AssetsValidatorProcessor.scala @@ -0,0 +1,56 @@ +package org.sunbird.mimetype.ecml.processor + +import java.io.File + +import org.apache.tika.Tika +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.Platform +import org.sunbird.common.exception.ClientException + +trait AssetsValidatorProcessor extends IProcessor { + + val MAX_FILE_SIZE:Double = {if(Platform.config.hasPath("MAX_ASSET_FILE_SIZE_LIMIT")) Platform.config.getDouble("MAX_ASSET_FILE_SIZE_LIMIT") else 20971520} + + abstract override def process(ecrf: Plugin)(implicit ss: StorageService): Plugin = { + validateAssets(ecrf) + super.process(ecrf) + } + + def validateAssets(plugin: Plugin) = { + if(null != plugin.manifest){ + val medias:List[Media] = plugin.manifest.medias + if(null != medias && !medias.isEmpty) + medias.map(media=> { + validateMedia(media) + }) + } + } + + def getAssetPath(media: Media): String = { + if(widgetTypeAssets.contains(media.`type`)) getBasePath() + File.separator + "widgets" + File.separator + media.src + else getBasePath() + File.separator + "assets" + File.separator + media.src + } + + def validateAssetMimeType(file: File) = { + val tika:Tika = new Tika() + val mimeType = tika.detect(file) + if(!(whiteListedMimeTypes.contains(mimeType) && !blackListedMimeTypes.contains(mimeType))) + throw new ClientException("INVALID_MIME_TYPE", "Error! Invalid Asset Mime-Type. | Asset " + file.getName + " has Invalid Mime-Type. :" + mimeType) + } + + def validateAssetSize(file: File) = { + if(MAX_FILE_SIZE < file.length()) + throw new ClientException("FILE_SIZE_EXCEEDS_LIMIT", "Error! File Size Exceeded the Limit. | Asset " + file.getName + " is Bigger in Size.: " + file.length()) + } + + def validateMedia(media: Media) = { + if(null != media) { + val file = new File(getAssetPath(media)) + if(file.exists()) { + validateAssetMimeType(file) + validateAssetSize(file) + } + } + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/BaseProcessor.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/BaseProcessor.scala new file mode 100644 index 000000000..8bf404fce --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/BaseProcessor.scala @@ -0,0 +1,9 @@ +package org.sunbird.mimetype.ecml.processor + +import org.sunbird.cloudstore.StorageService + +class BaseProcessor(basePath: String, identifier: String) extends IProcessor(basePath, identifier) { + override def process(ecrf: Plugin)(implicit ss: StorageService): Plugin = { + ecrf + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/EcrfObject.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/EcrfObject.scala new file mode 100644 index 000000000..8afe457c6 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/EcrfObject.scala @@ -0,0 +1,21 @@ +package org.sunbird.mimetype.ecml.processor + +import scala.annotation.Annotation + +case class Plugin(id: String, data: Map[String, AnyRef], innerText: String, cData: String, childrenPlugin: List[Plugin], manifest: Manifest, controllers: List[Controller], events: List[Event]) { + def this() = this("", null, "", "", null, null, null, null) +} +case class Manifest(id: String, data: Map[String, AnyRef], innerText: String, cData: String, medias: List[Media]) { + def this() = this("", null, "", "", null) +} +case class Controller(id: String, data: Map[String, AnyRef], innerText: String, cData: String) { + def this() = this("", null, "", "") +} +case class Media(id: String, data: Map[String, AnyRef], innerText: String, cData: String, src: String, `type`: String, childrenPlugin: List[Plugin]) { + def this() = this("", null, "", "", "", "", null) +} +case class Event(id: String, data: Map[String, AnyRef], innerText: String, cData: String, childrenPlugin: List[Plugin]) { + def this() = this("", null, "", "", null) +} + + diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/EmbedControllerProcessor.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/EmbedControllerProcessor.scala new file mode 100644 index 000000000..707c13316 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/EmbedControllerProcessor.scala @@ -0,0 +1,38 @@ +package org.sunbird.mimetype.ecml.processor + +import java.io.File +import java.nio.charset.StandardCharsets + +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.cloudstore.StorageService + +trait EmbedControllerProcessor extends IProcessor { + + abstract override def process(ecrf: Plugin)(implicit ss: StorageService): Plugin = { + val controllerList = embedController(ecrf) + super.process(Plugin(ecrf.id, ecrf.data, ecrf.innerText, ecrf.cData, ecrf.childrenPlugin, ecrf.manifest, controllerList, ecrf.events)) + } + + def embedController(plugin: Plugin):List[Controller] = { + val controllers = plugin.controllers + controllers.map(control => { + if(StringUtils.isBlank(control.cData)){ + val id:String = control.data.get("id").asInstanceOf[String] + val dataType:String = control.data.get("type").asInstanceOf[String] + if(StringUtils.isNoneBlank(id) && StringUtils.isNotBlank(dataType)) { + val file = { + if("items".equalsIgnoreCase(dataType)) + new File(getBasePath() + File.separator + "items" + File.separator + id + ".json") + else if("data".equalsIgnoreCase(dataType)) + new File(getBasePath() + File.separator + "data" + File.separator + id + ".json") + else null + } + if(null != file && file.exists()){ + Controller(control.id, control.data, control.innerText, FileUtils.readFileToString(file, StandardCharsets.UTF_8)) + }else control + }else control + }else control + }) + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/GlobalizeAssetProcessor.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/GlobalizeAssetProcessor.scala new file mode 100644 index 000000000..2afc0e722 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/GlobalizeAssetProcessor.scala @@ -0,0 +1,69 @@ +package org.sunbird.mimetype.ecml.processor + +import java.io.File + +import com.mashape.unirest.http.Unirest +import org.apache.commons.io.FilenameUtils +import org.apache.commons.lang3.StringUtils +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.{Platform, Slug} + +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext, Future} + +trait GlobalizeAssetProcessor extends IProcessor { + + val ASSET_DIR:String = "cloud_storage.asset.folder" + val OBJECT_DIR:String = "cloud_storage.content.folder" + val BASE_URL: String = Platform.getString("content.media.base_url", "https://dev.sunbirded.org") + val timeout: Long = if(Platform.config.hasPath("asset.max_upload_time")) Platform.config.getLong("asset.max_upload_time") else 60 + + abstract override def process(ecrf: Plugin)(implicit ss: StorageService): Plugin = { + val manifest = ecrf.manifest + val updatedMedias:List[Media] = uploadAssets(manifest.medias) + val updatedManifest:Manifest = Manifest(manifest.id, manifest.data, manifest.innerText, manifest.cData, updatedMedias) + super.process(Plugin(ecrf.id, ecrf.data, ecrf.innerText, ecrf.cData, ecrf.childrenPlugin, updatedManifest, ecrf.controllers, ecrf.events)) + } + + def uploadAssets(medias: List[Media])(implicit ss: StorageService, ec: ExecutionContext = concurrent.ExecutionContext.Implicits.global): List[Media] = { + if(null != medias) { + val future:Future[List[Media]] = Future.sequence(medias.filter(media=> StringUtils.isNotBlank(media.id) && StringUtils.isNotBlank(media.src) && StringUtils.isNotBlank(media.`type`)) + .map(media => { + Future{ + val file: File = { + if (widgetTypeAssets.contains(media.`type`)) new File(getBasePath() + File.separator + "widgets" + File.separator + media.src) + else new File(getBasePath() + File.separator + "assets" + File.separator + media.src) + } + val mediaSrc = media.data.getOrElse("src", "").asInstanceOf[String] + val cloudDirName = if (!(mediaSrc.startsWith("http"))) FilenameUtils.getFullPathNoEndSeparator(mediaSrc).replace("assets/public/", "").replace("content-plugins/", "").trim else mediaSrc + val blobUrl = if (!(mediaSrc.startsWith("http"))) { + if (mediaSrc.startsWith("/")) BASE_URL + mediaSrc else BASE_URL + File.separator + mediaSrc + } else mediaSrc + val uploadFileUrl: Array[String] = if (StringUtils.isNoneBlank(cloudDirName) && getBlobLength(blobUrl) == 0) ss.uploadFile(cloudDirName, file) + else new Array[String](1) + if (null != uploadFileUrl && uploadFileUrl.size > 1) { + if (!(mediaSrc.startsWith("http") || mediaSrc.startsWith("/"))) { + val temp = media.data ++ Map("src" -> ("/" + mediaSrc)) + Media(media.id, temp, media.innerText, media.cData, uploadFileUrl(1), media.`type`, media.childrenPlugin) + } else + Media(media.id, media.data, media.innerText, media.cData, uploadFileUrl(1), media.`type`, media.childrenPlugin) + } + else media + } + })) + val mediaList:List[Media] = Await.result(future, Duration.apply(timeout, "second")) + if(null != mediaList && !mediaList.isEmpty) + mediaList + else medias + } else medias + } + + private def getBlobLength(url:String):Long = { + val response = Unirest.head(url).header("Content-Type", "application/json").asString + if (response.getStatus == 200 && null!=response.getHeaders) { + val size : Long = if(response.getHeaders.containsKey("Content-Length")) response.getHeaders.get("Content-Length").get(0).toLong + else if(response.getHeaders.containsKey("content-length")) response.getHeaders.get("content-length").get(0).toLong else 0.toLong + size + } else 0.toLong + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/IProcessor.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/IProcessor.scala new file mode 100644 index 000000000..6c32c566c --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/IProcessor.scala @@ -0,0 +1,15 @@ +package org.sunbird.mimetype.ecml.processor + +import org.sunbird.cloudstore.StorageService + +abstract class IProcessor(basePath: String, identifier: String) { + + implicit val ss = new StorageService + val widgetTypeAssets:List[String] = List("js", "css", "json", "plugin") + val whiteListedMimeTypes: List[String] = List("application/vnd.ekstep.ecml-archive","application/vnd.ekstep.html-archive","application/vnd.android.package-archive","application/vnd.ekstep.content-archive","application/vnd.ekstep.content-collection","application/octet-stream","application/json","application/javascript","application/xml","text/plain","text/html","text/javascript","text/xml","text/css","image/jpeg","image/jpg","image/png","image/tiff","image/bmp","image/gif","image/svg+xml","image/x-quicktime","image/x-quicktime","image/x-quicktime","video/avi","video/avi","video/msvideo","video/x-msvideo","video/mpeg","video/quicktime","video/quicktime","video/x-qtc","video/3gpp","video/mp4","video/ogg","video/webm","video/mpeg","video/x-mpeg","audio/mp3","audio/mpeg3","audio/x-mpeg-3","audio/mp4","audio/mpeg","audio/ogg","audio/vorbis","audio/webm","audio/x-wav","application/x-font-ttf") + val blackListedMimeTypes: List[String] = List() + def process(ecrf: Plugin)(implicit ss: StorageService): Plugin + + def getBasePath():String = basePath + def getIdentifier():String = identifier +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/JsonParser.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/JsonParser.scala new file mode 100644 index 000000000..a5da226e1 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/JsonParser.scala @@ -0,0 +1,199 @@ +package org.sunbird.mimetype.ecml.processor + +import org.apache.commons.lang3.StringUtils +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.utils.ScalaJsonUtils +import scala.collection.mutable.ListBuffer + +object JsonParser { + val nonPluginElements: List[String] = List("manifest", "controller", "media", "events", "event", "__cdata", "__text") + + def parse(jsonString: String): Plugin = { + val jsonMap:Map[String, AnyRef] = ScalaJsonUtils.deserialize[Map[String, AnyRef]](jsonString) + processDocument(jsonMap) + } + + def processDocument(json: Map[String, AnyRef]): Plugin = { + if(json.keySet.contains("theme")){ + val root = json.get("theme").get.asInstanceOf[Map[String, AnyRef]] + Plugin(getId(root), getData(root, "theme"), getInnerText(root), getCdata(root), getChildrenPlugin(root), getManifest(root, true), getControllers(root), getEvents(root)) + } else classOf[Plugin].newInstance() + } + + + def getDatafromJsonObject(jsonObject: Map[String, AnyRef], elementName: String): String = { + if(null != jsonObject && jsonObject.keySet.contains(elementName)){ + jsonObject.get(elementName).get.asInstanceOf[String] + }else "" + } + + + def getId(jsonObject: Map[String, AnyRef]): String = getDatafromJsonObject(jsonObject, "id") + + def getData(jsonObject: Map[String, AnyRef], elementName: String): Map[String, AnyRef] = { + if(null != jsonObject && StringUtils.isNotBlank(elementName)){ + var result = jsonObject.filter(p => !p._1.equalsIgnoreCase("__cdata") && !p._1.equalsIgnoreCase("__text")) + result += ("cwp_element_name" -> elementName) + result + } else Map[String, AnyRef]() + } + + def getInnerText(jsonObject: Map[String, AnyRef]): String = getDatafromJsonObject(jsonObject, "__text") + + def getCdata(jsonObject: Map[String, AnyRef]): String = ScalaJsonUtils.serialize(jsonObject.getOrElse("__cdata","")) + + def getChildrenPlugin(jsonObject: Map[String, AnyRef]): List[Plugin] = { + var childPluginList: ListBuffer[Plugin] = ListBuffer() + val filteredObject = jsonObject.filter(entry => null != entry._2) + childPluginList ++= filteredObject.filter(entry=> entry._2.isInstanceOf[List[Map[String, AnyRef]]] && !nonPluginElements.contains(entry._1)).map(entry => { + val objectList:List[Map[String, AnyRef]] = entry._2.asInstanceOf[List[Map[String, AnyRef]]] + objectList.map(obj => Plugin(getId(obj), getData(obj, entry._1), getInnerText(obj), getCdata(obj), getChildrenPlugin(obj), getManifest(obj, false), getControllers(obj), getEvents(obj))) + }).toList.flatten + childPluginList ++= filteredObject.filter(entry => entry._2.isInstanceOf[Map[String, AnyRef]] && !nonPluginElements.contains(entry._2)).map(entry => { + val obj = entry._2.asInstanceOf[Map[String, AnyRef]] + Plugin(getId(obj), getData(obj, entry._1), getInnerText(obj), getCdata(obj), getChildrenPlugin(obj), getManifest(obj, false), getControllers(obj), getEvents(obj)) + }).toList + childPluginList.toList + } + + def getManifest(jsonObject: Map[String, AnyRef], validateMedia: Boolean): Manifest = { + if(null != jsonObject && jsonObject.keySet.contains("manifest") && jsonObject.get("manifest").get.isInstanceOf[List[Map[String, AnyRef]]]) throw new ClientException("EXPECTED_JSON_OBJECT", "Error! JSON Object is Expected for the Element. manifest") + else if(jsonObject.get("manifest").isInstanceOf[Map[String, AnyRef]] && jsonObject.get("manifest").asInstanceOf[Map[String, AnyRef]].keySet.contains("media")){ + val manifestObject = jsonObject.get("manifest").get.asInstanceOf[Map[String, AnyRef]] + Manifest(getId(manifestObject), getData(manifestObject, "manifest"), getInnerText(manifestObject), getCdata(manifestObject), getMedias(manifestObject.get("media").get, validateMedia)) + }else classOf[Manifest].newInstance() + } + + def getControllers(jsonObject: Map[String, AnyRef]): List[Controller] = { + if(null != jsonObject && jsonObject.keySet.contains("controller") && jsonObject.get("controller").get.isInstanceOf[List[Map[String, Object]]]){ + val controllerList:List[Map[String, AnyRef]] = jsonObject.get("controller").get.asInstanceOf[List[Map[String, Object]]] + controllerList.map(obj =>{ + validateController(obj) + Controller(getId(obj), getData(obj, "controller"), getInnerText(obj), getCdata(obj)) + }) + } else if(null != jsonObject && jsonObject.keySet.contains("controller") && jsonObject.get("controller").isInstanceOf[Map[String, Object]]) { + val obj = jsonObject.get("controller").get.asInstanceOf[Map[String, Object]] + validateController(obj) + List(Controller(getId(obj), getData(obj, "controller"), getInnerText(obj), getCdata(obj))) + }else List() + } + + def validateController(obj: Map[String, AnyRef]) = { + val id = obj.get("id").get.asInstanceOf[String] + val `type` = obj.get("type").get.asInstanceOf[String] + + if(null == id || StringUtils.isBlank(id.toString)) + throw new ClientException("INVALID_CONTROLLER", "Error! Invalid Controller ('id' is required.)") + if(null == `type` || StringUtils.isBlank(`type`)) + throw new ClientException("INVALID_CONTROLLER", "Error! Invalid Controller ('type' is required.)") + if(!"items".equalsIgnoreCase(`type`) && !"data".equalsIgnoreCase(`type`)) + throw new ClientException("INVALID_CONTROLLER", "Error! Invalid Controller ('type' should be either 'items' or 'data')") + } + + def getEventsfromObject(jsonObject: AnyRef): List[Event] = { + if(null != jsonObject && jsonObject.isInstanceOf[List[Map[String, AnyRef]]]){ + val jsonList:List[Map[String, AnyRef]] = jsonObject.asInstanceOf[List[Map[String, AnyRef]]] + jsonList.map(obj => Event(getId(obj), getData(obj, "event"), getInnerText(obj), getCdata(obj), getChildrenPlugin(obj))) + }else if(null != jsonObject && jsonObject.isInstanceOf[Map[String, AnyRef]]) { + List(Event(getId(jsonObject.asInstanceOf[Map[String, AnyRef]]), getData(jsonObject.asInstanceOf[Map[String, AnyRef]], "event"), getInnerText(jsonObject.asInstanceOf[Map[String, AnyRef]]), getCdata(jsonObject.asInstanceOf[Map[String, AnyRef]]), getChildrenPlugin(jsonObject.asInstanceOf[Map[String, AnyRef]]))) + }else List() + } + + def getEvents(jsonObject: Map[String, AnyRef]): List[Event] = { + var eventList: ListBuffer[Event] = ListBuffer() + if(null != jsonObject && jsonObject.keySet.contains("events")) { + val value = jsonObject.get("events").get + if(value.isInstanceOf[List[Map[String, AnyRef]]]){ + val jsonList:List[Map[String, AnyRef]] = value.asInstanceOf[List[Map[String, AnyRef]]] + eventList ++= jsonList.map(obj => Event(getId(obj), getData(obj, "event"), getInnerText(obj), getCdata(obj), getChildrenPlugin(obj))) + } else if(value.isInstanceOf[Map[String, AnyRef]]){ + eventList ++= getEventsfromObject(value) + } + } else if(jsonObject.keySet.contains("event")) { + eventList ++= getEventsfromObject(jsonObject.get("event").get) + } + eventList.toList + } + + def getMedias(manifestObject: AnyRef, validateMedia: Boolean): List[Media] = { + if(null != manifestObject && manifestObject.isInstanceOf[List[Map[String, AnyRef]]]) { + val jsonList:List[Map[String, AnyRef]] = manifestObject.asInstanceOf[List[Map[String, AnyRef]]] + jsonList.map(json => getMedia(json, validateMedia)) + } else if(null != manifestObject && manifestObject.isInstanceOf[Map[String, AnyRef]]) { + List(getMedia(manifestObject.asInstanceOf[Map[String, AnyRef]], validateMedia)) + } else List() + } + + def getMedia(mediaJson: Map[String, AnyRef], validateMedia: Boolean): Media = { + if(null != mediaJson) { + val id = getDatafromJsonObject(mediaJson, "id") + val src = getDatafromJsonObject(mediaJson, "src") + val `type` = getDatafromJsonObject(mediaJson, "type") + if(StringUtils.isBlank(id)) + throw new ClientException("INVALID_MEDIA", "Error! Invalid Media ('id' is required.)") + if(!(StringUtils.isNotBlank(`type`) &&(`type`.equalsIgnoreCase("js") || `type`.equalsIgnoreCase("css")))) + throw new ClientException("INVALID_MEDIA", "Error! Invalid Media ('type' is required.)") + if(StringUtils.isBlank(src)) + throw new ClientException("INVALID_MEDIA", "Error! Invalid Media ('src' is required.)") + Media(id, getData(mediaJson, "media"), getInnerText(mediaJson), getCdata(mediaJson), src, `type`, getChildrenPlugin(mediaJson)) + } else classOf[Media].newInstance() + } + + + /** + * serialize + * @param plugin + * @return + */ + def toString(plugin: Plugin): String = { + val map:Map[String, AnyRef] = plugin.data ++ Map[String, AnyRef]("__text" -> plugin.innerText, "__cdata" -> plugin.cData) ++ getManifestMap(plugin.manifest) ++ getControllersMap(plugin.controllers) ++ getEventsMap(plugin.events) + ScalaJsonUtils.serialize(Map[String, AnyRef]("theme" -> map)) + } + + def getManifestMap(manifest: Manifest):Map[String, AnyRef] = { + if(null != manifest && null != manifest.medias && !manifest.medias.isEmpty){ + val map:Map[String, AnyRef] = manifest.data ++ Map[String, AnyRef]("__text" -> manifest.innerText, "__cdata" -> manifest.cData) ++ getMediaMap(manifest.medias) + Map[String, AnyRef]("manifest" -> map) + }else Map[String, AnyRef]() + } + + def getControllersMap(controllers: List[Controller]):Map[String, AnyRef] = { + if(null != controllers && !controllers.isEmpty){ + val controllerMap:List[Map[String, AnyRef]] = controllers.map(controller => { + controller.data ++ Map[String, AnyRef]("__text" -> controller.innerText, "__cdata" -> controller.cData) + }) + Map[String, AnyRef]("controller" -> controllerMap) + }else Map[String, AnyRef]() + } + + def getEventsMap(events: List[Event]):Map[String, AnyRef] = { + if(null != events && !events.isEmpty){ + val eventsList: List[Map[String, AnyRef]] = events.map(event => event.data ++ Map[String, AnyRef]("__text" -> event.innerText, "__cdata" -> event.cData) ++ getChildPluginMap(event.childrenPlugin)) + + if(eventsList.length > 1) { + Map[String, AnyRef]("events" -> Map[String, AnyRef]("event" -> eventsList)) + }else { + Map[String, AnyRef]("event" -> eventsList.head) + } + } else Map[String, AnyRef]() + } + + def getMediaMap(medias: List[Media]): Map[String, AnyRef] = { + val mediasMap = medias.map(media => media.data ++ Map[String, AnyRef]("__text" -> media.innerText, "__cdata" -> media.cData) ++ getChildPluginMap(media.childrenPlugin)) + Map[String, AnyRef]("media" -> mediasMap) + } + + def getChildPluginMap(plugins: List[Plugin]):Map[String, AnyRef] = { + if(null != plugins && !plugins.isEmpty){ + plugins.filter(plugin => StringUtils.isNotBlank(plugin.data.get("cwp_element_name").get.asInstanceOf[String])) + .groupBy(plugin => plugin.data.get("cwp_element_name").get.asInstanceOf[String]) + .map(entry => { + if(entry._2.size == 1) + entry._1 -> entry._2.map(plugin => plugin.data ++ Map[String, AnyRef]("__text" -> plugin.innerText, "__cdata" -> plugin.cData) ++ getManifestMap(plugin.manifest) ++ getControllersMap(plugin.controllers) ++ getEventsMap(plugin.events)).head + else + entry._1 -> entry._2.map(plugin => plugin.data ++ Map[String, AnyRef]("__text" -> plugin.innerText, "__cdata" -> plugin.cData) ++ getManifestMap(plugin.manifest) ++ getControllersMap(plugin.controllers) ++ getEventsMap(plugin.events)) + }) + }else Map[String, AnyRef]() + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/MissingAssetValidatorProcessor.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/MissingAssetValidatorProcessor.scala new file mode 100644 index 000000000..b6d28b515 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/MissingAssetValidatorProcessor.scala @@ -0,0 +1,45 @@ +package org.sunbird.mimetype.ecml.processor + +import java.io.File + +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException + +trait MissingAssetValidatorProcessor extends IProcessor { + + abstract override def process(ecrf: Plugin)(implicit ss: StorageService): Plugin = { + validateMissingAssets(ecrf) + super.process(ecrf) + } + + def getMediaId(media: Media): String = { + if(null != media.data && !media.data.isEmpty){ + val plugin = media.data.get("plugin") + val ver = media.data.get("version") + if((null != plugin && !plugin.toString.isEmpty) && (null != ver && !ver.toString.isEmpty)) + media.id + "_" + plugin+ "_" + ver + else media.id + }else media.id + } + + def validateMissingAssets(ecrf: Plugin) = { + if(null != ecrf.manifest){ + val medias:List[Media] = ecrf.manifest.medias + if(null != medias){ + val mediaIds = medias.map(media => getMediaId(media)).toList + if(mediaIds.size != mediaIds.distinct.size) + throw new ClientException("DUPLICATE_ASSET_ID", "Error! Duplicate Asset Id used in the manifest. Asset Ids are: " + + mediaIds.groupBy(identity).mapValues(_.size).filter(p => p._2 > 1).keySet) + + val nonYoutubeMedias = medias.filter(media => !"youtube".equalsIgnoreCase(media.`type`)) + nonYoutubeMedias.map(media => { + if(widgetTypeAssets.contains(media.`type`) && !new File(getBasePath() + File.separator + "widgets" + File.separator + media.src).exists()) + throw new ClientException("MISSING_ASSETS", "Error! Missing Asset. | [Asset Id '" + media.id) + else if(!widgetTypeAssets.contains(media.`type`) && !new File(getBasePath() + File.separator + "assets" + File.separator + media.src).exists()) + throw new ClientException("MISSING_ASSETS", "Error! Missing Asset. | [Asset Id '" + media.id) + }) + } + + } + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/MissingControllerValidatorProcessor.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/MissingControllerValidatorProcessor.scala new file mode 100644 index 000000000..cc2e5aacc --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/MissingControllerValidatorProcessor.scala @@ -0,0 +1,35 @@ +package org.sunbird.mimetype.ecml.processor + +import java.io.File + +import org.apache.commons.lang3.StringUtils +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException + +trait MissingControllerValidatorProcessor extends IProcessor { + + abstract override def process(ecrf: Plugin)(implicit ss: StorageService): Plugin = { + validateControllers(ecrf) + super.process(ecrf) + } + + def validateControllers(plugin: Plugin) = { + if(null != plugin.controllers && !plugin.controllers.isEmpty){ + val controllers:List[Controller] = plugin.controllers + val controllerIds:List[String] = controllers.map(ctrl => ctrl.id) + if(controllerIds.size != controllerIds.distinct.size) + throw new ClientException("DUPLICATE_CONTROLLER_ID", "Error! Duplicate Controller Id used in the ECML. " + + controllerIds.groupBy(identity).mapValues(_.size).filter(p => p._2 > 1).keySet + +"' are used more than once in the ECML.]") + + val blankCdata:List[Controller] = controllers.filter(ctrl => StringUtils.isBlank(ctrl.cData)) + if(!blankCdata.isEmpty) blankCdata.map(ctrl => { + val controllerType:String = ctrl.data.get("type").asInstanceOf[String] + if("data".equalsIgnoreCase(controllerType) && !new File(getBasePath() + File.separator + "data" + File.separator + ctrl.id + ".json").exists()) + throw new ClientException("MISSING_CONTROLLER_FILE", "Error! Missing Controller file. | [Controller Id '" + ctrl.id +"' is missing.]") + else if("items".equalsIgnoreCase(controllerType) && !new File(getBasePath() + File.separator + "items" + File.separator + ctrl.id + ".json").exists()) + throw new ClientException("MISSING_CONTROLLER_FILE", "Error! Missing Controller file. | [Controller Id '" + ctrl.id +"' is missing.]") + }) + } + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/XMLLoaderWithCData.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/XMLLoaderWithCData.scala new file mode 100644 index 000000000..2ae89fc61 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/XMLLoaderWithCData.scala @@ -0,0 +1,36 @@ +package org.sunbird.mimetype.ecml.processor + +import org.xml.sax.InputSource +import org.xml.sax.ext.{DefaultHandler2, LexicalHandler} + +import scala.xml.{Elem, PCData, SAXParser, TopScope} +import scala.xml.factory.XMLLoader +import scala.xml.parsing.FactoryAdapter + +object XMLLoaderWithCData extends XMLLoader[Elem] { + def lexicalHandler(adapter: FactoryAdapter): LexicalHandler = + new DefaultHandler2 { + def captureCData(): Unit = { + adapter.hStack push PCData(adapter.buffer.toString) + adapter.buffer.clear() + } + + override def startCDATA(): Unit = adapter.captureText() + override def endCDATA(): Unit = captureCData() + } + + override def loadXML(source: InputSource, parser: SAXParser): Elem = { + val newAdapter = adapter + + val xmlReader = parser.getXMLReader + xmlReader.setProperty( + "http://xml.org/sax/properties/lexical-handler", + lexicalHandler(newAdapter)) + + newAdapter.scopeStack push TopScope + parser.parse(source, newAdapter) + newAdapter.scopeStack.pop() + + newAdapter.rootElem.asInstanceOf[Elem] + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/XmlParser.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/XmlParser.scala new file mode 100644 index 000000000..3f5453c3f --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/ecml/processor/XmlParser.scala @@ -0,0 +1,229 @@ +package org.sunbird.mimetype.ecml.processor + +import org.apache.commons.lang.{StringEscapeUtils, StringUtils} +import org.sunbird.common.exception.ClientException + +import scala.collection.mutable.ListBuffer +import scala.xml._ + +object XmlParser { + + val nonPluginElements: List[String] = List("manifest", "controller", "media", "events", "event", "__cdata", "__text") + val START_TAG_OPENING: String = "<" + val END_TAG_OPENING: String = "" + val ATTRIBUTE_KEY_VALUE_SEPARATOR: String = "=" + val BLANK_SPACE: String = " " + val DOUBLE_QUOTE: String = "\"" + + + def parse(xml: String): Plugin = { + val xmlObj: Node = XMLLoaderWithCData.loadString(xml) + processDocument(xmlObj) + } + + def processDocument(root: Node): Plugin = { + if(null != root) { + Plugin(getId(root), getData(root), "", getCdata(root), getChildrenPlugin(root), getManifest(root, true), getControllers(root \ "controllers"), getEvents(root)) + }else classOf[Plugin].newInstance() + } + + def getAttributesMap(node: Node):Map[String, AnyRef] = { + node.attributes.asAttrMap + } + + def getId(node: Node): String = { + getAttributesMap(node).getOrElse("id", "").asInstanceOf[String] + } + + def getData(node: Node): Map[String, AnyRef] = { + if(null != node) Map("cwp_element_name" -> node.label) ++ getAttributesMap(node) else Map() + } + + //TODO: Review the below code, this is as per the existing logic + def getCdata(node: Node): String = { + if(null != node && !node.child.isEmpty){ + val childNodes = node.child + var cdata = "" + childNodes.toList.filter(childNode => childNode.isInstanceOf[PCData]).map(childNode => { + cdata = childNode.text + }) + cdata + }else "" + } + + def getChildrenPlugin(node: Node): List[Plugin] = { + if(null != node && !node.child.isEmpty){ + val nodeList = node.child + nodeList.toList.filter(childNode => childNode.isInstanceOf[Elem] && !nonPluginElements.contains(childNode.label) && !"event".equalsIgnoreCase(childNode.label)) + .map(chilNode => Plugin(getId(chilNode), getData(chilNode), getInnerText(chilNode), getCdata(chilNode), getChildrenPlugin(chilNode), getManifest(chilNode, false), getControllers(chilNode \"controllers"), getEvents(chilNode))) + }else { + List() + } + } + + def getInnerText(node: Node): String = { + if(null != node && node.isInstanceOf[Elem] && !node.child.isEmpty){ + val childNodes = node.child + val innerTextlist = childNodes.toList.filter(childNode => childNode.isInstanceOf[Text]).map(item => item.text) + if(!innerTextlist.isEmpty) innerTextlist.head else "" + }else "" + } + + def getMedia(node: Node, validateNode: Boolean): Media = { + if(null != node){ + val attributeMap = getAttributesMap(node) + val id: String = attributeMap.getOrElse("id", "").asInstanceOf[String] + val `type`: String = attributeMap.getOrElse("type", "").asInstanceOf[String] + val src: String = attributeMap.getOrElse("src", "").asInstanceOf[String] + if(validateNode){ + if(StringUtils.isBlank(id)) + throw new ClientException("INVALID_MEDIA", "Error! Invalid Media ('id' is required.) in '" + node.buildString(true) + "' ...") + if(StringUtils.isBlank(id) && !(StringUtils.isNotBlank(`type`) && (StringUtils.equalsIgnoreCase(`type`, "js") || StringUtils.equalsIgnoreCase(`type`, "css")))) + throw new ClientException("INVALID_MEDIA", "Error! Invalid Media ('type' is required.) in '" + node.buildString(true) + "' ...") + if(StringUtils.isBlank(`type`)) + throw new ClientException("INVALID_MEDIA", "Error! Invalid Media ('type' is required.) in '" + node.buildString(true) + "' ...") + if(StringUtils.isBlank(src)) + throw new ClientException("INVALID_MEDIA", "Error! Invalid Media ('src' is required.) in '" + node.buildString(true) + "' ...") + } + Media(id, getData(node), getInnerText(node), getCdata(node), src, `type`, getChildrenPlugin(node)) + } else classOf[Media].newInstance() + } + + def getManifest(node: Node, validateNode: Boolean): Manifest = { + val childNodes = node.child + var manifestNode : Node = null + childNodes.toList.filter(childNode => StringUtils.equalsIgnoreCase(childNode.label, "manifest")).map(childNode => manifestNode = childNode) + val mediaList = { + if(null != manifestNode && !manifestNode.child.isEmpty){ + manifestNode.child.toList.filter(childNode => childNode.isInstanceOf[Elem] && "media".equalsIgnoreCase(childNode.label)).map(childNode => getMedia(childNode, validateNode)) + } else List() + } + if(null != manifestNode){ + Manifest(getId(manifestNode), getData(manifestNode), getInnerText(manifestNode), getCdata(manifestNode), mediaList) + } else classOf[Manifest].newInstance() + } + + def getControllers(nodeList: Seq[Node]): List[Controller] = { + if(null != nodeList && nodeList.length > 0) { + nodeList.toList.filter(node => node.isInstanceOf[Elem]).map(node => Controller(node.\@("id") , getData(node), getInnerText(node), getCdata(node))) + } else { + List() + } + } + + def getEvents(node: Node): List[Event] = { + var eventsList: ListBuffer[Event] = ListBuffer() + if(null != node && !node.child.isEmpty){ + val childNodes = node.child + childNodes.toList.map(childNode => { + if(childNode.isInstanceOf[Elem] && "events".equalsIgnoreCase(childNode.label)){ + eventsList ++= getEvents(childNode) + } + if(childNode.isInstanceOf[Elem] && "event".equalsIgnoreCase(childNode.label)){ + eventsList += Event(getId(childNode), getData(childNode), getInnerText(childNode), getCdata(childNode), getChildrenPlugin(childNode)) + } + }) + } + eventsList.toList + } + + + /** + * serialize + * + */ + def toString(plugin: Plugin): String = { + val strBuilder = StringBuilder.newBuilder + if(null != plugin) { + strBuilder.append(getElementXml(plugin.data)) + .append(getInnerTextXml(plugin.innerText)) + .append(getCdataXml(plugin.cData)) + .append(getContentManifestXml(plugin.manifest)) + .append(getContentControllersXml(plugin.controllers)) + .append(getPluginsXml(plugin.childrenPlugin)) + .append(getEventsXml(plugin.events)) + .append(getEndTag(plugin.data.getOrElse("cwp_element_name", "").asInstanceOf[String])) + } + strBuilder.toString() + } + + def getElementXml(data: Map[String, AnyRef]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if(null != data){ + strBuilder.append(START_TAG_OPENING + data.get("cwp_element_name").get) + data.filterKeys(key=>(!StringUtils.equalsIgnoreCase("cwp_element_name", key))).map(entry => strBuilder.append(BLANK_SPACE + entry._1 + ATTRIBUTE_KEY_VALUE_SEPARATOR + DOUBLE_QUOTE + entry._2 + DOUBLE_QUOTE)) + strBuilder.append(TAG_CLOSING) + } + strBuilder + } + + def getInnerTextXml(innerText: String): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if(StringUtils.isNotBlank(innerText)) strBuilder.append(StringEscapeUtils.escapeXml(innerText)) + strBuilder + } + + def getCdataXml(cData: String): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if(StringUtils.isNotBlank(cData)) strBuilder.append("") + strBuilder + } + + def getContentManifestXml(manifest: Manifest): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if(null != manifest && null != manifest.medias && !manifest.medias.isEmpty){ + strBuilder.append(getElementXml(manifest.data)).append(getInnerTextXml(manifest.innerText)) + .append(getCdataXml(manifest.cData)) + .append(getMediaXml(manifest.medias)) + .append(getEndTag(manifest.data.getOrElse("cwp_element_name", "").asInstanceOf[String])) + } + strBuilder + } + + def getPluginsXml(childrenPlugin: List[Plugin]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if(null != childrenPlugin && !childrenPlugin.isEmpty){ + childrenPlugin.map(plugin => strBuilder.append(toString(plugin))) + } + strBuilder + } + + def getContentControllersXml(controllers: List[Controller]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if(null != controllers && !controllers.isEmpty) { + controllers.map(controller => { + strBuilder.append(getElementXml(controller.data)) + .append(getInnerTextXml(controller.innerText)) + .append(getCdataXml(controller.cData)) + .append(getEndTag(controller.data.getOrElse("cwp_element_name", "").asInstanceOf[String])) + }) + } + strBuilder + } + + def getEventsXml(events: List[Event]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if(null != events && !events.isEmpty){ + if(events.size > 1) strBuilder.append(START_TAG_OPENING + "events" + TAG_CLOSING) + events.map(event => strBuilder.append(getElementXml(event.data)).append(getInnerTextXml(event.innerText)).append(getCdataXml(event.cData)).append(getPluginsXml(event.childrenPlugin)).append(getEndTag("event")) ) + if(events.size > 1) strBuilder.append(getEndTag("events")) + } + strBuilder + } + + def getEndTag(str: String): String = { + if(StringUtils.isNotBlank(str)) END_TAG_OPENING + str + TAG_CLOSING + else "" + } + + def getMediaXml(medias: List[Media]): StringBuilder = { + val strBuilder = StringBuilder.newBuilder + if(null != medias && !medias.isEmpty){ + medias.map(media => { + strBuilder.append(getElementXml(media.data)).append(getInnerTextXml(media.innerText)).append(media.cData).append(getPluginsXml(media.childrenPlugin)).append(getEndTag("media")) + }) + } + strBuilder + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/factory/MimeTypeManagerFactory.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/factory/MimeTypeManagerFactory.scala new file mode 100644 index 000000000..3f59ff22a --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/factory/MimeTypeManagerFactory.scala @@ -0,0 +1,40 @@ +package org.sunbird.mimetype.factory + +import org.apache.commons.lang3.StringUtils +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.Platform +import org.sunbird.mimetype.mgr.MimeTypeManager +import org.sunbird.mimetype.mgr.impl.{ApkMimeTypeMgrImpl, AssetMimeTypeMgrImpl, CollectionMimeTypeMgrImpl, DefaultMimeTypeMgrImpl, DocumentMimeTypeMgrImpl, EcmlMimeTypeMgrImpl, H5PMimeTypeMgrImpl, HtmlMimeTypeMgrImpl, PluginMimeTypeMgrImpl, YouTubeMimeTypeMgrImpl} + +object MimeTypeManagerFactory { + + implicit val ss: StorageService = new StorageService + val ONLINE_MIMETYPES: java.util.List[String] = Platform.getStringList("content.mimeType.online", java.util.Arrays.asList("video/youtube", "video/x-youtube", "text/x-url")) + + val defaultMimeTypeMgrImpl = new DefaultMimeTypeMgrImpl + val mimeTypeMgr = Map[String, MimeTypeManager]( + "video/youtube" -> new YouTubeMimeTypeMgrImpl,"video/x-youtube" -> new YouTubeMimeTypeMgrImpl, + "text/x-url" -> new YouTubeMimeTypeMgrImpl, + "application/pdf" -> new DocumentMimeTypeMgrImpl, "application/epub" -> new DocumentMimeTypeMgrImpl, + "application/msword" -> new DocumentMimeTypeMgrImpl, + "assets" -> new AssetMimeTypeMgrImpl, + "application/vnd.ekstep.ecml-archive" -> new EcmlMimeTypeMgrImpl, + "application/vnd.ekstep.html-archive" -> new HtmlMimeTypeMgrImpl, + "application/vnd.ekstep.content-collection" -> new CollectionMimeTypeMgrImpl, + "application/vnd.ekstep.plugin-archive" -> new PluginMimeTypeMgrImpl, + "application/vnd.ekstep.h5p-archive" -> new H5PMimeTypeMgrImpl, + "application/vnd.android.package-archive" -> new ApkMimeTypeMgrImpl + ) + + def getManager(objectType: String, mimeType: String): MimeTypeManager = { + if(ONLINE_MIMETYPES.contains(mimeType)) + mimeTypeMgr.getOrElse(mimeType.toLowerCase(), defaultMimeTypeMgrImpl) + else if (StringUtils.equalsIgnoreCase("Asset", objectType)) { + mimeTypeMgr.get("assets").get + } else { + if(null != mimeType) + mimeTypeMgr.getOrElse(mimeType.toLowerCase(), defaultMimeTypeMgrImpl) + else defaultMimeTypeMgrImpl + } + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/BaseMimeTypeManager.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/BaseMimeTypeManager.scala new file mode 100644 index 000000000..5822eff51 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/BaseMimeTypeManager.scala @@ -0,0 +1,271 @@ +package org.sunbird.mimetype.mgr + +import java.io.{File, FileInputStream, FileOutputStream, IOException} +import java.net.URL +import java.nio.file.{Files, Path, Paths} +import java.util.zip.{ZipEntry, ZipFile, ZipOutputStream} + +import org.apache.commons.io.{FileUtils, FilenameUtils} +import org.apache.commons.lang3.StringUtils +import org.apache.commons.validator.routines.UrlValidator +import org.apache.tika.Tika +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.{ClientException, ServerException} +import org.sunbird.common.{HttpUtil, Platform, Slug} +import org.sunbird.graph.dac.model.Node +import org.sunbird.telemetry.logger.TelemetryManager + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + + +class BaseMimeTypeManager(implicit ss: StorageService) { + + protected val TEMP_FILE_LOCATION = Platform.getString("content.upload.temp_location", "/tmp/content") + private val CONTENT_FOLDER = "cloud_storage.content.folder" + private val ARTIFACT_FOLDER = "cloud_storage.artifact.folder" + private val validator = new UrlValidator() + protected val extractableMimeTypes = List("application/vnd.ekstep.ecml-archive", "application/vnd.ekstep.html-archive", "application/vnd.ekstep.plugin-archive", "application/vnd.ekstep.h5p-archive") + protected val extractablePackageExtensions = List(".zip", ".h5p", ".epub") + private val H5P_MIMETYPE: String = "application/vnd.ekstep.h5p-archive" + private val H5P_LIBRARY_PATH: String = Platform.config.getString("content.h5p.library.path") + val DASH= "-" + val CONTENT_PLUGINS = "content-plugins" + val FILENAME_EXTENSION_SEPARATOR = "." + val DEFAULT_ZIP_EXTENSION = "zip" + private val tika: Tika = new Tika() + val httpUtil = new HttpUtil + + val IDX_S3_KEY = 0 + val IDX_S3_URL = 1 + + protected val UPLOAD_DENIED_ERR_MSG = "FILE_UPLOAD_ERROR | Upload operation not supported for given mimeType" + val COMPOSED_H5P_ZIP: String = "composed-h5p-zip" + + def validateUploadRequest(objectId: String, node: Node, data: AnyRef)(implicit ec: ExecutionContext): Unit = { + if (StringUtils.isBlank(objectId)) + throw new ClientException("ERR_INVALID_ID", "Please Provide Valid Identifier!") + if (null == node) + throw new ClientException("ERR_INVALID_NODE", "Please Provide Valid Node!") + if (null == data) + throw new ClientException("ERR_INVALID_DATA", "Please Provide Valid File Or File Url!") + if (data.isInstanceOf[String]) + validateUrl(data.toString) + else if (data.isInstanceOf[File]) + validateFile(data.asInstanceOf[File]) + } + + def validateFile(file: File): Unit = { + if(null==file || !file.exists()) + throw new ClientException("ERR_INVALID_FILE", "Please Provide Valid File!") + } + + def validateUrl(fileUrl: String): Unit = { + if (!validator.isValid(fileUrl)) + throw new ClientException("ERR_INVALID_FILE_URL", "Please Provide Valid File Url!") + } + + def uploadArtifactToCloud(uploadedFile: File, identifier: String, filePath: Option[String] = None): Array[String] = { + var urlArray = new Array[String](2) + try { + val folder = if(filePath.isDefined) filePath.get + File.separator + Platform.getString(CONTENT_FOLDER, "content") + File.separator + Slug.makeSlug(identifier, true) + File.separator + Platform.getString(ARTIFACT_FOLDER, "artifact") else Platform.getString(CONTENT_FOLDER, "content") + File.separator + Slug.makeSlug(identifier, true) + File.separator + Platform.getString(ARTIFACT_FOLDER, "artifact") + urlArray = ss.uploadFile(folder, uploadedFile) + } catch { + case e: Exception => + TelemetryManager.error("Error while uploading the file.", e) + throw new ServerException("ERR_CONTENT_UPLOAD_FILE", "Error while uploading the File.", e) + } + urlArray + } + + def getBasePath(objectId: String): String = { + if (!StringUtils.isBlank(objectId)) TEMP_FILE_LOCATION + File.separator + System.currentTimeMillis + "_temp" + File.separator + objectId else "" + } + + def delete(file: File): Unit = { + if (null != file && file.isDirectory) + FileUtils.deleteDirectory(file) + else file.delete() + } + + def copyURLToFile(objectId: String, fileUrl: String): File = try { + val fileName = getBasePath(objectId) + File.separator + getFileNameFromURL(fileUrl) + val file = new File(fileName) + FileUtils.copyURLToFile(new URL(fileUrl), file) + file + } catch { + case e: IOException => + throw new ClientException("ERR_INVALID_FILE_URL", "Please Provide Valid File Url!") + } + + def getFileNameFromURL(fileUrl: String): String = { + var fileName = FilenameUtils.getBaseName(fileUrl) + "_" + System.currentTimeMillis + if (!FilenameUtils.getExtension(fileUrl).isEmpty) fileName += "." + FilenameUtils.getExtension(fileUrl) + fileName + } + + def getFileSize(file: File): Double = { + if (null != file && file.exists) file.length else 0 + } + + def isValidPackageStructure(file: File, checkParams: List[String]): Boolean = { + if (null != file && file.exists()) { + val zipFile: ZipFile = new ZipFile(file) + try { + val entries = checkParams + .filter(fileName => null != zipFile.getEntry(fileName)) + null != entries && !entries.isEmpty + } + catch { + case e: Exception => throw new ClientException("ERR_INVALID_FILE", "Please Provide Valid File!") + } finally { + if (null != zipFile) zipFile.close() + } + } else false + } + + def isValidMimeType(file: File, expectedMimeType: String): Boolean = { + val mimeType = tika.detect(file) + expectedMimeType.equalsIgnoreCase(mimeType) + } + + def extractPackage(file: File, basePath: String) = { + val zipFile = new ZipFile(file) + for (entry <- zipFile.entries().asScala) { + val path = Paths.get(basePath + File.separator + entry.getName) + if (entry.isDirectory) Files.createDirectories(path) + else { + Files.createDirectories(path.getParent) + Files.copy(zipFile.getInputStream(entry), path) + } + } + } + + protected def getCloudStoredFileSize(key: String)(implicit ss: StorageService): Double = { + val size = 0 + if (StringUtils.isNotBlank(key)) try return ss.getObjectSize(key) + catch { + case e: Exception => + TelemetryManager.error("Error While getting the file size from Cloud Storage: " + key, e) + } + size + } + + protected def getMetadata(url: String, headers: java.util.Map[String, String] = new java.util.HashMap[String, String]()): java.util.Map[String, Object] = { + httpUtil.getMetadata(url, headers) + } + + def getFileMimeType(file: File): String = { + val tika = new Tika() + try tika.detect(file) + catch { + case e: IOException => { + e.printStackTrace() + "" + } + } + } + + def extractH5pPackage(objectId: String, extractionBasePath: String) = { + val h5pLibraryDownloadPath:String = getBasePath(objectId + File.separator + "h5p") + try{ + val url: URL = new URL(H5P_LIBRARY_PATH) + val file = new File(h5pLibraryDownloadPath + File.separator + getFileNameFromURL(url.getPath)) + FileUtils.copyURLToFile(url, file) + extractPackage(file, extractionBasePath) + } + finally{ + if(new File(h5pLibraryDownloadPath).exists()){ + FileUtils.deleteDirectory(new File(h5pLibraryDownloadPath)) + } + } + } + + def getExtractionPath(objectId: String, node: Node, extractionType: String, mimeType: String): String = { + val baseFolder = Platform.config.getString(CONTENT_FOLDER) + val pathSuffix: String = { + if(extractionType.equalsIgnoreCase("version")) { + val version = String.valueOf(node.getMetadata.get("pkgVersion").asInstanceOf[Double]) + if("application/vnd.ekstep.plugin-archive".equalsIgnoreCase(mimeType) && StringUtils.isNotBlank(node.getMetadata.get("semanticVersion").asInstanceOf[String])){ + node.getMetadata.get("semanticVersion").asInstanceOf[String] + } else version + }else extractionType + } + + mimeType match { + case "application/vnd.ekstep.ecml-archive" => baseFolder + File.separator + "ecml" + File.separator + objectId + DASH + pathSuffix + case "application/vnd.ekstep.html-archive" => baseFolder + File.separator + "html" + File.separator + objectId + DASH + pathSuffix + case "application/vnd.ekstep.h5p-archive" => baseFolder + File.separator + "h5p" + File.separator + objectId + DASH + pathSuffix + case "application/vnd.ekstep.plugin-archive" => CONTENT_PLUGINS + File.separator + objectId + DASH + pathSuffix + case _ => "" + } + } + + def extractPackageInCloud(objectId: String, uploadFile: File, node: Node, extractionType: String, slugFile: Boolean)(implicit ss: StorageService) = { + val file = Slug.createSlugFile(uploadFile) + val mimeType = node.getMetadata.get("mimeType").asInstanceOf[String] + validationForCloudExtraction(file, extractionType, mimeType) + if(extractableMimeTypes.contains(mimeType)){ + val extractionBasePath = getBasePath(objectId) + extractPackage(file, extractionBasePath) + ss.uploadDirectory(getExtractionPath(objectId, node, extractionType, mimeType), new File(extractionBasePath), Option(slugFile)) + } + } + + def extractH5PPackageInCloud(objectId: String, extractionBasePath: String, node: Node, extractionType: String, slugFile: Boolean)(implicit ec: ExecutionContext): Future[List[String]] = { + val mimeType = node.getMetadata.get("mimeType").asInstanceOf[String] + if(null == extractionType) + throw new ClientException("INVALID_EXTRACTION", "Error! Invalid Content Extraction Type.") + ss.uploadDirectoryAsync(getExtractionPath(objectId, node, extractionType, mimeType), new File(extractionBasePath), Option(slugFile)) + } + + def validationForCloudExtraction(file: File, extractionType: String, mimeType: String) = { + if(!file.exists() || (!extractablePackageExtensions.contains(FILENAME_EXTENSION_SEPARATOR + FilenameUtils.getExtension(file.getName)) && extractableMimeTypes.contains(mimeType))) + throw new ClientException("INVALID_FILE", "Error! File doesn't Exist.") + if(null == extractionType) + throw new ClientException("INVALID_EXTRACTION", "Error! Invalid Content Extraction Type.") + } + + def createZipPackage(basePath: String, zipFileName: String): Unit = + if (!StringUtils.isBlank(zipFileName)) { + TelemetryManager.log("Creating Zip File: " + zipFileName) + val fileList: List[String] = generateFileList(basePath) + zipIt(zipFileName, fileList, basePath) + } + + + private def generateFileList(sourceFolder: String): List[String] = + Files.walk(Paths.get(new File(sourceFolder).getPath)).toArray() + .map(path => path.asInstanceOf[Path]) + .filter(path => Files.isRegularFile(path)) + .map(path => generateZipEntry(path.toString, sourceFolder)).toList + + + private def generateZipEntry(file: String, sourceFolder: String): String = file.substring(sourceFolder.length, file.length) + + private def zipIt(zipFile: String, fileList: List[String], sourceFolder: String): Unit = { + val buffer = new Array[Byte](1024) + var zos: ZipOutputStream = null + try { + zos = new ZipOutputStream(new FileOutputStream(zipFile)) + TelemetryManager.log("Creating Zip File: " + zipFile) + fileList.foreach(file => { + val ze = new ZipEntry(file) + zos.putNextEntry(ze) + val in = new FileInputStream(sourceFolder + File.separator + file) + try { + var len = in.read(buffer) + while (len > 0) { + zos.write(buffer, 0, len) + len = in.read(buffer) + } + } finally if (in != null) in.close() + zos.closeEntry() + }) + } catch { + case e: IOException => TelemetryManager.error("Error! Something Went Wrong While Creating the ZIP File: " + e.getMessage, e) + } finally if (zos != null) zos.close() + } + +} + diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/MimeTypeManager.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/MimeTypeManager.scala new file mode 100644 index 000000000..198443829 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/MimeTypeManager.scala @@ -0,0 +1,18 @@ +package org.sunbird.mimetype.mgr + +import java.io.File + +import org.sunbird.models.UploadParams +import org.sunbird.graph.dac.model.Node + +import scala.concurrent.{ExecutionContext, Future} + +trait MimeTypeManager { + + @throws[Exception] + def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] + + @throws[Exception] + def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/ApkMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/ApkMimeTypeMgrImpl.scala new file mode 100644 index 000000000..b84d249d2 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/ApkMimeTypeMgrImpl.scala @@ -0,0 +1,29 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} + +import scala.concurrent.{ExecutionContext, Future} + +class ApkMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, uploadFile) + val result: Array[String] = uploadArtifactToCloud(uploadFile, objectId, filePath) + Future { + Map("identifier" -> objectId, "artifactUrl" -> result(1), "downloadUrl" -> result(1), "cloudStorageKey" -> result(0), "s3Key" -> result(0), "size" -> getCloudStoredFileSize(result(0)).asInstanceOf[AnyRef]) + } + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + val file = copyURLToFile(objectId, fileUrl) + Future { + Map[String, AnyRef]("identifier" -> objectId, "artifactUrl" -> fileUrl, "downloadUrl" -> fileUrl, "size" -> getFileSize(file).asInstanceOf[AnyRef]) + } + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/AssetMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/AssetMimeTypeMgrImpl.scala new file mode 100644 index 000000000..8f3f924e2 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/AssetMimeTypeMgrImpl.scala @@ -0,0 +1,37 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.apache.commons.lang3.StringUtils +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} +import org.sunbird.telemetry.logger.TelemetryManager + +import scala.concurrent.{ExecutionContext, Future} + +class AssetMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, uploadFile) + val fileMimeType = getFileMimeType(uploadFile) + val nodeMimeType = node.getMetadata.getOrDefault("mimeType", "").asInstanceOf[String] + TelemetryManager.log("Uploading Asset MimeType: " + fileMimeType) + if (!StringUtils.equalsIgnoreCase(fileMimeType, nodeMimeType)) { + TelemetryManager.log("Uploaded File MimeType is not same as Node (Object) MimeType. [Uploading MimeType: " + fileMimeType + " | Node (Object) MimeType: " + nodeMimeType + "]") + } + val result: Array[String] = uploadArtifactToCloud(uploadFile, objectId, filePath) + //TODO: depreciate s3Key. use cloudStorageKey instead + Future { + Map("identifier" -> objectId, "artifactUrl" -> result(1), "downloadUrl" -> result(1), "cloudStorageKey" -> result(0), "s3Key" -> result(0), "size" -> getCloudStoredFileSize(result(0)).asInstanceOf[AnyRef]) + } + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + Future {Map[String, AnyRef]("identifier" -> objectId, "artifactUrl" -> fileUrl, "downloadUrl" -> fileUrl,"size" -> getMetadata(fileUrl).get("Content-Length"))} + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/CollectionMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/CollectionMimeTypeMgrImpl.scala new file mode 100644 index 000000000..72c22cb07 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/CollectionMimeTypeMgrImpl.scala @@ -0,0 +1,21 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} + +import scala.concurrent.{ExecutionContext, Future} + +class CollectionMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + throw new ClientException("UPLOAD_DENIED", UPLOAD_DENIED_ERR_MSG) + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + throw new ClientException("UPLOAD_DENIED", UPLOAD_DENIED_ERR_MSG) + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/DefaultMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/DefaultMimeTypeMgrImpl.scala new file mode 100644 index 000000000..13f90c076 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/DefaultMimeTypeMgrImpl.scala @@ -0,0 +1,38 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.sunbird.models.UploadParams +import org.sunbird.common.exception.ClientException +import org.sunbird.cloudstore.StorageService +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} +import org.sunbird.telemetry.logger.TelemetryManager + +import scala.concurrent.{ExecutionContext, Future} + +class DefaultMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, uploadFile) + val nodeMimeType = node.getMetadata.getOrDefault("mimeType", "").asInstanceOf[String] + if (params.validation.getOrElse(true)) + if (!isValidMimeType(uploadFile, nodeMimeType)) { + TelemetryManager.log("Uploaded File MimeType is not same as Node (Object) MimeType. [Node (Object) MimeType: " + nodeMimeType + "]") + throw new ClientException("VALIDATION_ERROR", "Uploaded File MimeType is not same as Node (Object) MimeType.") + } + val result: Array[String] = uploadArtifactToCloud(uploadFile, objectId, filePath) + //TODO: depreciate s3Key. use cloudStorageKey instead + Future { + Map("identifier" -> objectId, "artifactUrl" -> result(1), "downloadUrl" -> result(1), "cloudStorageKey" -> result(0), "s3Key" -> result(0), "size" -> getCloudStoredFileSize(result(0)).asInstanceOf[AnyRef]) + } + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + Future { + Map[String, AnyRef]("identifier" -> objectId, "artifactUrl" -> fileUrl, "downloadUrl" -> fileUrl, "size" -> getMetadata(fileUrl).get("Content-Length")) + } + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/DocumentMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/DocumentMimeTypeMgrImpl.scala new file mode 100644 index 000000000..9848b2b70 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/DocumentMimeTypeMgrImpl.scala @@ -0,0 +1,89 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.{File, IOException} +import java.util + +import org.apache.commons.io.{FileUtils, FilenameUtils} +import org.apache.commons.lang3.StringUtils +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.Platform +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +class DocumentMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + + val DEFAULT_ALLOWED_EXTENSIONS_WORD = util.Arrays.asList("doc", "docx", "ppt", "pptx", "key", "odp", "pps", "odt", "wpd", "wps", "wks") + val ALLOWED_EXTENSIONS_WORD: List[String] = Platform.getStringList("mimetype.allowed_extensions.word", DEFAULT_ALLOWED_EXTENSIONS_WORD).asScala.toList + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, uploadFile) + val mimeType = node.getMetadata().getOrDefault("mimeType", "").asInstanceOf[String] + validateFileExtension(mimeType, uploadFile) + val file: File = + if (StringUtils.equalsAnyIgnoreCase("application/epub", mimeType) && StringUtils.endsWith(uploadFile.getName(), ".epub")) { + val basePath = getBasePath(objectId) + "/index.epub" + val tempFile = new File(basePath) + try FileUtils.moveFile(uploadFile, tempFile) + catch { + case e: IOException => e.printStackTrace() + } + tempFile + } else uploadFile + val result: Array[String] = uploadArtifactToCloud(file, objectId, filePath) + //TODO: depreciate s3Key. use cloudStorageKey instead + Future { + Map("identifier" -> objectId, "artifactUrl" -> result(1), "cloudStorageKey" -> result(0), "s3Key" -> result(0), "size" -> getCloudStoredFileSize(result(0)).asInstanceOf[AnyRef]) + } + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + validateFileUrlExtension(node.getMetadata.getOrDefault("mimeType", "").asInstanceOf[String], fileUrl) + Future { + Map[String, AnyRef]("identifier" -> objectId, "artifactUrl" -> fileUrl, "downloadUrl" -> fileUrl, "size" -> getMetadata(fileUrl).get("Content-Length")) + } + } + + def validateFileExtension(mimeType: String, file: File) = { + val fileType = getFileMimeType(file) + val fileExt = FilenameUtils.getExtension(file.getPath) + mimeType match { + case "application/pdf" => { + if (!(StringUtils.equalsIgnoreCase(fileExt, "pdf") && StringUtils.equals("application/pdf", fileType))) + throw new ClientException("ERR_INVALID_FILE", "Uploaded file is not a pdf file. Please upload a valid pdf file.") + } + case "application/epub" => { + if (!(StringUtils.equalsIgnoreCase(fileExt, "epub") && StringUtils.equals("application/epub+zip", fileType))) + throw new ClientException("ERR_INVALID_FILE", "Uploaded file is not a epub file. Please upload a valid epub file.") + } + case "application/msword" => { + if (!(StringUtils.isNotBlank(fileExt) && ALLOWED_EXTENSIONS_WORD.contains(fileExt))) + throw new ClientException("ERR_INVALID_FILE", "Uploaded file is not a word file. Please upload a valid word file.") + } + } + } + + def validateFileUrlExtension(mimeType: String, fileUrl: String) = { + val fileExt = FilenameUtils.getExtension(fileUrl) + mimeType match { + case "application/pdf" => { + if (!StringUtils.equalsIgnoreCase(fileExt, "pdf")) + throw new ClientException("ERR_INVALID_FILE_URL", "Please Provide Valid Pdf File Url!") + } + case "application/epub" => { + if (!StringUtils.equalsIgnoreCase(fileExt, "epub")) + throw new ClientException("ERR_INVALID_FILE_URL", "Please Provide Valid Epub File Url!") + } + case "application/msword" => { + if (!(StringUtils.isNotBlank(fileExt) && ALLOWED_EXTENSIONS_WORD.contains(fileExt))) + throw new ClientException("ERR_INVALID_FILE_URL", "Please Provide Valid Document File Url!") + } + } + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/EcmlMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/EcmlMimeTypeMgrImpl.scala new file mode 100644 index 000000000..02e2e6cf6 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/EcmlMimeTypeMgrImpl.scala @@ -0,0 +1,93 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.Platform +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.ecml.ECMLExtractor +import org.sunbird.mimetype.ecml.processor.{JsonParser, Plugin, XmlParser} +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} + +import scala.concurrent.{ExecutionContext, Future} + +class EcmlMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + + + private val DEFAULT_PACKAGE_MIME_TYPE = "application/zip" + private val maxPackageSize = if(Platform.config.hasPath("MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT")) Platform.config.getDouble("MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT") else 52428800 + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateFilePackage(uploadFile) + //generateECRF + val basePath:String = getBasePath(objectId) + extractPackage(uploadFile, basePath) + + val ecmlType: String = getEcmlType(basePath) + val ecml = getFileString(basePath, ecmlType) + // generate ECML + val ecrf: Plugin = getEcrfObject(ecmlType, ecml); + + val processedEcrf: Plugin = new ECMLExtractor(basePath, objectId).process(ecrf) + val processedEcml: String = getEcmlStringFromEcrf(processedEcrf, ecmlType) + //upload file + val result: Array[String] = uploadArtifactToCloud(uploadFile, objectId, filePath) + //extractFile + extractPackageInCloud(objectId, uploadFile, node, "snapshot", true) + + Future{Map("identifier"->objectId,"artifactUrl" -> result(1), "cloudStorageKey" -> result(0), "s3Key" -> result(0), "body" -> processedEcml)} + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + val file: File = copyURLToFile(objectId, fileUrl) + upload(objectId, node, file, filePath, params) + } + + def getEcmlType(basePath: String):String = { + val jsonFile = new File(basePath + File.separator + "index.json") + val xmlFile = new File(basePath + File.separator + "index.ecml") + if(null != jsonFile && jsonFile.exists() && null != xmlFile && xmlFile.exists()) throw new ClientException("MULTIPLE_ECML", "MULTIPLE_ECML_FILES_FOUND | [index.json and index.ecml]") + if(jsonFile.exists()) "json" + else if(xmlFile.exists()) "ecml" + else "" + } + + def getEcrfObject(ecmlType: String, ecml: String): Plugin = { + ecmlType match { + case "ecml" => XmlParser.parse(ecml) + case "json" => JsonParser.parse(ecml) + case _ => classOf[Plugin].newInstance() + } + } + + def getFileString(basePath: String, ecmlType: String):String = { + ecmlType match { + case "ecml" => org.apache.commons.io.FileUtils.readFileToString(new File(basePath + File.separator + "index.ecml"), "UTF-8") + case "json" => org.apache.commons.io.FileUtils.readFileToString(new File(basePath + File.separator + "index.json"), "UTF-8") + case _ => "" + } + } + + def getEcmlStringFromEcrf(processedEcrf: Plugin, ecmlType: String): String = { + ecmlType match { + case "ecml" => XmlParser.toString(processedEcrf) + case "json" => JsonParser.toString(processedEcrf) + case _ => "" + } + } + + def validateFilePackage(file: File) = { + if(null != file && file.exists()){ + if(!isValidMimeType(file, DEFAULT_PACKAGE_MIME_TYPE)) throw new ClientException("VALIDATOR_ERROR", "INVALID_CONTENT_PACKAGE_FILE_MIME_TYPE_ERROR | [The uploaded package is invalid]") + if(!isValidPackageStructure(file, List("index.json", "index.ecml", "/index.json", "/index.ecml"))) throw new ClientException("VALIDATOR_ERROR", "INVALID_CONTENT_PACKAGE_STRUCTURE_ERROR | ['index' file and other folders (assets, data & widgets) should be at root location]") + if(file.length() > maxPackageSize) throw new ClientException("VALIDATOR_ERROR", "INVALID_CONTENT_PACKAGE_SIZE_ERROR | [Content Package file size is too large]") + } + else{ + throw new ClientException("ERR_INVALID_FILE", "File does not exists") + } + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/H5PMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/H5PMimeTypeMgrImpl.scala new file mode 100644 index 000000000..01e7fcf57 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/H5PMimeTypeMgrImpl.scala @@ -0,0 +1,84 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File +import java.util.concurrent.CompletionException + +import org.apache.hadoop.util.StringUtils +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.Slug +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} +import org.sunbird.telemetry.logger.TelemetryManager + +import scala.concurrent.{ExecutionContext, Future} + +class H5PMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager()(ss) with MimeTypeManager { + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, uploadFile) + val validationParams = if (StringUtils.equalsIgnoreCase(params.fileFormat.getOrElse(""), COMPOSED_H5P_ZIP)) + List[String]("/content/h5p.json", "content/h5p.json") else List[String]("h5p.json", "/h5p.json") + if (isValidPackageStructure(uploadFile, validationParams)) { + val extractionBasePath = getBasePath(objectId) + val zipFile = if (!StringUtils.equalsIgnoreCase(params.fileFormat.getOrElse(""), COMPOSED_H5P_ZIP)) { + val zippedFileName = createH5PZipFile(extractionBasePath, uploadFile, objectId) + new File(zippedFileName) + } else { + extractPackage(uploadFile, extractionBasePath) + uploadFile + } + val urls: Array[String] = uploadArtifactToCloud(zipFile, objectId, filePath) + if (zipFile.exists) zipFile.delete + Future { + extractH5PPackageInCloud(objectId, extractionBasePath, node, "snapshot", false).map(resp => + TelemetryManager.info("H5P content snapshot folder upload success for " + objectId) + ) onFailure { case e: Throwable => + TelemetryManager.error("H5P content snapshot folder upload failed for " + objectId, e.getCause) + } + } + Future { Map[String, AnyRef]("identifier" -> objectId, "artifactUrl" -> urls(IDX_S3_URL), "size" -> getFileSize(uploadFile).asInstanceOf[AnyRef], "s3Key" -> urls(IDX_S3_KEY)) } + } else { + TelemetryManager.error("ERR_INVALID_FILE" + "Please Provide Valid File! with file name: " + uploadFile.getName) + throw new ClientException("ERR_INVALID_FILE", "Please Provide Valid File!") + } + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + val file = copyURLToFile(objectId, fileUrl) + upload(objectId, node, file, filePath, params) + } + + /** + * + * @param extractionBasePath + * @param uploadFiled + * @param objectId + * @return + */ + def createH5PZipFile(extractionBasePath: String, uploadFiled: File, objectId: String): String = { + // Download the H5P Libraries and Un-Zip the H5P Library Files + extractH5pPackage(objectId, extractionBasePath) + // UnZip the Content Package + extractPackage(uploadFiled, extractionBasePath + File.separator + "content") + // Create 'ZIP' Package + val zipFileName = extractionBasePath + File.separator + System.currentTimeMillis + "_" + Slug.makeSlug(objectId) + FILENAME_EXTENSION_SEPARATOR + DEFAULT_ZIP_EXTENSION + createZipPackage(extractionBasePath, zipFileName) + zipFileName + } + + def copyH5P(uploadFile: File, node: Node)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + val objectId: String = node.getIdentifier + val extractionBasePath = getBasePath(objectId) + extractPackage(uploadFile, extractionBasePath) + val urls: Array[String] = uploadArtifactToCloud(uploadFile, objectId, None) + node.getMetadata.put("s3Key", urls(IDX_S3_KEY)) + node.getMetadata.put("artifactUrl", urls(IDX_S3_URL)) + extractH5PPackageInCloud(objectId, extractionBasePath, node, "snapshot", false).map(resp => + Map[String, AnyRef]("identifier" -> objectId, "artifactUrl" -> urls(IDX_S3_URL), "size" -> getFileSize(uploadFile).asInstanceOf[AnyRef], "s3Key" -> urls(IDX_S3_KEY)) + ) recoverWith { case e: CompletionException => throw e.getCause } + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/HtmlMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/HtmlMimeTypeMgrImpl.scala new file mode 100644 index 000000000..80df3773e --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/HtmlMimeTypeMgrImpl.scala @@ -0,0 +1,37 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} +import org.sunbird.telemetry.logger.TelemetryManager + +import scala.concurrent.{ExecutionContext, Future} + +class HtmlMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, uploadFile) + if (isValidPackageStructure(uploadFile, List[String]("index.html"))) { + val urls = uploadArtifactToCloud(uploadFile, objectId, filePath) + node.getMetadata.put("s3Key", urls(IDX_S3_KEY)) + node.getMetadata.put("artifactUrl", urls(IDX_S3_URL)) + extractPackageInCloud(objectId, uploadFile, node, "snapshot", false) + Future(Map[String, AnyRef]("identifier" -> objectId, "artifactUrl" -> urls(IDX_S3_URL), "s3Key" -> urls(IDX_S3_KEY), "size" -> getFileSize(uploadFile).asInstanceOf[AnyRef])) + } else { + TelemetryManager.error("ERR_INVALID_FILE" + "Please Provide Valid File! with file name: " + uploadFile.getName) + throw new ClientException("ERR_INVALID_FILE", "Please Provide Valid File!") + } + + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + val file = copyURLToFile(objectId, fileUrl) + upload(objectId, node, file, filePath, params) + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/PluginMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/PluginMimeTypeMgrImpl.scala new file mode 100644 index 000000000..1bd4d56aa --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/PluginMimeTypeMgrImpl.scala @@ -0,0 +1,61 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File +import java.nio.charset.StandardCharsets + +import org.apache.commons.io.FileUtils +import org.sunbird.models.UploadParams +import org.sunbird.common.JsonUtils +import org.sunbird.common.exception.ClientException +import org.sunbird.cloudstore.StorageService +import org.sunbird.graph.dac.model.Node +import org.sunbird.graph.utils.ScalaJsonUtils +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} + +import scala.collection.JavaConverters +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} + +class PluginMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + private val DEF_CONTENT_PACKAGE_MIME_TYPE: String = "application/zip" + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, uploadFile) + validatePluginPackage(uploadFile) + val basePath = getBasePath(objectId) + extractPackage(uploadFile, basePath) + val manifestFile = new File(basePath + File.separator + "manifest.json") + val data:Map[String, AnyRef] = readDataFromManifest(manifestFile, objectId) + FileUtils.deleteDirectory(new File(basePath)) + val result = uploadArtifactToCloud(uploadFile, objectId, filePath) + extractPackageInCloud(objectId, uploadFile, node, "snapshot", true) + Future{data ++ Map("identifier"->objectId,"artifactUrl" -> result(1), "cloudStorageKey" -> result(0), "s3Key" -> result(0))} + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + val file = copyURLToFile(objectId, fileUrl) + upload(objectId, node, file, filePath, params) + } + + def validatePluginPackage(uploadFile: File) = { + if(!isValidMimeType(uploadFile, DEF_CONTENT_PACKAGE_MIME_TYPE)) throw new ClientException("VALIDATION_ERROR", "Error! Invalid Content Package Mime Type.") + if(!isValidPackageStructure(uploadFile, List("manifest.json"))) throw new ClientException("VALIDATION_ERROR", "Error !Invalid Content Package File Structure. | [manifest.json should be at root location]" ) + } + + def readDataFromManifest(manifestFile: File, objectId: String): Map[String, AnyRef] = { + if(manifestFile.exists()){ + val json: String = FileUtils.readFileToString(manifestFile, StandardCharsets.UTF_8) + val dataMap: Map[String, AnyRef] = ScalaJsonUtils.deserialize[Map[String, AnyRef]](json) + if(!objectId.contentEquals(dataMap.getOrElse("id", "").asInstanceOf[String])) + throw new ClientException("ERR_INVALID_PLUGIN_ID", "'id' in manifest.json is not same as the plugin identifier.") + val version = dataMap.getOrElse("ver", throw new ClientException("ERR_MISSING_VERSION", "'ver' is not specified in the plugin manifest.json.")) + val targets = dataMap.getOrElse("targets", List[AnyRef]()) + val targetList: java.util.List[Object] = { + if(targets.isInstanceOf[String]) JsonUtils.deserialize(targets.asInstanceOf[String], classOf[java.util.List[Object]]) + else targets.asInstanceOf[List[AnyRef]].asJava.asInstanceOf[java.util.List[Object]] + } + Map[String, AnyRef]("semanticVersion" -> version, "targets" -> targetList) + }else Map[String, AnyRef]() + } +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/YouTubeMimeTypeMgrImpl.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/YouTubeMimeTypeMgrImpl.scala new file mode 100644 index 000000000..31f5ad048 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/mimetype/mgr/impl/YouTubeMimeTypeMgrImpl.scala @@ -0,0 +1,25 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.{BaseMimeTypeManager, MimeTypeManager} + +import scala.concurrent.{ExecutionContext, Future} + + +class YouTubeMimeTypeMgrImpl(implicit ss: StorageService) extends BaseMimeTypeManager with MimeTypeManager { + + override def upload(objectId: String, node: Node, uploadFile: File, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + throw new ClientException("UPLOAD_DENIED", UPLOAD_DENIED_ERR_MSG) + } + + override def upload(objectId: String, node: Node, fileUrl: String, filePath: Option[String], params: UploadParams)(implicit ec: ExecutionContext): Future[Map[String, AnyRef]] = { + validateUploadRequest(objectId, node, fileUrl) + Future{Map[String, AnyRef]("identifier" -> objectId, "artifactUrl" -> fileUrl)} + } + +} diff --git a/platform-modules/mimetype-manager/src/main/scala/org/sunbird/models/UploadParams.scala b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/models/UploadParams.scala new file mode 100644 index 000000000..4ff9f4ec0 --- /dev/null +++ b/platform-modules/mimetype-manager/src/main/scala/org/sunbird/models/UploadParams.scala @@ -0,0 +1,5 @@ +package org.sunbird.models + +case class UploadParams(fileFormat: Option[String] = Some(""), validation: Option[Boolean] = Some(true)) + + diff --git a/platform-modules/mimetype-manager/src/test/resources/application.conf b/platform-modules/mimetype-manager/src/test/resources/application.conf new file mode 100644 index 000000000..f538019ed --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/resources/application.conf @@ -0,0 +1,478 @@ +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +# ~~~~~ +# Play uses HOCON as its configuration file format. HOCON has a number +# of advantages over other config formats, but there are two things that +# can be used when modifying settings. +# +# You can include other configuration files in this main application.conf file: +#include "extra-config.conf" +# +# You can declare variables and substitute for them: +#mykey = ${some.value} +# +# And if an environment variable exists when there is no other substitution, then +# HOCON will fall back to substituting environment variable: +#mykey = ${JAVA_HOME} + +## Akka +# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration +# https://www.playframework.com/documentation/latest/JavaAkka#Configuration +# ~~~~~ +# Play uses Akka internally and exposes Akka Streams and actors in Websockets and +# other streaming HTTP responses. +akka { + # "akka.log-config-on-start" is extraordinarly useful because it log the complete + # configuration at INFO level, including defaults and overrides, so it s worth + # putting at the very top. + # + # Put the following in your conf/logback.xml file: + # + # + # + # And then uncomment this line to debug the configuration. + # + #log-config-on-start = true +} + +## Secret key +# http://www.playframework.com/documentation/latest/ApplicationSecret +# ~~~~~ +# The secret key is used to sign Play's session cookie. +# This must be changed for production, but we don't recommend you change it in this file. +play.http.secret.key = a-long-secret-to-calm-the-rage-of-the-entropy-gods + +## Modules +# https://www.playframework.com/documentation/latest/Modules +# ~~~~~ +# Control which modules are loaded when Play starts. Note that modules are +# the replacement for "GlobalSettings", which are deprecated in 2.5.x. +# Please see https://www.playframework.com/documentation/latest/GlobalSettings +# for more information. +# +# You can also extend Play functionality by using one of the publically available +# Play modules: https://playframework.com/documentation/latest/ModuleDirectory +play.modules { + # By default, Play will load any class called Module that is defined + # in the root package (the "app" directory), or you can define them + # explicitly below. + # If there are any built-in modules that you want to enable, you can list them here. + #enabled += my.application.Module + + # If there are any built-in modules that you want to disable, you can list them here. + #disabled += "" +} + +## IDE +# https://www.playframework.com/documentation/latest/IDE +# ~~~~~ +# Depending on your IDE, you can add a hyperlink for errors that will jump you +# directly to the code location in the IDE in dev mode. The following line makes +# use of the IntelliJ IDEA REST interface: +#play.editor="http://localhost:63342/api/file/?file=%s&line=%s" + +## Internationalisation +# https://www.playframework.com/documentation/latest/JavaI18N +# https://www.playframework.com/documentation/latest/ScalaI18N +# ~~~~~ +# Play comes with its own i18n settings, which allow the user's preferred language +# to map through to internal messages, or allow the language to be stored in a cookie. +play.i18n { + # The application languages + langs = [ "en" ] + + # Whether the language cookie should be secure or not + #langCookieSecure = true + + # Whether the HTTP only attribute of the cookie should be set to true + #langCookieHttpOnly = true +} + +## Play HTTP settings +# ~~~~~ +play.http { + ## Router + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # Define the Router object to use for this application. + # This router will be looked up first when the application is starting up, + # so make sure this is the entry point. + # Furthermore, it's assumed your route file is named properly. + # So for an application router like `my.application.Router`, + # you may need to define a router file `conf/my.application.routes`. + # Default to Routes in the root package (aka "apps" folder) (and conf/routes) + #router = my.application.Router + + ## Action Creator + # https://www.playframework.com/documentation/latest/JavaActionCreator + # ~~~~~ + #actionCreator = null + + ## ErrorHandler + # https://www.playframework.com/documentation/latest/JavaRouting + # https://www.playframework.com/documentation/latest/ScalaRouting + # ~~~~~ + # If null, will attempt to load a class called ErrorHandler in the root package, + #errorHandler = null + + ## Session & Flash + # https://www.playframework.com/documentation/latest/JavaSessionFlash + # https://www.playframework.com/documentation/latest/ScalaSessionFlash + # ~~~~~ + session { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + + # Sets the max-age field of the cookie to 5 minutes. + # NOTE: this only sets when the browser will discard the cookie. Play will consider any + # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, + # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. + #maxAge = 300 + + # Sets the domain on the session cookie. + #domain = "example.com" + } + + flash { + # Sets the cookie to be sent only over HTTPS. + #secure = true + + # Sets the cookie to be accessed only by the server. + #httpOnly = true + } +} + +## Netty Provider +# https://www.playframework.com/documentation/latest/SettingsNetty +# ~~~~~ +play.server.netty { + # Whether the Netty wire should be logged + log.wire = true + + # If you run Play on Linux, you can use Netty's native socket transport + # for higher performance with less garbage. + transport = "native" +} + +## WS (HTTP Client) +# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS +# ~~~~~ +# The HTTP client primarily used for REST APIs. The default client can be +# configured directly, but you can also create different client instances +# with customized settings. You must enable this by adding to build.sbt: +# +# libraryDependencies += ws // or javaWs if using java +# +play.ws { + # Sets HTTP requests not to follow 302 requests + #followRedirects = false + + # Sets the maximum number of open HTTP connections for the client. + #ahc.maxConnectionsTotal = 50 + + ## WS SSL + # https://www.playframework.com/documentation/latest/WsSSL + # ~~~~~ + ssl { + # Configuring HTTPS with Play WS does not require programming. You can + # set up both trustManager and keyManager for mutual authentication, and + # turn on JSSE debugging in development with a reload. + #debug.handshake = true + #trustManager = { + # stores = [ + # { type = "JKS", path = "exampletrust.jks" } + # ] + #} + } +} + +## Cache +# https://www.playframework.com/documentation/latest/JavaCache +# https://www.playframework.com/documentation/latest/ScalaCache +# ~~~~~ +# Play comes with an integrated cache API that can reduce the operational +# overhead of repeated requests. You must enable this by adding to build.sbt: +# +# libraryDependencies += cache +# +play.cache { + # If you want to bind several caches, you can bind the individually + #bindCaches = ["db-cache", "user-cache", "session-cache"] +} + +## Filter Configuration +# https://www.playframework.com/documentation/latest/Filters +# ~~~~~ +# There are a number of built-in filters that can be enabled and configured +# to give Play greater security. +# +play.filters { + + # Enabled filters are run automatically against Play. + # CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default. + enabled = [] + + # Disabled filters remove elements from the enabled list. + # disabled += filters.CSRFFilter + + + ## CORS filter configuration + # https://www.playframework.com/documentation/latest/CorsFilter + # ~~~~~ + # CORS is a protocol that allows web applications to make requests from the browser + # across different domains. + # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has + # dependencies on CORS settings. + cors { + # Filter paths by a whitelist of path prefixes + #pathPrefixes = ["/some/path", ...] + + # The allowed origins. If null, all origins are allowed. + #allowedOrigins = ["http://www.example.com"] + + # The allowed HTTP methods. If null, all methods are allowed + #allowedHttpMethods = ["GET", "POST"] + } + + ## Security headers filter configuration + # https://www.playframework.com/documentation/latest/SecurityHeaders + # ~~~~~ + # Defines security headers that prevent XSS attacks. + # If enabled, then all options are set to the below configuration by default: + headers { + # The X-Frame-Options header. If null, the header is not set. + #frameOptions = "DENY" + + # The X-XSS-Protection header. If null, the header is not set. + #xssProtection = "1; mode=block" + + # The X-Content-Type-Options header. If null, the header is not set. + #contentTypeOptions = "nosniff" + + # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. + #permittedCrossDomainPolicies = "master-only" + + # The Content-Security-Policy header. If null, the header is not set. + #contentSecurityPolicy = "default-src 'self'" + } + + ## Allowed hosts filter configuration + # https://www.playframework.com/documentation/latest/AllowedHostsFilter + # ~~~~~ + # Play provides a filter that lets you configure which hosts can access your application. + # This is useful to prevent cache poisoning attacks. + hosts { + # Allow requests to example.com, its subdomains, and localhost:9000. + #allowed = [".example.com", "localhost:9000"] + } +} + +# Learning-Service Configuration +content.metadata.visibility.parent=["textbookunit", "courseunit", "lessonplanunit"] + +# Cassandra Configuration +content.keyspace.name=content_store +content.keyspace.table=content_data +#TODO: Add Configuration for assessment. e.g: question_data +orchestrator.keyspace.name=script_store +orchestrator.keyspace.table=script_data +cassandra.lp.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" +cassandra.lpa.connection="127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042" + +# Redis Configuration +redis.host=localhost +redis.port=6379 +redis.maxConnections=128 + +#Condition to enable publish locally +content.publish_task.enabled=true + +#directory location where store unzip file +dist.directory=/data/tmp/dist/ +output.zipfile=/data/tmp/story.zip +source.folder=/data/tmp/temp2/ +save.directory=/data/tmp/temp/ + +# Content 2 vec analytics URL +CONTENT_TO_VEC_URL="http://172.31.27.233:9000/content-to-vec" + +# FOR CONTENT WORKFLOW PIPELINE (CWP) + +#--Content Workflow Pipeline Mode +OPERATION_MODE=TEST + +#--Maximum Content Package File Size Limit in Bytes (500 MB) +MAX_CONTENT_PACKAGE_FILE_SIZE_LIMIT=524288000 + +#--Maximum Asset File Size Limit in Bytes (200 MB) +MAX_ASSET_FILE_SIZE_LIMIT=209715200 + +#--No of Retry While File Download Fails +RETRY_ASSET_DOWNLOAD_COUNT=1 + +#Google-vision-API +google.vision.tagging.enabled = false + +#Orchestrator env properties +env="https://dev.ekstep.in/api/learning" + +#Current environment +cloud_storage.env=dev + + +#Folder configuration +cloud_storage.content.folder=content +cloud_storage.asset.folder=assets +cloud_storage.artefact.folder=artifact +cloud_storage.bundle.folder=bundle +cloud_storage.media.folder=media +cloud_storage.ecar.folder=ecar_files + +# Media download configuration +content.media.base.url="https://dev.open-sunbird.org" +plugin.media.base.url="https://dev.open-sunbird.org" + +# Configuration +graph.dir=/data/testingGraphDB +akka.request_timeout=30 +environment.id=10000000 +graph.ids=["domain"] +graph.passport.key.base=31b6fd1c4d64e745c867e61a45edc34a +route.domain="bolt://localhost:7687" +route.bolt.write.domain="bolt://localhost:7687" +route.bolt.read.domain="bolt://localhost:7687" +route.bolt.comment.domain="bolt://localhost:7687" +route.all="bolt://localhost:7687" +route.bolt.write.all="bolt://localhost:7687" +route.bolt.read.all="bolt://localhost:7687" +route.bolt.comment.all="bolt://localhost:7687" + +shard.id=1 +platform.auth.check.enabled=false +platform.cache.ttl=3600000 + +# Elasticsearch properties +search.es_conn_info="localhost:9200" +search.fields.query=["name^100","title^100","lemma^100","code^100","tags^100","domain","subject","description^10","keywords^25","ageGroup^10","filter^10","theme^10","genre^10","objects^25","contentType^100","language^200","teachingMode^25","skills^10","learningObjective^10","curriculum^100","gradeLevel^100","developer^100","attributions^10","owner^50","text","words","releaseNotes"] +search.fields.date=["lastUpdatedOn","createdOn","versionDate","lastSubmittedOn","lastPublishedOn"] +search.batch.size=500 +search.connection.timeout=30 +platform-api-url="http://localhost:8080/language-service" +MAX_ITERATION_COUNT_FOR_SAMZA_JOB=2 + + +# DIAL Code Configuration +dialcode.keyspace.name="dialcode_store" +dialcode.keyspace.table="dial_code" +dialcode.max_count=1000 + +# System Configuration +system.config.keyspace.name="dialcode_store" +system.config.table="system_config" + +#Publisher Configuration +publisher.keyspace.name="dialcode_store" +publisher.keyspace.table="publisher" + +#DIAL Code Generator Configuration +dialcode.strip.chars="0" +dialcode.length=6.0 +dialcode.large.prime_number=1679979167 + +#DIAL Code ElasticSearch Configuration +dialcode.index=true +dialcode.object_type="DialCode" + +framework.max_term_creation_limit=200 + +# Enable Suggested Framework in Get Channel API. +channel.fetch.suggested_frameworks=true + +# Kafka configuration details +kafka.topics.instruction="local.learning.job.request" +kafka.urls="localhost:9092" + +#Youtube Standard Licence Validation +learning.content.youtube.validate.license=true +learning.content.youtube.application.name=fetch-youtube-license +youtube.license.regex.pattern=["\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)","^([A-Za-z0-9\\-\\_]*)"] + +#Top N Config for Search Telemetry +telemetry_env=dev +telemetry.search.topn=5 + +installation.id=ekstep + +channel.default="in.ekstep" + +# DialCode Link API Config +learning.content.link_dialcode_validation=true +dialcode.api.search.url="http://localhost:8080/learning-service/v3/dialcode/search" +dialcode.api.authorization=auth_key + +# Language-Code Configuration +platform.language.codes=["as","bn","en","gu","hi","hoc","jun","ka","mai","mr","unx","or","san","sat","ta","te","urd", "pj"] + +# Kafka send event to topic enable +kafka.topic.send.enable=false + +learning.valid_license=["creativeCommon"] +learning.service_provider=["youtube"] + +stream.mime.type=video/mp4 +compositesearch.index.name="compositesearch" + +hierarchy.keyspace.name=hierarchy_store +content.hierarchy.table=content_hierarchy +framework.hierarchy.table=framework_hierarchy + +# Kafka topic for definition update event. +kafka.topic.system.command="dev.system.command" + +learning.reserve_dialcode.content_type=["TextBook"] +# restrict.metadata.objectTypes=["Content", "ContentImage", "AssessmentItem", "Channel", "Framework", "Category", "CategoryInstance", "Term"] + +#restrict.metadata.objectTypes="Content,ContentImage" + +publish.collection.fullecar.disable=true + +# Consistency Level for Multi Node Cassandra cluster +cassandra.lp.consistency.level=QUORUM + + +content.nested.fields="badgeAssertions,targets,badgeAssociations" + +content.cache.ttl=86400 +content.cache.enable=true +collection.cache.enable=true +content.discard.status=["Draft","FlagDraft"] + +framework.categories_cached=["subject", "medium", "gradeLevel", "board"] +framework.cache.ttl=86400 +framework.cache.read=true + + +# Max size(width/height) of thumbnail in pixels +max.thumbnail.size.pixels=150 + +schema.base_path="../../../../schemas/" +content.hierarchy.removed_props_for_leafNodes=["collections","children","usedByContent","item_sets","methods","libraries","editorState"] + +collection.keyspace = "hierarchy_store" +content.keyspace = "content_store" + +collection.image.migration.enabled=true + + + +content.h5p.library.path="https://s3.ap-south-1.amazonaws.com/ekstep-public-dev/content/templates/h5p-library-latest.zip" +# This is added to handle large artifacts sizes differently +content.artifact.size.for_online=209715200 +cloud_storage_type="azure" +azure_storage_key="asdfgh" +azure_storage_secret="jhgfdcvb" +azure_storage_container="sunbird-content-dev" \ No newline at end of file diff --git a/platform-modules/mimetype-manager/src/test/resources/content/valid_h5p_content.h5p b/platform-modules/mimetype-manager/src/test/resources/content/valid_h5p_content.h5p new file mode 100644 index 000000000..c033f5d68 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/content/valid_h5p_content.h5p differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZip/human_vs_robot-.jpg b/platform-modules/mimetype-manager/src/test/resources/filesToZip/human_vs_robot-.jpg new file mode 100644 index 000000000..46b7fd357 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZip/human_vs_robot-.jpg differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZip/mitochondria.jpeg b/platform-modules/mimetype-manager/src/test/resources/filesToZip/mitochondria.jpeg new file mode 100644 index 000000000..691bb5074 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZip/mitochondria.jpeg differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZip/sample.pdf b/platform-modules/mimetype-manager/src/test/resources/filesToZip/sample.pdf new file mode 100644 index 000000000..dbf091df9 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZip/sample.pdf differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZip/slice.pdf b/platform-modules/mimetype-manager/src/test/resources/filesToZip/slice.pdf new file mode 100644 index 000000000..3c968f9f2 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZip/slice.pdf differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/2a4b8abd789184932399d222d03d9b5c.jpg b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/2a4b8abd789184932399d222d03d9b5c.jpg new file mode 100644 index 000000000..121299bed Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/2a4b8abd789184932399d222d03d9b5c.jpg differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/human_vs_robot-.jpg b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/human_vs_robot-.jpg new file mode 100644 index 000000000..46b7fd357 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/human_vs_robot-.jpg differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/mitochondria.jpeg b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/mitochondria.jpeg new file mode 100644 index 000000000..691bb5074 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/mitochondria.jpeg differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/sample.pdf b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/sample.pdf new file mode 100644 index 000000000..dbf091df9 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/sample.pdf differ diff --git a/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/slice.pdf b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/slice.pdf new file mode 100644 index 000000000..3c968f9f2 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/filesToZipNested/filesToZip/slice.pdf differ diff --git a/platform-modules/mimetype-manager/src/test/resources/igp-twss.epub b/platform-modules/mimetype-manager/src/test/resources/igp-twss.epub new file mode 100644 index 000000000..f4fd2a955 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/igp-twss.epub differ diff --git a/platform-modules/mimetype-manager/src/test/resources/invalidHtmlContent.zip b/platform-modules/mimetype-manager/src/test/resources/invalidHtmlContent.zip new file mode 100644 index 000000000..8f9868bea Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/invalidHtmlContent.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/invalid_h5p_content.h5p.zip b/platform-modules/mimetype-manager/src/test/resources/invalid_h5p_content.h5p.zip new file mode 100644 index 000000000..22314da62 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/invalid_h5p_content.h5p.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/invalid_h5p_content.zip b/platform-modules/mimetype-manager/src/test/resources/invalid_h5p_content.zip new file mode 100644 index 000000000..22314da62 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/invalid_h5p_content.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/manifestNoId.json b/platform-modules/mimetype-manager/src/test/resources/manifestNoId.json new file mode 100644 index 000000000..1cdd1fa86 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/resources/manifestNoId.json @@ -0,0 +1,39 @@ +{ + "ver": "1.0", + "author": "Jagadish P", + "title": "Question Set Summary Plugin", + "description": "Plugin to add question set summary to content", + "publishedDate": "", + "editor": { + "main": "editor/plugin.js", + "dependencies": [ + + ], + "menu": [ + { + "id": "question-set-summary", + "category": "main", + "type": "icon", + "toolTip": "Question Set Summary", + "title": "Question set summary", + "iconClass": "fa fa-list-alt", + "onclick": { + "id": "org.ekstep.summary:addSummary" + } + } + ] + }, + "renderer": { + "main": "renderer/plugin.js", + "dependencies": [ + { + "type": "js", + "src": "renderer/summary-template.js" + }, + { + "type": "css", + "src": "renderer/style.css" + } + ] + } +} diff --git a/platform-modules/mimetype-manager/src/test/resources/manifestNoVer.json b/platform-modules/mimetype-manager/src/test/resources/manifestNoVer.json new file mode 100644 index 000000000..ecee46098 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/resources/manifestNoVer.json @@ -0,0 +1,39 @@ +{ + "id": "org.ekstep.summary", + "author": "Jagadish P", + "title": "Question Set Summary Plugin", + "description": "Plugin to add question set summary to content", + "publishedDate": "", + "editor": { + "main": "editor/plugin.js", + "dependencies": [ + + ], + "menu": [ + { + "id": "question-set-summary", + "category": "main", + "type": "icon", + "toolTip": "Question Set Summary", + "title": "Question set summary", + "iconClass": "fa fa-list-alt", + "onclick": { + "id": "org.ekstep.summary:addSummary" + } + } + ] + }, + "renderer": { + "main": "renderer/plugin.js", + "dependencies": [ + { + "type": "js", + "src": "renderer/summary-template.js" + }, + { + "type": "css", + "src": "renderer/style.css" + } + ] + } +} diff --git a/platform-modules/mimetype-manager/src/test/resources/manifestStringTargets.json b/platform-modules/mimetype-manager/src/test/resources/manifestStringTargets.json new file mode 100644 index 000000000..ee7c0a53a --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/resources/manifestStringTargets.json @@ -0,0 +1,41 @@ +{ + "id": "org.ekstep.summary", + "ver": "1.0", + "author": "Jagadish P", + "title": "Question Set Summary Plugin", + "description": "Plugin to add question set summary to content", + "targets": "[\"Hey\", \"Bye\"]", + "publishedDate": "", + "editor": { + "main": "editor/plugin.js", + "dependencies": [ + + ], + "menu": [ + { + "id": "question-set-summary", + "category": "main", + "type": "icon", + "toolTip": "Question Set Summary", + "title": "Question set summary", + "iconClass": "fa fa-list-alt", + "onclick": { + "id": "org.ekstep.summary:addSummary" + } + } + ] + }, + "renderer": { + "main": "renderer/plugin.js", + "dependencies": [ + { + "type": "js", + "src": "renderer/summary-template.js" + }, + { + "type": "css", + "src": "renderer/style.css" + } + ] + } +} diff --git a/platform-modules/mimetype-manager/src/test/resources/plugin.zip b/platform-modules/mimetype-manager/src/test/resources/plugin.zip new file mode 100644 index 000000000..99efe06c0 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/plugin.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/sample.pdf b/platform-modules/mimetype-manager/src/test/resources/sample.pdf new file mode 100644 index 000000000..dbf091df9 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/sample.pdf differ diff --git a/platform-modules/mimetype-manager/src/test/resources/test_ecml.zip b/platform-modules/mimetype-manager/src/test/resources/test_ecml.zip new file mode 100755 index 000000000..2d86b461c Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/test_ecml.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/uploadAPK.apk b/platform-modules/mimetype-manager/src/test/resources/uploadAPK.apk new file mode 100644 index 000000000..d5d059567 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/uploadAPK.apk differ diff --git a/platform-modules/mimetype-manager/src/test/resources/validEcmlContent.zip b/platform-modules/mimetype-manager/src/test/resources/validEcmlContent.zip new file mode 100644 index 000000000..4f8c7f30a Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/validEcmlContent.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/validHtml.zip b/platform-modules/mimetype-manager/src/test/resources/validHtml.zip new file mode 100644 index 000000000..59d2b1f2b Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/validHtml.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/valid_composed_h5p.zip b/platform-modules/mimetype-manager/src/test/resources/valid_composed_h5p.zip new file mode 100644 index 000000000..893b04fb2 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/valid_composed_h5p.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/valid_h5p_content.h5p b/platform-modules/mimetype-manager/src/test/resources/valid_h5p_content.h5p new file mode 100644 index 000000000..c033f5d68 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/valid_h5p_content.h5p differ diff --git a/platform-modules/mimetype-manager/src/test/resources/validecml.zip b/platform-modules/mimetype-manager/src/test/resources/validecml.zip new file mode 100644 index 000000000..7b226c301 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/validecml.zip differ diff --git a/platform-modules/mimetype-manager/src/test/resources/validecml_withjson.zip b/platform-modules/mimetype-manager/src/test/resources/validecml_withjson.zip new file mode 100644 index 000000000..0ae5021f5 Binary files /dev/null and b/platform-modules/mimetype-manager/src/test/resources/validecml_withjson.zip differ diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/cloudstore/StorageServiceTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/cloudstore/StorageServiceTest.scala new file mode 100644 index 000000000..7cd55d82e --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/cloudstore/StorageServiceTest.scala @@ -0,0 +1,17 @@ +package org.sunbird.cloudstore + +import org.scalatest.{AsyncFlatSpec, Matchers} + +class StorageServiceTest extends AsyncFlatSpec with Matchers { + val ss = new StorageService + + "getService" should "return a Storage Service" in { + val service = ss.getService() + assert(service != null) + } + + "getContainerName" should "return the container name" in { + val container = ss.getContainerName() + assert(container == "sunbird-content-dev") + } +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/factory/MimeTypeManagerFactoryTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/factory/MimeTypeManagerFactoryTest.scala new file mode 100644 index 000000000..84510af8a --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/factory/MimeTypeManagerFactoryTest.scala @@ -0,0 +1,110 @@ +package org.sunbird.mimetype.factory + +import org.scalatest.{FlatSpec, Matchers} +import org.sunbird.mimetype.mgr.impl.{AssetMimeTypeMgrImpl, CollectionMimeTypeMgrImpl, DefaultMimeTypeMgrImpl, DocumentMimeTypeMgrImpl, EcmlMimeTypeMgrImpl, H5PMimeTypeMgrImpl, HtmlMimeTypeMgrImpl, PluginMimeTypeMgrImpl, YouTubeMimeTypeMgrImpl} + +class MimeTypeManagerFactoryTest extends FlatSpec with Matchers { + + "getManager with mimeType text/x-url" should "give instance of YouTubeMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("TextBook", "text/x-url") + assert(null != mgr) + assert(mgr.isInstanceOf[YouTubeMimeTypeMgrImpl]) + } + + "getManager with mimeType video/x-youtube" should "give instance of YouTubeMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("TextBook", "video/x-youtube") + assert(null != mgr) + assert(mgr.isInstanceOf[YouTubeMimeTypeMgrImpl]) + } + + "getManager with mimeType video/youtube" should "give instance of YouTubeMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("TextBook", "video/youtube") + assert(null != mgr) + assert(mgr.isInstanceOf[YouTubeMimeTypeMgrImpl]) + } + + "getManager with mimeType application/pdf" should "give instance of DocumentMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("TextBook", "application/pdf") + assert(null != mgr) + assert(mgr.isInstanceOf[DocumentMimeTypeMgrImpl]) + } + + "getManager with mimeType application/epub" should "give instance of DocumentMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("TextBook", "application/epub") + assert(null != mgr) + assert(mgr.isInstanceOf[DocumentMimeTypeMgrImpl]) + } + + "getManager with mimeType application/msword" should "give instance of DocumentMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("TextBook", "application/msword") + assert(null != mgr) + assert(mgr.isInstanceOf[DocumentMimeTypeMgrImpl]) + } + + "getManager with mimeType assets" should "give instance of AssetMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Resource", "assets") + assert(null != mgr) + assert(mgr.isInstanceOf[AssetMimeTypeMgrImpl]) + } + + "getManager with contentType Assets" should "give instance of AssetMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Asset", "image/jpeg") + assert(null != mgr) + assert(mgr.isInstanceOf[AssetMimeTypeMgrImpl]) + } + + "getManager with mimeType application/vnd.ekstep.ecml-archive" should "give instance of EcmlMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Resource", "application/vnd.ekstep.ecml-archive") + assert(null != mgr) + assert(mgr.isInstanceOf[EcmlMimeTypeMgrImpl]) + } + + "getManager with mimeType application/vnd.ekstep.html-archive" should "give instance of HtmlMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Resource", "application/vnd.ekstep.html-archive") + assert(null != mgr) + assert(mgr.isInstanceOf[HtmlMimeTypeMgrImpl]) + } + + "getManager with mimeType application/vnd.ekstep.content-collection" should "give instance of CollectionMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Resource", "application/vnd.ekstep.content-collection") + assert(null != mgr) + assert(mgr.isInstanceOf[CollectionMimeTypeMgrImpl]) + } + + "getManager with mimeType application/vnd.ekstep.plugin-archive" should "give instance of PluginMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Resource", "application/vnd.ekstep.plugin-archive") + assert(null != mgr) + assert(mgr.isInstanceOf[PluginMimeTypeMgrImpl]) + } + + "getManager with mimeType application/vnd.ekstep.h5p-archive" should "give instance of H5PMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Resource", "application/vnd.ekstep.h5p-archive") + assert(null != mgr) + assert(mgr.isInstanceOf[H5PMimeTypeMgrImpl]) + } + + "getManager with blank mimeType" should "give instance of DefaultMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Resource", null) + assert(null != mgr) + assert(mgr.isInstanceOf[DefaultMimeTypeMgrImpl]) + } + + "getManager with mimeType text/x-url and contentType Asset" should "give instance of YouTubeMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Asset", "text/x-url") + assert(null != mgr) + assert(mgr.isInstanceOf[YouTubeMimeTypeMgrImpl]) + } + + "getManager with mimeType video/x-youtube and contentType Asset" should "give instance of YouTubeMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Asset", "video/x-youtube") + assert(null != mgr) + assert(mgr.isInstanceOf[YouTubeMimeTypeMgrImpl]) + } + + "getManager with mimeType video/youtube and contentType Asset" should "give instance of YouTubeMimeTypeMgrImpl" in { + val mgr = MimeTypeManagerFactory.getManager("Asset", "video/youtube") + assert(null != mgr) + assert(mgr.isInstanceOf[YouTubeMimeTypeMgrImpl]) + } + +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/BaseMimeTypeManagerTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/BaseMimeTypeManagerTest.scala new file mode 100644 index 000000000..8ac7df04c --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/BaseMimeTypeManagerTest.scala @@ -0,0 +1,92 @@ +package org.sunbird.mimetype.mgr + +import java.io.File + +import com.google.common.io.Resources +import org.apache.commons.io.FileUtils +import org.sunbird.graph.dac.model.Node +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException + +class BaseMimeTypeManagerTest extends AsyncFlatSpec with Matchers { + implicit val ss: StorageService = new StorageService() + val mgr = new BaseMimeTypeManager + + "validateUploadRequest with empty data" should "throw ClientException" in { + val exception = intercept[ClientException] { + mgr.validateUploadRequest("do_123", new Node(), null) + } + exception.getMessage shouldEqual "Please Provide Valid File Or File Url!" + } + + "getBasePath with valid objectId" should "return a valid path" in { + val result = mgr.getBasePath("do_123") + assert(result.contains("/tmp/content/")) + assert(result.contains("_temp/do_123")) + } + + "getFieNameFromURL" should "return file name from url" in { + val result = mgr.getFileNameFromURL("http://abc.com/content/sample.pdf") + assert(result.contains("sample_")) + assert(result.endsWith(".pdf")) + } + + "getFileSize with invalid file" should "return 0 size" in { + assert(0==mgr.getFileSize(new File("/tmp/sample2.pdf"))) + } + + "copyURLToFile with valid url" should "return a valid file" in { + val result = mgr.copyURLToFile("do_123","https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf") + assert(result.exists()) + assert(true) + } + + "isValidPackageStructure with valid html zip file" should "return true" ignore { + val file: File = new File(Resources.getResource("validHtml.zip").toURI) + val result = mgr.isValidPackageStructure(file, List("index.html")) + assert(result) + } + + "isValidPackageStructure with invalid html zip file" should "return false" in { + val file: File = new File(Resources.getResource("invalidHtmlContent.zip").toURI) + val result = mgr.isValidPackageStructure(file, List("index.html")) + assert(!result) + } + + "isValidPackageStructure with valid ecml zip file" should "return true" in { + val file: File = new File(Resources.getResource("validEcmlContent.zip").toURI) + val result = mgr.isValidPackageStructure(file, List("index.ecml","index.json")) + assert(result) + } + + "extractPackage" should "extract package in specified basePath" in { + FileUtils.deleteDirectory(new File("/tmp/validEcmlContent")) + val file: File = new File(Resources.getResource("validEcmlContent.zip").toURI) + val result = mgr.extractPackage(file, "/tmp/validEcmlContent") + assert(new File("/tmp/validEcmlContent/index.ecml").exists()) + } + + "createZipPackage" should "Zip the files into a single package" in { + try { + mgr.createZipPackage(Resources.getResource("filesToZip").getPath, Resources.getResource("filesToZip").getPath.replace("filesToZip", "") + + "test_zip.zip") + assert(new File(Resources.getResource("test_zip.zip").getPath).exists()) + } finally { + FileUtils.deleteQuietly(new File(Resources.getResource("test_zip.zip").getPath)) + } + } + + + "createZipPackageWithNestedFiles" should "Zip the files into a single package" in { + try { + mgr.createZipPackage(Resources.getResource("filesToZipNested").getPath, Resources.getResource("filesToZip").getPath.replace("filesToZip", "") + + "test_zip_nested.zip") + assert(new File(Resources.getResource("test_zip_nested.zip").getPath).exists()) + } + finally { + FileUtils.deleteQuietly(new File(Resources.getResource("test_zip_nested.zip").getPath)) + } + } + +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/ApkMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/ApkMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..c16c9cfb6 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/ApkMimeTypeMgrImplTest.scala @@ -0,0 +1,54 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File +import java.util + +import com.google.common.io.Resources +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.graph.dac.model.Node + +class ApkMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + + it should "upload apk file and return public url" in { + val node = getNode() + val identifier = "do_123" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + val resFuture = new ApkMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("uploadAPK.apk").toURI), None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + + assert(true) + } + + it should "upload apk fileurl and return public url" in { + val node = getNode() + val identifier = "do_123" + implicit val ss = mock[StorageService] + val resFuture = new ApkMimeTypeMgrImpl().upload(identifier, node, "https://ekstep-public-prod.s3-ap-south-1.amazonaws.com/content/do_30083930/artifact/aser-6.0.0.17_215_1505458979_1505459188679.apk", None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + + assert(true) + } + + def getNode(): Node = { + val node = new Node() + node.setIdentifier("do_123") + node.setMetadata(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_123") + put("mimeType", "application/vnd.android.package-archive") + put("status","Draft") + put("contentType", "Plugin") + }}) + node + } + +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/AssetMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/AssetMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..1c49a55d2 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/AssetMimeTypeMgrImplTest.scala @@ -0,0 +1,80 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File +import java.util + +import com.google.common.io.Resources +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node + + +class AssetMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + + implicit val ss: StorageService = new StorageService + + "upload with valid file" should "return artifactUrl with successful response" in { + val node = getNode() + node.getMetadata.put("mimeType", "image/jpg") + val identifier = "do_123" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + val resFuture = new AssetMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("filesToZip/human_vs_robot-.jpg").toURI), None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + assert(true) + } + + "upload with file" should "throw client exception" in { + val exception = intercept[ClientException] { + new AssetMimeTypeMgrImpl().upload("do_123", new Node(), new File("/tmp/test.pdf"), None, UploadParams()) + } + exception.getMessage shouldEqual "Please Provide Valid File!" + } + + def getNode(): Node = { + val node = new Node() + node.setIdentifier("org.ekstep.video") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "org.ekstep.video") + put("status", "Draft") + put("contentType", "Asset") + put("mimeType", "application/pdf") + } + }) + node + } + + "upload with valid fileUrl" should "return artifactUrl with successful response" in { + val node = getNode() + val inputUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" + val resFuture = new AssetMimeTypeMgrImpl().upload("do_123", node, inputUrl, None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(!result.isEmpty) + assert("do_123" == result.getOrElse("identifier", "")) + assert(inputUrl == result.getOrElse("artifactUrl", "")) + }) + } + + + "upload with valid 3GB file url for big video testing" should "return artifactUrl with successful response" in { + val node = getNode() + node.getMetadata.put("mimeType", "video/mp4") + val inputUrl = "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/assets/do_1130384356456120321307/3point4gb.mp4" + val resFuture = new AssetMimeTypeMgrImpl().upload("do_123", node, inputUrl, None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(!result.isEmpty) + assert("do_123" == result.getOrElse("identifier", "")) + assert(inputUrl == result.getOrElse("artifactUrl", "")) + }) + } + +} \ No newline at end of file diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/CollectionMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/CollectionMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..d113c411d --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/CollectionMimeTypeMgrImplTest.scala @@ -0,0 +1,28 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node + +class CollectionMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers { + + implicit val ss: StorageService = new StorageService + + "upload with file" should "throw client exception" in { + val exception = intercept[ClientException] { + new CollectionMimeTypeMgrImpl().upload("do_123", new Node(), new File("/tmp/test.pdf"), None, UploadParams()) + } + exception.getMessage shouldEqual "FILE_UPLOAD_ERROR | Upload operation not supported for given mimeType" + } + + "upload with fileUrl" should "throw client exception" in { + val exception = intercept[ClientException] { + new CollectionMimeTypeMgrImpl().upload("do_123", new Node(), "https://abc.com/content/sample.pdf", None, UploadParams()) + } + exception.getMessage shouldEqual "FILE_UPLOAD_ERROR | Upload operation not supported for given mimeType" + } +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/DefaultMimeTypeImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/DefaultMimeTypeImplTest.scala new file mode 100644 index 000000000..177d0fc62 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/DefaultMimeTypeImplTest.scala @@ -0,0 +1,72 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import com.google.common.io.Resources +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node + +class DefaultMimeTypeImplTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + + "upload pdf file" should "upload pdf file and return public url" in { + val node = new Node() + node.setMetadata(new java.util.HashMap[String, AnyRef]() {{ + put("mimeType", "application/pdf") + }}) + val identifier ="do_123" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + val resFuture = new DefaultMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("sample.pdf").toURI), None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + + assert(true) + } + + it should "upload apk fileurl and return public url" in { + val node = new Node() + val identifier = "do_123" + implicit val ss = mock[StorageService] + val resFuture = new DefaultMimeTypeMgrImpl().upload(identifier, node, "https://ekstep-public-prod.s3-ap-south-1.amazonaws.com/content/do_30083930/artifact/aser-6.0.0.17_215_1505458979_1505459188679.apk", None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + + assert(true) + } + + "upload pdf file" should "throw client error for mimeType mismatch" in { + val node = new Node() + node.setMetadata(new java.util.HashMap[String, AnyRef]() {{ + put("mimeType", "application/msword") + }}) + implicit val ss = mock[StorageService] + val exception = intercept[ClientException] { + new DefaultMimeTypeMgrImpl().upload("do_123", node, new File(Resources.getResource("sample.pdf").toURI), None, UploadParams()) + } + exception.getMessage shouldEqual "Uploaded File MimeType is not same as Node (Object) MimeType." + } + + "upload pdf file with invalid mimetype but validation false" should "upload pdf file and return public url" in { + val node = new Node() + node.setMetadata(new java.util.HashMap[String, AnyRef]() {{ + put("mimeType", "application/pdf") + }}) + val identifier ="do_123" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + val resFuture = new DefaultMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("sample.pdf").toURI), None, UploadParams(Some(""), Some(false))) + resFuture.map(result => { + println("Response: " + result) + assert(result != null) + }) + } + +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/DocumentMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/DocumentMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..78346be7e --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/DocumentMimeTypeMgrImplTest.scala @@ -0,0 +1,143 @@ +package org.sunbird.mimetype.mgr.impl +import java.io.File +import java.util + +import com.google.common.io.Resources +import org.scalamock.scalatest.AsyncMockFactory +import org.sunbird.graph.dac.model.Node +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException + +class DocumentMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + implicit val ss: StorageService = new StorageService + + "upload with valid file url" should "return artifactUrl with successful response" in { + val inputUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" + val node = new Node() + node.setMetadata(new util.HashMap[String, AnyRef](){{ + put("mimeType","application/pdf") + }}) + val resFuture = new DocumentMimeTypeMgrImpl().upload("do_123", node, inputUrl, None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(!result.isEmpty) + assert("do_123" == result.getOrElse("identifier","")) + assert(inputUrl == result.getOrElse("artifactUrl","")) + }) + } + + "validateFileUrlExtension with invalid pdf file url" should "return client exception" in { + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().validateFileUrlExtension("application/pdf", "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.txt") + } + exception.getMessage shouldEqual "Please Provide Valid Pdf File Url!" + } + + "validateFileUrlExtension with invalid epub file url" should "return client exception" in { + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().validateFileUrlExtension("application/epub", "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.txt") + } + exception.getMessage shouldEqual "Please Provide Valid Epub File Url!" + } + + "validateFileUrlExtension with invalid msword file url" should "return client exception" in { + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().validateFileUrlExtension("application/msword", "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf") + } + exception.getMessage shouldEqual "Please Provide Valid Document File Url!" + } + + "upload with invalid file url" should "return client exception" in { + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().upload("do_123", new Node(), "abcd", None, UploadParams()) + } + exception.getMessage shouldEqual "Please Provide Valid File Url!" + } + + "upload with empty objectId" should "throw client exception" in { + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().upload("", new Node(), "https://abc.com/content/sample.pdf", None, UploadParams()) + } + exception.getMessage shouldEqual "Please Provide Valid Identifier!" + } + + "upload with empty node object" should "throw client exception" in { + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().upload("do_123", null, "https://abc.com/content/sample.pdf", None, UploadParams()) + } + exception.getMessage shouldEqual "Please Provide Valid Node!" + } + + "upload with different file type for pdf mimeType" should "throw client exception" in { + val file: File = new File(Resources.getResource("invalidHtmlContent.zip").toURI) + val node = new Node() + node.setMetadata(new java.util.HashMap[String, AnyRef]() {{ + put("mimeType", "application/pdf") + }}) + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().upload("do_123", node, file, None, UploadParams()) + } + exception.getMessage shouldEqual "Uploaded file is not a pdf file. Please upload a valid pdf file." + } + + "upload with different file type for epub mimeType" should "throw client exception" in { + val file: File = new File(Resources.getResource("sample.pdf").toURI) + val node = new Node() + node.setMetadata(new java.util.HashMap[String, AnyRef]() {{ + put("mimeType", "application/epub") + }}) + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().upload("do_123", node, file, None, UploadParams()) + } + exception.getMessage shouldEqual "Uploaded file is not a epub file. Please upload a valid epub file." + } + + "upload with different file type for word mimeType" should "throw client exception" in { + val file: File = new File(Resources.getResource("sample.pdf").toURI) + val node = new Node() + node.setMetadata(new java.util.HashMap[String, AnyRef]() {{ + put("mimeType", "application/msword") + }}) + val exception = intercept[ClientException] { + new DocumentMimeTypeMgrImpl().upload("do_123", node, file, None, UploadParams()) + } + exception.getMessage shouldEqual "Uploaded file is not a word file. Please upload a valid word file." + } + + "upload pdf file" should "upload pdf file and return public url" in { + val node = new Node() + node.setMetadata(new java.util.HashMap[String, AnyRef]() {{ + put("mimeType", "application/pdf") + }}) + val identifier ="do_123" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + val resFuture = new DocumentMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("sample.pdf").toURI), None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + + assert(true) + } + + "upload epub file" should "upload epub file and return public url" in { + val node = new Node() + node.setMetadata(new java.util.HashMap[String, AnyRef]() {{ + put("mimeType", "application/epub") + }}) + val identifier ="do_123" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + val resFuture = new DocumentMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("igp-twss.epub").toURI), None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + + assert(true) + } + +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/EcmlMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/EcmlMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..c7e9b84c7 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/EcmlMimeTypeMgrImplTest.scala @@ -0,0 +1,94 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File +import java.util + +import com.google.common.io.Resources +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node + +import scala.concurrent.ExecutionContext + +class EcmlMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers with AsyncMockFactory{ + + implicit val ss = mock[StorageService] + + it should "Throw Client Exception for null file url" in { + val exception = intercept[ClientException] { + new EcmlMimeTypeMgrImpl().upload("do_1234", getNode(), "", None, UploadParams()) + } + exception.getMessage shouldEqual "Please Provide Valid File Url!" + } + + it should "Throw Client Except for non zip file " in { + val exception = intercept[ClientException] { + new EcmlMimeTypeMgrImpl().upload("do_1234", getNode(), new File(Resources.getResource("sample.pdf").toURI), None, UploadParams()) + } + exception.getMessage shouldEqual "INVALID_CONTENT_PACKAGE_FILE_MIME_TYPE_ERROR | [The uploaded package is invalid]" + } + + + it should "upload ECML zip file and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)).repeated(3) + (ss.uploadDirectory(_:String, _:File, _: Option[Boolean])).expects(*, *, *) + val resFuture = new EcmlMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("validecml.zip").toURI), None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(result.nonEmpty) + assert("do_123" == result.getOrElse("identifier","")) + }) + + assert(true) + } + + it should "upload ECML with json zip file and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectory(_:String, _:File, _: Option[Boolean])).expects(*, *, *) + val resFuture = new EcmlMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("validecml_withjson.zip").toURI), None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(result.nonEmpty) + assert("do_123" == result.getOrElse("identifier","")) + }) + + assert(true) + } + + it should "upload ECML with json zip file URL and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectory(_:String, _:File, _: Option[Boolean])).expects(*, *, *) + val resFuture = new EcmlMimeTypeMgrImpl().upload(identifier, node, "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/kp_ft_1594670084387/artifact/ecml_with_json.zip", None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(result.nonEmpty) + assert("do_123" == result.getOrElse("identifier","")) + }) + + assert(true) + } + + def getNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setMetadata(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_1234") + put("mimeType", "application/vnd.ekstep.ecml-archive") + put("status","Draft") + put("contentType", "Resource") + }}) + node + } +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/H5PMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/H5PMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..12e2d06be --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/H5PMimeTypeMgrImplTest.scala @@ -0,0 +1,128 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File +import java.util + +import com.google.common.io.Resources +import org.apache.commons.io.FileUtils +import org.apache.commons.lang.StringUtils +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node +import org.sunbird.mimetype.mgr.BaseMimeTypeManager +import org.sunbird.models.UploadParams + +import scala.concurrent.{ExecutionContext, Future} + +class H5PMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + + "H5PMimeTypeManager" should "upload and download H5P file with validation" in { + implicit val ss = mock[StorageService] + val exception = intercept[ClientException] { + new H5PMimeTypeMgrImpl().upload("do_123", new Node(), new File(Resources.getResource("validEcmlContent.zip").toURI), None, UploadParams()) + } + exception.getMessage shouldEqual "Please Provide Valid File!" + } + + it should "create a H5P zip file with required dependencies to play" in { + implicit val ss = new StorageService + val mgr = new BaseMimeTypeManager + var zipFile = "" + try { + val extractionBasePath = mgr.getBasePath("do_1234") + zipFile = new H5PMimeTypeMgrImpl().createH5PZipFile(extractionBasePath, new File(Resources.getResource("valid_h5p_content.h5p").getPath), "do_1234") + assert(StringUtils.isNotBlank(zipFile)) + } finally { + FileUtils.deleteQuietly(new File(zipFile)) + } + } + + it should "upload H5P zip file and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectoryAsync(_:String, _:File, _: Option[Boolean])(_: ExecutionContext)).expects(*, *, *, *).returns(Future(List(identifier, identifier))) + val resFuture = new H5PMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("valid_h5p_content.h5p").toURI), None, UploadParams()) + resFuture.map(result => { + assert("do_1234" == result.getOrElse("identifier", "")) + assert(result.get("artifactUrl") != null) + assert(result.get("s3Key") != null) + assert(result.get("size") != null) + }) + } + + it should "upload H5P zip file Url and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectoryAsync(_:String, _:File, _: Option[Boolean])(_: ExecutionContext)).expects(*, *, *, *).returns(Future(List(identifier, identifier))) + val resFuture = new H5PMimeTypeMgrImpl().upload(identifier, node,"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/test-cases/valid_h5p_content.h5p", None, UploadParams()) + resFuture.map(result => { + assert("do_1234" == result.getOrElse("identifier", "do_1234")) + assert(result.get("artifactUrl") != null) + assert(result.get("s3Key") != null) + assert(result.get("size") != null) + }) + } + + it should "upload H5P composed zip file url extension and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectoryAsync(_:String, _:File, _: Option[Boolean])(_: ExecutionContext)).expects(*, *, *, *).returns(Future(List(identifier, identifier))) + val resFuture = new H5PMimeTypeMgrImpl().upload(identifier, node,"https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_11297886517561753611/artifact/1584334130903_do_11297886517561753611.zip", None, UploadParams(Some("composed-h5p-zip"))) + resFuture.map(result => { + assert("do_1234" == result.getOrElse("identifier", "do_1234")) + assert(result.get("artifactUrl") != null) + assert(result.get("s3Key") != null) + assert(result.get("size") != null) + }) + } + + it should "upload H5P composed zip file url extension with old zips and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectoryAsync(_:String, _:File, _: Option[Boolean])(_: ExecutionContext)).expects(*, *, *, *).returns(Future(List(identifier, identifier))) + val resFuture = new H5PMimeTypeMgrImpl().upload(identifier, node,"https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/content/do_112499826618179584111/artifact/1525857774447_do_112499826618179584111.zip", None, UploadParams(Some("composed-h5p-zip"))) + resFuture.map(result => { + assert("do_1234" == result.getOrElse("identifier", "do_1234")) + assert(result.get("artifactUrl") != null) + assert(result.get("s3Key") != null) + assert(result.get("size") != null) + }) + } + + it should "upload H5P composed zip file and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_:String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectoryAsync(_:String, _:File, _: Option[Boolean])(_: ExecutionContext)).expects(*, *, *, *).returns(Future(List(identifier, identifier))) + val resFuture = new H5PMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("valid_composed_h5p.zip").toURI), None, UploadParams(Some("composed-h5p-zip"))) + resFuture.map(result => { + assert("do_1234" == result.getOrElse("identifier", "")) + assert(result.get("artifactUrl") != null) + assert(result.get("s3Key") != null) + assert(result.get("size") != null) + }) + } + + def getNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setMetadata(new util.HashMap[String, AnyRef](){{ + put("identifier", "do_1234") + put("mimeType", "application/vnd.ekstep.h5p-archive") + put("status","Draft") + put("contentType", "Resource") + }}) + node + } +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/HtmlMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/HtmlMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..6b2dc6af4 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/HtmlMimeTypeMgrImplTest.scala @@ -0,0 +1,83 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File +import java.util + +import com.google.common.io.Resources +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node + +import scala.concurrent.ExecutionContext + +class HtmlMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + implicit val ss: StorageService = new StorageService + + "upload invalid html file" should "return client exception" in { + val exception = intercept[ClientException] { + new HtmlMimeTypeMgrImpl().upload("do_123", new Node(), new File(Resources.getResource("invalidHtmlContent.zip").toURI), None, UploadParams()) + } + exception.getMessage shouldEqual "Please Provide Valid File!" + } + + "upload invalid html file url" should "return client exception" in { + val exception = intercept[ClientException] { + new HtmlMimeTypeMgrImpl().upload("do_123", new Node(), "invalid.html", None, UploadParams()) + } + exception.getMessage shouldEqual "Please Provide Valid File Url!" + } + + it should "upload HTML zip file and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_: String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectory(_:String, _:File, _: Option[Boolean])).expects(*, *, *) + val resFuture = new HtmlMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("validHtml.zip").toURI), None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(result.nonEmpty) + assert("do_1234" == result.getOrElse("identifier", "")) + assert(result.get("artifactUrl") != null) + assert(result.get("s3Key") != null) + assert(result.get("size") != null) + + }) + } + + it should "upload HTML zip Url and return public url" in { + val node = getNode() + val identifier = "do_1234" + implicit val ss = mock[StorageService] + (ss.uploadFile(_: String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectory(_:String, _:File, _: Option[Boolean])).expects(*, *, *) + val resFuture = new HtmlMimeTypeMgrImpl().upload(identifier, node, "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/do_1126981080516608001180/artifact/1.-_1550062024041.zip", None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(result.nonEmpty) + assert("do_1234" == result.getOrElse("identifier", "")) + assert(result.get("artifactUrl") != null) + assert(result.get("s3Key") != null) + assert(result.get("size") != null) + + }) + } + + def getNode(): Node = { + val node = new Node() + node.setIdentifier("do_1234") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "do_1234") + put("mimeType", "application/vnd.ekstep.html-archive") + put("status", "Draft") + put("contentType", "Resource") + } + }) + node + } + +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/PluginMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/PluginMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..d726fa688 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/PluginMimeTypeMgrImplTest.scala @@ -0,0 +1,106 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File +import java.util + +import com.google.common.io.Resources +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node + +class PluginMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { + + it should "upload plugin zip file and return public url" in { + implicit val ss = mock[StorageService] + val node = getNode() + val identifier = "org.ekstep.video" + (ss.uploadFile(_: String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectory(_: String, _: File, _: Option[Boolean])).expects(*, *, *) + val resFuture = new PluginMimeTypeMgrImpl().upload(identifier, node, new File(Resources.getResource("plugin.zip").toURI), None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + + assert(true) + } + + it should "upload plugin zip file url and return public url" in { + implicit val ss = mock[StorageService] + val node = getNode() + val identifier = "org.ekstep.summary" + (ss.uploadFile(_: String, _: File, _: Option[Boolean])).expects(*, *, *).returns(Array(identifier, identifier)) + (ss.uploadDirectory(_: String, _: File, _: Option[Boolean])).expects(*, *, *) + val resFuture = new PluginMimeTypeMgrImpl().upload(identifier, node, "https://sunbirddev.blob.core.windows.net/sunbird-content-dev/content/org.ekstep.summary/artifact/org.ekstep.summary-1.0_1576230748183.zip", None, UploadParams()) + resFuture.map(result => { + println("Response: " + result) + result + }) + + assert(true) + } + + it should "upload Invalid plugin zip file url and Throw Client Exception" in { + implicit val ss = new StorageService + val exception = intercept[ClientException] { + new PluginMimeTypeMgrImpl().upload("org.ekstep.video", new Node(), "https://ekstep-public-dev.s3-ap-south-1.amazonaws.com/content/do_11218758555843788817/artifact/akshara_kan_1487743191313.zip", None, UploadParams()) + } + exception.getMessage shouldEqual "Error !Invalid Content Package File Structure. | [manifest.json should be at root location]" + } + + it should "upload Invalid plugin zip file and Throw Client Exception" in { + implicit val ss = new StorageService + val exception = intercept[ClientException] { + new PluginMimeTypeMgrImpl().upload("org.ekstep.video", new Node(), new File(Resources.getResource("validEcmlContent.zip").toURI), None, UploadParams()) + } + exception.getMessage shouldEqual "Error !Invalid Content Package File Structure. | [manifest.json should be at root location]" + } + + it should "upload Invalid File for plugin and Throw Client Exception" in { + implicit val ss = new StorageService + val exception = intercept[ClientException] { + new PluginMimeTypeMgrImpl().upload("org.ekstep.video", new Node(), new File(Resources.getResource("sample.pdf").toURI), None, UploadParams()) + } + exception.getMessage shouldEqual "Error! Invalid Content Package Mime Type." + } + + "read data from manifest" should "throw client exception when id is not present" in { + implicit val ss = new StorageService + val exception = intercept[ClientException] { + new PluginMimeTypeMgrImpl().readDataFromManifest(new File(Resources.getResource("manifestNoId.json").toURI), "org.ekstep.summary") + } + exception.getMessage shouldEqual "'id' in manifest.json is not same as the plugin identifier." + } + + "read data from manifest" should "throw client exception when Version is not present" in { + implicit val ss = new StorageService + val exception = intercept[ClientException] { + new PluginMimeTypeMgrImpl().readDataFromManifest(new File(Resources.getResource("manifestNoVer.json").toURI), "org.ekstep.summary") + } + exception.getMessage shouldEqual "'ver' is not specified in the plugin manifest.json." + } + + "read data from manifest" should "Convert manifest when Targets is String and return map " in { + implicit val ss = new StorageService + val manifestMap = new PluginMimeTypeMgrImpl().readDataFromManifest(new File(Resources.getResource("manifestStringTargets.json").toURI), "org.ekstep.summary") + assert(manifestMap != null) + } + + def getNode(): Node = { + val node = new Node() + node.setIdentifier("org.ekstep.video") + node.setMetadata(new util.HashMap[String, AnyRef]() { + { + put("identifier", "org.ekstep.video") + put("mimeType", "application/vnd.ekstep.plugin-archive") + put("status", "Draft") + put("contentType", "Plugin") + } + }) + node + } + +} diff --git a/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/YouTubeMimeTypeMgrImplTest.scala b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/YouTubeMimeTypeMgrImplTest.scala new file mode 100644 index 000000000..7f1afe8a8 --- /dev/null +++ b/platform-modules/mimetype-manager/src/test/scala/org/sunbird/mimetype/mgr/impl/YouTubeMimeTypeMgrImplTest.scala @@ -0,0 +1,32 @@ +package org.sunbird.mimetype.mgr.impl + +import java.io.File + +import org.scalatest.{AsyncFlatSpec, Matchers} +import org.sunbird.models.UploadParams +import org.sunbird.cloudstore.StorageService +import org.sunbird.common.exception.ClientException +import org.sunbird.graph.dac.model.Node + +class YouTubeMimeTypeMgrImplTest extends AsyncFlatSpec with Matchers { + implicit val ss: StorageService = new StorageService + + "upload with valid youtube url" should "return artifactUrl with successful response" in { + val inputUrl = "https://www.youtube.com/watch?v=8irSFvoyLHQ" + val resFuture = new YouTubeMimeTypeMgrImpl().upload("do_123", new Node(), inputUrl, None, UploadParams()) + resFuture.map(result => { + assert(null != result) + assert(!result.isEmpty) + assert("do_123" == result.getOrElse("identifier","")) + assert(inputUrl == result.getOrElse("artifactUrl","")) + }) + } + + "upload with file" should "throw client exception" in { + val exception = intercept[ClientException] { + new YouTubeMimeTypeMgrImpl().upload("do_123", new Node(), new File("/tmp/test.pdf"), None, UploadParams()) + } + exception.getMessage shouldEqual "FILE_UPLOAD_ERROR | Upload operation not supported for given mimeType" + } + +} diff --git a/platform-modules/pom.xml b/platform-modules/pom.xml new file mode 100644 index 000000000..4fe723a78 --- /dev/null +++ b/platform-modules/pom.xml @@ -0,0 +1,79 @@ + + + + knowledge-platform + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + platform-modules + pom + + url-manager + mimetype-manager + import-manager + + + + + org.apache.commons + commons-lang3 + 3.9 + + + junit + junit + 4.13.1 + test + + + + + + + + maven-assembly-plugin + 2.3 + + + src/assembly/bin.xml + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + ${scala.version} + true + true + + + + org.jacoco + jacoco-maven-plugin + 0.7.9 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + + + + \ No newline at end of file diff --git a/platform-modules/url-manager/pom.xml b/platform-modules/url-manager/pom.xml new file mode 100644 index 000000000..8badd2073 --- /dev/null +++ b/platform-modules/url-manager/pom.xml @@ -0,0 +1,92 @@ + + + + platform-modules + org.sunbird + 1.0-SNAPSHOT + + 4.0.0 + url-manager + jar + + + + org.sunbird + platform-common + 1.0-SNAPSHOT + + + org.sunbird + platform-telemetry + 1.0-SNAPSHOT + + + com.google.apis + google-api-services-youtube + v3-rev182-1.22.0 + + + com.google.guava + guava-jdk5 + + + + + com.google.api-client + google-api-client + 1.24.1 + + + com.google.apis + google-api-services-drive + v3-rev136-1.25.0 + + + com.google.oauth-client + google-oauth-client-jetty + 1.22.0 + + + com.google.http-client + google-http-client + 1.22.0 + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 11 + + + + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + + + \ No newline at end of file diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/common/URLErrorCodes.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/common/URLErrorCodes.java new file mode 100644 index 000000000..ed4b420ef --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/common/URLErrorCodes.java @@ -0,0 +1,11 @@ +package org.sunbird.url.common; + +/** + * This Enum Holds All Error Codes for url-manager module + * + * @see org.sunbird.url.mgr.IURLManager + */ +public enum URLErrorCodes { + ERR_FILE_NOT_FOUND, SYSTEM_ERROR, ERR_INVALID_URL, ERR_GOOGLE_DRIVE_SIZE_VALIDATION, + ERR_GOOGLE_DRIVE_GET_METADATA, ERR_GOOGLE_SERVICE, ERR_YOUTUBE_SERVICE, ERR_YOUTUBE_LICENSE_VALIDATION +} diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/IURLManager.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/IURLManager.java new file mode 100644 index 000000000..cdd7743a4 --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/IURLManager.java @@ -0,0 +1,26 @@ +package org.sunbird.url.mgr; + +import java.util.Map; + +/** + * Contract for URL Utilities + */ +public interface IURLManager { + + /** + * This Method Validate The Given Url Based On Given Criteria + * + * @param url + * @param validationCriteria + * @return Map + */ + Map validateURL(String url, String validationCriteria); + + /** + * This Method Returns The Metadata Of Given Url + * + * @param url + * @return Map + */ + Map readMetadata(String url); +} diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/GeneralURLManagerImpl.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/GeneralURLManagerImpl.java new file mode 100644 index 000000000..58a1287f0 --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/GeneralURLManagerImpl.java @@ -0,0 +1,41 @@ +package org.sunbird.url.mgr.impl; + +import org.apache.commons.lang3.StringUtils; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResponseCode; +import org.sunbird.url.mgr.IURLManager; +import org.sunbird.url.util.HTTPUrlUtil; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This Class Holds Implementation Of IURLManager For General Url + * + * @see IURLManager + */ +public class GeneralURLManagerImpl implements IURLManager { + + private static final List validCriteria = Arrays.asList("size"); + private static long sizeLimit = 50000000; + + @Override + public Map validateURL(String url, String validationCriteria) { + if (StringUtils.isNotBlank(validationCriteria) && validCriteria.contains(validationCriteria)) { + Map metadata = HTTPUrlUtil.getMetadata(url); + Long size = metadata.get("size") == null ? 0 : (Long) metadata.get("size"); + Map result = new HashMap<>(); + result.put("value", size); + result.put("valid", size <= sizeLimit); + return result; + } else throw new ClientException(ResponseCode.CLIENT_ERROR.name(), "Please Provide Valid Criteria For Validation. Supported Criteria : " + validCriteria); + } + + @Override + public Map readMetadata(String url) { + return HTTPUrlUtil.getMetadata(url); + } + +} diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/GoogleDriveURLManagerImpl.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/GoogleDriveURLManagerImpl.java new file mode 100644 index 000000000..a68df2dae --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/GoogleDriveURLManagerImpl.java @@ -0,0 +1,41 @@ +package org.sunbird.url.mgr.impl; + +import org.apache.commons.lang3.StringUtils; +import org.sunbird.common.Platform; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResponseCode; +import org.sunbird.url.mgr.IURLManager; +import org.sunbird.url.util.GoogleDriveUrlUtil; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This Class Holds Implementation Of IURLManager For Google Drive Url + * + * @see IURLManager + */ +public class GoogleDriveURLManagerImpl implements IURLManager { + + private static final List validCriteria = Arrays.asList("size"); + private static long sizeLimit = Platform.config.hasPath("MAX_ASSET_FILE_SIZE_LIMIT") + ? Platform.config.getLong("MAX_ASSET_FILE_SIZE_LIMIT") : 52428800; + + @Override + public Map validateURL(String url, String validationCriteria) { + if (StringUtils.isNotBlank(validationCriteria) && validCriteria.contains(validationCriteria)) { + Long size = GoogleDriveUrlUtil.getSize(url); + Map result = new HashMap<>(); + result.put("value", size); + result.put("valid", size <= sizeLimit); + return result; + } else throw new ClientException(ResponseCode.CLIENT_ERROR.name(), "Please Provide Valid Criteria For Validation. Supported Criteria : " + validCriteria); + } + + @Override + public Map readMetadata(String url) { + return GoogleDriveUrlUtil.getMetadata(url); + } +} diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/URLFactoryManager.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/URLFactoryManager.java new file mode 100644 index 000000000..febb2a96a --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/URLFactoryManager.java @@ -0,0 +1,37 @@ +package org.sunbird.url.mgr.impl; + +import org.apache.commons.lang3.StringUtils; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResponseCode; +import org.sunbird.url.mgr.IURLManager; + +/** + * This Class Provides Factory Implementation for Url Managers. + */ +public class URLFactoryManager { + + private static final IURLManager youtubeUrlManager = new YouTubeURLManagerImpl(); + private static final IURLManager googleDriveUrlManager = new GoogleDriveURLManagerImpl(); + private static final IURLManager generalUrlManager = new GeneralURLManagerImpl(); + + private URLFactoryManager(){} + + /** + * This Method Returns Instance Of Url Manager Based On Given Provider + * + * @param provider + * @return IURLManager + */ + public static IURLManager getUrlManager(String provider) { + switch (StringUtils.lowerCase(provider)) { + case "youtube": + return youtubeUrlManager; + case "googledrive": + return googleDriveUrlManager; + case "general": + return generalUrlManager; + default: + throw new ClientException(ResponseCode.CLIENT_ERROR.name(), "Please Provide Valid Provider"); + } + } +} diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/YouTubeURLManagerImpl.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/YouTubeURLManagerImpl.java new file mode 100644 index 000000000..63f0fbbe1 --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/mgr/impl/YouTubeURLManagerImpl.java @@ -0,0 +1,42 @@ +package org.sunbird.url.mgr.impl; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ResponseCode; +import org.sunbird.url.mgr.IURLManager; +import org.sunbird.url.util.YouTubeUrlUtil; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This Class Holds Implementation Of IURLManager For YouTube Url + * + * @see IURLManager + */ +public class YouTubeURLManagerImpl implements IURLManager { + + private static final List validCriteria = Arrays.asList("license"); + + @Override + public Map validateURL(String url, String validationCriteria) { + if (StringUtils.isNotBlank(validationCriteria) && validCriteria.contains(validationCriteria)) { + String license = YouTubeUrlUtil.getLicense(url); + boolean isValidLicense = YouTubeUrlUtil.isValidLicense(license); + Map result = new HashMap<>(); + if (StringUtils.isNotBlank(license) && BooleanUtils.isTrue(isValidLicense)) { + result.put("value", license); + result.put("valid", isValidLicense); + } + return result; + } else throw new ClientException(ResponseCode.CLIENT_ERROR.name(), "Please Provide Valid Criteria For Validation. Supported Criteria : " + validCriteria); + } + + @Override + public Map readMetadata(String url) { + return null; + } +} diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/util/GoogleDriveUrlUtil.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/util/GoogleDriveUrlUtil.java new file mode 100644 index 000000000..97b2c4899 --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/util/GoogleDriveUrlUtil.java @@ -0,0 +1,141 @@ +package org.sunbird.url.util; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.drive.Drive; +import com.google.api.services.drive.Drive.Files.Get; +import com.google.api.services.drive.model.File; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.sunbird.common.Platform; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ServerException; +import org.sunbird.telemetry.logger.TelemetryManager; +import org.sunbird.url.common.URLErrorCodes; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This Class Provides Utility Methods Which Process Given Google Drive File Url + */ +public class GoogleDriveUrlUtil { + + private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); + private static final JsonFactory JSON_FACTORY = new JacksonFactory(); + + private static final String GOOGLE_DRIVE_URL_REGEX = "[-\\w]{25,}"; + private static final String DRIVE_FIELDS = "id, name, size"; + private static final String APP_NAME = Platform.config.hasPath("learning.content.drive.application.name") + ? Platform.config.getString("learning.content.drive.application.name") : "google-drive-url-validation"; + private static final String API_KEY = Platform.config.getString("learning_content_drive_apiKey"); + + private static final String ERR_MSG = "Please Provide Valid Google Drive URL!"; + private static final String SERVICE_ERROR = "Unable to Connect To Google Service. Please Try Again After Sometime!"; + private static final List ERROR_CODES = Arrays.asList("dailyLimitExceeded402", "limitExceeded", + "dailyLimitExceeded", "quotaExceeded", "userRateLimitExceeded", "quotaExceeded402", "keyExpired", + "keyInvalid"); + + private static boolean limitExceeded = false; + private static Drive drive = null; + + static { + drive = new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, null).setApplicationName(APP_NAME).build(); + } + + private GoogleDriveUrlUtil(){} + + /** + * This Method Returns Metadata For Given Google Drive File Url + * + * @param driveUrl + * @return Map + */ + public static Map getMetadata(String driveUrl) { + String fileId = getDriveFileId(driveUrl); + if (StringUtils.isBlank(fileId)) + throw new ClientException(URLErrorCodes.ERR_INVALID_URL.name(), ERR_MSG); + Map result = new HashMap<>(); + File driveFile = getDriveFile(fileId); + if (null != driveFile) { + result.put("id", driveFile.get("id")); + result.put("name", driveFile.get("name")); + result.put("size", driveFile.get("size")); + } + if (MapUtils.isEmpty(result) && !limitExceeded) + throw new ClientException(URLErrorCodes.ERR_GOOGLE_DRIVE_GET_METADATA.name(), ERR_MSG); + + return result; + } + + /** + * This Method Returns File Size Of Given Google Drive Url + * + * @param driveUrl + * @return Long + */ + public static Long getSize(String driveUrl) { + String fileId = getDriveFileId(driveUrl); + if (StringUtils.isBlank(fileId)) + throw new ClientException(URLErrorCodes.ERR_INVALID_URL.name(), ERR_MSG); + File driveFile = getDriveFile(fileId); + Long size = Long.valueOf(0); + if (null != driveFile) + size = driveFile.get("size") == null ? 0 : (Long) driveFile.get("size"); + + if (size == 0 && !limitExceeded) + throw new ClientException(URLErrorCodes.ERR_GOOGLE_DRIVE_SIZE_VALIDATION.name(), ERR_MSG); + + return size; + } + + /** + * This Method Extract And Returns Google Drive File Identifier From Url + * + * @param url + * @return String + */ + public static String getDriveFileId(String url) { + Pattern compiledPattern = Pattern.compile(GOOGLE_DRIVE_URL_REGEX); + Matcher matcher = compiledPattern.matcher(url); + if (matcher.find()) + return matcher.group(); + return ""; + } + + /** + * This Method Returns Google Drive File Object For Given Drive Url + * + * @param fileId + * @return File + */ + public static File getDriveFile(String fileId) { + File googleDriveFile = null; + try { + Get getFile = drive.files().get(fileId); + getFile.setKey(API_KEY); + getFile.setFields(DRIVE_FIELDS); + googleDriveFile = getFile.execute(); + } catch (GoogleJsonResponseException ex) { + Map error = ex.getDetails().getErrors().get(0); + String reason = (String) error.get("reason"); + if (ERROR_CODES.contains(reason)) { + limitExceeded = true; + TelemetryManager.log("Google Drive API Limit Exceeded. Reason is: " + reason + " | Error Details : " + ex); + } + } catch (Exception e) { + throw new ServerException(URLErrorCodes.SYSTEM_ERROR.name(), + "Something Went Wrong While Processing Your Request. Please Try Again After Sometime!"); + } + if (limitExceeded) + throw new ServerException(URLErrorCodes.ERR_GOOGLE_SERVICE.name(), SERVICE_ERROR); + return googleDriveFile; + } +} diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/util/HTTPUrlUtil.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/util/HTTPUrlUtil.java new file mode 100644 index 000000000..a1c6124ff --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/util/HTTPUrlUtil.java @@ -0,0 +1,56 @@ +package org.sunbird.url.util; + +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ServerException; +import org.sunbird.url.common.URLErrorCodes; + +import java.io.FileNotFoundException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +/** + * This Class Provides Utility Methods Which Process Given General Http Based Url + */ +public class HTTPUrlUtil { + + private HTTPUrlUtil(){} + + /** + * This Method Returns Size And Type For Given Url + * + * @param fileUrl + * @return Map + */ + public static Map getMetadata(String fileUrl) { + URLConnection conn = null; + Map metadata = new HashMap<>(); + try { + URL url = new URL(fileUrl); + conn = url.openConnection(); + if (conn instanceof HttpURLConnection) { + ((HttpURLConnection) conn).setRequestMethod("HEAD"); + } + conn.getInputStream(); + metadata.put("size", conn.getContentLengthLong()); + metadata.put("type", conn.getContentType()); + return metadata; + } catch (UnknownHostException e) { + throw new ClientException(URLErrorCodes.ERR_INVALID_URL.name(), "Please Provide Valid Url."); + } catch (FileNotFoundException e) { + throw new ClientException(URLErrorCodes.ERR_FILE_NOT_FOUND.name(), "File Not Found."); + } catch (Exception e) { + throw new ServerException(URLErrorCodes.SYSTEM_ERROR.name(), + "Something Went Wrong While Processing Your Request. Please Try Again After Sometime!"); + } finally { + if (conn instanceof HttpURLConnection) { + ((HttpURLConnection) conn).disconnect(); + } + } + } + + +} diff --git a/platform-modules/url-manager/src/main/java/org/sunbird/url/util/YouTubeUrlUtil.java b/platform-modules/url-manager/src/main/java/org/sunbird/url/util/YouTubeUrlUtil.java new file mode 100644 index 000000000..3474c2964 --- /dev/null +++ b/platform-modules/url-manager/src/main/java/org/sunbird/url/util/YouTubeUrlUtil.java @@ -0,0 +1,232 @@ +package org.sunbird.url.util; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.Video; +import com.google.api.services.youtube.model.VideoListResponse; +import org.apache.commons.lang3.StringUtils; +import org.sunbird.common.Platform; +import org.sunbird.common.exception.ClientException; +import org.sunbird.common.exception.ServerException; +import org.sunbird.telemetry.logger.TelemetryManager; +import org.sunbird.url.common.URLErrorCodes; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This Class Provides Utility Methods Which Process Given YouTube Url + */ +public class YouTubeUrlUtil { + + private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); + private static final JsonFactory JSON_FACTORY = new JacksonFactory(); + + private static final String APP_NAME = Platform.config.hasPath("learning.content.youtube.application.name") + ? Platform.config.getString("learning.content.youtube.application.name") : "fetch-youtube-license"; + + private static final String API_KEY = Platform.config.getString("learning_content_youtube_apikey"); + + private static final List VALID_LICENSES = Platform.config.hasPath("learning.valid_license") ? + Platform.config.getStringList("learning.valid_license") : Arrays.asList("creativeCommon"); + + private static final List videoIdRegex = Platform.config.hasPath("youtube.license.regex.pattern") ? + Platform.config.getStringList("youtube.license.regex.pattern") : + Arrays.asList("\\?vi?=([^&]*)", "watch\\?.*v=([^&]*)", "(?:embed|vi?)/([^/?]*)", "^([A-Za-z0-9\\-\\_]*)"); + + private static final String ERR_MSG = "Please Provide Valid YouTube URL!"; + private static final String SERVICE_ERROR = "Unable to Check License. Please Try Again After Sometime!"; + private static final List errorCodes = Arrays.asList("dailyLimitExceeded402", "limitExceeded", + "dailyLimitExceeded", "quotaExceeded", "userRateLimitExceeded", "quotaExceeded402", "keyExpired", + "keyInvalid"); + + private static boolean limitExceeded = false; + private static YouTube youtube = null; + + static { + youtube = new YouTube.Builder(HTTP_TRANSPORT, JSON_FACTORY, new HttpRequestInitializer() { + public void initialize(HttpRequest request) throws IOException { + } + }).setApplicationName(APP_NAME).build(); + } + + private YouTubeUrlUtil(){} + + /** + * This Method will fetch license for given YouTube Video URL. + * + * @param videoUrl + * @return licenceType + */ + public static String getLicense(String videoUrl) { + Video video = null; + String videoId = getIdFromUrl(videoUrl); + if (StringUtils.isBlank(videoId)) + throw new ClientException(URLErrorCodes.ERR_INVALID_URL.name(), ERR_MSG); + + String license = ""; + List