Merge remote-tracking branch 'remotes/origin/master' into ks-20200120-near-search

# Conflicts:
#	hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_2_0/changes.yaml
This commit is contained in:
Ken Stevens 2020-01-25 18:40:55 -05:00
commit eff4b294df
9 changed files with 172 additions and 141 deletions

View File

@ -19,7 +19,7 @@ jobs:
steps: steps:
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: 'maven | "$(Agent.OS)" | **/pom.xml' key: 'maven | "$(Agent.OS)" | ./pom.xml'
path: $(MAVEN_CACHE_FOLDER) path: $(MAVEN_CACHE_FOLDER)
- task: Bash@3 - task: Bash@3
inputs: inputs:

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1679
title: "An issue with the new flyway migrator hashCode generation was resolved.
Thanks to Jafer Khan for the pull request!"

View File

@ -82,6 +82,9 @@ Sean McIlvenna for reporting!"
title: "When using a custom structure that changes the cardinality from 0..* to 0..1, the Parser was encoding title: "When using a custom structure that changes the cardinality from 0..* to 0..1, the Parser was encoding
a plain field instead of an array (as required by the FHIR specification). Thanks to a plain field instead of an array (as required by the FHIR specification). Thanks to
Petro Mykhailysyn for the pull request!" Petro Mykhailysyn for the pull request!"
- item:
type: "fix"
title: "A meomery leak was resolved in the JPA terminology service delta upload operations."
- item: - item:
type: "add" type: "add"
title: "Searching Location.position by latitude, longitude and distance for DSTU3, R4 and R5 is now supported using a simple 'box' search. title: "Searching Location.position by latitude, longitude and distance for DSTU3, R4 and R5 is now supported using a simple 'box' search.

View File

