Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(api-v2): Switch from JSONLD-Java to Titanium #1715

Merged
merged 4 commits into from Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/05-internals/design/api-v2/json-ld.md
Expand Up @@ -22,7 +22,7 @@ License along with Knora. If not, see <http://www.gnu.org/licenses/>.
## JsonLDUtil

Knora provides a utility object called `JsonLDUtil`, which wraps the
[JSON-LD Java API](https://github.com/jsonld-java/jsonld-java), and parses JSON-LD text to a
[titanium-json-ld Java library](https://github.com/filip26/titanium-json-ld), and parses JSON-LD text to a
Knora data structure called `JsonLDDocument`. These classes provide commonly needed
functionality for extracting and validating data from JSON-LD documents, as well
as for constructing new documents.
Expand Down
11 changes: 6 additions & 5 deletions third_party/dependencies.bzl
Expand Up @@ -97,8 +97,9 @@ def dependencies():
"de.heikoseeberger:akka-http-circe_2.12:1.21.0",
"com.fasterxml.jackson.module:jackson-module-scala_2.12:2.9.4",

"com.github.jsonld-java:jsonld-java:0.12.0",
"com.apicatalog:titanium-json-ld:0.8.3",
"com.apicatalog:titanium-json-ld:0.8.5",
"javax.json:javax.json-api:1.1.4",
"org.glassfish:jakarta.json:1.1.6",

# swagger (api documentation)
"com.github.swagger-akka-http:swagger-akka-http_2.12:0.14.0",
Expand Down Expand Up @@ -182,8 +183,8 @@ BASE_TEST_DEPENDENCIES_WITH_JSON = BASE_TEST_DEPENDENCIES + [
]

BASE_TEST_DEPENDENCIES_WITH_JSON_LD = BASE_TEST_DEPENDENCIES + [
"@maven//:com_fasterxml_jackson_core_jackson_core",
"@maven//:com_github_jsonld_java_jsonld_java",
"@maven//:io_spray_spray_json_2_12",
"@maven//:com_apicatalog_titanium_json_ld",
"@maven//:javax_json_javax_json_api",
"@maven//:org_glassfish_jakarta_json"
]

5 changes: 2 additions & 3 deletions webapi/BUILD.bazel
Expand Up @@ -60,11 +60,11 @@ scala_library(
#
"@maven//:ch_megard_akka_http_cors_2_12",
"@maven//:com_fasterxml_jackson_core_jackson_annotations",
"@maven//:com_fasterxml_jackson_core_jackson_core",
"@maven//:com_fasterxml_jackson_core_jackson_databind",
"@maven//:com_github_andrewoma_dexx_collection",
"@maven//:com_github_jsonld_java_jsonld_java",
"@maven//:com_apicatalog_titanium_json_ld",
"@maven//:javax_json_javax_json_api",
"@maven//:org_glassfish_jakarta_json",
"@maven//:com_github_swagger_akka_http_swagger_akka_http_2_12",
"@maven//:com_google_gwt_gwt_servlet",
"@maven//:com_ibm_icu_icu4j",
Expand Down Expand Up @@ -168,7 +168,6 @@ scala_library(
# Test Libs
"@maven//:com_typesafe_akka_akka_testkit_2_12",
"@maven//:com_typesafe_akka_akka_http_testkit_2_12",
"@maven//:com_github_jsonld_java_jsonld_java",
"@maven//:com_jsuereth_scala_arm_2_12",
"@maven//:com_typesafe_akka_akka_actor_2_12",
"@maven//:com_typesafe_akka_akka_http_2_12",
Expand Down
5 changes: 3 additions & 2 deletions webapi/src/main/scala/org/knora/webapi/messages/BUILD.bazel
Expand Up @@ -13,15 +13,16 @@ scala_library(
"//webapi/src/main/scala/org/knora/webapi/settings",
"//webapi/src/main/scala/org/knora/webapi/util",
"//webapi/src/main/scala/org/knora/webapi/util/cache",
"@maven//:com_fasterxml_jackson_core_jackson_core",
"@maven//:com_github_jsonld_java_jsonld_java",
"@maven//:com_google_gwt_gwt_servlet",
"@maven//:com_ibm_icu_icu4j",
"@maven//:com_sksamuel_diff_diff",
"@maven//:com_typesafe_akka_akka_actor_2_12",
"@maven//:com_typesafe_akka_akka_http_2_12",
"@maven//:com_typesafe_akka_akka_http_core_2_12",
"@maven//:com_typesafe_akka_akka_http_spray_json_2_12",
"@maven//:com_apicatalog_titanium_json_ld",
"@maven//:javax_json_javax_json_api",
"@maven//:org_glassfish_jakarta_json",
"@maven//:com_typesafe_akka_akka_stream_2_12",
"@maven//:com_typesafe_play_twirl_api_2_12",
"@maven//:com_typesafe_scala_logging_scala_logging_2_12",
Expand Down
164 changes: 111 additions & 53 deletions webapi/src/main/scala/org/knora/webapi/messages/util/JsonLDUtil.scala
Expand Up @@ -19,17 +19,29 @@

package org.knora.webapi.messages.util

import java.io.{StringReader, StringWriter}
import java.util
import java.util.UUID

import com.github.jsonldjava.core.{JsonLdOptions, JsonLdProcessor}
import com.github.jsonldjava.utils.JsonUtils
import com.apicatalog.jsonld._
import com.apicatalog.jsonld.document._
import javax.json._
import javax.json.stream.JsonGenerator
import org.knora.webapi._
import org.knora.webapi.exceptions.{BadRequestException, InconsistentTriplestoreDataException}
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import org.knora.webapi.messages.{OntologyConstants, SmartIri, StringFormatter}
import org.knora.webapi.util.JavaUtil

/*

The classes in this file provide a Scala API for formatting and parsing JSON-LD. The implementation
uses the javax.json API and a Java implementation of the JSON-LD API <https://www.w3.org/TR/json-ld11-api/>
(currently <https://github.com/filip26/titanium-json-ld>). This shields the rest of Knora from the details
of the JSON-LD implementation. These classes also provide Knora-specific JSON-LD functionality to facilitate
reading data from Knora API requests and constructing Knora API responses.

*/

/**
* Constant strings used in JSON-LD.
Expand All @@ -53,10 +65,9 @@ object JsonLDConstants {
*/
sealed trait JsonLDValue extends Ordered[JsonLDValue] {
/**
* Converts this JSON-LD value to a Scala object that can be passed to [[org.knora.webapi.util.JavaUtil.deepScalaToJava]],
* whose return value can then be passed to the JSON-LD Java library.
* Converts this JSON-LD value to a `javax.json` [[JsonValue]].
*/
def toAny: Any
def toJavaxJsonValue: JsonValue
}

/**
Expand All @@ -65,7 +76,9 @@ sealed trait JsonLDValue extends Ordered[JsonLDValue] {
* @param value the underlying string.
*/
case class JsonLDString(value: String) extends JsonLDValue {
override def toAny: Any = value
override def toJavaxJsonValue: JsonString = {
Json.createValue(value)
}

override def compare(that: JsonLDValue): Int = {
that match {
Expand All @@ -81,7 +94,9 @@ case class JsonLDString(value: String) extends JsonLDValue {
* @param value the underlying integer.
*/
case class JsonLDInt(value: Int) extends JsonLDValue {
override def toAny: Any = value
override def toJavaxJsonValue: JsonNumber = {
Json.createValue(value)
}

override def compare(that: JsonLDValue): Int = {
that match {
Expand All @@ -97,7 +112,13 @@ case class JsonLDInt(value: Int) extends JsonLDValue {
* @param value the underlying boolean value.
*/
case class JsonLDBoolean(value: Boolean) extends JsonLDValue {
override def toAny: Any = value
override def toJavaxJsonValue: JsonValue = {
if (value) {
JsonValue.TRUE
} else {
JsonValue.FALSE
}
}

override def compare(that: JsonLDValue): Int = {
that match {
Expand All @@ -113,8 +134,14 @@ case class JsonLDBoolean(value: Boolean) extends JsonLDValue {
* @param value a map of keys to JSON-LD values.
*/
case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue {
override def toAny: Map[String, Any] = value.map {
case (k, v) => (k, v.toAny)
override def toJavaxJsonValue: JsonObject = {
val builder = Json.createObjectBuilder()

for ((entryKey, entryValue) <- value) {
builder.add(entryKey, entryValue.toJavaxJsonValue)
}

builder.build
}

/**
Expand Down Expand Up @@ -530,7 +557,15 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue {
case class JsonLDArray(value: Seq[JsonLDValue]) extends JsonLDValue {
implicit private val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

override def toAny: Seq[Any] = value.map(_.toAny)
override def toJavaxJsonValue: JsonArray = {
val builder = Json.createArrayBuilder()

for (elem <- value) {
builder.add(elem.toJavaxJsonValue)
}

builder.build
}

/**
* Tries to interpret the elements of this array as JSON-LD objects containing `@language` and `@value`,
Expand Down Expand Up @@ -681,21 +716,39 @@ case class JsonLDDocument(body: JsonLDObject, context: JsonLDObject = JsonLDObje
def maybeUUID(key: String): Option[UUID] = body.maybeUUID(key: String)

/**
* Converts this JSON-LD object to its compacted Java representation.
* Converts this JSON-LD document to its compacted representation.
*/
private def makeCompactedJavaxJsonObject: JsonObject = {
val bodyAsTitaniumJsonDocument: JsonDocument = JsonDocument.of(body.toJavaxJsonValue)
val contextAsTitaniumJsonDocument: JsonDocument = JsonDocument.of(context.toJavaxJsonValue)
JsonLd.compact(bodyAsTitaniumJsonDocument, contextAsTitaniumJsonDocument).get
}

/**
* Formats this JSON-LD document as a string, using the specified [[JsonWriterFactory]].
*
* @param jsonWriterFactory a [[JsonWriterFactory]] configured with the desired options.
* @return the formatted document.
*/
private def makeCompactedObject: java.util.Map[IRI, AnyRef] = {
val contextAsJava = JavaUtil.deepScalaToJava(context.toAny)
val jsonAsJava = JavaUtil.deepScalaToJava(body.toAny)
JsonLdProcessor.compact(jsonAsJava, contextAsJava, new JsonLdOptions())
private def formatWithJsonWriterFactory(jsonWriterFactory: JsonWriterFactory): String = {
val compactedJavaxJsonObject: JsonObject = makeCompactedJavaxJsonObject
val stringWriter = new StringWriter()
val jsonWriter = jsonWriterFactory.createWriter(stringWriter)
jsonWriter.write(compactedJavaxJsonObject)
jsonWriter.close()
stringWriter.toString
}

/**
* Converts this [[JsonLDDocument]] to a pretty-printed JSON-LD string.
* Converts this JSON-LD document to a pretty-printed JSON-LD string.
*
* @return the formatted document.
*/
def toPrettyString: String = {
JsonUtils.toPrettyString(makeCompactedObject)
val config = new util.HashMap[String, Boolean]()
config.put(JsonGenerator.PRETTY_PRINTING, true)
val jsonWriterFactory: JsonWriterFactory = Json.createWriterFactory(config)
formatWithJsonWriterFactory(jsonWriterFactory)
}

/**
Expand All @@ -704,7 +757,9 @@ case class JsonLDDocument(body: JsonLDObject, context: JsonLDObject = JsonLDObje
* @return the formatted document.
*/
def toCompactString: String = {
JsonUtils.toString(makeCompactedObject)
val config = new util.HashMap[String, Boolean]()
val jsonWriterFactory: JsonWriterFactory = Json.createWriterFactory(config)
formatWithJsonWriterFactory(jsonWriterFactory)
}
}

Expand Down Expand Up @@ -845,56 +900,59 @@ object JsonLDUtil {
* @return a [[JsonLDDocument]].
*/
def parseJsonLD(jsonLDString: String): JsonLDDocument = {
val jsonObject: AnyRef = try {
JsonUtils.fromString(jsonLDString)
} catch {
case e: com.fasterxml.jackson.core.JsonParseException => throw BadRequestException(s"Couldn't parse JSON-LD: ${e.getMessage}")
}
// Parse the string into a javax.json.JsonStructure.
val stringReader = new StringReader(jsonLDString)
val jsonReader: JsonReader = Json.createReader(stringReader)
val jsonStructure: JsonStructure = jsonReader.read()

val context: java.util.HashMap[String, Any] = new java.util.HashMap[String, Any]()
val options: JsonLdOptions = new JsonLdOptions()
val compact: java.util.Map[IRI, AnyRef] = JsonLdProcessor.compact(jsonObject, context, options)
val scalaColl: Any = JavaUtil.deepJavaToScala(compact)
// Convert the JsonStructure to a Titanium JsonDocument.
val titaniumDocument: JsonDocument = JsonDocument.of(jsonStructure)

val scalaMap: Map[String, Any] = try {
scalaColl.asInstanceOf[Map[String, Any]]
} catch {
case _: java.lang.ClassCastException => throw BadRequestException(s"Expected JSON-LD object: $scalaColl")
}
// Use Titanium to compact the document with an empty context.
val emptyContext = JsonDocument.of(Json.createObjectBuilder().build())
val compactedJsonObject: JsonObject = JsonLd.compact(titaniumDocument, emptyContext).get

mapToJsonLDDocument(scalaMap)
// Convert the resulting javax.json.JsonObject to a JsonLDDocument.
javaxJsonObjectToJsonLDDocument(compactedJsonObject)
}

/**
* Converts a map into a [[JsonLDDocument]].
* Converts JSON object into a [[JsonLDDocument]].
*
* @param docContent a map representing a JSON-LD object.
* @param jsonObject a JSON object.
* @return
*/
private def mapToJsonLDDocument(docContent: Map[String, Any]): JsonLDDocument = {
def anyToJsonLDValue(anyVal: Any): JsonLDValue = {
anyVal match {
case string: String => JsonLDString(string)
case int: Int => JsonLDInt(int)
case bool: Boolean => JsonLDBoolean(bool)

case obj: Map[_, _] =>
val content: Map[String, JsonLDValue] = obj.map {
case (key: String, value: Any) => key -> anyToJsonLDValue(value)
case (otherKey, otherValue) => throw BadRequestException(s"Unexpected types in JSON-LD object: $otherKey, $otherValue")
}
private def javaxJsonObjectToJsonLDDocument(jsonObject: JsonObject): JsonLDDocument = {
import collection.JavaConverters._

def jsonValueToJsonLDValue(jsonValue: JsonValue): JsonLDValue = {
jsonValue match {
case jsonString: JsonString => JsonLDString(jsonString.getString)
case jsonNumber: JsonNumber => JsonLDInt(jsonNumber.intValue)
case JsonValue.TRUE => JsonLDBoolean(true)
case JsonValue.FALSE => JsonLDBoolean(false)

case jsonObject: JsonObject =>
val content: Map[IRI, JsonLDValue] = jsonObject.keySet.asScala.toSet.map {
key: IRI => key -> jsonValueToJsonLDValue(jsonObject.get(key))
}.toMap

JsonLDObject(content)

case array: Seq[Any] => JsonLDArray(array.map(value => anyToJsonLDValue(value)))
case jsonArray: JsonArray =>
val content: Seq[JsonLDValue] = jsonArray.asScala.map {
elem => jsonValueToJsonLDValue(elem)
}

JsonLDArray(content)

case _ => throw BadRequestException(s"Unexpected type in JSON-LD input: $anyVal")
case _ => throw BadRequestException(s"Unexpected type in JSON-LD input: $jsonValue")
}
}

anyToJsonLDValue(docContent) match {
jsonValueToJsonLDValue(jsonObject) match {
case obj: JsonLDObject => JsonLDDocument(body = obj, context = JsonLDObject(Map.empty[IRI, JsonLDValue]))
case _ => throw BadRequestException(s"Expected JSON-LD object: $docContent")
case _ => throw BadRequestException(s"Expected JSON-LD object: $jsonObject")
}
}
}
Expand Up @@ -199,7 +199,7 @@ case class CreateMappingResponseV2(mappingIri: IRI, label: String, projectIri: S
val body = JsonLDObject(Map(
JsonLDConstants.ID -> JsonLDString(mappingIri),
JsonLDConstants.TYPE -> JsonLDString(OntologyConstants.KnoraBase.XMLToStandoffMapping.toSmartIri.toOntologySchema(targetSchema).toString),
"rdfs:label" -> JsonLDString(label),
OntologyConstants.Rdfs.Label -> JsonLDString(label),
OntologyConstants.KnoraApiV2Complex.AttachedToProject.toSmartIri.toOntologySchema(targetSchema).toString -> JsonLDUtil.iriToJsonLDObject(projectIri.toString)
))

Expand Down
37 changes: 0 additions & 37 deletions webapi/src/main/scala/org/knora/webapi/util/JavaUtil.scala
Expand Up @@ -46,43 +46,6 @@ object JavaUtil {
def biFunction[A, B, C](f: (A, B) => C): BiFunction[A, B, C] =
(a: A, b: B) => f(a, b)

/**
* Recursively converts a Java collection into a Scala collection.
*
* Usage: `val scalaObj = deepScalaToJava(javaObj).asInstanceOf[Map[String, Any]]`
*
* @param javaObj the Java collection to be converted.
* @return an equivalent Scala collection.
*/
def deepJavaToScala(javaObj: Any): Any = {
import collection.JavaConverters._
javaObj match {
case x: java.util.HashMap[_, _] => x.asScala.toMap.mapValues(deepJavaToScala)
case x: java.util.ArrayList[_] => x.asScala.toList.map(deepJavaToScala)
case _ => javaObj
}
}

/**
* Recursively converts a Scala collection into a Java collection.
*
* @param scalaCollection the Scala collection to be converted.
* @return an equivalent Java collection.
*/
def deepScalaToJava(scalaCollection: Any): Any = {
import collection.JavaConverters._

scalaCollection match {
case x: List[_] => x.map(deepScalaToJava).asJava
case x: Seq[_] => x.map(deepScalaToJava).asJava
case x: Array[_] => x.map(deepScalaToJava)
case x: collection.mutable.Map[_, _] => x.mapValues(deepScalaToJava).asJava
case x: collection.immutable.Map[_, _] => x.mapValues(deepScalaToJava).asJava
case x: collection.Map[_, _] => x.mapValues(deepScalaToJava).asJava
case _ => scalaCollection
}
}

/**
* Helps turn matches for optional regular expression groups, which can be null, into Scala Option objects. See
* [[https://stackoverflow.com/a/18794646]].
Expand Down