diff --git a/README.md b/README.md index 3c7a32c..c54eca6 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ For incremental indexing to work, you need to have two sets of unique ids, one f http://www.w3.org/XML/1998/namespace - + @@ -87,6 +87,8 @@ For incremental indexing to work, you need to have two sets of unique ids, one f ``` +An Optional `VisibleBy` attribute can be used to restrict data access when searching the Algolia index + A `rootObject` is equivalent to an object inside an Algolia Index. We create one "rootObject" either for each document, or document fragment (if you specify a path attribute on the rootObject). An `attribute` (represents a JSON object attribute, not to be confused with an XML attribute) is a simple key/value pair that is extracted from the XML and placed into the Algolia object ("rootObject" as we call it). All of the text nodes or attribute values indicated by the "path" on the "attribute" element will be serialized to a string (and then converted if you set an explicit "type" attribute). @@ -99,6 +101,14 @@ An `object` represents a JSON object, and this is where things become fun, we ba The `name` attribute that is available on the "attribute" and "object" elements allows you to set the name of the field in the JSON object of the Algolia index, this means that name names of your data fields can be different in Algolia to eXist if you wish. +## limiting Objects access to certain users +You can limit data access by setting the `visibleBy` attribute in `collection.xconf` then matching the path in your XML data preferably in the header +You can use this example from out test suit + +xml: https://github.com/BCDH/exist-algolia-index/tree/master/src/test/resources/integration/user-specified-visibleBy/VSK.TEST.xml + +collection.xconf https://github.com/BCDH/exist-algolia-index/tree/master/src/test/resources/integration/user-specified-visibleBy/collection.xconf + ## Enable logging in eXist (optional) diff --git a/src/main/resources/xsd/exist-algolia-index-config.xsd b/src/main/resources/xsd/exist-algolia-index-config.xsd index 9c86164..d4e01e4 100644 --- a/src/main/resources/xsd/exist-algolia-index-config.xsd +++ b/src/main/resources/xsd/exist-algolia-index-config.xsd @@ -123,6 +123,11 @@ Indicates an element or attribute to use the value of as a unique id for the document, if ommitted the document's id is used + + + Sets the rule of who can request the records, if omitted the default value will be public + + diff --git a/src/main/scala/org/humanistika/exist/index/algolia/AlgoliaStreamListener.scala b/src/main/scala/org/humanistika/exist/index/algolia/AlgoliaStreamListener.scala index 6b39fd1..0d6747f 100644 --- a/src/main/scala/org/humanistika/exist/index/algolia/AlgoliaStreamListener.scala +++ b/src/main/scala/org/humanistika/exist/index/algolia/AlgoliaStreamListener.scala @@ -17,7 +17,6 @@ package org.humanistika.exist.index.algolia -import java.io.StringWriter import java.util.{ArrayDeque, Deque, HashMap => JHashMap, Map => JMap, Properties => JProperties} import javax.xml.namespace.QName @@ -31,15 +30,13 @@ import org.exist_db.collection_config._1.{Algolia, LiteralType, Properties, Root import org.exist_db.collection_config._1.LiteralType._ import Serializer._ import akka.actor.ActorRef -import com.fasterxml.jackson.core.{JsonFactory, JsonGenerator} import grizzled.slf4j.Logger import org.exist.indexing.StreamListener.ReindexMode import org.exist.numbering.DLN import org.humanistika.exist.index.algolia.NodePathWithPredicates.{AtomicEqualsComparison, AtomicNotEqualsComparison, ComponentType, SequenceEqualsComparison} import org.humanistika.exist.index.algolia.backend.IncrementalIndexingManagerActor.{Add, FinishDocument, RemoveForDocument, StartDocument} import org.w3c.dom._ -import JsonUtil.writeValueField -import org.exist.util.serializer.SAXSerializer + import cats.syntax.either._ @@ -158,7 +155,7 @@ object AlgoliaStreamListener { .getOrElse(new NodePath()) } - case class UserSpecifiedDocumentPathId(path: NodePath, value: Option[UserSpecifiedDocumentId]) + case class UserSpecifiedOption(path: NodePath, value: Option[String]) case class PartialRootObject(indexName: IndexName, config: RootObject, indexable: IndexableRootObject) { def identityEquals(other: PartialRootObject) : Boolean = { @@ -202,7 +199,8 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i private var replacingDocument: Boolean = false private var processing: Map[NodePath, Seq[PartialRootObject]] = Map.empty - private var userSpecifiedDocumentIds: Map[IndexName, UserSpecifiedDocumentPathId] = Map.empty + private var userSpecifiedDocumentIds: Map[IndexName, UserSpecifiedOption] = Map.empty + private var userSpecifiedVisibleByIds: Map[IndexName, UserSpecifiedOption] = Map.empty private var userSpecifiedNodeIds: Map[(IndexName, NodePath), Option[UserSpecifiedNodeId]] = Map.empty case class ContextElement(name: QName, attributes: Map[QName, String]) @@ -224,8 +222,13 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i override def startIndexDocument(transaction: Txn) { // find any User Specified Document IDs that we need to complete this.userSpecifiedDocumentIds = indexConfigs - .map{ case (indexName, index) => indexName -> Option(index.getDocumentId).map(path => UserSpecifiedDocumentPathId(nodePath(ns, path), None)) } - .collect{ case (indexName, Some(usdid)) => indexName -> usdid } + .map{ case (indexName, index) => Tuple2(indexName , Option(index.getDocumentId).map(path => UserSpecifiedOption(nodePath(ns, path), None))) } + .collect{ case (indexName, Some(usdid)) => Tuple2(indexName , usdid) } + + // find any User Specified VisibleBYs that we need to complete + this.userSpecifiedVisibleByIds = indexConfigs + .map{ case (indexName, index) => Tuple2(indexName , Option(index.getVisibleBy).map(path => UserSpecifiedOption(nodePath(ns, path), None))) } + .collect{ case (indexName, Some(usvb)) => Tuple2(indexName , usvb) } getWorker.getMode() match { case ReindexMode.STORE => @@ -243,9 +246,6 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i // update the current context context.push(ContextElement(element.getQName.toJavaQName, Map.empty)) - // update any userSpecifiedDocumentIds which we haven't yet completed and that match this element path - updateUserSpecifiedDocumentIds(pathClone, element.asLeft) - getWorker.getMode() match { case ReindexMode.STORE => startElementForStore(transaction, element, pathClone) @@ -282,6 +282,13 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i super.attribute(transaction, attrib, pathClone) } + + override def characters(transaction: Txn, text: AbstractCharacterData, path: NodePath): Unit = { + val pathClone = path.duplicate + // update any userSpecifiedVisibleIds which we haven't yet completed and that match this element path + updateUserSpecifiedVisibleIds(pathClone, text) + } + override def endElement(transaction: Txn, element: ElementImpl, path: NodePath) { getWorker.getMode() match { case ReindexMode.STORE => @@ -313,6 +320,7 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i // clear any User Specified Document IDs this.userSpecifiedDocumentIds = Map.empty + this.userSpecifiedVisibleByIds = Map.empty this.context.clear() @@ -344,6 +352,21 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i } } + private def updateUserSpecifiedVisibleIds(path: NodePath, node: Node): Unit = { + for ((indexName, usvb) <- userSpecifiedVisibleByIds if usvb.value.isEmpty && usvb.path.equals(path)) { //TODO(AR) do we need to compare the index name? + getStringFromNode(node) match { + case Right(idValue) if (!idValue.isEmpty) => + this.userSpecifiedVisibleByIds = userSpecifiedVisibleByIds + (indexName -> usvb.copy(value = Some(idValue))) + + case Right(idValue) if (idValue.isEmpty) => + logger.error(s"UserSpecifiedNodeIds: Unable to use empty string for attribute path=${path}") + + case Left(ts) => + logger.error(s"UserSpecifiedNodeIds: Unable to serialize attribute for path=${path})") + } + } + } + private def updateUserSpecifiedNodeIds(path: NodePath, attrib: AttrImpl): Unit = { for (((indexName, nodeIdPath), usnid) <- userSpecifiedNodeIds if usnid.isEmpty && nodeIdPath.equals(path)) { //TODO(AR) do we need to compare the index name? getString(attrib.asRight) match { @@ -365,7 +388,7 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i private def removeForDocument() = { val docId = getWorker.getDocument.getDocId for(indexName <- indexConfigs.keys) { - incrementalIndexingActor ! RemoveForDocument(indexName, docId, userSpecifiedDocumentIds.get(indexName).flatMap(_.value)) + incrementalIndexingActor ! RemoveForDocument(indexName, docId, userSpecifiedDocumentIds.get(indexName).flatMap(_.value), userSpecifiedVisibleByIds.get(indexName).flatMap(_.value)) } } @@ -379,7 +402,7 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i if (documentRootObjects.nonEmpty) { // as we are just starting a document, // we aren't processing these yet, so let's record them - val processingAtPath = documentRootObjects.map(rootObjectConfig => PartialRootObject(rootObjectConfig._1, rootObjectConfig._2, IndexableRootObject(indexWorker.getDocument.getCollection.getURI.getCollectionPath, indexWorker.getDocument().getCollection.getId, indexWorker.getDocument().getDocId, None, None, None, Seq.empty))) + val processingAtPath = documentRootObjects.map(rootObjectConfig => PartialRootObject(rootObjectConfig._1, rootObjectConfig._2, IndexableRootObject(indexWorker.getDocument.getCollection.getURI.getCollectionPath, indexWorker.getDocument().getCollection.getId, indexWorker.getDocument().getDocId, None, None, None, None, Seq.empty))) this.processing = processing + (DOCUMENT_NODE_PATH -> processingAtPath) } } @@ -390,7 +413,7 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i if (elementRootObjects.nonEmpty) { // record the new RootObjects that we are processing - val newElementRootObjects: Seq[PartialRootObject] = elementRootObjects.map(rootObjectConfig => PartialRootObject(rootObjectConfig._1, rootObjectConfig._2, IndexableRootObject(indexWorker.getDocument().getCollection.getURI.getCollectionPath, indexWorker.getDocument().getCollection.getId, indexWorker.getDocument().getDocId, None, Some(element.getNodeId.toString), None, Seq.empty))) + val newElementRootObjects: Seq[PartialRootObject] = elementRootObjects.map(rootObjectConfig => PartialRootObject(rootObjectConfig._1, rootObjectConfig._2, IndexableRootObject(indexWorker.getDocument().getCollection.getURI.getCollectionPath, indexWorker.getDocument().getCollection.getId, indexWorker.getDocument().getDocId, None, None, Some(element.getNodeId.toString), None, Seq.empty))) val processingAtPath = processing.get(pathClone) match { case Some(existingElementRootObjects) => // we filter out newElementRootObjects that are equivalent to elementRootObjects which we are already processing @@ -421,7 +444,7 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i if (elementRootObjects.nonEmpty) { // index them elementRootObjects - .foreach(partialRootObject => index(partialRootObject.indexName, partialRootObject.indexable.copy(userSpecifiedDocumentId = getUserSpecifiedDocumentIdOrWarn(partialRootObject.indexName), userSpecifiedNodeId = getUserSpecifiedNodeIdOrWarn(partialRootObject.indexName, pathClone)))) + .foreach(partialRootObject => index(partialRootObject.indexName, partialRootObject.indexable.copy(userSpecifiedDocumentId = getUserSpecifiedDocumentIdOrWarn(partialRootObject.indexName), userSpecifiedVisibleBy = getUserSpecifiedVisibleByOrWarn(partialRootObject.indexName), userSpecifiedNodeId = getUserSpecifiedNodeIdOrWarn(partialRootObject.indexName, pathClone)))) // finished... so remove them from the map of things we are processing this.processing = processing.view.filterKeys(_ != pathClone).toMap @@ -441,12 +464,13 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i } // finish indexing any documents for which we have IndexableRootObjects - indexConfigs.keys.foreach(indexName => finishDocumentIndex(indexName, userSpecifiedDocumentIds.get(indexName).flatMap(_.value), indexWorker.getDocument.getCollection.getId, indexWorker.getDocument.getDocId)) + indexConfigs.keys.foreach(indexName => finishDocumentIndex(indexName, userSpecifiedDocumentIds.get(indexName).flatMap(_.value), userSpecifiedVisibleByIds.get(indexName).flatMap(_.value), indexWorker.getDocument.getCollection.getId, indexWorker.getDocument.getDocId)) // finished... so clear the map of things we are processing this.processing = Map.empty this.userSpecifiedDocumentIds = Map.empty + this.userSpecifiedVisibleByIds = Map.empty this.userSpecifiedNodeIds = Map.empty } @@ -465,6 +489,21 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i } } + private def getUserSpecifiedVisibleByOrWarn(indexName: IndexName) : Option[UserSpecifiedVisibleBy] = { + userSpecifiedVisibleByIds.get(indexName) match { + case Some(userSpecifiedVisibleBy) => + userSpecifiedVisibleBy.value match { + case value : Some[UserSpecifiedVisibleBy] => + value + case None => + logger.warn(s"Unable to find user specified document id for index=${indexName} at path=${userSpecifiedVisibleBy.path}, will use default!") + None + } + case None => + None + } + } + private def getUserSpecifiedNodeIdOrWarn(indexName: IndexName, rootObjectPath: NodePath) : Option[UserSpecifiedNodeId] = { val maybeKey = userSpecifiedNodeIds .keySet @@ -576,6 +615,14 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i private def getString(node: ElementOrAttributeImpl): Either[Seq[Throwable], String] = node.fold(serializeAsText, _.getValue.asRight) + private def getStringFromNode(node: Node): Either[Seq[Throwable], String] = { + node match { + case attr: Attr => + attr.getValue.asRight + case other => + serializeAsText(other) + } + } private def updateProcessingChildren(path: NodePath, node: ElementOrAttributeImpl) { def nodeIdStr(node: ElementOrAttributeImpl) : String = foldNode(node, _.getNodeId.toString) @@ -839,7 +886,7 @@ class AlgoliaStreamListener(indexWorker: AlgoliaIndexWorker, broker: DBBroker, i incrementalIndexingActor ! Add(indexName, indexableRootObject) } - private def finishDocumentIndex(indexName: IndexName, userSpecifiedDocumentId: Option[String], collectionId: CollectionId, documentId: DocumentId) { + private def finishDocumentIndex(indexName: IndexName, userSpecifiedDocumentId: Option[String], userSpecifiedVisibleBy: Option[String], collectionId: CollectionId, documentId: DocumentId) { incrementalIndexingActor ! FinishDocument(indexName, userSpecifiedDocumentId, collectionId, documentId) } } diff --git a/src/main/scala/org/humanistika/exist/index/algolia/IndexableRootObjectJsonSerializer.scala b/src/main/scala/org/humanistika/exist/index/algolia/IndexableRootObjectJsonSerializer.scala index 0ab79fc..f7acaaf 100644 --- a/src/main/scala/org/humanistika/exist/index/algolia/IndexableRootObjectJsonSerializer.scala +++ b/src/main/scala/org/humanistika/exist/index/algolia/IndexableRootObjectJsonSerializer.scala @@ -29,6 +29,7 @@ object IndexableRootObjectJsonSerializer { val OBJECT_ID_FIELD_NAME = "objectID" val COLLECTION_PATH_FIELD_NAME = "collection" val DOCUMENT_ID_FIELD_NAME = "documentID" + val RECORD_VISIBLE_BY_FIELD_NAME = "visible_by" } class IndexableRootObjectJsonSerializer extends JsonSerializer[IndexableRootObject] { @@ -47,6 +48,11 @@ class IndexableRootObjectJsonSerializer extends JsonSerializer[IndexableRootObje case None => gen.writeNumberField(DOCUMENT_ID_FIELD_NAME, value.documentId) } + value.userSpecifiedVisibleBy match { + case Some(usv) => + gen.writeStringField(RECORD_VISIBLE_BY_FIELD_NAME, usv) + case None => // do nothing + } serializeChildren(value.children, gen, serializers) diff --git a/src/main/scala/org/humanistika/exist/index/algolia/backend/AlgoliaIndexManagerActor.scala b/src/main/scala/org/humanistika/exist/index/algolia/backend/AlgoliaIndexManagerActor.scala index 0533dea..9a8d5aa 100644 --- a/src/main/scala/org/humanistika/exist/index/algolia/backend/AlgoliaIndexManagerActor.scala +++ b/src/main/scala/org/humanistika/exist/index/algolia/backend/AlgoliaIndexManagerActor.scala @@ -59,7 +59,7 @@ class AlgoliaIndexManagerActor extends Actor { val indexActor = getOrCreatePerIndexActor(indexName) indexActor ! changes - case rfd @ RemoveForDocument(indexName, documentId, userSpecifiedDocumentId) => + case rfd @ RemoveForDocument(indexName, documentId, userSpecifiedDocumentId, userSpecifiedVisibleBy) => if(logger.isTraceEnabled) { logger.trace(s"Initiating RemoveForDocument (id=${documentId}, userSpecificDocId=${userSpecifiedDocumentId}) for index: $indexName") } @@ -199,7 +199,7 @@ class AlgoliaIndexActor(indexName: IndexName, algoliaIndex: Index[IndexableRootO - case RemoveForDocument(_, documentId, userSpecifiedDocumentId) => + case RemoveForDocument(_, documentId, userSpecifiedDocumentId, userSpecifiedVisibleBy) => val batchLogMsgGroupId: BatchLogMsgGroupId = System.nanoTime() logger.info(s"Sending remove document (msgId=$batchLogMsgGroupId) to Algolia for documentId=$documentId, userSpecificDocId=$userSpecifiedDocumentId in index: $indexName") diff --git a/src/main/scala/org/humanistika/exist/index/algolia/backend/IncrementalIndexingManagerActor.scala b/src/main/scala/org/humanistika/exist/index/algolia/backend/IncrementalIndexingManagerActor.scala index d32ab0f..be9a0d2 100644 --- a/src/main/scala/org/humanistika/exist/index/algolia/backend/IncrementalIndexingManagerActor.scala +++ b/src/main/scala/org/humanistika/exist/index/algolia/backend/IncrementalIndexingManagerActor.scala @@ -33,7 +33,7 @@ object IncrementalIndexingManagerActor { case class Add(indexName: IndexName, indexableRootObject: IndexableRootObject) case class FinishDocument(indexName: IndexName, userSpecifiedDocumentId: Option[String], collectionId: CollectionId, documentId: DocumentId) case class IndexChanges(indexName: IndexName, changes: Changes) - case class RemoveForDocument(indexName: IndexName, documentId: DocumentId, userSpecifiedDocumentId: Option[String]) + case class RemoveForDocument(indexName: IndexName, documentId: DocumentId, userSpecifiedDocumentId: Option[String], userSpecifiedVisibleBy: Option[String]) case class RemoveForCollection(indexName: IndexName, collectionPath: String) case object DropIndexes } diff --git a/src/main/scala/org/humanistika/exist/index/algolia/backend/IndexLocalStoreManagerActor.scala b/src/main/scala/org/humanistika/exist/index/algolia/backend/IndexLocalStoreManagerActor.scala index 793407b..8eed299 100644 --- a/src/main/scala/org/humanistika/exist/index/algolia/backend/IndexLocalStoreManagerActor.scala +++ b/src/main/scala/org/humanistika/exist/index/algolia/backend/IndexLocalStoreManagerActor.scala @@ -84,7 +84,7 @@ class IndexLocalStoreManagerActor(dataDir: Path) extends Actor { case indexChanges : IndexChanges => context.parent ! indexChanges - case removeForDocument @ RemoveForDocument(indexName, _, _) => + case removeForDocument @ RemoveForDocument(indexName, _, _, _) => val indexActor = getOrCreatePerIndexActor(indexName) indexActor ! removeForDocument @@ -131,7 +131,7 @@ class IndexLocalStoreActor(indexesDir: Path, indexName: String) extends Actor { this.processing = processing + (documentId -> timestamp) getOrCreatePerDocumentActor(documentId) - case Add(_, iro @ IndexableRootObject(_, _, documentId, _, _, _, _)) => + case Add(_, iro @ IndexableRootObject(_, _, documentId, _, _, _, _, _)) => val perDocumentActor = getOrCreatePerDocumentActor(documentId) val timestamp = processing(documentId) perDocumentActor ! Write(timestamp, iro) @@ -152,7 +152,7 @@ class IndexLocalStoreActor(indexesDir: Path, indexName: String) extends Actor { context.parent ! IndexChanges(indexName, changes) //TODO(AR) when to delete previous timestamp (after upload into Algolia) - case RemoveForDocument(_, documentId, userSpecifiedDocumentId) => + case RemoveForDocument(_, documentId, userSpecifiedDocumentId, userSpecifiedVisibleBy) => val perDocumentActor = getOrCreatePerDocumentActor(documentId) val maybeTimestamp = processing.get(documentId) perDocumentActor ! RemoveDocument(documentId, userSpecifiedDocumentId, maybeTimestamp) // perDocumentActor will stop itself! diff --git a/src/main/scala/org/humanistika/exist/index/algolia/package.scala b/src/main/scala/org/humanistika/exist/index/algolia/package.scala index fa483f8..d6b3ee1 100644 --- a/src/main/scala/org/humanistika/exist/index/algolia/package.scala +++ b/src/main/scala/org/humanistika/exist/index/algolia/package.scala @@ -47,6 +47,7 @@ package object algolia { } type UserSpecifiedDocumentId = String + type UserSpecifiedVisibleBy = String type UserSpecifiedNodeId = String type CollectionPath = String @@ -54,7 +55,7 @@ package object algolia { type DocumentId = Int type objectID = String - @JsonSerialize(using=classOf[IndexableRootObjectJsonSerializer]) case class IndexableRootObject(collectionPath: CollectionPath, collectionId: CollectionId, documentId: DocumentId, userSpecifiedDocumentId: Option[UserSpecifiedDocumentId], nodeId: Option[String], userSpecifiedNodeId: Option[UserSpecifiedNodeId], children: Seq[Either[IndexableAttribute, IndexableObject]]) + @JsonSerialize(using=classOf[IndexableRootObjectJsonSerializer]) case class IndexableRootObject(collectionPath: CollectionPath, collectionId: CollectionId, documentId: DocumentId, userSpecifiedDocumentId: Option[UserSpecifiedDocumentId], userSpecifiedVisibleBy: Option[UserSpecifiedVisibleBy], nodeId: Option[String], userSpecifiedNodeId: Option[UserSpecifiedNodeId], children: Seq[Either[IndexableAttribute, IndexableObject]]) case class IndexableAttribute(name: Name, values: IndexableValues, literalType: LiteralTypeConfig.LiteralTypeConfig) case class IndexableObject(name: Name, values: IndexableValues) diff --git a/src/test/resources/integration/user-specified-visibleBy/VSK.TEST.xml b/src/test/resources/integration/user-specified-visibleBy/VSK.TEST.xml new file mode 100644 index 0000000..a63b431 --- /dev/null +++ b/src/test/resources/integration/user-specified-visibleBy/VSK.TEST.xml @@ -0,0 +1,99 @@ + + + + + Title + + +