@ -97,7 +97,7 @@ public class TermConcept implements Serializable {
@Column(name = "PARENT_PIDS", nullable = true) @Column(name = "PARENT_PIDS", nullable = true)
private String myParentPids; private String myParentPids;
@OneToMany(cascade = {}, fetch = FetchType.LAZY, mappedBy = "myChild") @OneToMany(cascade = {}, fetch = FetchType.LAZY, mappedBy = "myChild")
private Collection<TermConceptParentChildLink> myParents; private List<TermConceptParentChildLink> myParents;
@Column(name = "CODE_SEQUENCE", nullable = true) @Column(name = "CODE_SEQUENCE", nullable = true)
private Integer mySequence; private Integer mySequence;
@ -269,7 +269,7 @@ public class TermConcept implements Serializable {
return myParentPids; return myParentPids;
} }
public Collection<TermConceptParentChildLink> getParents() { public List<TermConceptParentChildLink> getParents() {
if (myParents == null) { if (myParents == null) {
myParents = new ArrayList<>(); myParents = new ArrayList<>();
} }

View File

@ -24,10 +24,21 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.*; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao;
import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptParentChildLinkDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
import ca.uhn.fhir.jpa.entity.TermConceptProperty;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
@ -166,10 +177,17 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
TypedQuery<TermConcept> typedQuery = myEntityManager.createQuery(query.select(root)); TypedQuery<TermConcept> typedQuery = myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConcept> hibernateQuery = (org.hibernate.query.Query<TermConcept>) typedQuery; org.hibernate.query.Query<TermConcept> hibernateQuery = (org.hibernate.query.Query<TermConcept>) typedQuery;
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
int count = 0;
try (ScrollableResultsIterator<TermConcept> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults)) { try (ScrollableResultsIterator<TermConcept> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults)) {
while (scrollableResultsIterator.hasNext()) { while (scrollableResultsIterator.hasNext()) {
TermConcept next = scrollableResultsIterator.next(); TermConcept next = scrollableResultsIterator.next();
codeToConceptPid.put(next.getCode(), next.getId()); codeToConceptPid.put(next.getCode(), next.getId());
// We don't want to keep the loaded entities in the L1 cache because they can take up a loooot of memory
if (count % 100 == 0) {
myEntityManager.clear();
}
count++;
} }
} }
ourLog.info("Loaded {} concepts in {}", codeToConceptPid.size(), sw.toString()); ourLog.info("Loaded {} concepts in {}", codeToConceptPid.size(), sw.toString());
@ -199,12 +217,20 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
String childCode = next.getChild().getCode(); String childCode = next.getChild().getCode();
parentCodeToChildCodes.put(parentCode, childCode); parentCodeToChildCodes.put(parentCode, childCode);
childCodeToParentCodes.put(childCode, parentCode); childCodeToParentCodes.put(childCode, parentCode);
// We don't want to keep the loaded entities in the L1 cache because they can take up a loooot of memory
if (count % 100 == 0) {
myEntityManager.clear();
}
count++; count++;
} }
} }
ourLog.info("Loaded {} parent/child relationships in {}", count, sw.toString()); ourLog.info("Loaded {} parent/child relationships in {}", count, sw.toString());
} }
ourLog.trace("Starting delta application");
// Account for root codes in the parent->child map // Account for root codes in the parent->child map
for (String nextCode : codeToConceptPid.keySet()) { for (String nextCode : codeToConceptPid.keySet()) {
if (childCodeToParentCodes.get(nextCode).isEmpty()) { if (childCodeToParentCodes.get(nextCode).isEmpty()) {
@ -217,13 +243,7 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
// Add root concepts // Add root concepts
for (TermConcept nextRootConcept : theAdditions.getRootConcepts()) { for (TermConcept nextRootConcept : theAdditions.getRootConcepts()) {
List<String> parentCodes = Collections.emptyList(); List<String> parentCodes = Collections.emptyList();
addConcept(csv, codeToConceptPid, parentCodes, nextRootConcept, parentCodeToChildCodes, retVal, true); addConcept(csv, codeToConceptPid, parentCodes, nextRootConcept, parentCodeToChildCodes, retVal, theAdditions.getRootConceptCodes(), true);
}
// Add unanchored child concepts
for (TermConcept nextUnanchoredChild : theAdditions.getUnanchoredChildConceptsToParentCodes().keySet()) {
List<String> nextParentCodes = theAdditions.getUnanchoredChildConceptsToParentCodes().get(nextUnanchoredChild);
addConcept(csv, codeToConceptPid, nextParentCodes, nextUnanchoredChild, parentCodeToChildCodes, retVal, true);
} }
return retVal; return retVal;
@ -499,10 +519,11 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
Validate.isTrue(myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), "Terminology operations only supported in DSTU3+ mode"); Validate.isTrue(myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), "Terminology operations only supported in DSTU3+ mode");
} }
private void addConcept(TermCodeSystemVersion theCsv, Map<String, Long> theCodeToConceptPid, Collection<String> theParentCodes, TermConcept theConceptToAdd, ListMultimap<String, String> theParentCodeToChildCodes, UploadStatistics theStatisticsTracker, boolean theForceResequence) { private void addConcept(TermCodeSystemVersion theCsv, Map<String, Long> theCodeToConceptPid, Collection<String> theParentCodes, TermConcept theConceptToAdd, ListMultimap<String, String> theParentCodeToChildCodes, UploadStatistics theStatisticsTracker, Set<String> theAdditionSetRootConceptCodes, boolean theRootConcept) {
TermConcept nextConceptToAdd = theConceptToAdd; TermConcept conceptToAdd = theConceptToAdd;
List<TermConceptParentChildLink> childrenToAdd = theConceptToAdd.getChildren();
String nextCodeToAdd = nextConceptToAdd.getCode(); String nextCodeToAdd = conceptToAdd.getCode();
String parentDescription = "(root concept)"; String parentDescription = "(root concept)";
Set<TermConcept> parentConcepts = new HashSet<>(); Set<TermConcept> parentConcepts = new HashSet<>();
if (!theParentCodes.isEmpty()) { if (!theParentCodes.isEmpty()) {
@ -522,12 +543,12 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
TermConcept existingCode = myConceptDao.getOne(theCodeToConceptPid.get(nextCodeToAdd)); TermConcept existingCode = myConceptDao.getOne(theCodeToConceptPid.get(nextCodeToAdd));
existingCode.setIndexStatus(null); existingCode.setIndexStatus(null);
existingCode.setDisplay(nextConceptToAdd.getDisplay()); existingCode.setDisplay(conceptToAdd.getDisplay());
nextConceptToAdd = existingCode; conceptToAdd = existingCode;
} }
if (theConceptToAdd.getSequence() == null || theForceResequence) { if (conceptToAdd.getSequence() == null || !theRootConcept) {
// If this is a new code, give it a sequence number based on how many concepts the // If this is a new code, give it a sequence number based on how many concepts the
// parent already has (or the highest number, if the code has multiple parents) // parent already has (or the highest number, if the code has multiple parents)
int sequence = 0; int sequence = 0;
@ -539,17 +560,15 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
theParentCodeToChildCodes.put("", nextCodeToAdd); theParentCodeToChildCodes.put("", nextCodeToAdd);
sequence = Math.max(sequence, theParentCodeToChildCodes.get("").size()); sequence = Math.max(sequence, theParentCodeToChildCodes.get("").size());
} }
nextConceptToAdd.setSequence(sequence); conceptToAdd.setSequence(sequence);
} }
// Drop any old parent-child links if they aren't explicitly specified in the // Drop any old parent-child links if they aren't explicitly specified in the
// hierarchy being added // hierarchy being added
for (Iterator<TermConceptParentChildLink> iter = nextConceptToAdd.getParents().iterator(); iter.hasNext(); ) { if (!theRootConcept) {
TermConceptParentChildLink nextLink = iter.next(); for (Iterator<TermConceptParentChildLink> iter = conceptToAdd.getParents().iterator(); iter.hasNext(); ) {
String parentCode = nextLink.getParent().getCode(); TermConceptParentChildLink nextLink = iter.next();
boolean shouldRemove = !theParentCodes.contains(parentCode); String parentCode = nextLink.getParent().getCode();
if (shouldRemove) {
ourLog.info("Dropping existing parent/child link from {} -> {}", parentCode, nextCodeToAdd); ourLog.info("Dropping existing parent/child link from {} -> {}", parentCode, nextCodeToAdd);
myConceptParentChildLinkDao.delete(nextLink); myConceptParentChildLinkDao.delete(nextLink);
iter.remove(); iter.remove();
@ -559,42 +578,49 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
} }
} }
nextConceptToAdd.setParentPids(null); // Null out the hierarchy PIDs for this concept always. We do this because we're going to
nextConceptToAdd.setCodeSystemVersion(theCsv); // force a reindex, and it'll be regenerated then
nextConceptToAdd = myConceptDao.save(nextConceptToAdd); conceptToAdd.setParentPids(null);
conceptToAdd.setCodeSystemVersion(theCsv);
conceptToAdd = myConceptDao.save(conceptToAdd);
Long nextConceptPid = nextConceptToAdd.getId(); Long nextConceptPid = conceptToAdd.getId();
Validate.notNull(nextConceptPid); Validate.notNull(nextConceptPid);
theCodeToConceptPid.put(nextCodeToAdd, nextConceptPid); theCodeToConceptPid.put(nextCodeToAdd, nextConceptPid);
theStatisticsTracker.incrementUpdatedConceptCount(); theStatisticsTracker.incrementUpdatedConceptCount();
// Add link to new child to the parent if this link doesn't already exist (this will be the // Add link to new child to the parent
// case for concepts being added to an existing child concept, but won't be the case when
// we're recursively adding children)
for (TermConcept nextParentConcept : parentConcepts) { for (TermConcept nextParentConcept : parentConcepts) {
if (nextParentConcept.getChildren().stream().noneMatch(t -> t.getChild().getCode().equals(nextCodeToAdd))) { TermConceptParentChildLink parentLink = new TermConceptParentChildLink();
TermConceptParentChildLink parentLink = new TermConceptParentChildLink(); parentLink.setParent(nextParentConcept);
parentLink.setParent(nextParentConcept); parentLink.setChild(conceptToAdd);
parentLink.setChild(nextConceptToAdd); parentLink.setCodeSystem(theCsv);
parentLink.setCodeSystem(theCsv); parentLink.setRelationshipType(TermConceptParentChildLink.RelationshipTypeEnum.ISA);
parentLink.setRelationshipType(TermConceptParentChildLink.RelationshipTypeEnum.ISA); nextParentConcept.getChildren().add(parentLink);
nextParentConcept.getChildren().add(parentLink); conceptToAdd.getParents().add(parentLink);
nextConceptToAdd.getParents().add(parentLink); ourLog.info("Saving parent/child link - Parent[{}] Child[{}]", parentLink.getParent().getCode(), parentLink.getChild().getCode());
myConceptParentChildLinkDao.save(parentLink); myConceptParentChildLinkDao.save(parentLink);
}
} }
ourLog.trace("About to save parent-child links");
// Save children recursively // Save children recursively
for (TermConceptParentChildLink nextChildConceptLink : nextConceptToAdd.getChildren()) { for (TermConceptParentChildLink nextChildConceptLink : new ArrayList<>(childrenToAdd)) {
TermConcept nextChild = nextChildConceptLink.getChild(); TermConcept nextChild = nextChildConceptLink.getChild();
Collection<String> parentCodes = nextChild.getParents().stream().map(t -> t.getParent().getCode()).collect(Collectors.toList());
addConcept(theCsv, theCodeToConceptPid, parentCodes, nextChild, theParentCodeToChildCodes, theStatisticsTracker, false);
if (nextChildConceptLink.getId() == null) { for (int i = 0; i < nextChild.getParents().size(); i++) {
nextChildConceptLink.setCodeSystem(theCsv); if (nextChild.getParents().get(i).getId() == null) {
myConceptParentChildLinkDao.save(nextChildConceptLink); String parentCode = nextChild.getParents().get(i).getParent().getCode();
Long parentPid = theCodeToConceptPid.get(parentCode);
TermConcept parentConcept = myConceptDao.findById(parentPid).orElseThrow(() -> new IllegalArgumentException("Unknown parent code: " + parentCode));
nextChild.getParents().get(i).setParent(parentConcept);
}
} }
Collection<String> parentCodes = nextChild.getParents().stream().map(t -> t.getParent().getCode()).collect(Collectors.toList());
addConcept(theCsv, theCodeToConceptPid, parentCodes, nextChild, theParentCodeToChildCodes, theStatisticsTracker, theAdditionSetRootConceptCodes, false);
} }
} }

