diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2327-expand-filter.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2327-expand-filter.yaml new file mode 100644 index 00000000000..30eae25c7bb --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2327-expand-filter.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 2327 +title: "The $expand filter parameter was not matching the ValueSet display value in all cases. E.g. a ValueSet +with name 'abc def ghi' would match 'abc def' and 'def' but not 'def ghi'. This has been corrected so the ValueSet +will match the filter if any substring of the ValueSet display value matches the $expand filter." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java index f8fcedd3669..1a78e39863d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java @@ -186,6 +186,7 @@ import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNoneBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.lowerCase; +import static org.apache.commons.lang3.StringUtils.startsWithIgnoreCase; public abstract class BaseTermReadSvcImpl implements ITermReadSvc { public static final int DEFAULT_FETCH_SIZE = 250; @@ -644,27 +645,44 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } } - public boolean applyFilter(final String theInput, final String thePrefixToken) { + public boolean applyFilter(final String theDisplay, final String theFilterDisplay) { //-- safety check only, no need to apply filter - if (theInput == null || thePrefixToken == null) + if (theDisplay == null || theFilterDisplay == null) return true; // -- sentence case - if (org.apache.commons.lang3.StringUtils.startsWithIgnoreCase(theInput, thePrefixToken)) + if (startsWithIgnoreCase(theDisplay, theFilterDisplay)) return true; //-- token case - // return true only e.g. the input is 'Body height', thePrefixToken is "he", or 'bo' - StringTokenizer tok = new StringTokenizer(theInput); - while (tok.hasMoreTokens()) { - if (org.apache.commons.lang3.StringUtils.startsWithIgnoreCase(tok.nextToken(), thePrefixToken)) - return true; - } - + if (startsWithByWordBoundaries(theDisplay, theFilterDisplay)) return true; + return false; } - + + private boolean startsWithByWordBoundaries(String theDisplay, String theFilterDisplay) { + // return true only e.g. the input is 'Body height', theFilterDisplay is "he", or 'bo' + StringTokenizer tok = new StringTokenizer(theDisplay); + List tokens = new ArrayList<>(); + while (tok.hasMoreTokens()) { + String token = tok.nextToken(); + if (startsWithIgnoreCase(token, theFilterDisplay)) + return true; + tokens.add(token); + } + + // Allow to search by the end of the phrase. E.g. "working proficiency" will match "Limited working proficiency" + for (int start = 0; start <= tokens.size() - 1; ++ start) { + for (int end = start + 1; end <= tokens.size(); ++end) { + String sublist = String.join(" ", tokens.subList(start, end)); + if (startsWithIgnoreCase(sublist, theFilterDisplay)) + return true; + } + } + return false; + } + @Override @Transactional(propagation = Propagation.REQUIRED) public void expandValueSet(ValueSetExpansionOptions theExpansionOptions, ValueSet theValueSetToExpand, IValueSetConceptAccumulator theValueSetCodeAccumulator) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java new file mode 100644 index 00000000000..45e7b561598 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java @@ -0,0 +1,43 @@ +package ca.uhn.fhir.jpa.term; + +import org.junit.jupiter.api.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BaseTermReadSvcImplTest { + + private final TermReadSvcR5 mySvc = new TermReadSvcR5(); + + @Test + void applyFilterMatchWords() { + assertTrue(mySvc.applyFilter("abc def", "abc def")); + assertTrue(mySvc.applyFilter("abc def", "abc")); + assertTrue(mySvc.applyFilter("abc def", "def")); + assertTrue(mySvc.applyFilter("abc def ghi", "abc def ghi")); + assertTrue(mySvc.applyFilter("abc def ghi", "abc def")); + assertTrue(mySvc.applyFilter("abc def ghi", "def ghi")); + } + + @Test + void applyFilterSentenceStart() { + assertTrue(mySvc.applyFilter("manifold", "man")); + assertTrue(mySvc.applyFilter("manifest destiny", "man")); + assertTrue(mySvc.applyFilter("deep sight", "deep sigh")); + assertTrue(mySvc.applyFilter("sink cottage", "sink cot")); + } + + @Test + void applyFilterSentenceEnd() { + assertFalse(mySvc.applyFilter("rescue", "cue")); + assertFalse(mySvc.applyFilter("very picky", "icky")); + } + + @Test + void applyFilterSubwords() { + assertFalse(mySvc.applyFilter("splurge", "urge")); + assertFalse(mySvc.applyFilter("sink cottage", "ink cot")); + assertFalse(mySvc.applyFilter("sink cottage", "ink cottage")); + assertFalse(mySvc.applyFilter("clever jump startle", "lever jump star")); + } +}