Skip to content

Commit

Permalink
3.2.1: preliminary POST /ont/term (issue #35)
Browse files Browse the repository at this point in the history
  • Loading branch information
carueda committed Feb 14, 2017
1 parent 645d030 commit d9c3a58
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 14 deletions.
21 changes: 21 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
## change log ##

* 2017-02-14: 3.2.1
- \#35: "API operation to insert new terms in vocabulary"
- preliminary `POST /ont/term`.
Adds a single new term. Parameters:

- vocUri: URI of ontology
- version: ontology version (optional)
- classUri: class of specific vocabulary (optional). If not given,
and there's only one class in the ontology, then uses it.
Otherwise, error.
- one of termName or termUri to indicate the URI for the new term (required)
- attributes: array of array of strings for the property values (required)

Example request for a vocabulary with only a class having one property:

```
$ http -a username:password post http://localhost:8081/api/v0/ont/term \
vocUri=http://localhost:9001/src/app/~carueda/vocab1 \
termName=baz attributes:='[ ["some prop value", "other value for same prop] ]'
```

* 2017-02-08: 3.2.0
- some logging to debug email sending based on cfg.notifications.recipientsFilename

Expand Down
2 changes: 1 addition & 1 deletion project/build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import scoverage.ScoverageKeys._
object build extends Build {
val Organization = "org.mmisw"
val Name = "orr-ont"
val Version = "3.2.0"
val Version = "3.2.1"

val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.0"
Expand Down
40 changes: 39 additions & 1 deletion src/main/scala/org/mmisw/orr/ont/app/OntController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import javax.servlet.annotation.MultipartConfig
import com.mongodb.casbah.Imports._
import com.novus.salat._
import com.novus.salat.global._
import com.typesafe.scalalogging.{StrictLogging => Logging}
import com.typesafe.scalalogging.{StrictLogging Logging}
import org.json4s.JsonAST.{JArray, JValue}
import org.json4s._
import org.json4s.native.JsonMethods._
import org.mmisw.orr.ont._
Expand Down Expand Up @@ -158,6 +159,43 @@ class OntController(implicit setup: Setup,
}
}

/*
* Add terms to existing vocabulary
* TODO preliminary ...
*/
post("/term") {
val vocUri = requireParam("vocUri")
val versionOpt = getParam("version")
val classUriOpt = getParam("classUri")
val termNameOpt = getParam("termName")
val termUriOpt = getParam("termUri")
val map = body()
val attributesParam = getArray(map, "attributes")

if (termNameOpt.isDefined == termUriOpt.isDefined)
error(400, s"One of termName and termUri must be given")

val attributes = attributesParam map { a
if (!a.isInstanceOf[JArray]) error(400, "'attributes' must be an array or arrays")
a.asInstanceOf[JArray]
}
val (ont, ontVersion, version) = resolveOntologyVersion(vocUri, versionOpt)

verifyOwnerName(ont.ownerName)

Try(ontService.addTerm(ont, ontVersion, version,
classUriOpt, termNameOpt, termUriOpt, attributes)
) match {
case Success(result) =>
loadOntologyInTripleStore(vocUri, reload = true)
Created(result)

case Failure(exc: NoSuch) error(404, exc.details)
case Failure(exc: Invalid) error(409, exc.details)
case Failure(exc) error500(exc)
}
}

private def getVisibilityParam: Option[String] = getParam("visibility") match {
case Some(v) => OntVisibility.withName(v) orElse error(400, s"invalid visibility value: $v")
case None => None
Expand Down
10 changes: 8 additions & 2 deletions src/main/scala/org/mmisw/orr/ont/app/OrrOntStack.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ trait OrrOntStack extends ScalatraServlet with NativeJsonSupport with CorsSuppor

protected def bug(msg: String): Nothing = error500(s"$msg. Please notify this bug.")

protected def require(map: Params, paramName: String) = {
protected def require(map: Params, paramName: String): String = {
val value = map.getOrElse(paramName, missing(paramName)).trim
if (value.length > 0) value else error(400, s"'$paramName' param value missing")
}
Expand All @@ -52,7 +52,7 @@ trait OrrOntStack extends ScalatraServlet with NativeJsonSupport with CorsSuppor
if (json != JNothing) Some(json.extract[Map[String, JValue]]) else None
}

protected def require(map: Map[String, JValue], paramName: String) = {
protected def require(map: Map[String, JValue], paramName: String): String = {
val value = map.getOrElse(paramName, missing(paramName))
if (!value.isInstanceOf[JString]) error(400, s"'$paramName' param value is not a string")
val str = value.asInstanceOf[JString].values.trim
Expand All @@ -74,6 +74,12 @@ trait OrrOntStack extends ScalatraServlet with NativeJsonSupport with CorsSuppor
arr map (_.asInstanceOf[JString].values)
}

protected def getArray(map: Map[String, JValue], paramName: String): List[JValue] = {
val value = map.getOrElse(paramName, missing(paramName))
if (!value.isInstanceOf[JArray]) error(400, s"'$paramName' param value is not an array")
value.asInstanceOf[JArray].arr
}

protected def getSet(map: Map[String, JValue], paramName: String, canBeEmpty: Boolean = false): Set[String] =
getSeq(map, paramName, canBeEmpty).toSet

Expand Down
9 changes: 9 additions & 0 deletions src/main/scala/org/mmisw/orr/ont/results.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.mmisw.orr.ont

import org.joda.time.DateTime
import org.json4s.JValue


case class UserResult(
Expand Down Expand Up @@ -67,6 +68,14 @@ case class OntologySummaryResult(
visibility: Option[String] = None
)

case class TermRegistrationResult(
vocUri: String,
classUri: String,
termName: Option[String],
termUri: Option[String],
attributes: List[JValue]
)

case class OntologySubjectsResult(
uri: String,
version: String,
Expand Down
96 changes: 93 additions & 3 deletions src/main/scala/org/mmisw/orr/ont/service/OntService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import java.io.File
import java.net.{URI, URISyntaxException}

import com.mongodb.casbah.Imports._
import com.typesafe.scalalogging.{StrictLogging => Logging}
import com.typesafe.scalalogging.{StrictLogging Logging}
import org.joda.time.DateTime
import org.mmisw.orr.ont.db.{Ontology, OntologyVersion}
import org.mmisw.orr.ont.swld.{PossibleOntologyInfo, ontFileLoader, ontUtil}
import org.mmisw.orr.ont.swld._
import org.mmisw.orr.ont._

import scala.util.matching.Regex
import scala.util.{Failure, Success, Try}


Expand All @@ -29,7 +30,7 @@ case class OrgOntOwner(orgName: String) extends OntOwner
case class UserOntOwner(userName: String) extends OntOwner

object OntOwner {
val ownerNamePattern = "(~?)(.*)".r
val ownerNamePattern: Regex = "(~?)(.*)".r

def apply(ownerName: String): OntOwner = {
ownerName match {
Expand Down Expand Up @@ -492,6 +493,95 @@ class OntService(implicit setup: Setup) extends BaseService(setup) with Logging
*/
def deleteAll() = ontDAO.remove(MongoDBObject())

def addTerm(ont: Ontology,
ontVersion: OntologyVersion,
version: String,
classUriOpt: Option[String],
termNameOpt: Option[String],
termUriOpt: Option[String],
attributes: List[org.json4s.JArray]
): TermRegistrationResult = {

if (termNameOpt.isDefined == termUriOpt.isDefined)
throw new IllegalArgumentException(s"only one of termNameOpt and termUriOpt must be defined")

val vocUri = ont.uri
val (file, actualFormat) = getOntologyFile(vocUri, version, ontVersion.format)

if (actualFormat != "v2r") throw NotAnOrrVocabulary(vocUri, version)

logger.debug(s"addTerm: loading V2RModel from file=$file")
val oldV2r = v2r.loadV2RModel(file)

// get the affected specific vocabulary:
val vocab: Vocab = classUriOpt match {
case None
if (oldV2r.vocabs.size == 1) oldV2r.vocabs.head
else throw MissingClassUri(vocUri, version)

case Some(classUri)
oldV2r.vocabs.find(v v.`class`.getUri() == classUri).getOrElse(
throw NoSuchVocabClassUri(vocUri, version, classUri))
}

val vocNamespace = vocUri + "/"

val actualClassUri = vocab.`class`.getUri(Some(vocNamespace))

if (vocab.properties.length != attributes.length)
throw TermAttributesError(vocUri, version, actualClassUri, vocab.properties.length, attributes.length)

val newTerm = Term(
name = termNameOpt,
uri = termUriOpt,
attributes = attributes
)

val newTermUri = newTerm.getUri(Some(vocNamespace))

val existingTermUris = vocab.terms.view.map(_.getUri(Some(vocNamespace)))

if (logger.underlying.isDebugEnabled()) {
logger.debug(s"\n terms:")
existingTermUris foreach { uri logger.debug(s" uri = $uri") }
logger.debug(s"newTermUri = $newTermUri")
}

if (existingTermUris.contains(newTermUri)) {
throw if (termNameOpt.isDefined)
TermNameAlreadyRegistered(vocUri, version, actualClassUri, termNameOpt.get)
else
TermUriAlreadyRegistered(vocUri, version, actualClassUri, termUriOpt.get)
}

val adjustedNewTerm = termUriOpt match {
case Some(givenUri)
val (ns, localName) = ontUtil.getNamespaceAndLocalName(givenUri)
if (ns == vocNamespace)
newTerm.copy(uri = None, name = Some(localName))
else newTerm

case None newTerm
}

val newVocab = vocab.copy(terms = vocab.terms :+ adjustedNewTerm)

val updatedVocabs: List[Vocab] = oldV2r.vocabs.map { v
if (v.`class`.getUri() == actualClassUri) newVocab
else v
}
val newV2r = oldV2r.copy(vocabs = updatedVocabs)
v2r.saveV2RModel(newV2r, file)

TermRegistrationResult(
vocUri,
actualClassUri,
termName = termNameOpt,
termUri = termUriOpt,
attributes
)
}

///////////////////////////////////////////////////////////////////////////

/**
Expand Down
30 changes: 30 additions & 0 deletions src/main/scala/org/mmisw/orr/ont/service/errors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,36 @@ case class InvalidUri(uri: String, error: String)
case class OntologyAlreadyRegistered(uri: String)
extends Invalid("uri" -> uri, "error" -> "Ontology URI already registered")

case class NotAnOrrVocabulary(uri: String, version: String)
extends NoSuch("uri" -> uri, "version" -> version,
"error" -> "Not an ORR vocabulary")

case class MissingClassUri(uri: String, version: String)
extends Invalid("uri" -> uri, "version" -> version,
"error" -> "Class URI is required because vocabulary contains multiple classes")

case class NoSuchVocabClassUri(uri: String, version: String, classUri: String)
extends NoSuch("uri" -> uri, "version" -> version, "classUri" classUri,
"error" -> "No such class in the given vocabulary")

case class TermUriAlreadyRegistered(uri: String, version: String, classUri: String, termUri: String)
extends Invalid("uri" -> uri, "version" -> version, "classUri" -> classUri,
"termUri" -> termUri,
"error" -> "Term URI already registered")

case class TermNameAlreadyRegistered(uri: String, version: String, classUri: String, termName: String)
extends Invalid("uri" -> uri, "version" -> version, "classUri" -> classUri,
"termName" -> termName,
"error" -> "Term name already registered")

case class TermAttributesError(uri: String, version: String, classUri: String,
numProperties: Int,
numAttributes: Int)
extends Invalid("uri" -> uri, "version" -> version, "classUri" -> classUri,
"attributesExpected" numProperties.toString,
"attributesGiven" numAttributes.toString,
"error" -> "Mismatch in number of given attributes")

case class NotAMember(userName: String, orgName: String)
extends Invalid("userName" -> userName, "orgName" -> orgName, "error" -> "User is not a member of the organization")

Expand Down
11 changes: 10 additions & 1 deletion src/main/scala/org/mmisw/orr/ont/swld/ontUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ object ontUtil extends AnyRef with Logging {
val storedFormats = List("rdf", "owl", "n3", "owx", "jsonld", "v2r", "m2r")

// for the files actually stored
def storedFormat(format: String) = format.toLowerCase match {
def storedFormat(format: String): String = format.toLowerCase match {
case "owx" => "owx" // https://www.w3.org/TR/owl-xml-serialization/
case "v2r" => "v2r"
case "m2r" => "m2r"
Expand Down Expand Up @@ -483,4 +483,13 @@ object ontUtil extends AnyRef with Logging {
}
}

/**
* @return Splits input at the rightmost '/' or '#'.
* Eg: "foo/bar/baz" -> ("foo/bar/", "baz")
*/
def getNamespaceAndLocalName(uri: String): (String,String) = {
val i = math.max(uri.lastIndexOf('#'), uri.lastIndexOf('/'))
(uri.substring(0, i + 1), uri.substring(i + 1))
}

}
21 changes: 15 additions & 6 deletions src/main/scala/org/mmisw/orr/ont/swld/v2r.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ case class IdL(name: Option[String] = None,
label: Option[String] = None,
valueClassUri: Option[String] = None
) {
def getUri(namespaceOpt: Option[String] = None) =
def getUri(namespaceOpt: Option[String] = None): String =
uri.getOrElse(namespaceOpt.getOrElse("") + name.get)

def getLabel: String = label.getOrElse(name.getOrElse {
Expand All @@ -32,7 +32,7 @@ case class Term(name: Option[String] = None,
uri: Option[String] = None,
attributes: List[JValue]
) {
def getUri(namespaceOpt: Option[String] = None) =
def getUri(namespaceOpt: Option[String] = None): String =
uri.getOrElse(namespaceOpt.getOrElse("") + name.get)
}

Expand Down Expand Up @@ -97,7 +97,7 @@ case class V2RModel(uri: Option[String],
uriOpt
}

implicit val formats = Serialization.formats(NoTypeHints)
implicit val formats: Formats = Serialization.formats(NoTypeHints)

def toJson: String = write(this)

Expand Down Expand Up @@ -125,13 +125,22 @@ object v2r extends AnyRef with Logging {
def loadOntModel(file: File, uriOpt: Option[String] = None): OntModelLoadedResult = {
logger.debug(s"v2r.loadOntModel: loading file=$file uriOpt=$uriOpt")

implicit val formats = DefaultFormats
val json = parse(file)
val vr = json.extract[V2RModel]
val vr = loadV2RModel(file)

val altUriOpt = uriOpt orElse Some(file.getCanonicalFile.toURI.toString)
val model = getModel(vr, altUriOpt)
OntModelLoadedResult(file, "v2r", model)
}

def loadV2RModel(file: File): V2RModel = {
implicit val formats = DefaultFormats
val json = parse(file)
json.extract[V2RModel]
}

def saveV2RModel(vr: V2RModel, file: File): Unit = {
java.nio.file.Files.write(file.toPath,
vr.toPrettyJson.getBytes(java.nio.charset.StandardCharsets.UTF_8))
}

}

0 comments on commit d9c3a58

Please sign in to comment.