View File

@ -28,40 +28,38 @@ import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import org.apache.commons.csv.QuoteMode; import org.apache.commons.csv.QuoteMode;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.util.*; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class CustomTerminologySet { public class CustomTerminologySet {
private final int mySize; private final int mySize;
private final ListMultimap<TermConcept, String> myUnanchoredChildConceptsToParentCodes;
private final List<TermConcept> myRootConcepts; private final List<TermConcept> myRootConcepts;
/** /**
* Constructor for an empty object * Constructor for an empty object
*/ */
public CustomTerminologySet() { public CustomTerminologySet() {
this(0, ArrayListMultimap.create(), new ArrayList<>()); this(0, new ArrayList<>());
} }
/** /**
* Constructor * Constructor
*/ */
private CustomTerminologySet(int theSize, ListMultimap<TermConcept, String> theUnanchoredChildConceptsToParentCodes, Collection<TermConcept> theRootConcepts) { private CustomTerminologySet(int theSize, List<TermConcept> theRootConcepts) {
this(theSize, theUnanchoredChildConceptsToParentCodes, new ArrayList<>(theRootConcepts));
}
/**
* Constructor
*/
private CustomTerminologySet(int theSize, ListMultimap<TermConcept, String> theUnanchoredChildConceptsToParentCodes, List<TermConcept> theRootConcepts) {
mySize = theSize; mySize = theSize;
myUnanchoredChildConceptsToParentCodes = theUnanchoredChildConceptsToParentCodes;
myRootConcepts = theRootConcepts; myRootConcepts = theRootConcepts;
} }
@ -80,10 +78,6 @@ public class CustomTerminologySet {
} }
public ListMultimap<TermConcept, String> getUnanchoredChildConceptsToParentCodes() {
return Multimaps.unmodifiableListMultimap(myUnanchoredChildConceptsToParentCodes);
}
public int getSize() { public int getSize() {
return mySize; return mySize;
} }
@ -111,22 +105,9 @@ public class CustomTerminologySet {
return Collections.unmodifiableList(myRootConcepts); return Collections.unmodifiableList(myRootConcepts);
} }
public void addUnanchoredChildConcept(String theParentCode, String theCode, String theDisplay) {
Validate.notBlank(theParentCode);
Validate.notBlank(theCode);
TermConcept code = new TermConcept()
.setCode(theCode)
.setDisplay(theDisplay);
myUnanchoredChildConceptsToParentCodes.put(code, theParentCode);
}
public void validateNoCycleOrThrowInvalidRequest() { public void validateNoCycleOrThrowInvalidRequest() {
Set<String> codes = new HashSet<>(); Set<String> codes = new HashSet<>();
validateNoCycleOrThrowInvalidRequest(codes, getRootConcepts()); validateNoCycleOrThrowInvalidRequest(codes, getRootConcepts());
for (TermConcept next : myUnanchoredChildConceptsToParentCodes.keySet()) {
validateNoCycleOrThrowInvalidRequest(codes, next);
}
} }
private void validateNoCycleOrThrowInvalidRequest(Set<String> theCodes, List<TermConcept> theRootConcepts) { private void validateNoCycleOrThrowInvalidRequest(Set<String> theCodes, List<TermConcept> theRootConcepts) {
@ -142,25 +123,30 @@ public class CustomTerminologySet {
validateNoCycleOrThrowInvalidRequest(theCodes, next.getChildCodes()); validateNoCycleOrThrowInvalidRequest(theCodes, next.getChildCodes());
} }
public Set<String> getRootConceptCodes() {
return getRootConcepts()
.stream()
.map(TermConcept::getCode)
.collect(Collectors.toSet());
}
@Nonnull @Nonnull
public static CustomTerminologySet load(LoadedFileDescriptors theDescriptors, boolean theFlat) { public static CustomTerminologySet load(LoadedFileDescriptors theDescriptors, boolean theFlat) {
final Map<String, TermConcept> code2concept = new LinkedHashMap<>(); final Map<String, TermConcept> code2concept = new LinkedHashMap<>();
ArrayListMultimap<TermConcept, String> unanchoredChildConceptsToParentCodes = ArrayListMultimap.create();
// Concepts // Concepts
IRecordHandler conceptHandler = new ConceptHandler(code2concept); IRecordHandler conceptHandler = new ConceptHandler(code2concept);
TermLoaderSvcImpl.iterateOverZipFile(theDescriptors, TermLoaderSvcImpl.CUSTOM_CONCEPTS_FILE, conceptHandler, ',', QuoteMode.NON_NUMERIC, false); TermLoaderSvcImpl.iterateOverZipFile(theDescriptors, TermLoaderSvcImpl.CUSTOM_CONCEPTS_FILE, conceptHandler, ',', QuoteMode.NON_NUMERIC, false);
if (theFlat) { if (theFlat) {
return new CustomTerminologySet(code2concept.size(), ArrayListMultimap.create(), code2concept.values()); return new CustomTerminologySet(code2concept.size(), new ArrayList<>(code2concept.values()));
} else { } else {
// Hierarchy // Hierarchy
if (theDescriptors.hasFile(TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE)) { if (theDescriptors.hasFile(TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE)) {
IRecordHandler hierarchyHandler = new HierarchyHandler(code2concept, unanchoredChildConceptsToParentCodes); IRecordHandler hierarchyHandler = new HierarchyHandler(code2concept);
TermLoaderSvcImpl.iterateOverZipFile(theDescriptors, TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE, hierarchyHandler, ',', QuoteMode.NON_NUMERIC, false); TermLoaderSvcImpl.iterateOverZipFile(theDescriptors, TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE, hierarchyHandler, ',', QuoteMode.NON_NUMERIC, false);
} }
@ -178,7 +164,7 @@ public class CustomTerminologySet {
} }
// Sort children so they appear in the same order as they did in the concepts.csv file // Sort children so they appear in the same order as they did in the concepts.csv file
nextConcept.getChildren().sort((o1,o2)->{ nextConcept.getChildren().sort((o1, o2) -> {
String code1 = o1.getChild().getCode(); String code1 = o1.getChild().getCode();
String code2 = o2.getChild().getCode(); String code2 = o2.getChild().getCode();
int order1 = codesInOrder.get(code1); int order1 = codesInOrder.get(code1);
@ -188,7 +174,7 @@ public class CustomTerminologySet {
} }
return new CustomTerminologySet(code2concept.size(), unanchoredChildConceptsToParentCodes, rootConcepts); return new CustomTerminologySet(code2concept.size(), rootConcepts);
} }
} }

View File

@ -24,7 +24,6 @@ import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
import ca.uhn.fhir.jpa.term.IRecordHandler; import ca.uhn.fhir.jpa.term.IRecordHandler;
import ca.uhn.fhir.util.ValidateUtil; import ca.uhn.fhir.util.ValidateUtil;
import com.google.common.collect.ArrayListMultimap;
import org.apache.commons.csv.CSVRecord; import org.apache.commons.csv.CSVRecord;
import java.util.Map; import java.util.Map;
@ -37,11 +36,9 @@ public class HierarchyHandler implements IRecordHandler {
public static final String PARENT = "PARENT"; public static final String PARENT = "PARENT";
public static final String CHILD = "CHILD"; public static final String CHILD = "CHILD";
private final Map<String, TermConcept> myCode2Concept; private final Map<String, TermConcept> myCode2Concept;
private final ArrayListMultimap<TermConcept, String> myUnanchoredChildConceptsToParentCodes;
public HierarchyHandler(Map<String, TermConcept> theCode2concept, ArrayListMultimap<TermConcept, String> theunanchoredChildConceptsToParentCodes) { public HierarchyHandler(Map<String, TermConcept> theCode2concept) {
myCode2Concept = theCode2concept; myCode2Concept = theCode2concept;
myUnanchoredChildConceptsToParentCodes = theunanchoredChildConceptsToParentCodes;
} }
@Override @Override
@ -51,14 +48,12 @@ public class HierarchyHandler implements IRecordHandler {
if (isNotBlank(parent) && isNotBlank(child)) { if (isNotBlank(parent) && isNotBlank(child)) {
TermConcept childConcept = myCode2Concept.get(child); TermConcept childConcept = myCode2Concept.get(child);
ValidateUtil.isNotNullOrThrowUnprocessableEntity(childConcept, "Child code %s not found", child); ValidateUtil.isNotNullOrThrowUnprocessableEntity(childConcept, "Child code %s not found in file", child);
TermConcept parentConcept = myCode2Concept.get(parent); TermConcept parentConcept = myCode2Concept.get(parent);
if (parentConcept == null) { ValidateUtil.isNotNullOrThrowUnprocessableEntity(parentConcept, "Parent code %s not found in file", child);
myUnanchoredChildConceptsToParentCodes.put(childConcept, parent);
} else { parentConcept.addChild(childConcept, TermConceptParentChildLink.RelationshipTypeEnum.ISA);
parentConcept.addChild(childConcept, TermConceptParentChildLink.RelationshipTypeEnum.ISA);
}
} }
} }
} }

View File

@ -2,8 +2,6 @@ package ca.uhn.fhir.jpa.term;
import ca.uhn.fhir.context.support.IContextValidationSupport; import ca.uhn.fhir.context.support.IContextValidationSupport;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
@ -12,23 +10,26 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.test.context.TestPropertySource;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.contains; import static org.junit.Assert.assertEquals;
import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertThat;
import static org.junit.Assert.*; import static org.junit.Assert.fail;
public class TerminologySvcDeltaR4Test extends BaseJpaR4Test { public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(TerminologySvcDeltaR4Test.class); private static final Logger ourLog = LoggerFactory.getLogger(TerminologySvcDeltaR4Test.class);
@ -114,8 +115,9 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
}); });
delta = new CustomTerminologySet(); delta = new CustomTerminologySet();
delta.addUnanchoredChildConcept("RootA", "ChildAA", "Child AA"); TermConcept root = delta.addRootConcept("RootA", "Root A");
delta.addUnanchoredChildConcept("RootA", "ChildAB", "Child AB"); root.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("ChildAA").setDisplay("Child AA");
root.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("ChildAB").setDisplay("Child AB");
myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta); myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta);
assertHierarchyContains( assertHierarchyContains(
"RootA seq=1", "RootA seq=1",
@ -135,25 +137,29 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
delta = new CustomTerminologySet(); delta = new CustomTerminologySet();
delta.addRootConcept("RootA", "Root A") delta.addRootConcept("RootA", "Root A")
.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("ChildAA").setDisplay("Child AA"); .addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("ChildAA").setDisplay("Child AA")
.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("ChildAAA").setDisplay("Child AAA");
delta.addRootConcept("RootB", "Root B"); delta.addRootConcept("RootB", "Root B");
outcome = myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta); outcome = myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta);
assertHierarchyContains( assertHierarchyContains(
"RootA seq=1", "RootA seq=1",
" ChildAA seq=1", " ChildAA seq=1",
" ChildAAA seq=1",
"RootB seq=2" "RootB seq=2"
); );
assertEquals(3, outcome.getUpdatedConceptCount()); assertEquals(4, outcome.getUpdatedConceptCount());
delta = new CustomTerminologySet(); delta = new CustomTerminologySet();
delta.addUnanchoredChildConcept("RootB", "ChildAA", "Child AA"); delta.addRootConcept("RootB", "Root B")
.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("ChildAA").setDisplay("Child AA");
outcome = myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta); outcome = myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta);
assertEquals(1, outcome.getUpdatedConceptCount());
assertHierarchyContains( assertHierarchyContains(
"RootA seq=1", "RootA seq=1",
"RootB seq=2", "RootB seq=2",
" ChildAA seq=1" " ChildAA seq=1",
" ChildAAA seq=1"
); );
assertEquals(2, outcome.getUpdatedConceptCount());
} }
@ -166,11 +172,6 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
myCodeSystemDao.create(cs); myCodeSystemDao.create(cs);
CodeSystem delta = new CodeSystem();
delta
.addConcept()
.setCode("codeA")
.setDisplay("displayA");
try { try {
myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo", new CustomTerminologySet()); myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo", new CustomTerminologySet());
fail(); fail();
@ -180,6 +181,44 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
} }
@Test
public void testAddChildToExistingChild() {
CustomTerminologySet set;
// Create not-present
CodeSystem cs = new CodeSystem();
cs.setUrl("http://foo");
cs.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT);
myCodeSystemDao.create(cs);
// Add parent with 1 child
set = new CustomTerminologySet();
set.addRootConcept("ParentA", "Parent A")
.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("ChildA").setDisplay("Child A");
myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo", set);
// Check so far
assertHierarchyContains(
"ParentA seq=1",
" ChildA seq=1"
);
// Add sub-child to existing child
ourLog.info("*** Adding child to existing child");
set = new CustomTerminologySet();
set.addRootConcept("ChildA", "Child A")
.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("ChildAA").setDisplay("Child AA");
myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo", set);
// Check so far
assertHierarchyContains(
"ParentA seq=1",
" ChildA seq=1",
" ChildAA seq=1"
);
}
@Test @Test
public void testAddWithoutPreExistingCodeSystem() { public void testAddWithoutPreExistingCodeSystem() {
@ -229,27 +268,6 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
assertEquals("CODEA1", myTermSvc.lookupCode(myFhirCtx, "http://foo", "codea").getCodeDisplay()); assertEquals("CODEA1", myTermSvc.lookupCode(myFhirCtx, "http://foo", "codea").getCodeDisplay());
} }
@Test
public void testAddUnanchoredWithUnknownParent() {
createNotPresentCodeSystem();
// Add root code
CustomTerminologySet delta = new CustomTerminologySet();
delta.addRootConcept("CodeA", "Code A");
UploadStatistics outcome = myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo", delta);
assertEquals(1, outcome.getUpdatedConceptCount());
// Try to add child to nonexistent root code
delta = new CustomTerminologySet();
delta.addUnanchoredChildConcept("CodeB", "CodeBB", "Code BB");
try {
myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Unable to add code \"CodeBB\" to unknown parent: CodeB"));
}
}
@Test @Test
public void testAddRelocateHierarchy() { public void testAddRelocateHierarchy() {
createNotPresentCodeSystem(); createNotPresentCodeSystem();
@ -280,9 +298,11 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
// Move a single child code to a new spot and make sure the hierarchy comes along // Move a single child code to a new spot and make sure the hierarchy comes along
// for the ride.. // for the ride..
delta = new CustomTerminologySet(); delta = new CustomTerminologySet();
delta.addUnanchoredChildConcept("CodeB", "CodeAA", "Code AA"); delta
.addRootConcept("CodeB", "Code B")
.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA).setCode("CodeAA").setDisplay("Code AA");
outcome = myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta); outcome = myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd("http://foo/cs", delta);
assertEquals(3, outcome.getUpdatedConceptCount()); assertEquals(2, outcome.getUpdatedConceptCount());
assertHierarchyContains( assertHierarchyContains(
"CodeA seq=1", "CodeA seq=1",
"CodeB seq=2", "CodeB seq=2",
@ -431,8 +451,6 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
} }
private ValueSet expandNotPresentCodeSystem() { private ValueSet expandNotPresentCodeSystem() {
ValueSet vs = new ValueSet(); ValueSet vs = new ValueSet();
vs.setUrl("http://foo/vs"); vs.setUrl("http://foo/vs");

View File

@ -146,8 +146,6 @@ public class RenameColumnTask extends BaseTableTask<RenameColumnTask> {
return new EqualsBuilder() return new EqualsBuilder()
.appendSuper(super.equals(theO)) .appendSuper(super.equals(theO))
.append(myIsOkayIfNeitherColumnExists, that.myIsOkayIfNeitherColumnExists)
.append(myDeleteTargetColumnFirstIfBothExist, that.myDeleteTargetColumnFirstIfBothExist)
.append(myOldName, that.myOldName) .append(myOldName, that.myOldName)
.append(myNewName, that.myNewName) .append(myNewName, that.myNewName)
.isEquals(); .isEquals();