org.openehealth.ipf.modules.hl7.kotlin.Hl7Extensions.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ipf-modules-hl7-kotlin Show documentation
Show all versions of ipf-modules-hl7-kotlin Show documentation
HL7 extensions and HL7 DSL support
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openehealth.ipf.modules.hl7.kotlin
import ca.uhn.hl7v2.HL7Exception
import ca.uhn.hl7v2.HapiContext
import ca.uhn.hl7v2.Severity
import ca.uhn.hl7v2.Version
import ca.uhn.hl7v2.model.*
import ca.uhn.hl7v2.parser.EncodingCharacters
import ca.uhn.hl7v2.parser.FixFieldDataType
import ca.uhn.hl7v2.util.DeepCopy
import ca.uhn.hl7v2.validation.builder.EncodingRuleBuilder
import ca.uhn.hl7v2.validation.builder.MessageRuleBuilder
import ca.uhn.hl7v2.validation.builder.PrimitiveRuleBuilder
import ca.uhn.hl7v2.validation.impl.SimpleValidationExceptionHandler
import org.openehealth.ipf.commons.core.config.ContextFacade
import org.openehealth.ipf.commons.core.modules.api.ValidationException
import org.openehealth.ipf.commons.map.MappingService
import org.openehealth.ipf.modules.hl7.kotlin.validation.model.LambdaEncodingRule
import org.openehealth.ipf.modules.hl7.kotlin.validation.model.LambdaMessageRule
import org.openehealth.ipf.modules.hl7.kotlin.validation.model.LambdaPrimitiveTypeRule
/**
* @author Christian Ohr
* @since 3.5
*/
// Extension functions/properties for Visitable ---------------------------------------------------
/**
* Visitable#isEmpty is not originally exposed as property because it throws an exception
*/
val Visitable.empty: Boolean get() = isEmpty
fun Visitable.accept(visitor: T): T = MessageVisitors.visit(this, visitor)
// Extension functions/properties for Type --------------------------------------------------------
/**
* getValue()/value/nullValue Properties for Type
*/
val Type.value: String? get() = stringValue(this)
val Type.value2: String? get() = if (nullValue) "" else value
val Type.nullValue: Boolean get() = stringValue(this) == "\"\""
/**
* getValue or a default if null
*/
fun Type.getValueOr(v: String?): String? = if (value != null) value else v
fun Type.valueOr(v: String?): String? = getValueOr(v)
/**
* Sets a type to the specified [source] value. Depending on the source and target type, only a subset
* of the value is used
*
* @param source value for the type
*/
fun Type.from(source: Any?): Unit =
when (this) {
is Primitive -> value = stringValue(source)
is Composite -> if (source is Composite) DeepCopy.copy(source, this) else get(1).from(stringValue(source))
is Variable -> data.from(source)
is RepeatableField -> elementAt(0).from(source)
else -> unknownType()
}
/**
* Returns the component [idx] of the given type. If applied on [Primitive], the idx must be 1. If applied
* on a repeatable field, the component [idx] of its first repetition is returned.
* @param idx component index, starting with 1
* @return component [idx]
* @throws Hl7DslException if a component with index > 1 shall be retrieved from a [Primitive]
*/
operator fun Type.get(idx: Int): Type =
when (this) {
is Primitive -> if (idx == 1) this else throw Hl7DslException("Index out of bounds for primitive")
is Variable -> data[idx]
is Composite -> getComponent(componentIndex(idx))
is RepeatableField -> elementAt(0)[idx]
else -> unknownType()
}
/**
* Sets the component [idx] to value [v]. If applied on [Primitive], the idx must be 1. If applied
* on a repeatable field, the component [idx] of its first repetition is set.
*
* @param idx component index, starting with 1
* @param v any value, i.e. another HL7 type or object implementing toString
*/
operator fun Type.set(idx: Int, v: Any?) = get(idx).from(v)
/**
* Access repetition [rep] of a type. If the type is not repeatable, [rep] must be 0.
*
* @param rep repetition, starting with 0
* @return type
*/
operator fun Type.invoke(rep: Int): Type =
(this as? RepeatableField)?.elementAt(rep) ?: if (rep > 0) notRepeatable() else this
/**
* Access repetitions of a type as array. If the type is not repeatable, an array with one
* type is returned
*
* @return type array
*/
operator fun Type.invoke(): Array =
(this as? RepeatableField)?.elements ?: arrayOf(this)
// Destructuring support for up to 10 components
operator fun Type.component1(): Type = get(1)
operator fun Type.component2(): Type = get(2)
operator fun Type.component3(): Type = get(3)
operator fun Type.component4(): Type = get(4)
operator fun Type.component5(): Type = get(5)
operator fun Type.component6(): Type = get(6)
operator fun Type.component7(): Type = get(7)
operator fun Type.component8(): Type = get(8)
operator fun Type.component9(): Type = get(9)
operator fun Type.component10(): Type = get(10)
fun Type.map(key: Any?): Any? = mappingService().get(key, encode())
fun Type.map(key: Any?, defaultValue: Any?): Any? = mappingService().get(key, encode(), defaultValue)
fun Type.mapReverse(key: Any?): Any? = mappingService().getKey(key, encode())
fun Type.mapReverse(key: Any?, defaultValue: Any?): Any? = mappingService().getKey(key, encode(), defaultValue)
// Extension functions/properties for ExtraComponent ------------------------------------------
/**
* getValue()/value/nullValue Properties for ExtraComponents
*/
val ExtraComponents.value: String? get() = stringValue(this)
val ExtraComponents.value2: String? get() = if (nullValue) "" else value
val ExtraComponents.nullValue: Boolean get() = stringValue(this) == "\"\""
operator fun ExtraComponents.get(idx: Int): Variable = getComponent(idx)
// Extension functions/properties for Structure -----------------------------------------------
/**
* Returns the field [idx] of the given [Segment]. If applied
* on a repeatable segment, the field [idx] of its first repetition is returned.
*
* @param idx field index, starting with 1
* @return field [idx]
* @throws Hl7DslException if applied on a [Group]
*/
operator fun Structure.get(idx: Int): Type =
when (this) {
is Group -> useStructureName()
is Segment -> {
val field = getField(idx)
if (getMaxCardinality(idx) == 1) {
if (field.isEmpty()) getField(idx, 0) else field[0]
} else {
RepeatableField(getField(idx), this, idx)
}
}
is RepeatableStructure -> elementAt(0)[idx]
else -> unknownType()
}
/**
* Returns repetition [rep] of the field [idx] of the of the [Segment].
* If applied on a non-repeatable field, the repetition [rep] must be 0.
*
* @param idx field index, starting with 1
* @param rep repetition index, starting with 0
* @return field [idx]
* @throws Hl7DslException if applied on a [Group] or a non-repeatable field with [rep] > 0
*/
operator fun Structure.get(idx: Int, rep: Int): Type = get(idx)(rep)
/**
* Returns the structure with name [name] within the given [Group]. If applied
* on a repeatable group, the structure [name] of its first repetition is returned.
*
* @param name structure name
* @return structure
* @throws Hl7DslException if applied on a [Segment]
*/
operator fun Structure.get(name: String): Structure =
when (this) {
is Segment -> useFieldNumber()
is Group -> if (isRepeating(name)) RepeatableStructure(getAll(name), this, name) else get(name)
is RepeatableStructure -> elementAt(0)[name]
else -> unknownType()
}
/**
* Returns repetition [rep] of the structure with name [name] within the given [Group].
* If applied on a non-repeatable structure, the repetition [rep] must be 0.
*
* @param name structure name
* @param rep repetition index, starting with 0
* @return name structure name
* @throws Hl7DslException if applied on a [Segment] or a non-repeatable structure with [rep] > 0
*/
operator fun Structure.get(name: String, rep: Int): Structure = get(name)(rep)
/**
* Returns repetition [rep] of a structure. If applied on a non-repeatable structure, the repetition [rep] must be 0.
* @param rep repetition index, starting with 0
* @return structure repetition
* @throws Hl7DslException if applied on a non-repeatable structure with [rep] > 0
*/
operator fun Structure.invoke(rep: Int): Structure =
(this as? RepeatableStructure)?.elementAt(rep) ?: if (rep > 0) notRepeatable() else this
/**
* Returns all repetitions of a structure as array. If the structure is not repeatable, an array with one
* structure is returned
*
* @return array of structures
*/
operator fun Structure.invoke(): Array =
(this as? RepeatableStructure)?.elements ?: arrayOf(this)
/**
* Sets a segment to the specified [source] segment.
*
* @param source value for the type
* @throws Hl7DslException if applied on a [Group]
*/
fun Structure.from(source: Segment): Unit =
when (this) {
is Segment -> DeepCopy.copy(source, this)
is RepeatableStructure -> elementAt(0).from(source)
else -> throw Hl7DslException("Unsupported operation, cannot set a group")
}
/**
* Sets the field [idx] to value [v]. If applied
* on a repeatable field, the component [idx] of its first repetition is set.
*
* @param idx component index, starting with 1
* @param v any value, i.e. another HL7 type or object implementing toString
*/
operator fun Structure.set(idx: Int, v: Any?) = get(idx).from(v)
/**
* Sets the repetition [rep] of field [idx] to value [v].
*
* @param idx component index, starting with 1
* @param rep repetition starting with 0
* @param v any value, i.e. another HL7 type or object implementing toString
*/
operator fun Structure.set(idx: Int, rep: Int, v: Any?) = get(idx, rep).from(v)
/**
* Sets the segment with name [name] from the provided [segment]. If applied
* on a repeatable structure, the segment [name] of its first repetition is set.
*
* @param name structure name
* @param segment segment of a matching class
*/
operator fun Structure.set(name: String, segment: Segment) = get(name).from(segment)
/**
* Sets the repetition [rep] of the segment with name [name] from the provided [segment].
*
* @param name structure name
* @param rep repetition
* @param segment segment of a matching class
*/
operator fun Structure.set(name: String, rep: Int, segment: Segment) = get(name, rep).from(segment)
// Counting
/**
* Returns the number of repetitions of the field with index [idx] in this [Segment].
*
* @param idx field index, starting with 1
* @return number of repetitions
* @throws Hl7DslException if applied on a [Group]
*/
fun Structure.count(idx: Int): Int =
when (this) {
is Group -> useStructureName()
is Segment -> getField(idx).size
is RepeatableStructure -> elementAt(0).count(idx)
else -> unknownType()
}
/**
* Returns the number of repetitions of the structure with name [name] in this [Group].
*
* @param name structure name
* @return number of repetitions
* @throws Hl7DslException if applied on a [Segment]
*/
fun Structure.count(name: String): Int =
when (this) {
is Group -> getAll(name).size
is Segment -> useFieldNumber()
is RepeatableStructure -> elementAt(0).count(name)
else -> unknownType()
}
/**
* Adds and returns a repetition to the field with index [idx] in this [Segment].
*
* @param idx field index, starting with 1
* @return new field
* @throws Hl7DslException if applied on a [Group]
*/
fun Structure.nrp(idx: Int): Type =
when (this) {
is Group -> useStructureName()
is Segment -> getField(idx, count(idx))
is RepeatableStructure -> elementAt(0).nrp(idx)
else -> unknownType()
}
/**
* Adds and returns a repetition to the structure with name [name] in this [Group].
*
* @param name structure name
* @return new field
* @throws Hl7DslException if applied on a [Segment]
*/
fun Structure.nrp(name: String): Structure =
when (this) {
is Group -> get(name, count(name))
is Segment -> useFieldNumber()
is RepeatableStructure -> elementAt(0).nrp(name)
else -> unknownType()
}
/**
* Returns the path of the structure within its message
*/
val Structure.path: String? get() = message.findIndexOf { it === this }
/**
* Sets the [Varies] field OBX-5 to represent the provided [type]
*/
fun Structure.setObx5Type(type: String, desiredRepetitionsCount: Int = 1) {
val className = this::class.qualifiedName
if (className != null && !className.endsWith(".OBX")) throw Hl7DslException("only OBX segments can be served by this method")
for (i in 0 until desiredRepetitionsCount - count(5)) {
nrp(5)
}
this[2] = type
FixFieldDataType.fixOBX5(this as Segment, message.parser.factory, message.parser.parserConfiguration)
}
// Extension functions/properties for Group -----------------------------------------------
fun Group.eachWithIndex(c: (Structure, String) -> Unit): Group = eachWithIndex(this, c)
fun Group.findIndexValues(c: (Structure) -> Boolean): List = findIndexValues(this, c)
fun Group.findIndexOf(c: (Structure) -> Boolean): String? = findIndexOf(this, c)
fun Group.findLastIndexOf(c: (Structure) -> Boolean): String? = findLastIndexOf(this, c)
/**
* Opens a HL7 [Group]/[Message] to all methods and extension functions applicable for kotlin Iterables
*
* @return iterable group
*/
fun Group.asIterable(): IterableGroup = IterableGroup(this)
/**
* Allows to use a forach loop on a HL7 [Group]/[Message]
*
* @return iterator
*/
operator fun Group.iterator(): Iterator = asIterable().iterator()
// Extension functions/properties for Message -----------------------------------------------
/**
* Copies a message
*
* @return message copy
*/
fun T.copy(): T = parser.parse(encode()) as T
fun Message.validate(context: HapiContext?) {
val ctx = context ?: parser.hapiContext
// We could also write an exception handler on top of SimpleValidationExceptionHandler that
// encapsulates the behavior below, but that may restrict custom validation...
val handler = SimpleValidationExceptionHandler(ctx)
handler.minimumSeverityToCollect = Severity.ERROR
try {
if (ctx.getMessageValidator().validate(this, handler)) {
throw ValidationException("Message validation failed", handler.exceptions)
}
} catch (e: HL7Exception) {
throw ValidationException("Message validation failed", e)
}
}
val Message.eventType: String get() = get("MSH")[9][1].value!!
val Message.triggerEvent: String get() = get("MSH")[9][2].value!!
val Message.messageStructure: String
get() = messageStructure(
eventType, triggerEvent, version, parser.hapiContext.modelClassFactory)
val Message?.encodingCharacters: EncodingCharacters
get() =
if (this != null)
EncodingCharacters.getInstance(this) else
EncodingCharacters('|', "^~\\&")
/**
* @return a response message with the basic MSH fields already populated
*/
fun Message.respond(responseEvent: String, responseTrigger: String? = null): T {
// make message of correct version
val trigger = responseTrigger ?: this.triggerEvent
val out = newMessage(parser.hapiContext, responseEvent, trigger, version)
// populate outbound MSH using data from inbound message ...
val mshIn = get("MSH")
val mshOut = out["MSH"]
mshOut[1] = mshIn[1]
mshOut[2] = mshIn[2]
mshOut[3] = mshIn[5]
mshOut[4] = mshIn[6]
mshOut[5] = mshIn[3]
mshOut[6] = mshIn[4]
mshOut[11] = mshIn[11]
if ("MSA" in out.names) {
out["MSA"][2] = mshIn[10]
}
return out as T
}
/**
* @return true if the version of this [Message] is at least [otherVersion]
*/
fun Message.atLeastVersion(otherVersion: String): Boolean =
Version.versionOf(this.version) >= Version.versionOf(otherVersion)
// Extension functions/properties for Rule Builders -----------------------------------------------
fun MessageRuleBuilder.checkIf(check: (Message) -> Array): MessageRuleBuilder = test(LambdaMessageRule(check))
fun EncodingRuleBuilder.checkIf(check: (String) -> Array): EncodingRuleBuilder = test(LambdaEncodingRule(check))
fun PrimitiveRuleBuilder.checkIf(check: (String?) -> Array): PrimitiveRuleBuilder = test(LambdaPrimitiveTypeRule(check))
// Exceptions due to invalid access
private fun Structure.unknownType(): T = throw Hl7DslException("Unknown structure type ${this::class.simpleName}")
private fun Type.unknownType(): T = throw Hl7DslException("Unknown field type ${this::class.simpleName}")
private fun Visitable.notRepeatable(): T = throw Hl7DslException("Type ${this::class.simpleName} is not repeatable")
private fun useFieldNumber(): T = throw Hl7DslException("Use field number as index")
private fun useStructureName(): T = throw Hl7DslException("Use structure name as index")
private fun componentIndex(idx: Int) = if (idx < 1) throw Hl7DslException("component index must be in range 1..n") else idx - 1
private fun mappingService(): MappingService = ContextFacade.getBean(MappingService::class.java)
// Get a string value out of types or any object
private fun stringValue(o: Any?): String? =
when (o) {
null -> null
is Primitive -> o.value
is Variable -> stringValue(o.data)
is Composite -> stringValue(o[1])
is RepeatableField -> stringValue(o(0))
is ExtraComponents -> stringValue(o.getComponent(1))
else -> o.toString()
}