Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert rm data from 1.4 to 2 and back #392

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
14 changes: 14 additions & 0 deletions aom/src/main/java/com/nedap/archie/aom/utils/NodeIdUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -68,7 +71,18 @@ public List<Integer> 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("."));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ public interface ModelInfoLookup {
*/
String getArchetypeNodeIdFromRMObject(Object rmObject);

void setArchetypeNodeId(Object object, String newNodeId);

String getArchetypeIdFromArchetypedRmObject(Object rmObject);

/**
Expand Down
14 changes: 14 additions & 0 deletions aom/src/main/java/com/nedap/archie/rminfo/RMListener.java
Original file line number Diff line number Diff line change
@@ -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);

}
55 changes: 55 additions & 0 deletions aom/src/main/java/com/nedap/archie/rminfo/RMTreeWalker.java
Original file line number Diff line number Diff line change
@@ -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);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
119 changes: 119 additions & 0 deletions openehr-rm/src/main/java/com/nedap/archie/rmutil/RMADL2Converter.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
113 changes: 113 additions & 0 deletions tools/src/test/java/com/nedap/archie/rmutil/RMADL2ConverterTest.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> 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<Item> 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<Item> 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) { }
}

}