Skip to content

Commit

Permalink
Added support for UR (parsed as java.net.URI), OL and UC
Browse files Browse the repository at this point in the history
  • Loading branch information
KarlSjostrand committed Aug 14, 2018
1 parent 4a1f7d3 commit b081b87
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 12 deletions.
8 changes: 7 additions & 1 deletion src/main/scala/se/nimsa/dicom/data/DicomParsing.scala
Expand Up @@ -16,6 +16,7 @@

package se.nimsa.dicom.data

import java.net.URI
import java.time.format.{DateTimeFormatterBuilder, SignStyle}
import java.time.temporal.ChronoField._
import java.time.{LocalDate, LocalDateTime, ZoneOffset, ZonedDateTime}
Expand Down Expand Up @@ -219,7 +220,8 @@ trait DicomParsing {
def parseDT(value: ByteString, zoneOffset: ZoneOffset): Seq[ZonedDateTime] = split(value.utf8String).flatMap(parseDateTime(_, zoneOffset))
def parsePN(string: String): Seq[PatientName] = split(string).map(trimPadding(_, VR.PN.paddingByte)).flatMap(parsePatientName)
def parsePN(value: ByteString, characterSets: CharacterSets): Seq[PatientName] = parsePN(characterSets.decode(VR.PN, value))

def parseUR(string: String): Option[URI] = parseURI(string)
def parseUR(value: ByteString): Option[URI] = parseUR(trimPadding(value.utf8String, VR.UR.paddingByte))

// parsing of strings to more specific types

Expand Down Expand Up @@ -286,6 +288,10 @@ trait DicomParsing {
Option(PatientName(comps.head, comps(1), comps(2), comps(3), comps(4)))
}

def parseURI(s: String): Option[URI] = try Option(new URI(s)) catch {
case _: Throwable => None
}

}

object DicomParsing extends DicomParsing
7 changes: 7 additions & 0 deletions src/main/scala/se/nimsa/dicom/data/Elements.scala
@@ -1,5 +1,6 @@
package se.nimsa.dicom.data

import java.net.URI
import java.time.{LocalDate, ZoneOffset, ZonedDateTime}