Publication Information

+
+ +

Information about the source

+
+
+ + visibility + +
+ + +
+ +
+ Adam +
+ , + + m + + + + Адам + + . + +
+ +
+ Addam +
+ , + + m + + + + Аддам + + . + +
+ +
+ Adamm +
+ , + + m + + + + Адамм + + . + +
+ +
+ Adammm +
+ + + m + + + + Адаммм + + . + +
+ +
+ Adammmmm +
+ + + m. + + + + Адамммм + + . + +
+
+ +
+
\ No newline at end of file diff --git a/src/test/resources/integration/user-specified-visibleBy/collection.xconf b/src/test/resources/integration/user-specified-visibleBy/collection.xconf new file mode 100644 index 0000000..b454751 --- /dev/null +++ b/src/test/resources/integration/user-specified-visibleBy/collection.xconf @@ -0,0 +1,27 @@ + + + + + + tei + http://www.tei-c.org/ns/1.0 + + + xml + http://www.w3.org/XML/1998/namespace + + + r + http://raskovnik.org + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/scala/org/humanistika/exist/index/algolia/AlgoliaStreamListenerIntegrationSpec.scala b/src/test/scala/org/humanistika/exist/index/algolia/AlgoliaStreamListenerIntegrationSpec.scala index 3872e48..a8d5dd1 100644 --- a/src/test/scala/org/humanistika/exist/index/algolia/AlgoliaStreamListenerIntegrationSpec.scala +++ b/src/test/scala/org/humanistika/exist/index/algolia/AlgoliaStreamListenerIntegrationSpec.scala @@ -56,23 +56,23 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.4"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.4"), None, Seq( Left(("dict", Seq("1.5.2.2.4.1"))), Left(("lemma", Seq("1.5.2.2.4.3.3"))), Left(("tr", Seq("1.5.2.2.4.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.6"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.6"), None, Seq( Left(("dict", Seq("1.5.2.2.6.1"))), Left(("lemma", Seq("1.5.2.2.6.3.3"))), Left(("tr", Seq("1.5.2.2.6.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.8"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.8"), None, Seq( Left(("dict", Seq("1.5.2.2.8.1"))), Left(("lemma", Seq("1.5.2.2.8.3.3"))), Left(("tr", Seq("1.5.2.2.8.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.10"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.10"), None, Seq( Left(("dict", Seq("1.5.2.2.10.1"))), Left(("lemma", Seq("1.5.2.2.10.3.3"))), Left(("tr", Seq("1.5.2.2.10.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.12"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.12"), None, Seq( Left(("dict", Seq("1.5.2.2.12.1"))), Left(("lemma", Seq("1.5.2.2.12.3.3"))), Left(("tr", Seq("1.5.2.2.12.9.3.3"))))) @@ -103,12 +103,12 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.4"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.4"), None, Seq( Left(("lemma", Seq( "1.5.2.2.4.6.3", "1.5.2.2.4.8.5", "1.5.2.2.4.12.5"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.6"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.6"), None, Seq( Left(("lemma", Seq( "1.5.2.2.6.6.3"))))) expectMsg(FinishDocument(indexName, None, collectionId, docId)) @@ -138,23 +138,23 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.4"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.4"), None, Seq( Left(("dict", Seq("1.5.2.2.4.1"))), Left(("lemma", Seq("1.5.2.2.4.3.3"))), Left(("tr", Seq("1.5.2.2.4.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.6"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.6"), None, Seq( Left(("dict", Seq("1.5.2.2.6.1"))), Left(("lemma", Seq("1.5.2.2.6.3.3"))), Left(("tr", Seq("1.5.2.2.6.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.8"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.8"), None, Seq( Left(("dict", Seq("1.5.2.2.8.1"))), Left(("lemma", Seq("1.5.2.2.8.3.3"))), Left(("tr", Seq("1.5.2.2.8.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.10"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.10"), None, Seq( Left(("dict", Seq("1.5.2.2.10.1"))), Left(("inverse-lemma", Seq("1.5.2.2.10.3.3"))), Left(("tr", Seq("1.5.2.2.10.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.12"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.12"), None, Seq( Left(("dict", Seq("1.5.2.2.12.1"))), Left(("lemma", Seq("1.5.2.2.12.3.3"))))) expectMsg(FinishDocument(indexName, None, collectionId, docId)) @@ -183,7 +183,7 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some("1.5.2.2.4"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, None, Some("1.5.2.2.4"), None, Seq( Left(("trde", Seq("1.5.2.2.4.14.6.3", "1.5.2.2.4.16.6.3", "1.5.2.2.4.18.6.3", "1.5.2.2.4.18.8.3", "1.5.2.2.4.22.6.3", "1.5.2.2.4.24.8.3", "1.5.2.2.4.26.6.3"))), Left(("trla", Seq("1.5.2.2.4.14.8.3", "1.5.2.2.4.14.10.5", "1.5.2.2.4.16.8.3", "1.5.2.2.4.18.10.3", "1.5.2.2.4.18.12.3", "1.5.2.2.4.22.8.3", "1.5.2.2.4.24.10.3", "1.5.2.2.4.26.8.3"))))) expectMsg(FinishDocument(indexName, None, collectionId, docId)) @@ -214,29 +214,75 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.4"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.4"), None, Seq( Left(("dict", Seq("1.5.2.2.4.1"))), Left(("lemma", Seq("1.5.2.2.4.3.3"))), Left(("tr", Seq("1.5.2.2.4.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.6"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.6"), None, Seq( Left(("dict", Seq("1.5.2.2.6.1"))), Left(("lemma", Seq("1.5.2.2.6.3.3"))), Left(("tr", Seq("1.5.2.2.6.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.8"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.8"), None, Seq( Left(("dict", Seq("1.5.2.2.8.1"))), Left(("lemma", Seq("1.5.2.2.8.3.3"))), Left(("tr", Seq("1.5.2.2.8.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.10"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.10"), None, Seq( Left(("dict", Seq("1.5.2.2.10.1"))), Left(("lemma", Seq("1.5.2.2.10.3.3"))), Left(("tr", Seq("1.5.2.2.10.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.12"), None, Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.12"), None, Seq( Left(("dict", Seq("1.5.2.2.12.1"))), Left(("lemma", Seq("1.5.2.2.12.3.3"))), Left(("tr", Seq("1.5.2.2.12.9.3.3"))))) expectMsg(FinishDocument(indexName, Some(userSpecifiedDocId), collectionId, docId)) } + "produce the correct actor messages for a basic index config with user visibleBy" in new AkkaTestkitSpecs2Support { + + val indexName = "raskovnik-test-integration-user-visibleBy" + val userSpecifiedvisibleBy = "visibility" + val testCollectionPath = XmldbURI.create("/db/test-integration-user-visibleBy") + + // register our index + implicit val brokerPool: BrokerPool = getBrokerPool + val algoliaIndex = createAndRegisterAlgoliaIndex(system, Some(testActor)) + + // set up an index configuration + storeCollectionConfig(algoliaIndex, testCollectionPath, getTestResource("integration/user-specified-visibleBy/collection.xconf")) + + // store some data (which will be indexed) + val (collectionId, docId) = storeTestDocument(algoliaIndex, testCollectionPath, getTestResource("integration/user-specified-visibleBy/VSK.TEST.xml")) + + collectionId mustNotEqual -1 + docId mustNotEqual -1 + + val collectionPath = testCollectionPath.getRawCollectionPath + + expectMsg(Authentication("some-application-id", "some-admin-api-key")) + expectMsg(StartDocument(indexName, collectionId, docId)) + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some(userSpecifiedvisibleBy), Some("1.4.2.2.4"), None, Seq( + Left(("dict", Seq("1.4.2.2.4.1"))), + Left(("lemma", Seq("1.4.2.2.4.3.3"))), + Left(("tr", Seq("1.4.2.2.4.9.3.3"))))) + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some(userSpecifiedvisibleBy), Some("1.4.2.2.6"), None, Seq( + Left(("dict", Seq("1.4.2.2.6.1"))), + Left(("lemma", Seq("1.4.2.2.6.3.3"))), + Left(("tr", Seq("1.4.2.2.6.9.3.3"))))) + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some(userSpecifiedvisibleBy), Some("1.4.2.2.8"), None, Seq( + Left(("dict", Seq("1.4.2.2.8.1"))), + Left(("lemma", Seq("1.4.2.2.8.3.3"))), + Left(("tr", Seq("1.4.2.2.8.9.3.3"))))) + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some(userSpecifiedvisibleBy), Some("1.4.2.2.10"), None, Seq( + Left(("dict", Seq("1.4.2.2.10.1"))), + Left(("lemma", Seq("1.4.2.2.10.3.3"))), + Left(("tr", Seq("1.4.2.2.10.9.3.3"))))) + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, None, Some(userSpecifiedvisibleBy), Some("1.4.2.2.12"), None, Seq( + Left(("dict", Seq("1.4.2.2.12.1"))), + Left(("lemma", Seq("1.4.2.2.12.3.3"))), + Left(("tr", Seq("1.4.2.2.12.9.3.3"))))) + expectMsg(FinishDocument(indexName, None, collectionId, docId)) + } + "produce the correct actor messages for a basic index config with user specified docId and nodeId" in new AkkaTestkitSpecs2Support { @@ -262,23 +308,23 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.4"), Some("VSK.SR.Adam"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.4"), Some("VSK.SR.Adam"), Seq( Left(("dict", Seq("1.5.2.2.4.1"))), Left(("lemma", Seq("1.5.2.2.4.3.3"))), Left(("tr", Seq("1.5.2.2.4.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.6"), Some("VSK.SR.Addam"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.6"), Some("VSK.SR.Addam"), Seq( Left(("dict", Seq("1.5.2.2.6.1"))), Left(("lemma", Seq("1.5.2.2.6.3.3"))), Left(("tr", Seq("1.5.2.2.6.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.8"), Some("VSK.SR.Adamm"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.8"), Some("VSK.SR.Adamm"), Seq( Left(("dict", Seq("1.5.2.2.8.1"))), Left(("lemma", Seq("1.5.2.2.8.3.3"))), Left(("tr", Seq("1.5.2.2.8.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.10"), Some("VSK.SR.Adammm"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.10"), Some("VSK.SR.Adammm"), Seq( Left(("dict", Seq("1.5.2.2.10.1"))), Left(("lemma", Seq("1.5.2.2.10.3.3"))), Left(("tr", Seq("1.5.2.2.10.9.3.3"))))) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("1.5.2.2.12"), Some("VSK.SR.Adammmm"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("1.5.2.2.12"), Some("VSK.SR.Adammmm"), Seq( Left(("dict", Seq("1.5.2.2.12.1"))), Left(("lemma", Seq("1.5.2.2.12.3.3"))), Left(("tr", Seq("1.5.2.2.12.9.3.3"))))) @@ -309,7 +355,7 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("4.6.2.2.4"), Some("MZ.RGJS.наводаџија"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("4.6.2.2.4"), Some("MZ.RGJS.наводаџија"), Seq( Right(("e-e", Seq("4.6.2.2.4.7"))))) expectMsg(FinishDocument(indexName, Some(userSpecifiedDocId), collectionId, docId)) } @@ -338,7 +384,7 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("4.6.2.2.4"), Some("MZ.RGJS.наводаџија"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("4.6.2.2.4"), Some("MZ.RGJS.наводаџија"), Seq( Right(("e-e", Seq("4.6.2.2.4.7"))))) expectMsg(FinishDocument(indexName, Some(userSpecifiedDocId), collectionId, docId)) } @@ -367,7 +413,7 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("4.6.2.2.4"), Some("MZ.RGJS.наводаџија"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("4.6.2.2.4"), Some("MZ.RGJS.наводаџија"), Seq( Right(("e-e", Seq("4.6.2.2.4.7"))))) expectMsg(FinishDocument(indexName, Some(userSpecifiedDocId), collectionId, docId)) } @@ -396,7 +442,7 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("3.5.2.2.4"), Some("VSK.SR.баба2"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("3.5.2.2.4"), Some("VSK.SR.баба2"), Seq( Right(("e-e", Seq("3.5.2.2.4.8", "3.5.2.2.4.10", "3.5.2.2.4.12", "3.5.2.2.4.14"))))) expectMsg(FinishDocument(indexName, Some(userSpecifiedDocId), collectionId, docId)) } @@ -425,7 +471,7 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe expectMsg(Authentication("some-application-id", "some-admin-api-key")) expectMsg(StartDocument(indexName, collectionId, docId)) - assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), Some("4.6.2.2.4"), Some("VSK.SR.џукела"), Seq( + assertAdd(expectMsgType[Add])(indexName, collectionPath, collectionId, docId, Some(userSpecifiedDocId), None, Some("4.6.2.2.4"), Some("VSK.SR.џукела"), Seq( Left(("l", Seq("4.6.2.2.4.4.3"))), Left(("t-de", Seq("4.6.2.2.4.8.3.3"))), Left(("t-la", Seq("4.6.2.2.4.8.5.3"))))) @@ -439,12 +485,13 @@ class AlgoliaStreamListenerIntegrationSpec extends Specification with ExistServe type IndexableAttributeNameAndValueIds = NameAndValueIds type IndexableObjectNameAndValueIds = NameAndValueIds - private def assertAdd(addMsg: Add)(indexName: IndexName, collectionPath: CollectionPath, collectionId: CollectionId, documentId: DocumentId, userSpecifiedDocumentId: Option[UserSpecifiedDocumentId], nodeId: Option[String], userSpecifiedNodeId: Option[UserSpecifiedNodeId], children: Seq[Either[IndexableAttributeNameAndValueIds, IndexableObjectNameAndValueIds]]) = { + private def assertAdd(addMsg: Add)(indexName: IndexName, collectionPath: CollectionPath, collectionId: CollectionId, documentId: DocumentId, userSpecifiedDocumentId: Option[UserSpecifiedDocumentId], userSpecifiedVisibleBy: Option[UserSpecifiedVisibleBy] , nodeId: Option[String], userSpecifiedNodeId: Option[UserSpecifiedNodeId], children: Seq[Either[IndexableAttributeNameAndValueIds, IndexableObjectNameAndValueIds]]) = { addMsg.indexName mustEqual indexName addMsg.indexableRootObject.collectionPath mustEqual collectionPath addMsg.indexableRootObject.collectionId mustEqual collectionId addMsg.indexableRootObject.documentId mustEqual documentId addMsg.indexableRootObject.userSpecifiedDocumentId mustEqual userSpecifiedDocumentId + addMsg.indexableRootObject.userSpecifiedVisibleBy mustEqual userSpecifiedVisibleBy addMsg.indexableRootObject.nodeId mustEqual nodeId addMsg.indexableRootObject.userSpecifiedNodeId mustEqual userSpecifiedNodeId diff --git a/src/test/scala/org/humanistika/exist/index/algolia/IndexableRootObjectJsonSerializerSpec.scala b/src/test/scala/org/humanistika/exist/index/algolia/IndexableRootObjectJsonSerializerSpec.scala index e82dab5..4c38377 100644 --- a/src/test/scala/org/humanistika/exist/index/algolia/IndexableRootObjectJsonSerializerSpec.scala +++ b/src/test/scala/org/humanistika/exist/index/algolia/IndexableRootObjectJsonSerializerSpec.scala @@ -19,99 +19,105 @@ class IndexableRootObjectJsonSerializerSpec extends Specification { def is = s2" prefer the user specified document id $e2 have a nodeId (if provided) $e3 prefer the user specified node id $e4 + have a visibleBy (if provided) $e5 The JSON serialized result attributes for DOM Attributes must - be constructable $e5 - be float convertible $e6 - be int convertible $e7 - be boolean convertible $e8 - allow multiple $e9 - support arrays $e10 + be constructable $e6 + be float convertible $e7 + be int convertible $e8 + be boolean convertible $e9 + allow multiple $e10 + support arrays $e11 The JSON serialized result attributes for DOM Elements must - be constructable $e11 - be float convertible $e12 - be int convertible $e13 - be boolean convertible $e14 - allow multiple $e15 - serialize all text nodes $e16 - serialize all text nodes and not attributes $e17 - support arrays $e18 - support arrays (of text nodes and not attributes) $e19 - be valid when only child text nodes are provided $e20 + be constructable $e12 + be float convertible $e13 + be int convertible $e14 + be boolean convertible $e15 + allow multiple $e16 + serialize all text nodes $e17 + serialize all text nodes and not attributes $e18 + support arrays $e19 + support arrays (of text nodes and not attributes) $e20 + be valid when only child text nodes are provided $e21 The JSON serialized result objects for DOM Attributes must - be the same as a result attribute $e21 - support arrays $e22 + be the same as a result attribute $e22 + support arrays $e23 The JSON serialized result objects for DOM Elements must - be constructable $e23 - write nested elements $e24 - write array $e25 - write nested array $e26 - support arrays $e27 - be valid when only child text nodes are provided $e28 - be valid when only attributes are provided $e29 - be valid when only child text nodes and attributes are provided $e30 + be constructable $e24 + write nested elements $e25 + write array $e26 + write nested array $e27 + support arrays $e28 + be valid when only child text nodes are provided $e29 + be valid when only attributes are provided $e30 + be valid when only child text nodes and attributes are provided $e31 """ def e1 = { - val indexableRootObject = IndexableRootObject("/db/a1", 5, 46, None, None, None, Seq.empty) + val indexableRootObject = IndexableRootObject("/db/a1", 5, 46, None, None, None, None, Seq.empty) serializeJson(indexableRootObject) mustEqual """{"objectID":"5/46/0","collection":"/db/a1","documentID":46}""" } def e2 = { - val indexableRootObject = IndexableRootObject("/db/a1", 5, 46, Some("my-document-id"), None, None, Seq.empty) + val indexableRootObject = IndexableRootObject("/db/a1", 5, 46, Some("my-document-id"), None, None, None, Seq.empty) serializeJson(indexableRootObject) mustEqual """{"objectID":"5/46/0","collection":"/db/a1","documentID":"my-document-id"}""" } def e3 = { - val indexableRootObject = IndexableRootObject("/db/a1", 6, 47, None, Some("1.2.2"), None, Seq.empty) + val indexableRootObject = IndexableRootObject("/db/a1", 6, 47, None, None,Some("1.2.2"), None, Seq.empty) serializeJson(indexableRootObject) mustEqual """{"objectID":"6/47/1.2.2","collection":"/db/a1","documentID":47}""" } def e4 = { - val indexableRootObject = IndexableRootObject("/db/a1", 5, 46, None, None, Some("my-node-id"), Seq.empty) + val indexableRootObject = IndexableRootObject("/db/a1", 5, 46, None, None, None, Some("my-node-id"), Seq.empty) serializeJson(indexableRootObject) mustEqual """{"objectID":"my-node-id","collection":"/db/a1","documentID":46}""" } def e5 = { + val indexableRootObject = IndexableRootObject("/db/a1", 5, 46, None, Some("visibility"), None, None, Seq.empty) + serializeJson(indexableRootObject) mustEqual """{"objectID":"5/46/0","collection":"/db/a1","documentID":46,"visible_by":"visibility"}""" + } + + def e6 = { val attr1_kv = new AttributeKV(new QName("value"), "hello") val attributes = Seq(Left(IndexableAttribute("attr1", Seq(IndexableValue("1.1", Right(attr1_kv))), LiteralTypeConfig.String))) - val indexableRootObject = IndexableRootObject("/db/a1", 7, 48, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 7, 48, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"7/48/1","collection":"/db/a1","documentID":48,"attr1":"hello"}""" } - def e6 = { + def e7 = { val attr1_kv = new AttributeKV(new QName("value"), "99.9") val attributes = Seq(Left(IndexableAttribute("attr1", Seq(IndexableValue("1.1", Right(attr1_kv))), LiteralTypeConfig.Float))) - val indexableRootObject = IndexableRootObject("/db/a1", 2, 49, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 2, 49, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"2/49/1","collection":"/db/a1","documentID":49,"attr1":99.9}""" } - def e7 = { + def e8 = { val attr1_kv = new AttributeKV(new QName("value"), "1012") val attributes = Seq(Left(IndexableAttribute("attr1", Seq(IndexableValue("1.1", Right(attr1_kv))), LiteralTypeConfig.Integer))) - val indexableRootObject = IndexableRootObject("/db/a1", 9, 50, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 9, 50, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"9/50/1","collection":"/db/a1","documentID":50,"attr1":1012}""" } - def e8 = { + def e9 = { val attr1_kv = new AttributeKV(new QName("value"), "true") val attributes = Seq(Left(IndexableAttribute("attr1", Seq(IndexableValue("1.1", Right(attr1_kv))), LiteralTypeConfig.Boolean))) - val indexableRootObject = IndexableRootObject("/db/a1", 3, 51, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 3, 51, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"3/51/1","collection":"/db/a1","documentID":51,"attr1":true}""" } - def e9 = { + def e10 = { val attr1_kv = new AttributeKV(new QName("x"), "99.9") val attr2_kv = new AttributeKV(new QName("y"), "11.4") val attributes = Seq(Left(IndexableAttribute("attr1", Seq(IndexableValue("1.1", Right(attr1_kv))), LiteralTypeConfig.Float)), Left(IndexableAttribute("attr2", Seq(IndexableValue("1.2", Right(attr2_kv))), LiteralTypeConfig.Float))) - val indexableRootObject = IndexableRootObject("/db/a1", 3, 52, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 3, 52, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"3/52/1","collection":"/db/a1","documentID":52,"attr1":99.9,"attr2":11.4}""" } - def e10 = { + def e11 = { val attr1_1_kv = new AttributeKV(new QName("x"), "99.9") val attr1_2_kv = new AttributeKV(new QName("x"), "202.2") val attr2_1_kv = new AttributeKV(new QName("y"), "11.4") @@ -120,62 +126,62 @@ class IndexableRootObjectJsonSerializerSpec extends Specification { def is = s2" Left(IndexableAttribute("xx", Seq(IndexableValue("1.1", Right(attr1_1_kv)), IndexableValue("2.1", Right(attr1_2_kv))), LiteralTypeConfig.Float)), Left(IndexableAttribute("yy", Seq(IndexableValue("1.2", Right(attr2_1_kv)), IndexableValue("2.2", Right(attr2_2_kv))), LiteralTypeConfig.Float)) ) - val indexableRootObject = IndexableRootObject("/db/a1", 7, 42, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 7, 42, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"7/42/1","collection":"/db/a1","documentID":42,"xx":[99.9,202.2],"yy":[11.4,10.2]}""" } - def e11 = { + def e12 = { val elem1_kv = new ElementKV(new QName("w"), "hello") val attributes = Seq(Left(IndexableAttribute("elem1", Seq(IndexableValue("1.1", Left(elem1_kv))), LiteralTypeConfig.String))) - val indexableRootObject = IndexableRootObject("/db/a1", 6, 48, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 6, 48, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"6/48/1","collection":"/db/a1","documentID":48,"elem1":"hello"}""" } - def e12 = { + def e13 = { val elem1_kv = new ElementKV(new QName("x"), "99.9") val attributes = Seq(Left(IndexableAttribute("elem1", Seq(IndexableValue("1.1", Left(elem1_kv))), LiteralTypeConfig.Float))) - val indexableRootObject = IndexableRootObject("/db/a1", 7, 48, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 7, 48, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"7/48/1","collection":"/db/a1","documentID":48,"elem1":99.9}""" } - def e13 = { + def e14 = { val elem1_kv = new ElementKV(new QName("y"), "1012") val attributes = Seq(Left(IndexableAttribute("elem1", Seq(IndexableValue("1.1", Left(elem1_kv))), LiteralTypeConfig.Integer))) - val indexableRootObject = IndexableRootObject("/db/a1", 2, 48, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 2, 48, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"2/48/1","collection":"/db/a1","documentID":48,"elem1":1012}""" } - def e14 = { + def e15 = { val elem1_kv = new ElementKV(new QName("z"), "true") val attributes = Seq(Left(IndexableAttribute("elem1", Seq(IndexableValue("1.1", Left(elem1_kv))), LiteralTypeConfig.Boolean))) - val indexableRootObject = IndexableRootObject("/db/a1", 1, 48, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 1, 48, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"1/48/1","collection":"/db/a1","documentID":48,"elem1":true}""" } - def e15 = { + def e16 = { val elem1_kv = new ElementKV(new QName("x"), "99.9") val elem2_kv = new ElementKV(new QName("y"), "11.3") val attributes = Seq(Left(IndexableAttribute("elem1", Seq(IndexableValue("1.1", Left(elem1_kv))), LiteralTypeConfig.Float)), Left(IndexableAttribute("elem2", Seq(IndexableValue("1.2", Left(elem2_kv))), LiteralTypeConfig.Float))) - val indexableRootObject = IndexableRootObject("/db/a1", 7, 48, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 7, 48, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"7/48/1","collection":"/db/a1","documentID":48,"elem1":99.9,"elem2":11.3}""" } - def e16 = { + def e17 = { val elem1_kv = new ElementKV(new QName("x"), "hello world") val attributes = Seq(Left(IndexableAttribute("elem1", Seq(IndexableValue("1.1", Left(elem1_kv))), LiteralTypeConfig.String))) - val indexableRootObject = IndexableRootObject("/db/a1", 23, 48, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 23, 48, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"23/48/1","collection":"/db/a1","documentID":48,"elem1":"hello world"}""" } - def e17 = { + def e18 = { val elem1 = elem(dom("""hello world"""), "x") val elem1_kv = new ElementKV(new QName("x"), serializeElementForAttribute(elem1).valueOr(ts => throw ts.head)) val attributes = Seq(Left(IndexableAttribute("elem1", Seq(IndexableValue("1.1", Left(elem1_kv))), LiteralTypeConfig.String))) - val indexableRootObject = IndexableRootObject("/db/a1", 23, 48, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 23, 48, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"23/48/1","collection":"/db/a1","documentID":48,"elem1":"hello world"}""" } - def e18 = { + def e19 = { val dom1 = dom("""123.4-17.45456.1215.67""") val pos = elems(dom1, "pos") val elem1_1_kv = new ElementKV(new QName("x"), serializeElementForAttribute(childElem(pos(0), "x")).valueOr(ts => throw ts.head)) @@ -186,11 +192,11 @@ class IndexableRootObjectJsonSerializerSpec extends Specification { def is = s2" Left(IndexableAttribute("xx", Seq(IndexableValue("1.1", Left(elem1_1_kv)), IndexableValue("2.1", Left(elem1_2_kv))), LiteralTypeConfig.Float)), Left(IndexableAttribute("yy", Seq(IndexableValue("1.2", Left(elem2_1_kv)), IndexableValue("2.2", Left(elem2_2_kv))), LiteralTypeConfig.Float)) ) - val indexableRootObject = IndexableRootObject("/db/a1", 7, 42, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 7, 42, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"7/42/1","collection":"/db/a1","documentID":42,"xx":[123.4,456.12],"yy":[-17.45,15.67]}""" } - def e19 = { + def e20 = { val dom1 = dom("""123.4-17.45456.1215.67""") val pos = elems(dom1, "pos") val elem1_1_kv = new ElementKV(new QName("x"), serializeElementForAttribute(childElem(pos(0), "x")).valueOr(ts => throw ts.head)) @@ -201,73 +207,73 @@ class IndexableRootObjectJsonSerializerSpec extends Specification { def is = s2" Left(IndexableAttribute("xx", Seq(IndexableValue("1.1", Left(elem1_1_kv)), IndexableValue("2.1", Left(elem1_2_kv))), LiteralTypeConfig.Float)), Left(IndexableAttribute("yy", Seq(IndexableValue("1.2", Left(elem2_1_kv)), IndexableValue("2.2", Left(elem2_2_kv))), LiteralTypeConfig.Float)) ) - val indexableRootObject = IndexableRootObject("/db/a1", 7, 42, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 7, 42, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"7/42/1","collection":"/db/a1","documentID":42,"xx":[123.4,456.12],"yy":[-17.45,15.67]}""" } - def e20 = { + def e21 = { val dom1 = dom("""hello""") val elem1 = firstElem(dom1, "x").get val elem1_kv = new ElementKV(new QName("x"), serializeElementForAttribute(elem1).valueOr(ts => throw ts.head)) val attributes = Seq( Left(IndexableAttribute("obj1", Seq(IndexableValue("1.1", Left(elem1_kv))), LiteralTypeConfig.String)) ) - val indexableRootObject = IndexableRootObject("/db/a1", 6, 53, None, Some("1"), None, attributes) + val indexableRootObject = IndexableRootObject("/db/a1", 6, 53, None, None, Some("1"), None, attributes) serializeJson(indexableRootObject) mustEqual """{"objectID":"6/53/1","collection":"/db/a1","documentID":53,"obj1":"hello"}""".stripMargin } - def e21 = { + def e22 = { val attr1_kv = new AttributeKV(new QName("value"), "hello") val objects = Seq(Right(IndexableObject("obj1", Seq(IndexableValue("1.1", Right(attr1_kv)))))) - val indexableRootObject = IndexableRootObject("/db/a1", 45, 48, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 45, 48, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"45/48/1","collection":"/db/a1","documentID":48,"obj1":"hello"}""" } - def e22 = { + def e23 = { val attr1_1_kv = new AttributeKV(new QName("value"), "hello") val attr1_2_kv = new AttributeKV(new QName("value"), "world") val objects = Seq(Right(IndexableObject("obj1", Seq( IndexableValue("1.1.1", Right(attr1_1_kv)), IndexableValue("1.2.1", Right(attr1_2_kv)) )))) - val indexableRootObject = IndexableRootObject("/db/a1", 46, 49, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 46, 49, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"46/49/1","collection":"/db/a1","documentID":49,"obj1":["hello","world"]}""" } - def e23 = { + def e24 = { val elem1 = elem(dom("""helloworld"""), "w") val elem1_kv = new ElementKV(new QName("w"), serializeElementForObject("obj1", Map.empty, Map.empty)(elem1).valueOr(ts => throw ts.head)) val objects = Seq(Right(IndexableObject("obj1", Seq(IndexableValue("1.1", Left(elem1_kv)))))) - val indexableRootObject = IndexableRootObject("/db/a1", 5, 48, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 5, 48, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"5/48/1","collection":"/db/a1","documentID":48,"obj1":{"nodeId":"1.1","x":"hello","y":"world"}}""" } - def e24 = { + def e25 = { val elem1 = elem(dom("""helloworldagain"""), "w") val elem1_kv = new ElementKV(new QName("w"), serializeElementForObject("obj1", Map.empty, Map.empty)(elem1).valueOr(ts => throw ts.head)) val objects = Seq(Right(IndexableObject("obj1", Seq(IndexableValue("1.1", Left(elem1_kv)))))) - val indexableRootObject = IndexableRootObject("/db/a1", 2, 49, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 2, 49, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"2/49/1","collection":"/db/a1","documentID":49,"obj1":{"nodeId":"1.1","x":"hello","y":{"z":"world","zz":"again"}}}""" } - def e25 = { + def e26 = { val elem1 = elem(dom("""helloworldagain"""), "w") val elem1_kv = new ElementKV(new QName("w"), serializeElementForObject("obj1", Map.empty, Map.empty)(elem1).valueOr(ts => throw ts.head)) val objects = Seq(Right(IndexableObject("obj1", Seq(IndexableValue("1.1", Left(elem1_kv)))))) - val indexableRootObject = IndexableRootObject("/db/a1", 3, 50, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 3, 50, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"3/50/1","collection":"/db/a1","documentID":50,"obj1":{"nodeId":"1.1","x":"hello","y":["world","again"]}}""" } - def e26 = { + def e27 = { val elem1 = elem(dom("""helloworldagain"""), "w") val elem1_kv = new ElementKV(new QName("w"), serializeElementForObject("obj1", Map.empty, Map.empty)(elem1).valueOr(ts => throw ts.head)) val objects = Seq(Right(IndexableObject("obj1", Seq(IndexableValue("1.1", Left(elem1_kv)))))) - val indexableRootObject = IndexableRootObject("/db/a1", 6, 51, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 6, 51, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"6/51/1","collection":"/db/a1","documentID":51,"obj1":{"nodeId":"1.1","x":"hello","y":{"yy":["world","again"]}}}""" } - def e27 = { + def e28 = { val dom1 = dom("""helloworldagaingoodbyeuntilnext time""") val ww = elems(dom1, "w") val elem1_kv = new ElementKV(new QName("w"), serializeElementForObject("obj1", Map.empty, Map.empty)(ww(0)).valueOr(ts => throw ts.head)) @@ -276,51 +282,51 @@ class IndexableRootObjectJsonSerializerSpec extends Specification { def is = s2" IndexableValue("1.1", Left(elem1_kv)), IndexableValue("1.2", Left(elem2_kv)) )))) - val indexableRootObject = IndexableRootObject("/db/a1", 6, 52, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 6, 52, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"6/52/1","collection":"/db/a1","documentID":52,"obj1":[{"nodeId":"1.1","x":"hello","y":{"yy":["world","again"]}},{"nodeId":"1.2","x":"goodbye","y":{"yy":["until","next time"]}}]}""" } - def e28 = { + def e29 = { val dom1 = dom("""hello""") val elem1 = firstElem(dom1, "x").get val elem1_kv = new ElementKV(new QName("x"), serializeElementForObject("obj1", Map.empty, Map.empty)(elem1).valueOr(ts => throw ts.head)) val objects = Seq(Right(IndexableObject("obj1", Seq( IndexableValue("1.1", Left(elem1_kv)) )))) - val indexableRootObject = IndexableRootObject("/db/a1", 6, 53, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 6, 53, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"6/53/1","collection":"/db/a1","documentID":53,"obj1":{"nodeId":"1.1","#text":"hello"}}""".stripMargin } - def e29 = { + def e30 = { val dom1 = dom("""""") val elem1 = firstElem(dom1, "x").get val elem1_kv = new ElementKV(new QName("x"), serializeElementForObject("obj1", Map.empty, Map.empty)(elem1).valueOr(ts => throw ts.head)) val objects = Seq(Right(IndexableObject("obj1", Seq( IndexableValue("1.1", Left(elem1_kv)) )))) - val indexableRootObject = IndexableRootObject("/db/a1", 6, 53, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 6, 53, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"6/53/1","collection":"/db/a1","documentID":53,"obj1":{"nodeId":"1.1","type":"something"}}""".stripMargin } - def e30 = { + def e31 = { val dom1 = dom("""hello""") val elem = firstElem(dom1, "x").get val elem1_kv = new ElementKV(new QName("x"), serializeElementForObject("obj1", Map.empty, Map.empty)(elem).valueOr(ts => throw ts.head)) val objects = Seq(Right(IndexableObject("obj1", Seq( IndexableValue("1.1", Left(elem1_kv)) )))) - val indexableRootObject = IndexableRootObject("/db/a1", 6, 53, None, Some("1"), None, objects) + val indexableRootObject = IndexableRootObject("/db/a1", 6, 53, None, None, Some("1"), None, objects) serializeJson(indexableRootObject) mustEqual """{"objectID":"6/53/1","collection":"/db/a1","documentID":53,"obj1":{"nodeId":"1.1","type":"something","#text":"hello"}}""".stripMargin } private def serializeJson(indexableRootObject: IndexableRootObject): String = { Using(new StringWriter()) { writer => - val mapper = new ObjectMapper - mapper.writeValue(writer, indexableRootObject) - writer.toString + val mapper = new ObjectMapper + mapper.writeValue(writer, indexableRootObject) + writer.toString }.get } }