diff --git a/aom/src/main/java/com/nedap/archie/aom/utils/NodeIdUtil.java b/aom/src/main/java/com/nedap/archie/aom/utils/NodeIdUtil.java index 369b2e4ad..1a955f1b4 100644 --- a/aom/src/main/java/com/nedap/archie/aom/utils/NodeIdUtil.java +++ b/aom/src/main/java/com/nedap/archie/aom/utils/NodeIdUtil.java @@ -2,10 +2,13 @@ import com.google.common.base.Joiner; import com.nedap.archie.definitions.AdlCodeDefinitions; +import org.apache.commons.lang3.StringUtils; +import java.text.DecimalFormat; import java.util.List; import java.util.ArrayList; +import java.util.stream.Collectors; public class NodeIdUtil { @@ -68,7 +71,18 @@ public List getCodes() { public String toString() { return prefix + Joiner.on('.').join(codes); + } + + /** + * Convert to string, left padding all the node id codes if necessary, to create ADL 1.4 style node ids + * @param size the size of the result + * @return the padded node id + */ + public String toStringWithLeftPaddedCodes(int size) { + return prefix + codes.stream() + .map(code -> StringUtils.leftPad(Integer.toString(code), size, '0')) + .collect(Collectors.joining(".")); } diff --git a/aom/src/main/java/com/nedap/archie/rminfo/ArchieAOMInfoLookup.java b/aom/src/main/java/com/nedap/archie/rminfo/ArchieAOMInfoLookup.java index 34cc9492d..3bd1767bb 100644 --- a/aom/src/main/java/com/nedap/archie/rminfo/ArchieAOMInfoLookup.java +++ b/aom/src/main/java/com/nedap/archie/rminfo/ArchieAOMInfoLookup.java @@ -127,6 +127,11 @@ public String getArchetypeIdFromArchetypedRmObject(Object rmObject) { throw new UnsupportedOperationException("not supported");//TODO: split this to different classes } + @Override + public void setArchetypeNodeId(Object object, String newNodeId) { + throw new UnsupportedOperationException("not supported");//TODO: split this to different classes + } + @Override public String getNameFromRMObject(Object rmObject) { if(rmObject instanceof CObject) { diff --git a/aom/src/main/java/com/nedap/archie/rminfo/ModelInfoLookup.java b/aom/src/main/java/com/nedap/archie/rminfo/ModelInfoLookup.java index a0c0bc795..56f203546 100644 --- a/aom/src/main/java/com/nedap/archie/rminfo/ModelInfoLookup.java +++ b/aom/src/main/java/com/nedap/archie/rminfo/ModelInfoLookup.java @@ -121,6 +121,8 @@ public interface ModelInfoLookup { */ String getArchetypeNodeIdFromRMObject(Object rmObject); + void setArchetypeNodeId(Object object, String newNodeId); + String getArchetypeIdFromArchetypedRmObject(Object rmObject); /** diff --git a/aom/src/main/java/com/nedap/archie/rminfo/RMListener.java b/aom/src/main/java/com/nedap/archie/rminfo/RMListener.java new file mode 100644 index 000000000..6eb287976 --- /dev/null +++ b/aom/src/main/java/com/nedap/archie/rminfo/RMListener.java @@ -0,0 +1,14 @@ +package com.nedap.archie.rminfo; + +/** + * A very generic Listener class to walk the tree of RM Objects. + * Can be used directly, to implement very generic RM listeners, or can be used to create a more specific + * listener class for a specific RM + */ +public interface RMListener { + + void enterObject(Object object); + + void exitObject(Object object); + +} diff --git a/aom/src/main/java/com/nedap/archie/rminfo/RMTreeWalker.java b/aom/src/main/java/com/nedap/archie/rminfo/RMTreeWalker.java new file mode 100644 index 000000000..fd0122990 --- /dev/null +++ b/aom/src/main/java/com/nedap/archie/rminfo/RMTreeWalker.java @@ -0,0 +1,55 @@ +package com.nedap.archie.rminfo; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; + +/** + * + */ +public class RMTreeWalker { + + private final ModelInfoLookup lookup; + + public RMTreeWalker(ModelInfoLookup lookup) { + this.lookup = lookup; + } + + public void walk(Object rmObject, RMListener listener){ + if(rmObject == null) { + return; + } + listener.enterObject(rmObject); + + RMTypeInfo typeInfo = lookup.getTypeInfo(rmObject.getClass()); + if(typeInfo != null) { + for(RMAttributeInfo attributeInfo:typeInfo.getAttributes().values()) { + if(attributeInfo.isComputed()) { + continue; + } + Method getMethod = attributeInfo.getGetMethod(); + if(getMethod != null) { + Object result = null; + try { + result = getMethod.invoke(rmObject); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + if(result != null) { + if(result instanceof Collection) { + for(Object c: (Collection) result) { + walk(c, listener); + } + } else { + walk(result, listener); + } + } + } + } + } + listener.exitObject(rmObject); + + } +} diff --git a/openehr-rm/src/main/java/com/nedap/archie/rminfo/ArchieRMInfoLookup.java b/openehr-rm/src/main/java/com/nedap/archie/rminfo/ArchieRMInfoLookup.java index 98b570783..4ea7fd6a8 100644 --- a/openehr-rm/src/main/java/com/nedap/archie/rminfo/ArchieRMInfoLookup.java +++ b/openehr-rm/src/main/java/com/nedap/archie/rminfo/ArchieRMInfoLookup.java @@ -306,6 +306,16 @@ public String getArchetypeNodeIdFromRMObject(Object rmObject) { return null; } + @Override + public void setArchetypeNodeId(Object rmObject, String newNodeId) { + if(rmObject instanceof Locatable) { + Locatable locatable = (Locatable) rmObject; + locatable.setArchetypeNodeId(newNodeId); + } else { + throw new UnsupportedOperationException("Cannot set a Node Id unless given object is a Locatable"); + } + } + @Override public String getArchetypeIdFromArchetypedRmObject(Object rmObject) { if(rmObject instanceof Locatable) { diff --git a/openehr-rm/src/main/java/com/nedap/archie/rmutil/RMADL2Converter.java b/openehr-rm/src/main/java/com/nedap/archie/rmutil/RMADL2Converter.java new file mode 100644 index 000000000..78940af9a --- /dev/null +++ b/openehr-rm/src/main/java/com/nedap/archie/rmutil/RMADL2Converter.java @@ -0,0 +1,119 @@ +package com.nedap.archie.rmutil; + +import com.nedap.archie.adl14.ADL14NodeIDConverter; +import com.nedap.archie.aom.utils.AOMUtils; +import com.nedap.archie.aom.utils.NodeIdUtil; +import com.nedap.archie.rm.datatypes.CodePhrase; +import com.nedap.archie.rminfo.ArchieRMInfoLookup; +import com.nedap.archie.rminfo.ModelInfoLookup; +import com.nedap.archie.rminfo.RMListener; +import com.nedap.archie.rminfo.RMTreeWalker; + +import java.util.Objects; + +/** + * Converts OpenEHR RM data created with ADL 1.4 to RM data as legal with ADL 2, and the other way around. + * Only works on the OpenEHR RM, no generic RM support. + * Converts in place, so be sure to clone your objects first if that is undesired. + */ +public class RMADL2Converter { + + private final ModelInfoLookup lookup; + + public RMADL2Converter() { + this.lookup = ArchieRMInfoLookup.getInstance(); + } + + public void convertToADL2(Object rmObject) { + ToADL2ConvertingListener convertingListener = new ToADL2ConvertingListener(); + new RMTreeWalker(lookup).walk(rmObject, convertingListener); + + } + + public void convertToADL14(Object rmObject) { + ToADL14ConvertingListener convertingListener = new ToADL14ConvertingListener(); + new RMTreeWalker(lookup).walk(rmObject, convertingListener); + } + + private abstract class NodeIdConvertingListener implements RMListener { + + @Override + public void enterObject(Object object) { + String nodeId = lookup.getArchetypeNodeIdFromRMObject(object); + if(nodeId != null) { + if(shouldConvertNodeId(nodeId)) { + String newNodeId = convertNodeId(nodeId); + lookup.setArchetypeNodeId(object, newNodeId); + } + } else if (object instanceof CodePhrase) { + CodePhrase codePhrase = (CodePhrase) object; + if(codePhrase.getTerminologyId() != null) { + if(Objects.equals("local", codePhrase.getTerminologyId().getValue())) { + String newValueCode = convertValueCode(codePhrase.getCodeString()); + codePhrase.setCodeString(newValueCode); + } + } + } + } + + abstract boolean shouldConvertNodeId(String nodeId); + + abstract String convertNodeId(String nodeId); + + abstract String convertValueCode(String codeString); + + @Override + public void exitObject(Object object) { + + } + } + + private class ToADL2ConvertingListener extends NodeIdConvertingListener { + + @Override + boolean shouldConvertNodeId(String nodeId) { + return AOMUtils.isValueCode(nodeId); + } + + @Override + String convertNodeId(String nodeId) { + return ADL14NodeIDConverter.convertCode(nodeId, "id"); + } + + @Override + String convertValueCode(String codeString) { + return ADL14NodeIDConverter.convertCode(codeString, "at"); + } + } + + private class ToADL14ConvertingListener extends NodeIdConvertingListener { + + @Override + boolean shouldConvertNodeId(String nodeId) { + return AOMUtils.isIdCode(nodeId); + } + + @Override + String convertNodeId(String nodeId) { + return convertCodeToADL14(nodeId, "at"); + } + + @Override + String convertValueCode(String codeString) { + return convertCodeToADL14(codeString, "at"); + } + } + + private static String convertCodeToADL14(String oldCode, String newCodePrefix) { + NodeIdUtil nodeIdUtil = new NodeIdUtil(oldCode); + if (nodeIdUtil.getCodes().isEmpty()) { + return oldCode; + } + nodeIdUtil.setPrefix(newCodePrefix); //will automatically strip the leading zeroes due to integer-parsing + if(!oldCode.startsWith("at0.") && !oldCode.startsWith("ac0.") && !oldCode.startsWith("id0.")) { + //a bit tricky, since the root of an archetype starts with at0000.0, but that's different from this I guess + nodeIdUtil.getCodes().set(0, nodeIdUtil.getCodes().get(0) - 1); //increment with 1, old is 0-based + } + return nodeIdUtil.toStringWithLeftPaddedCodes(4); + } +} diff --git a/test-rm/src/main/java/com/nedap/archie/openehrtestrm/TestRMInfoLookup.java b/test-rm/src/main/java/com/nedap/archie/openehrtestrm/TestRMInfoLookup.java index 85b2686ed..3843c2e31 100644 --- a/test-rm/src/main/java/com/nedap/archie/openehrtestrm/TestRMInfoLookup.java +++ b/test-rm/src/main/java/com/nedap/archie/openehrtestrm/TestRMInfoLookup.java @@ -69,6 +69,11 @@ public String getArchetypeIdFromArchetypedRmObject(Object rmObject) { return null; } + @Override + public void setArchetypeNodeId(Object object, String newNodeId) { + + } + @Override public String getNameFromRMObject(Object rmObject) { if(rmObject == null) { diff --git a/tools/src/test/java/com/nedap/archie/rmutil/RMADL2ConverterTest.java b/tools/src/test/java/com/nedap/archie/rmutil/RMADL2ConverterTest.java new file mode 100644 index 000000000..97d19bd30 --- /dev/null +++ b/tools/src/test/java/com/nedap/archie/rmutil/RMADL2ConverterTest.java @@ -0,0 +1,113 @@ +package com.nedap.archie.rmutil; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nedap.archie.aom.Archetype; +import com.nedap.archie.aom.OperationalTemplate; +import com.nedap.archie.aom.utils.AOMUtils; +import com.nedap.archie.creation.ExampleJsonInstanceGenerator; +import com.nedap.archie.creation.RMObjectCreator; +import com.nedap.archie.flattener.Flattener; +import com.nedap.archie.flattener.FlattenerConfiguration; +import com.nedap.archie.flattener.SimpleArchetypeRepository; +import com.nedap.archie.json.JacksonUtil; +import com.nedap.archie.json.RMJacksonConfiguration; +import com.nedap.archie.rm.RMObject; +import com.nedap.archie.rm.archetyped.Locatable; +import com.nedap.archie.rm.datastructures.Element; +import com.nedap.archie.rm.datastructures.Item; +import com.nedap.archie.rm.datatypes.CodePhrase; +import com.nedap.archie.rm.datavalues.DvCodedText; +import com.nedap.archie.rm.integration.GenericEntry; +import com.nedap.archie.rminfo.ArchieRMInfoLookup; +import com.nedap.archie.rminfo.MetaModels; +import com.nedap.archie.rminfo.RMListener; +import com.nedap.archie.rminfo.RMTreeWalker; +import com.nedap.archie.testutil.TestUtil; +import org.junit.Test; +import org.openehr.referencemodels.BuiltinReferenceModels; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class RMADL2ConverterTest { + + @Test + public void convert2To14() throws Exception { + Archetype archetype = TestUtil.parseFailOnErrors("/basic.adl"); + MetaModels metaModels = BuiltinReferenceModels.getMetaModels(); + Flattener optCreator = new Flattener(new SimpleArchetypeRepository(), metaModels, FlattenerConfiguration.forOperationalTemplate()); + ExampleJsonInstanceGenerator generator = new ExampleJsonInstanceGenerator(metaModels, "en"); + generator.setTypePropertyName("_type"); + Map generated = generator.generate((OperationalTemplate) optCreator.flatten(archetype)); + RMJacksonConfiguration standardsCompliant = RMJacksonConfiguration.createStandardsCompliant(); + ObjectMapper objectMapper = JacksonUtil.getObjectMapper(standardsCompliant); + RMObject rmObject = objectMapper.readValue(objectMapper.writeValueAsString(generated), RMObject.class); + RMADL2Converter rmAdl2Converter = new RMADL2Converter(); + rmAdl2Converter.convertToADL14(rmObject); + + rmAdl2Converter.convertToADL2(rmObject); + //assert that all codes are in ADL 2 format again + new RMTreeWalker(ArchieRMInfoLookup.getInstance()).walk(rmObject, new NodeIdIsADL2Checker()); + } + + @Test + public void convertValueCodes() throws Exception { + Archetype archetype = TestUtil.parseFailOnErrors("/com/nedap/archie/aom/openEHR-EHR-GENERIC_ENTRY.included.v1.0.0.adls"); + MetaModels metaModels = BuiltinReferenceModels.getMetaModels(); + Flattener optCreator = new Flattener(new SimpleArchetypeRepository(), metaModels, FlattenerConfiguration.forOperationalTemplate()); + ExampleJsonInstanceGenerator generator = new ExampleJsonInstanceGenerator(metaModels, "en"); + generator.setTypePropertyName("_type"); + Map generated = generator.generate((OperationalTemplate) optCreator.flatten(archetype)); + RMJacksonConfiguration standardsCompliant = RMJacksonConfiguration.createStandardsCompliant(); + ObjectMapper objectMapper = JacksonUtil.getObjectMapper(standardsCompliant); + RMObject rmObject = objectMapper.readValue(objectMapper.writeValueAsString(generated), RMObject.class); + + RMADL2Converter rmAdl2Converter = new RMADL2Converter(); + rmAdl2Converter.convertToADL14(rmObject); + { + GenericEntry entry = (GenericEntry) rmObject; + assertEquals("at0000", entry.getArchetypeNodeId()); + List items = entry.getData().getItems(); + Element element1 = (Element) items.get(0); + assertEquals("at0001", element1.getArchetypeNodeId()); + DvCodedText codedText = (DvCodedText) element1.getValue(); + assertEquals("at0003", codedText.getDefiningCode().getCodeString()); + } + + rmAdl2Converter.convertToADL2(rmObject); + { + GenericEntry entry = (GenericEntry) rmObject; + assertEquals("id1", entry.getArchetypeNodeId()); + List items = entry.getData().getItems(); + Element element1 = (Element) items.get(0); + assertEquals("id2", element1.getArchetypeNodeId()); + DvCodedText codedText = (DvCodedText) element1.getValue(); + assertEquals("at4", codedText.getDefiningCode().getCodeString()); + } + + } + + private static class NodeIdIsADL2Checker implements RMListener { + + @Override + public void enterObject(Object object) { + if(object instanceof Locatable) { + Locatable locatable = (Locatable) object; + assertTrue(AOMUtils.isIdCode(locatable.getArchetypeNodeId())); + } + if(object instanceof CodePhrase) { + CodePhrase codePhrase = (CodePhrase) object; + if(codePhrase.getTerminologyId() != null && codePhrase.getTerminologyId().getValue().equals("local")) { + assertTrue(AOMUtils.isValueCode(codePhrase.getCodeString())); + } + } + } + + @Override + public void exitObject(Object object) { } + } + +}