import akka.util.ByteString
Expand Down Expand Up @@ -74,6 +75,7 @@ case class Elements(characterSets: CharacterSets, zoneOffset: ZoneOffset, data:
def getDateTime(tag: Int): Option[ZonedDateTime] = get(tag, v => v.value.toDateTime(v.vr, zoneOffset))
def getPatientNames(tag: Int): Seq[PatientName] = getAll(tag, v => v.value.toPatientNames(v.vr, characterSets))
def getPatientName(tag: Int): Option[PatientName] = get(tag, v => v.value.toPatientName(v.vr, characterSets))
def getURI(tag: Int): Option[URI] = get(tag, v => v.value.toURI(v.vr))

def getSequence(tag: Int): Option[Sequence] = apply(tag).flatMap {
case e: Sequence => Some(e)
Expand Down Expand Up @@ -253,6 +255,11 @@ case class Elements(characterSets: CharacterSets, zoneOffset: ZoneOffset, data:
def setPatientName(tag: Int, value: PatientName, bigEndian: Boolean = false, explicitVR: Boolean = true): Elements =
setPatientName(tag, Dictionary.vrOf(tag), value, bigEndian, explicitVR)

def setURI(tag: Int, vr: VR, value: URI, bigEndian: Boolean, explicitVR: Boolean): Elements =
setValue(tag, vr, Value.fromURI(vr, value), bigEndian, explicitVR)
def setURI(tag: Int, value: URI, bigEndian: Boolean = false, explicitVR: Boolean = true): Elements =
setURI(tag, Dictionary.vrOf(tag), value, bigEndian, explicitVR)

def remove(tag: Int): Elements = filter(_.tag != tag)
def filter(f: ElementSet => Boolean): Elements = copy(data = data.filter(f))

Expand Down
37 changes: 27 additions & 10 deletions src/main/scala/se/nimsa/dicom/data/Value.scala
@@ -1,5 +1,6 @@
package se.nimsa.dicom.data

import java.net.URI
import java.time.{LocalDate, ZoneOffset, ZonedDateTime}

import akka.util.ByteString
Expand Down Expand Up @@ -34,7 +35,8 @@ case class Value private[data](bytes: ByteString) {
case OW => Seq(split(bytes, 2).map(bytesToShort(_, bigEndian)).map(shortToHexString).mkString(" "))
case OF => Seq(parseFL(bytes, bigEndian).mkString(" "))
case OD => Seq(parseFD(bytes, bigEndian).mkString(" "))
case ST | LT | UT => Seq(trimPadding(characterSets.decode(vr, bytes), vr.paddingByte))
case ST | LT | UT | UR => Seq(trimPadding(characterSets.decode(vr, bytes), vr.paddingByte))
case UC => split(trimPadding(characterSets.decode(vr, bytes), vr.paddingByte))
case _ => split(characterSets.decode(vr, bytes)).map(trim)
}

Expand Down Expand Up @@ -148,6 +150,15 @@ case class Value private[data](bytes: ByteString) {
case _ => Seq.empty
}

/**
* @return this value as an option of a `URI`. If the value has no `URI` representation, an empty option is
* returned.
*/
def toURI(vr: VR = VR.UR): Option[URI] = vr match {
case UR => parseUR(bytes)
case _ => None
}

/**
* @return the first string representation of this value, if any
*/
Expand Down Expand Up @@ -228,7 +239,7 @@ case class Value private[data](bytes: ByteString) {
object Value {

private def combine(vr: VR, values: Seq[ByteString]): ByteString = vr match {
case AT | FL | FD | SL | SS | UL | US | OB | OW | OF | OD => values.reduce(_ ++ _)
case AT | FL | FD | SL | SS | UL | US | OB | OW | OL | OF | OD => values.reduce(_ ++ _)
case _ => if (values.isEmpty) ByteString.empty else values.tail.foldLeft(values.head)((bytes, b) => bytes ++ ByteString('\\') ++ b)
}

Expand All @@ -253,7 +264,7 @@ object Value {
case SS => shortToBytes(java.lang.Short.parseShort(value), bigEndian)
case UL => truncate(4, longToBytes(java.lang.Long.parseUnsignedLong(value), bigEndian), bigEndian)
case US => truncate(2, intToBytes(java.lang.Integer.parseUnsignedInt(value), bigEndian), bigEndian)
case OB | OW | OF | OD => throw new IllegalArgumentException("Cannot create binary array from string")
case OB | OW | OL | OF | OD => throw new IllegalArgumentException("Cannot create binary array from string")
case _ => ByteString(value)
}
def fromString(vr: VR, value: String, bigEndian: Boolean = false): Value = apply(vr, stringBytes(vr, value, bigEndian))
Expand All @@ -266,7 +277,7 @@ object Value {
case SS => shortToBytes(value, bigEndian)
case UL => intToBytes(java.lang.Short.toUnsignedInt(value), bigEndian)
case US => truncate(2, intToBytes(java.lang.Short.toUnsignedInt(value), bigEndian), bigEndian)
case OB | OW | OF | OD | AT => throw new IllegalArgumentException(s"Cannot create value of VR $vr from short")
case OB | OW | OL | OF | OD | AT => throw new IllegalArgumentException(s"Cannot create value of VR $vr from short")
case _ => ByteString(value.toString)
}
def fromShort(vr: VR, value: Short, bigEndian: Boolean = false): Value = apply(vr, shortBytes(vr, value, bigEndian))
Expand All @@ -280,7 +291,7 @@ object Value {
case SS => shortToBytes(value.toShort, bigEndian)
case UL => truncate(4, longToBytes(Integer.toUnsignedLong(value), bigEndian), bigEndian)
case US => truncate(6, longToBytes(Integer.toUnsignedLong(value), bigEndian), bigEndian)
case OB | OW | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from int")
case OB | OW | OL | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from int")
case _ => ByteString(value.toString)
}
def fromInt(vr: VR, value: Int, bigEndian: Boolean = false): Value = apply(vr, intBytes(vr, value, bigEndian))
Expand All @@ -294,7 +305,7 @@ object Value {
case SS => shortToBytes(value.toShort, bigEndian)
case UL => truncate(4, longToBytes(value, bigEndian), bigEndian)
case US => truncate(6, longToBytes(value, bigEndian), bigEndian)
case OB | OW | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from long")
case OB | OW | OL | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from long")
case _ => ByteString(value.toString)
}
def fromLong(vr: VR, value: Long, bigEndian: Boolean = false): Value = apply(vr, longBytes(vr, value, bigEndian))
Expand All @@ -308,7 +319,7 @@ object Value {
case SS => shortToBytes(value.toShort, bigEndian)
case UL => truncate(4, longToBytes(value.toLong, bigEndian), bigEndian)
case US => truncate(6, longToBytes(value.toLong, bigEndian), bigEndian)
case OB | OW | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from float")
case OB | OW | OL | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from float")
case _ => ByteString(value.toString)
}
def fromFloat(vr: VR, value: Float, bigEndian: Boolean = false): Value = apply(vr, floatBytes(vr, value, bigEndian))
Expand All @@ -322,21 +333,21 @@ object Value {
case SS => shortToBytes(value.toShort, bigEndian)
case UL => truncate(4, longToBytes(value.toLong, bigEndian), bigEndian)
case US => truncate(6, longToBytes(value.toLong, bigEndian), bigEndian)
case OB | OW | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from double")
case OB | OW | OL | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from double")
case _ => ByteString(value.toString)
}
def fromDouble(vr: VR, value: Double, bigEndian: Boolean = false): Value = apply(vr, doubleBytes(vr, value, bigEndian))
def fromDoubles(vr: VR, values: Seq[Double], bigEndian: Boolean = false): Value = apply(vr, combine(vr, values.map(doubleBytes(vr, _, bigEndian))))

private def dateBytes(vr: VR, value: LocalDate): ByteString = vr match {
case AT | FL | FD | SL | SS | UL | US | OB | OW | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from date")
case AT | FL | FD | SL | SS | UL | US | OB | OW | OL | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from date")
case _ => ByteString(formatDate(value))
}
def fromDate(vr: VR, value: LocalDate): Value = apply(vr, dateBytes(vr, value))
def fromDates(vr: VR, values: Seq[LocalDate]): Value = apply(vr, combine(vr, values.map(dateBytes(vr, _))))

private def dateTimeBytes(vr: VR, value: ZonedDateTime): ByteString = vr match {
case AT | FL | FD | SL | SS | UL | US | OB | OW | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from date-time")
case AT | FL | FD | SL | SS | UL | US | OB | OW | OL | OF | OD => throw new IllegalArgumentException(s"Cannot create value of VR $vr from date-time")
case _ => ByteString(formatDateTime(value))
}
def fromDateTime(vr: VR, value: ZonedDateTime): Value = apply(vr, dateTimeBytes(vr, value))
Expand All @@ -348,4 +359,10 @@ object Value {
}
def fromPatientName(vr: VR, value: PatientName): Value = apply(vr, patientNameBytes(vr, value))
def fromPatientNames(vr: VR, values: Seq[PatientName]): Value = apply(vr, combine(vr, values.map(patientNameBytes(vr, _))))

private def uriBytes(vr: VR, value: URI): ByteString = vr match {
case UR => ByteString(value.toString)
case _ => throw new IllegalArgumentException(s"Cannot create value of VR $vr from URI")
}
def fromURI(vr: VR, value: URI): Value = apply(vr, uriBytes(vr, value))
}
14 changes: 14 additions & 0 deletions src/test/scala/se/nimsa/dicom/data/DicomParsingTest.scala
Expand Up @@ -308,4 +308,18 @@ class DicomParsingTest extends FlatSpecLike with Matchers {
pns.head.givenName.alphabetic shouldBe "John"
pns(1).givenName.alphabetic shouldBe "Jane"
}

"A URI" should "be parsed from strings" in {
val uri = parseUR("https://example.com:8080/path")
uri shouldBe defined
uri.get.getHost shouldBe "example.com"
uri.get.getPort shouldBe 8080
uri.get.getPath shouldBe "/path"
uri.get.getScheme shouldBe "https"
}

it should "not accept malformed URIs" in {
val uri = parseUR("not < a > uri")
uri shouldBe empty
}
}
7 changes: 6 additions & 1 deletion src/test/scala/se/nimsa/dicom/data/ElementsTest.scala
@@ -1,5 +1,6 @@
package se.nimsa.dicom.data

import java.net.URI
import java.time.{LocalDate, ZoneOffset}

import akka.actor.ActorSystem
Expand Down Expand Up @@ -323,6 +324,11 @@ class ElementsTest extends TestKit(ActorSystem("ElementsSpec")) with FlatSpecLik
.getPatientNames(Tag.PatientName) shouldBe Seq(patientNames.head)
}

it should "set URI" in {
val uri = new URI("https://example.com:8080/path?q1=45")
elements.setURI(Tag.StorageURL, uri).getURI(Tag.StorageURL) shouldBe Some(uri)
}

it should "update character sets" in {
val updatedCs1 = elements.setCharacterSets(CharacterSets(ByteString("\\ISO 2022 IR 127"))).characterSets
updatedCs1.charsetNames shouldBe Seq("", "ISO 2022 IR 127")
Expand Down Expand Up @@ -393,7 +399,6 @@ class ElementsTest extends TestKit(ActorSystem("ElementsSpec")) with FlatSpecLik

it should "create file meta information" in {
val fmiList = Elements.fileMetaInformationElements("iuid", "cuid", "ts")
println(fmiList.flatMap(_.toParts).map(_.toString).mkString("\n"))
val fmi = Elements.empty().set(fmiList)
fmi.getInt(Tag.FileMetaInformationGroupLength).get shouldBe
(12 + 5 * 8 + 2 + 4 + 4 + 2 +
Expand Down
15 changes: 15 additions & 0 deletions src/test/scala/se/nimsa/dicom/data/ValueTest.scala
Expand Up @@ -356,6 +356,21 @@ class ValueTest extends FlatSpec with Matchers {
ComponentGroup("", "", "")))
}

"Parsing a URI" should "work for valid URI strings" in {
val uri = Value(ByteString("https://example.com:8080/path?q1=45&q2=46")).toURI()
uri shouldBe defined
uri.get.getScheme shouldBe "https"
uri.get.getHost shouldBe "example.com"
uri.get.getPort shouldBe 8080
uri.get.getPath shouldBe "/path"
uri.get.getQuery shouldBe "q1=45&q2=46"
}

it should "not parse invalid URIs" in {
val uri = Value(ByteString("not < a > uri")).toURI()
uri shouldBe empty
}

"An element" should "update its value bytes" in {
val updated = Value.empty ++ ByteString("ABC")
updated.bytes shouldBe ByteString("ABC") // not compliant
Expand Down

0 comments on commit b081b87

Please sign in to comment.