Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,29 @@ buildInfoPackage := "org.monarchinitiative.dosdp.cli"

val zioVersion = "2.1.26"
val zioLoggingVersion = "2.5.3"
val catsVersion = "2.13.0"
val circeVersion = "0.14.15"
val circeYamlVersion = "0.16.1"

libraryDependencies ++= {
Seq(
"dev.zio" %% "zio" % zioVersion,
"com.github.alexarchambault" %% "case-app" % "2.0.6",
"net.sourceforge.owlapi" % "owlapi-distribution" % "4.5.29",
"org.phenoscape" %% "scowl" % "1.4.1",
"org.phenoscape" %% "owlet" % "1.9" exclude("org.slf4j", "slf4j-log4j12"),
"org.semanticweb.elk" % "elk-owlapi" % "0.4.3" exclude("org.slf4j", "slf4j-log4j12"),
"io.github.liveontologies" % "elk-owlapi" % "0.6.0"
exclude("net.sourceforge.owlapi", "owlapi-apibinding")
exclude("net.sourceforge.owlapi", "owlapi-api")
exclude("net.sourceforge.owlapi", "owlapi-impl"),
"net.sourceforge.owlapi" % "org.semanticweb.hermit" % "1.4.3.456",
"net.sourceforge.owlapi" % "jfact" % "4.0.4",
"org.geneontology" %% "owl-diff" % "1.3.0",
"io.circe" %% "circe-yaml" % "0.14.2",
"io.circe" %% "circe-core" % "0.14.15",
"io.circe" %% "circe-generic" % "0.14.15",
"io.circe" %% "circe-parser" % "0.14.15",
"org.obolibrary.robot" % "robot-core" % "1.8.4"
exclude("ch.qos.logback", "logback-classic")
exclude("org.slf4j", "slf4j-log4j12")
exclude("org.geneontology", "whelk_2.12")
exclude("org.geneontology", "whelk-owlapi_2.12")
exclude("org.geneontology", "owl-diff_2.12"),
"com.github.pathikrit" %% "better-files" % "3.9.2",
"org.apache.jena" % "apache-jena-libs" % "4.10.0" exclude("org.slf4j", "slf4j-log4j12"),
"org.typelevel" %% "cats-core" % catsVersion,
"io.circe" %% "circe-yaml" % circeYamlVersion,
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"org.apache.jena" % "jena-arq" % "4.10.0",
"org.apache.jena" % "jena-core" % "4.10.0",
"com.github.tototoshi" %% "scala-csv" % "2.0.0",
"commons-codec" % "commons-codec" % "1.17.2",
"dev.zio" %% "zio-logging" % zioLoggingVersion,
"dev.zio" %% "zio-logging-slf4j2-bridge" % zioLoggingVersion,
"dev.zio" %% "zio-test" % zioVersion % Test,
Expand Down
121 changes: 121 additions & 0 deletions src/main/scala/org/monarchinitiative/dosdp/CatalogXmlIRIMapper.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.monarchinitiative.dosdp

import org.semanticweb.owlapi.model.{IRI, OWLOntologyIRIMapper}
import org.xml.sax.Attributes
import org.xml.sax.helpers.DefaultHandler

import java.io.{File, FileInputStream, InputStream}
import java.net.URL
import java.util.HashMap
import javax.xml.parsers.SAXParserFactory
import scala.util.control.NonFatal

/**
* OWL ontology IRI mapper backed by OASIS-style catalog XML `uri` entries.
*
* Adapted from ROBOT's `org.obolibrary.robot.CatalogXmlIRIMapper` and
* `CatalogElementHandler` implementations (BSD-3-Clause). Keeping this local
* avoids depending on all of `robot-core` for one small mapper.
*/
final class CatalogXmlIRIMapper private (mappings: java.util.Map[IRI, IRI]) extends OWLOntologyIRIMapper {

def this(catalogFile: String) =
this(CatalogXmlIRIMapper.parseCatalogFile(new File(catalogFile).getAbsoluteFile))

def this(catalogFile: File) =
this(CatalogXmlIRIMapper.parseCatalogFile(catalogFile.getAbsoluteFile))

def this(catalogFile: File, parentFolder: File) =
this(CatalogXmlIRIMapper.parseCatalogXml(new FileInputStream(catalogFile), Option(parentFolder)))

def this(catalogIRI: IRI) =
this(CatalogXmlIRIMapper.parseCatalogXml(catalogIRI.toURI.toURL))

def this(catalogURL: URL) =
this(CatalogXmlIRIMapper.parseCatalogXml(catalogURL))

def this(catalogURL: URL, parentFolder: File) =
this(CatalogXmlIRIMapper.parseCatalogXml(catalogURL.openStream(), Option(parentFolder)))

override def getDocumentIRI(ontologyIRI: IRI): IRI =
mappings.get(ontologyIRI)

}

private object CatalogXmlIRIMapper {

private def parseCatalogFile(catalogFile: File): java.util.Map[IRI, IRI] =
parseCatalogXml(new FileInputStream(catalogFile), Option(catalogFile.getParentFile))

private def parseCatalogXml(catalogURL: URL): java.util.Map[IRI, IRI] =
if (catalogURL.getProtocol == "file") {
val catalogFile = new File(catalogURL.toURI)
parseCatalogXml(new FileInputStream(catalogFile), Option(catalogFile.getParentFile))
} else parseCatalogXml(catalogURL.openStream(), None)

private def parseCatalogXml(inputStream: InputStream, parentFolder: Option[File]): java.util.Map[IRI, IRI] = {
val mappings = new HashMap[IRI, IRI]()
try {
val factory = SAXParserFactory.newInstance()
factory.setValidating(false)
factory.setNamespaceAware(true)
disableExternalXml(factory)
val saxParser = factory.newSAXParser()
saxParser.parse(inputStream, new CatalogElementHandler(parentFolder, mappings))
mappings
Comment thread
balhoff marked this conversation as resolved.
} finally inputStream.close()
}

private def disableExternalXml(factory: SAXParserFactory): Unit = {
setFeatureIfSupported(factory, "http://xml.org/sax/features/external-general-entities", false)
setFeatureIfSupported(factory, "http://xml.org/sax/features/external-parameter-entities", false)
setFeatureIfSupported(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
try factory.setXIncludeAware(false)
catch {
case NonFatal(_) => ()
}
}

private def setFeatureIfSupported(factory: SAXParserFactory, feature: String, value: Boolean): Unit =
try factory.setFeature(feature, value)
catch {
case NonFatal(_) => ()
}

private final class CatalogElementHandler(parentFolder: Option[File], mappings: java.util.Map[IRI, IRI]) extends DefaultHandler {

override def startElement(uri: String, localName: String, qName: String, attributes: Attributes): Unit = {
if (elementName(localName, qName) != "uri") return

val fromString = attributeValue(attributes, "name")
val toString = attributeValue(attributes, "uri")
if ((fromString == null) || (toString == null)) return

mappedIRI(toString).foreach { toIRI =>
mappings.put(IRI.create(fromString), toIRI)
}
}

private def elementName(localName: String, qName: String): String =
if (localName != null && localName.nonEmpty) localName
else Option(qName).fold("")(_.split(':').last)

private def attributeValue(attributes: Attributes, name: String): String =
Option(attributes.getValue("", name)).orElse(Option(attributes.getValue(name))).orNull

private def mappedIRI(value: String): Option[IRI] =
parentFolder.filter(_ => !value.contains(":")) match {
case Some(parent) =>
try {
val file = new File(value)
val resolved = if (file.isAbsolute) file else new File(parent, value)
Some(IRI.create(resolved.getCanonicalFile))
} catch {
case NonFatal(_) => None
}
case None => Some(IRI.create(value))
}

}

}
13 changes: 10 additions & 3 deletions src/main/scala/org/monarchinitiative/dosdp/DOSDP.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package org.monarchinitiative.dosdp
import io.circe._
import io.circe.generic.auto._
import io.circe.syntax._
import org.apache.commons.codec.digest.DigestUtils
import org.phenoscape.scowl._
import org.semanticweb.owlapi.model.{IRI, OWLAnnotationProperty}

import java.nio.charset.StandardCharsets
import java.security.MessageDigest

/**
* Basic data model for DOSDP schema, for serializing to/from JSON.
*/
Expand Down Expand Up @@ -77,10 +79,16 @@ object DOSDP {
case MultiValue(values) => values.toSeq.sorted.mkString("|")
}
}.mkString("&")
val hash = DigestUtils.sha1Hex(text)
val hash = sha1Hex(text)
IRI.create(s"$pattern#$hash")
}

private def sha1Hex(text: String): String =
MessageDigest.getInstance("SHA-1")
.digest(text.getBytes(StandardCharsets.UTF_8))
.map(byte => f"${byte & 0xff}%02x")
.mkString

}

trait PrintfText {
Expand Down Expand Up @@ -366,4 +374,3 @@ final case class Join(sep: String)
* will be used to generate additional annotations.
*/
final case class Permutation(`var`: String, annotationProperties: List[String])

Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.monarchinitiative.dosdp

import org.semanticweb.owlapi.apibinding.OWLManager
import org.semanticweb.owlapi.io.OWLObjectRenderer
import org.semanticweb.owlapi.manchestersyntax.renderer.ManchesterOWLSyntaxObjectRenderer
import org.semanticweb.owlapi.model.{IRI, OWLDataVisitor, OWLLiteral, OWLObject}
import org.semanticweb.owlapi.util.{ShortFormProvider, SimpleShortFormProvider}
import org.semanticweb.owlapi.vocab.XSDVocabulary

import java.io.{StringWriter, Writer}

/**
* Manchester syntax renderer that applies short forms to bare IRIs.
*
* Adapted from owl-diff's `ManchesterSyntaxOWLObjectRenderer`. The
* literal escaping is implemented locally so this
* project does not need owl-diff's transitive `commons-text` dependency.
*/
class ManchesterSyntaxOWLObjectRenderer extends OWLObjectRenderer {

import ManchesterSyntaxOWLObjectRenderer._

private object WriterDelegate extends Writer {

private var delegate = new StringWriter()

def reset(): Unit = delegate = new StringWriter()

override def toString: String = delegate.getBuffer.toString

override def close(): Unit = delegate.close()

override def flush(): Unit = delegate.flush()

override def write(cbuf: Array[Char], off: Int, len: Int): Unit = delegate.write(cbuf, off, len)

}

private var renderer: ManchesterOWLSyntaxObjectRenderer = new BetterIRIRenderer(WriterDelegate, new SimpleShortFormProvider())

override def render(obj: OWLObject): String = synchronized {
WriterDelegate.reset()
obj.accept(renderer)
WriterDelegate.toString
}

override def setShortFormProvider(shortFormProvider: ShortFormProvider): Unit = synchronized {
renderer = new BetterIRIRenderer(WriterDelegate, shortFormProvider)
}

}

object ManchesterSyntaxOWLObjectRenderer {

private val factory = OWLManager.getOWLDataFactory

private def escapeHtmlText(text: String): String =
text.flatMap {
case '&' => "&"
case '<' => "&lt;"
case '>' => "&gt;"
case '"' => "&quot;"
case char => char.toString
}

class BetterIRIRenderer(writer: Writer, entityShortFormProvider: ShortFormProvider) extends ManchesterOWLSyntaxObjectRenderer(writer, entityShortFormProvider) {

override def visit(iri: IRI): Unit = visit(factory.getOWLClass(iri))

override def visit(node: OWLLiteral): Unit = {

// xsd:decimal is the default datatype for literal forms like "33.3"
// with no specified datatype
if (XSDVocabulary.DECIMAL.getIRI.equals(node.getDatatype.getIRI)) {
write(node.getLiteral)
} else if (node.getDatatype.isFloat) {
write(node.getLiteral)
write("f")
} else if (node.getDatatype.isInteger) {
write(node.getLiteral)
} else if (node.getDatatype.isBoolean) {
write(node.getLiteral)
} else {
pushTab(getIndent)
write("\"")
write(escapeHtmlText(node.getLiteral))
write("\"")
if (node.hasLang()) {
write("@")
write(node.getLang)
} else if (!node.isRDFPlainLiteral) {
write("^^")
node.getDatatype.accept(this: OWLDataVisitor)
}
popTab()
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.monarchinitiative.dosdp

import org.semanticweb.owlapi.model.OWLEntity
import org.semanticweb.owlapi.util.ShortFormProvider

/**
* Short-form provider that renders OWL entities as Markdown links.
*
* Adapted from owl-diff's `MarkdownLinkShortFormProvider`. Keeping
* this local avoids depending on all of `owl-diff` for
* one small docs-rendering helper.
*/
class MarkdownLinkShortFormProvider(labelProvider: ShortFormProvider) extends ShortFormProvider {

override def getShortForm(entity: OWLEntity): String = {
val label = labelProvider.getShortForm(entity)
val iri = entity.getIRI.toString
s"[$label]($iri)"
}

override def dispose(): Unit = ()

}
Loading
Loading