From e15037ea5e7f694bad29ab33d53d0925958af30a Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Sat, 27 Mar 2021 12:27:26 -0400 Subject: [PATCH 1/6] Issue #2092, Issue #1733 - miscellaneous updates Signed-off-by: John T.E. Timm --- .../com/ibm/fhir/core/util/URLSupport.java | 119 +++++++++ .../fhir/core/util/test/URLSupportTest.java | 46 ++++ .../FHIRPathAbstractTermFunction.java | 39 +-- .../resource/FHIRRegistryResource.java | 4 +- .../SnomedRegistryResourceProvider.java | 85 +++--- ...licitValueSetRegistryResourceProvider.java | 58 +++++ .../ibm/fhir/term/util/CodeSystemSupport.java | 244 ++++++++++-------- .../service/test/CodeSystemSupportTest.java | 36 +++ 8 files changed, 443 insertions(+), 188 deletions(-) create mode 100644 fhir-core/src/main/java/com/ibm/fhir/core/util/URLSupport.java create mode 100644 fhir-core/src/test/java/com/ibm/fhir/core/util/test/URLSupportTest.java create mode 100644 fhir-term/src/main/java/com/ibm/fhir/term/registry/ImplicitValueSetRegistryResourceProvider.java create mode 100644 fhir-term/src/test/java/com/ibm/fhir/term/service/test/CodeSystemSupportTest.java diff --git a/fhir-core/src/main/java/com/ibm/fhir/core/util/URLSupport.java b/fhir-core/src/main/java/com/ibm/fhir/core/util/URLSupport.java new file mode 100644 index 00000000000..7559d13861e --- /dev/null +++ b/fhir-core/src/main/java/com/ibm/fhir/core/util/URLSupport.java @@ -0,0 +1,119 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.core.util; + +import static com.ibm.fhir.core.util.LRUCache.createLRUCache; + +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A utility class for working with URLs + */ +public class URLSupport { + private static final Map URL_CACHE = createLRUCache(128); + + private URLSupport() { } + + public static String decode(String s) { + try { + return URLDecoder.decode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public static String getFirst(Map> queryParameters, String key) { + List values = queryParameters.get(key); + return (values != null && !values.isEmpty()) ? values.get(0) : null; + } + + public static String getPath(String url) { + return getURL(url).getPath(); + } + + public static List getPathSegments(String url) { + return getPathSegments(url, true); + } + + public static List getPathSegments(String url, boolean decode) { + return parsePath(getPath(url), decode); + } + + public static String getQuery(String url) { + return getURL(url).getQuery(); + } + + public static Map> getQueryParameters(String url) { + return getQueryParameters(url, true); + } + + public static Map> getQueryParameters(String url, boolean decode) { + return parseQuery(getQuery(url), decode); + } + + public static URL getURL(String url) { + return URL_CACHE.computeIfAbsent(url, k -> computeURL(url)); + } + + public static List parsePath(String path) { + return parsePath(path, true); + } + + public static List parsePath(String path, boolean decode) { + return Arrays.stream(path.split("/")) + .skip(1) + .map(s -> decode ? decode(s) : s) + .collect(Collectors.toList()); + } + + public static Map> parseQuery(String query) { + return parseQuery(query, true); + } + + public static Map> parseQuery(String query, boolean decode) { + if (query == null || query.isEmpty()) { + return Collections.emptyMap(); + } + return Arrays.stream(query.split("&")) + .map(pair -> Arrays.asList(pair.split("=", 2))) + .collect(Collectors.collectingAndThen( + Collectors.toMap( + // key mapping function + pair -> decode ? decode(pair.get(0)) : pair.get(0), + // value mapping function + pair -> (pair.size() > 1) ? Collections.unmodifiableList(Arrays.stream(pair.get(1).split(",")) + .map(s -> decode ? decode(s) : s) + .collect(Collectors.toList())) : Collections.emptyList(), + // merge function + (u, v) -> { + List merged = new ArrayList<>(u); + merged.addAll(v); + return Collections.unmodifiableList(merged); + }, + // map supplier + LinkedHashMap::new), + Collections::unmodifiableMap)); + } + + private static URL computeURL(String url) { + try { + return new URL(url); + } catch (MalformedURLException e) { + throw new RuntimeException (e); + } + } +} diff --git a/fhir-core/src/test/java/com/ibm/fhir/core/util/test/URLSupportTest.java b/fhir-core/src/test/java/com/ibm/fhir/core/util/test/URLSupportTest.java new file mode 100644 index 00000000000..9e51db047bf --- /dev/null +++ b/fhir-core/src/test/java/com/ibm/fhir/core/util/test/URLSupportTest.java @@ -0,0 +1,46 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.core.util.test; + +import static com.ibm.fhir.core.util.URLSupport.getFirst; +import static com.ibm.fhir.core.util.URLSupport.getPathSegments; +import static com.ibm.fhir.core.util.URLSupport.getQueryParameters; +import static com.ibm.fhir.core.util.URLSupport.parseQuery; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.testng.Assert; +import org.testng.annotations.Test; + +public class URLSupportTest { + @Test + public void testGetQueryParameters() { + String url = "http://ibm.com/fhir/ValueSet/generalizes?system=http://ibm.com/fhir/CodeSystem/cs5&code=r"; + Map> queryParameters = getQueryParameters(url); + Assert.assertEquals(getFirst(queryParameters, "system"), "http://ibm.com/fhir/CodeSystem/cs5"); + Assert.assertEquals(getFirst(queryParameters, "code"), "r"); + } + + @Test + public void testGetPathSegments() { + String url = "http://ibm.com/fhir/ValueSet/generalizes?system=http://ibm.com/fhir/CodeSystem/cs5&code=r"; + List pathSegments = getPathSegments(url); + List expected = Arrays.asList("fhir", "ValueSet", "generalizes"); + Assert.assertEquals(pathSegments, expected); + } + + @Test + public void testParseQuery() throws Exception { + String query = "name1=value1%7Cvalue2%7Cvalue3"; + Map> queryParameters = parseQuery(query); + Assert.assertEquals(getFirst(queryParameters, "name1"), "value1|value2|value3"); + queryParameters = parseQuery(query, false); + Assert.assertEquals(getFirst(queryParameters, "name1"), "value1%7Cvalue2%7Cvalue3"); + } +} \ No newline at end of file diff --git a/fhir-path/src/main/java/com/ibm/fhir/path/function/FHIRPathAbstractTermFunction.java b/fhir-path/src/main/java/com/ibm/fhir/path/function/FHIRPathAbstractTermFunction.java index 6e4e7d4a9dc..1d307d0c833 100644 --- a/fhir-path/src/main/java/com/ibm/fhir/path/function/FHIRPathAbstractTermFunction.java +++ b/fhir-path/src/main/java/com/ibm/fhir/path/function/FHIRPathAbstractTermFunction.java @@ -1,11 +1,12 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2021 * * SPDX-License-Identifier: Apache-2.0 */ package com.ibm.fhir.path.function; +import static com.ibm.fhir.core.util.URLSupport.parseQuery; import static com.ibm.fhir.model.type.String.string; import static com.ibm.fhir.model.util.ModelSupport.FHIR_STRING; import static com.ibm.fhir.path.util.FHIRPathUtil.getElementNode; @@ -17,13 +18,9 @@ import static com.ibm.fhir.path.util.FHIRPathUtil.isSingleton; import static com.ibm.fhir.path.util.FHIRPathUtil.isStringValue; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -123,7 +120,7 @@ protected com.ibm.fhir.model.type.String getDisplay(FHIRPathTree tree, FHIRPathE protected Parameters getParameters(List> arguments) { if (arguments.size() == getMaxArity()) { String params = getString(arguments.get(arguments.size() - 1)); - Map> queryParameters = parse(params); + Map> queryParameters = parseQuery(params); return buildParameters(queryParameters); } return EMPTY_PARAMETERS; @@ -183,34 +180,4 @@ private List parameter( .build()) .collect(Collectors.toList()); } - - private Map> parse(String params) { - return Arrays.stream(params.split("&")) - .map(pair -> Arrays.asList(pair.split("=", 2))) - .collect(Collectors.collectingAndThen( - Collectors.toMap( - // key mapping function - pair -> decode(pair.get(0)), - // value mapping function - pair -> Collections.unmodifiableList(Arrays.stream(pair.get(1).split(",")) - .map(s -> decode(s)) - .collect(Collectors.toList())), - // merge function - (u, v) -> { - List merged = new ArrayList<>(u); - merged.addAll(v); - return Collections.unmodifiableList(merged); - }, - // map supplier - LinkedHashMap::new), - Collections::unmodifiableMap)); - } - - private String decode(String s) { - try { - return URLDecoder.decode(s, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } } diff --git a/fhir-registry/src/main/java/com/ibm/fhir/registry/resource/FHIRRegistryResource.java b/fhir-registry/src/main/java/com/ibm/fhir/registry/resource/FHIRRegistryResource.java index d10462d74d1..a3ead521c45 100644 --- a/fhir-registry/src/main/java/com/ibm/fhir/registry/resource/FHIRRegistryResource.java +++ b/fhir-registry/src/main/java/com/ibm/fhir/registry/resource/FHIRRegistryResource.java @@ -230,7 +230,9 @@ public int compareTo(Version version) { } public static FHIRRegistryResource from(Resource resource) { - Objects.requireNonNull(resource, "resource"); + if (resource == null) { + return null; + } Class resourceType = resource.getClass(); requireDefinitionalResourceType(resourceType); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java index d52317c0185..c0284b8389f 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java @@ -6,14 +6,13 @@ package com.ibm.fhir.term.graph.registry; -import static com.ibm.fhir.core.util.LRUCache.createLRUCache; +import static com.ibm.fhir.core.util.URLSupport.getFirst; +import static com.ibm.fhir.core.util.URLSupport.getQueryParameters; import static com.ibm.fhir.model.type.String.string; import java.io.InputStream; -import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.Map; -import java.util.logging.Level; import java.util.logging.Logger; import com.ibm.fhir.model.format.Format; @@ -34,11 +33,11 @@ import com.ibm.fhir.model.type.code.NarrativeStatus; import com.ibm.fhir.model.type.code.PublicationStatus; import com.ibm.fhir.registry.resource.FHIRRegistryResource; -import com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider; +import com.ibm.fhir.term.registry.ImplicitValueSetRegistryResourceProvider; import com.ibm.fhir.term.service.FHIRTermService; import com.ibm.fhir.term.spi.ValidationOutcome; -public class SnomedRegistryResourceProvider implements FHIRRegistryResourceProvider { +public class SnomedRegistryResourceProvider extends ImplicitValueSetRegistryResourceProvider { private static final Logger log = Logger.getLogger(SnomedRegistryResourceProvider.class.getName()); private static final String SNOMED_URL = "http://snomed.info/sct"; @@ -47,53 +46,14 @@ public class SnomedRegistryResourceProvider implements FHIRRegistryResourceProvi public static final CodeSystem SNOMED_CODE_SYSTEM = loadCodeSystem(); private static final FHIRRegistryResource SNOMED_CODE_SYSTEM_REGISTRY_RESOURCE = FHIRRegistryResource.from(SNOMED_CODE_SYSTEM); - private static final FHIRRegistryResource SNOMED_ALL_CONCEPTS_IMPLICIT_VALUE_SET_REGISTRY_RESOURCE = FHIRRegistryResource.from(buildAllConceptsImplicitValueSet()); - private static final Map SNOMED_SUBSUMED_BY_IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE = createLRUCache(128); + private static final ValueSet ALL_CONCEPTS_IMPLICIT_VALUE_SET = buildAllConceptsImplicitValueSet(); @Override public FHIRRegistryResource getRegistryResource(Class resourceType, String url, String version) { - if (url == null) { - return null; - } - if (ValueSet.class.equals(resourceType) && url.startsWith(SNOMED_IMPLICIT_VALUE_SET_PREFIX)) { - if (SNOMED_IMPLICIT_VALUE_SET_PREFIX.equals(url)) { - return SNOMED_ALL_CONCEPTS_IMPLICIT_VALUE_SET_REGISTRY_RESOURCE; - } else { - String[] tokens = url.split("=isa\\/"); - if (tokens.length == 2) { - String sctid = tokens[1]; - ValidationOutcome outcome = FHIRTermService.getInstance().validateCode(SNOMED_CODE_SYSTEM, Code.of(sctid), null); - if (outcome == null || Boolean.FALSE.equals(outcome.getResult())) { - log.log(Level.WARNING, "Code: " + sctid + " is invalid or SNOMED CT is not supported"); - return null; - } - return SNOMED_SUBSUMED_BY_IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE.computeIfAbsent(sctid, k -> FHIRRegistryResource.from(buildSubsumedByImplicitValueSet(sctid))); - } - } - } else if (CodeSystem.class.equals(resourceType) && SNOMED_URL.equals(url)) { + if (CodeSystem.class.equals(resourceType) && SNOMED_URL.equals(url)) { return SNOMED_CODE_SYSTEM_REGISTRY_RESOURCE; } - return null; - } - - @Override - public Collection getRegistryResources(Class resourceType) { - return Collections.emptyList(); - } - - @Override - public Collection getRegistryResources() { - return Collections.emptyList(); - } - - @Override - public Collection getProfileResources(String type) { - return Collections.emptyList(); - } - - @Override - public Collection getSearchParameterResources(String type) { - return Collections.emptyList(); + return super.getRegistryResource(resourceType, url, version); } private static ValueSet buildAllConceptsImplicitValueSet() { @@ -146,4 +106,33 @@ private static CodeSystem loadCodeSystem() { throw new Error(e); } } + + @Override + protected ValueSet buildImplicitValueSet(String url) { + if (SNOMED_IMPLICIT_VALUE_SET_PREFIX.equals(url)) { + return ALL_CONCEPTS_IMPLICIT_VALUE_SET; + } + Map> queryParameters = getQueryParameters(url); + String value = getFirst(queryParameters, "fhir_vs"); + if (value == null || value.isEmpty()) { + log.warning("The 'fhir_vs' parameter value is null or empty"); + return null; + } + String[] tokens = value.split("/"); + if (tokens.length == 2) { + String sctid = tokens[1]; + ValidationOutcome outcome = FHIRTermService.getInstance().validateCode(SNOMED_CODE_SYSTEM, Code.of(sctid), null); + if (outcome == null || Boolean.FALSE.equals(outcome.getResult())) { + log.warning("The code '" + sctid + "' is invalid or SNOMED CT is not supported"); + return null; + } + return buildSubsumedByImplicitValueSet(sctid); + } + return null; + } + + @Override + protected boolean isSupported(String url) { + return (url != null && url.startsWith(SNOMED_IMPLICIT_VALUE_SET_PREFIX)); + } } diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/registry/ImplicitValueSetRegistryResourceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/registry/ImplicitValueSetRegistryResourceProvider.java new file mode 100644 index 00000000000..4ddd3d2859b --- /dev/null +++ b/fhir-term/src/main/java/com/ibm/fhir/term/registry/ImplicitValueSetRegistryResourceProvider.java @@ -0,0 +1,58 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.registry; + +import static com.ibm.fhir.core.util.LRUCache.createLRUCache; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import com.ibm.fhir.model.resource.Resource; +import com.ibm.fhir.model.resource.ValueSet; +import com.ibm.fhir.registry.resource.FHIRRegistryResource; +import com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider; + +/** + * An abstract base class for implicit registry resource provider implementations (e.g. SNOMED, LOINC, etc.) + */ +public abstract class ImplicitValueSetRegistryResourceProvider implements FHIRRegistryResourceProvider { + private static final Map IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE = createLRUCache(1024); + + @Override + public Collection getProfileResources(String type) { + return Collections.emptyList(); + } + + @Override + public FHIRRegistryResource getRegistryResource(Class resourceType, String url, String version) { + if (url == null) { + return null; + } + if (ValueSet.class.equals(resourceType) && isSupported(url)) { + return IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE.computeIfAbsent(url, k -> FHIRRegistryResource.from(buildImplicitValueSet(url))); + } + return null; + } + + @Override + public Collection getRegistryResources() { + return IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE.values(); + } + @Override + public Collection getRegistryResources(Class resourceType) { + return ValueSet.class.equals(resourceType) ? IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE.values() : Collections.emptyList(); + } + + @Override + public Collection getSearchParameterResources(String type) { + return Collections.emptyList(); + } + + protected abstract ValueSet buildImplicitValueSet(String url); + protected abstract boolean isSupported(String url); +} diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/util/CodeSystemSupport.java b/fhir-term/src/main/java/com/ibm/fhir/term/util/CodeSystemSupport.java index 1fbcc4f0694..39f531b1741 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/util/CodeSystemSupport.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/util/CodeSystemSupport.java @@ -26,6 +26,7 @@ import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; import com.ibm.fhir.model.resource.ValueSet.Compose.Include; +import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.type.Boolean; import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.DateTime; @@ -37,6 +38,7 @@ import com.ibm.fhir.model.type.code.FilterOperator; import com.ibm.fhir.model.type.code.PropertyType; import com.ibm.fhir.registry.FHIRRegistry; +import com.ibm.fhir.term.service.FHIRTermService; /** * A utility class for FHIR code systems @@ -46,6 +48,8 @@ public final class CodeSystemSupport { private static final Map CASE_SENSITIVITY_CACHE = createLRUCache(2048); private static final Pattern IN_COMBINING_DIACRITICAL_MARKS_PATTERN = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); + private static final Map> ANCESTORS_AND_SELF_CACHE = createLRUCache(128); + private static final Map> DESCENDANTS_AND_SELF_CACHE = createLRUCache(128); private CodeSystemSupport() { } @@ -108,78 +112,8 @@ public static Concept findConcept(CodeSystem codeSystem, Concept concept, Code c return result; } - /** - * Determine whether a code system filter with the specified property code and filter operator exists - * in the provided code system. - * - * @param codeSystem - * the code system - * @param code - * the property code - * @param operator - * the filter operator - * @return - * true if the code system filter exists, false otherwise - */ - public static boolean hasCodeSystemFilter(CodeSystem codeSystem, Code code, FilterOperator operator) { - return getCodeSystemFilter(codeSystem, code, operator) != null; - } - - /** - * Determine whether a code system property with the specified code exists in the - * provided code system. - * - * @param codeSystem - * the code system - * @param code - * the property code - * @return - * true if the code system property exists, false otherwise - */ - public static boolean hasCodeSystemProperty(CodeSystem codeSystem, Code code) { - return getCodeSystemProperty(codeSystem, code) != null; - } - - /** - * Determine whether a concept property with the specified code exists on the - * provided concept. - * - * @param concept - * the concept - * @param code - * the property code - * @return - * true if the concept property exists, false otherwise - */ - public static boolean hasConceptProperty(Concept concept, Code code) { - return getConceptProperty(concept, code) != null; - } - - /** - * Indicates whether the code system is case sensitive - * - * @param codeSystem - * the code system - * @return - * true if the code system is case sensitive, false otherwise - */ - public static boolean isCaseSensitive(CodeSystem codeSystem) { - if (codeSystem != null && codeSystem.getCaseSensitive() != null) { - return java.lang.Boolean.TRUE.equals(codeSystem.getCaseSensitive().getValue()); - } - return false; - } - - /** - * Indicates whether the code system with the given url is case sensitive - * - * @param url - * the url - * @return - * true if the code system with the given is case sensitive, false otherwise - */ - public static boolean isCaseSensitive(java.lang.String url) { - return CASE_SENSITIVITY_CACHE.computeIfAbsent(url, k -> isCaseSensitive(getCodeSystem(url))); + public static Set getAncestorsAndSelf(CodeSystem codeSystem, Code code) { + return ANCESTORS_AND_SELF_CACHE.computeIfAbsent(code.getValue(), k -> computeAncestorsAndSelf(codeSystem, code)); } /** @@ -346,6 +280,84 @@ public static Set getConcepts(Concept concept) { return concepts; } + public static Set getDescendantsAndSelf(CodeSystem codeSystem, Code code) { + return DESCENDANTS_AND_SELF_CACHE.computeIfAbsent(code.getValue(), k -> computeDescendantsAndSelf(codeSystem, code)); + } + + /** + * Determine whether a code system filter with the specified property code and filter operator exists + * in the provided code system. + * + * @param codeSystem + * the code system + * @param code + * the property code + * @param operator + * the filter operator + * @return + * true if the code system filter exists, false otherwise + */ + public static boolean hasCodeSystemFilter(CodeSystem codeSystem, Code code, FilterOperator operator) { + return getCodeSystemFilter(codeSystem, code, operator) != null; + } + + /** + * Determine whether a code system property with the specified code exists in the + * provided code system. + * + * @param codeSystem + * the code system + * @param code + * the property code + * @return + * true if the code system property exists, false otherwise + */ + public static boolean hasCodeSystemProperty(CodeSystem codeSystem, Code code) { + return getCodeSystemProperty(codeSystem, code) != null; + } + + /** + * Determine whether a concept property with the specified code exists on the + * provided concept. + * + * @param concept + * the concept + * @param code + * the property code + * @return + * true if the concept property exists, false otherwise + */ + public static boolean hasConceptProperty(Concept concept, Code code) { + return getConceptProperty(concept, code) != null; + } + + /** + * Indicates whether the code system is case sensitive + * + * @param codeSystem + * the code system + * @return + * true if the code system is case sensitive, false otherwise + */ + public static boolean isCaseSensitive(CodeSystem codeSystem) { + if (codeSystem != null && codeSystem.getCaseSensitive() != null) { + return java.lang.Boolean.TRUE.equals(codeSystem.getCaseSensitive().getValue()); + } + return false; + } + + /** + * Indicates whether the code system with the given url is case sensitive + * + * @param url + * the url + * @return + * true if the code system with the given is case sensitive, false otherwise + */ + public static boolean isCaseSensitive(java.lang.String url) { + return CASE_SENSITIVITY_CACHE.computeIfAbsent(url, k -> isCaseSensitive(getCodeSystem(url))); + } + /** * Normalize the string by making it case and accent insensitive. * @@ -374,60 +386,60 @@ public static Boolean toBoolean(String value) { } /** - * Convert the given FHIR string value to an Element value based on the provided property type. + * Convert the given Java string value to an Element based on the provided property type. * * @param value - * the FHIR string value + * the Java string value * @param type * the property type * @return - * the Element value equivalent of the given FHIR string based on the provided property type, + * the Element value equivalent of the given Java string based on the provided property type, * or null if the type isn't supported */ - public static Element toElement(String value, PropertyType type) { + public static Element toElement(java.lang.String value, PropertyType type) { switch (type.getValueAsEnumConstant()) { case BOOLEAN: - return Boolean.of(value.getValue()); + return Boolean.of(value); case CODE: - return Code.of(value.getValue()); + return Code.of(value); case DATE_TIME: - return DateTime.of(value.getValue()); + return DateTime.of(value); case DECIMAL: - return Decimal.of(value.getValue()); + return Decimal.of(value); case INTEGER: - return Integer.of(value.getValue()); + return Integer.of(value); case STRING: - return value; + return string(value); default: return null; } } /** - * Convert the given Java string value to an Element based on the provided property type. + * Convert the given FHIR string value to an Element value based on the provided property type. * * @param value - * the Java string value + * the FHIR string value * @param type * the property type * @return - * the Element value equivalent of the given Java string based on the provided property type, + * the Element value equivalent of the given FHIR string based on the provided property type, * or null if the type isn't supported */ - public static Element toElement(java.lang.String value, PropertyType type) { + public static Element toElement(String value, PropertyType type) { switch (type.getValueAsEnumConstant()) { case BOOLEAN: - return Boolean.of(value); + return Boolean.of(value.getValue()); case CODE: - return Code.of(value); + return Code.of(value.getValue()); case DATE_TIME: - return DateTime.of(value); + return DateTime.of(value.getValue()); case DECIMAL: - return Decimal.of(value); + return Decimal.of(value.getValue()); case INTEGER: - return Integer.of(value); + return Integer.of(value.getValue()); case STRING: - return string(value); + return value; default: return null; } @@ -488,6 +500,32 @@ private static Code code(String value) { return Code.of(value.getValue()); } + private static Set computeAncestorsAndSelf(CodeSystem codeSystem, Code code) { + Set concepts = FHIRTermService.getInstance().getConcepts(codeSystem, Collections.singletonList(Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.GENERALIZES) + .value(code) + .build())); + Set ancestorsAndSelf = new LinkedHashSet<>(concepts.size()); + for (Concept concept : concepts) { + ancestorsAndSelf.add(concept.getCode().getValue()); + } + return ancestorsAndSelf; + } + + private static Set computeDescendantsAndSelf(CodeSystem codeSystem, Code code) { + Set concepts = FHIRTermService.getInstance().getConcepts(codeSystem, Collections.singletonList(Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(code) + .build())); + Set descendantsAndSelf = new LinkedHashSet<>(concepts.size()); + for (Concept concept : concepts) { + descendantsAndSelf.add(concept.getCode().getValue()); + } + return descendantsAndSelf; + } + private static ConceptFilter createDescendentOfFilter(CodeSystem codeSystem, Include.Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { Concept concept = findConcept(codeSystem, code(filter.getValue())); @@ -704,17 +742,6 @@ public boolean accept(Concept concept) { } } - static class NotInFilter extends InFilter { - public NotInFilter(Code property, Set set) { - super(property, set); - } - - @Override - public boolean accept(Concept concept) { - return !super.accept(concept); - } - } - private static class RegexFilter implements ConceptFilter { private final Code property; private final Pattern pattern; @@ -735,4 +762,15 @@ public boolean accept(Concept concept) { return false; } } + + static class NotInFilter extends InFilter { + public NotInFilter(Code property, Set set) { + super(property, set); + } + + @Override + public boolean accept(Concept concept) { + return !super.accept(concept); + } + } } diff --git a/fhir-term/src/test/java/com/ibm/fhir/term/service/test/CodeSystemSupportTest.java b/fhir-term/src/test/java/com/ibm/fhir/term/service/test/CodeSystemSupportTest.java new file mode 100644 index 00000000000..bf9c1496f4f --- /dev/null +++ b/fhir-term/src/test/java/com/ibm/fhir/term/service/test/CodeSystemSupportTest.java @@ -0,0 +1,36 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.service.test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.term.util.CodeSystemSupport; + +public class CodeSystemSupportTest { + @Test + public void testGetAncestorsAndSelf() { + CodeSystem codeSystem = CodeSystemSupport.getCodeSystem("http://ibm.com/fhir/CodeSystem/cs5|1.0.0"); + Set actual = CodeSystemSupport.getAncestorsAndSelf(codeSystem, Code.of("r")); + Set expected = new HashSet<>(Arrays.asList("r", "p", "m")); + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetDescendantsAndSelf() { + CodeSystem codeSystem = CodeSystemSupport.getCodeSystem("http://ibm.com/fhir/CodeSystem/cs5|1.0.0"); + Set actual = CodeSystemSupport.getDescendantsAndSelf(codeSystem, Code.of("m")); + Set expected = new HashSet<>(Arrays.asList("m", "p", "q", "r")); + Assert.assertEquals(actual, expected); + } +} From 7e7b630fc9b286fa1f5de55e9f4e31191ad280a2 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Sat, 27 Mar 2021 12:34:44 -0400 Subject: [PATCH 2/6] Issue #2092, Issue #1733 - updated modifier on NotInFilter, re-sorted Signed-off-by: John T.E. Timm --- .../ibm/fhir/term/util/CodeSystemSupport.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/util/CodeSystemSupport.java b/fhir-term/src/main/java/com/ibm/fhir/term/util/CodeSystemSupport.java index 39f531b1741..8a868e9bfab 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/util/CodeSystemSupport.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/util/CodeSystemSupport.java @@ -742,6 +742,17 @@ public boolean accept(Concept concept) { } } + private static class NotInFilter extends InFilter { + public NotInFilter(Code property, Set set) { + super(property, set); + } + + @Override + public boolean accept(Concept concept) { + return !super.accept(concept); + } + } + private static class RegexFilter implements ConceptFilter { private final Code property; private final Pattern pattern; @@ -762,15 +773,4 @@ public boolean accept(Concept concept) { return false; } } - - static class NotInFilter extends InFilter { - public NotInFilter(Code property, Set set) { - super(property, set); - } - - @Override - public boolean accept(Concept concept) { - return !super.accept(concept); - } - } } From b45ee455f4592718371b9c218930130664cd46fb Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Sat, 27 Mar 2021 13:19:09 -0400 Subject: [PATCH 3/6] Issue #2092, Issue #1733 - minor tweaks Signed-off-by: John T.E. Timm --- .../test/java/com/ibm/fhir/core/util/test/URLSupportTest.java | 4 ++-- .../registry/ImplicitValueSetRegistryResourceProvider.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fhir-core/src/test/java/com/ibm/fhir/core/util/test/URLSupportTest.java b/fhir-core/src/test/java/com/ibm/fhir/core/util/test/URLSupportTest.java index 9e51db047bf..f82f3ffc8e1 100644 --- a/fhir-core/src/test/java/com/ibm/fhir/core/util/test/URLSupportTest.java +++ b/fhir-core/src/test/java/com/ibm/fhir/core/util/test/URLSupportTest.java @@ -30,9 +30,9 @@ public void testGetQueryParameters() { @Test public void testGetPathSegments() { String url = "http://ibm.com/fhir/ValueSet/generalizes?system=http://ibm.com/fhir/CodeSystem/cs5&code=r"; - List pathSegments = getPathSegments(url); + List actual = getPathSegments(url); List expected = Arrays.asList("fhir", "ValueSet", "generalizes"); - Assert.assertEquals(pathSegments, expected); + Assert.assertEquals(actual, expected); } @Test diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/registry/ImplicitValueSetRegistryResourceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/registry/ImplicitValueSetRegistryResourceProvider.java index 4ddd3d2859b..5388530f2e2 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/registry/ImplicitValueSetRegistryResourceProvider.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/registry/ImplicitValueSetRegistryResourceProvider.java @@ -41,11 +41,11 @@ public FHIRRegistryResource getRegistryResource(Class resour @Override public Collection getRegistryResources() { - return IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE.values(); + return Collections.emptyList(); } @Override public Collection getRegistryResources(Class resourceType) { - return ValueSet.class.equals(resourceType) ? IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE.values() : Collections.emptyList(); + return Collections.emptyList(); } @Override From 1b120728ede7afa362a0c4c5f08255dbb242b6f5 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Sat, 27 Mar 2021 13:28:39 -0400 Subject: [PATCH 4/6] Issue #2092, Issue #1733 - more clean-up Signed-off-by: John T.E. Timm --- .../graph/registry/SnomedRegistryResourceProvider.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java index c0284b8389f..8666f64858f 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java @@ -40,13 +40,13 @@ public class SnomedRegistryResourceProvider extends ImplicitValueSetRegistryResourceProvider { private static final Logger log = Logger.getLogger(SnomedRegistryResourceProvider.class.getName()); + public static final CodeSystem SNOMED_CODE_SYSTEM = loadCodeSystem(); + private static final String SNOMED_URL = "http://snomed.info/sct"; private static final String SNOMED_IMPLICIT_VALUE_SET_PREFIX = SNOMED_URL + "?fhir_vs"; private static final String SNOMED_COPYRIGHT = "This value set includes content from SNOMED CT, which is copyright © 2002+ International Health Terminology Standards Development Organisation (SNOMED International), and distributed by agreement between SNOMED International and HL7. Implementer use of SNOMED CT is not covered by this agreement"; - - public static final CodeSystem SNOMED_CODE_SYSTEM = loadCodeSystem(); private static final FHIRRegistryResource SNOMED_CODE_SYSTEM_REGISTRY_RESOURCE = FHIRRegistryResource.from(SNOMED_CODE_SYSTEM); - private static final ValueSet ALL_CONCEPTS_IMPLICIT_VALUE_SET = buildAllConceptsImplicitValueSet(); + private static final ValueSet SNOMED_ALL_CONCEPTS_IMPLICIT_VALUE_SET = buildAllConceptsImplicitValueSet(); @Override public FHIRRegistryResource getRegistryResource(Class resourceType, String url, String version) { @@ -110,7 +110,7 @@ private static CodeSystem loadCodeSystem() { @Override protected ValueSet buildImplicitValueSet(String url) { if (SNOMED_IMPLICIT_VALUE_SET_PREFIX.equals(url)) { - return ALL_CONCEPTS_IMPLICIT_VALUE_SET; + return SNOMED_ALL_CONCEPTS_IMPLICIT_VALUE_SET; } Map> queryParameters = getQueryParameters(url); String value = getFirst(queryParameters, "fhir_vs"); From 0827687c0abd0ec2a526f1c4edb4b5f35cb17dc2 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Sat, 27 Mar 2021 22:33:17 -0400 Subject: [PATCH 5/6] Issue #1980 - add hasLabel when include filters list is empty Signed-off-by: John T.E. Timm --- .../fhir/term/graph/provider/GraphTermServiceProvider.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/provider/GraphTermServiceProvider.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/provider/GraphTermServiceProvider.java index 40178f9f5f2..607363e908e 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/provider/GraphTermServiceProvider.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/provider/GraphTermServiceProvider.java @@ -189,6 +189,10 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { first = false; } + if (filters.isEmpty()) { + g = g.hasLabel("Concept"); + } + g = g.timeLimit(timeLimit); TimeLimitStep timeLimitStep = getTimeLimitStep(g); From 9bfb3c54b672e55f4b88e94d3dd1c5fbbdc88ab0 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Mon, 29 Mar 2021 09:28:27 -0400 Subject: [PATCH 6/6] Issue #2092, Issue #1980 - updates per PR comments Signed-off-by: John T.E. Timm --- .../com/ibm/fhir/core/util/URLSupport.java | 142 +++++++++++++++++- .../FHIRPathAbstractTermFunction.java | 2 +- 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/fhir-core/src/main/java/com/ibm/fhir/core/util/URLSupport.java b/fhir-core/src/main/java/com/ibm/fhir/core/util/URLSupport.java index 7559d13861e..07772125f82 100644 --- a/fhir-core/src/main/java/com/ibm/fhir/core/util/URLSupport.java +++ b/fhir-core/src/main/java/com/ibm/fhir/core/util/URLSupport.java @@ -28,6 +28,14 @@ public class URLSupport { private URLSupport() { } + /** + * URL decode the input string + * + * @param s + * the string to URL decode + * @return + * the URL decoded string + */ public static String decode(String s) { try { return URLDecoder.decode(s, "UTF-8"); @@ -36,43 +44,149 @@ public static String decode(String s) { } } - public static String getFirst(Map> queryParameters, String key) { - List values = queryParameters.get(key); + /** + * Get the first value of the list for the specified key from the provided multivalued map + * + * @param map + * the multivalued map + * @param key + * the key + * @return + * the first value of the list for the specified key from the provided multivalued map or null if not exists + */ + public static String getFirst(Map> map, String key) { + List values = map.get(key); return (values != null && !values.isEmpty()) ? values.get(0) : null; } + /** + * Get the path part of the provided URL + * + * @param url + * the url + * @return + * the path part or empty if not exists + * @see + * URL#getPath() + */ public static String getPath(String url) { return getURL(url).getPath(); } + /** + * Get a list containing the path segments from the provided URL + * + *

The path segments are URL decoded + * + * @param url + * the url + * @return + * a list containing the path segments from the provided URL + */ public static List getPathSegments(String url) { return getPathSegments(url, true); } + /** + * Get a list containing the path segments from the provided URL + * + *

The path segments are URL decoded according to the specified parameter + * + * @param url + * the url + * @param decode + * indicates whether to decode the path segments + * @return + * a list containing the path segments from the provided URL + */ public static List getPathSegments(String url, boolean decode) { return parsePath(getPath(url), decode); } + /** + * Get the query part of the provided URL + * + * @param url + * the URL + * @return + * the query part of the provided URL or empty if not exists + * @see + * URL#getQuery() + */ public static String getQuery(String url) { return getURL(url).getQuery(); } + /** + * Get a multivalued map containing the query parameters for the provided URL + * + *

The keys and values of the multivalued map are URL decoded + * + * @param url + * the URL + * @return + * a multivalued map containing the query parameters for the provided URL + */ public static Map> getQueryParameters(String url) { return getQueryParameters(url, true); } + /** + * Get a multivalued map containing the query parameters for the provided URL + * + *

The keys and values of the multivalued map are URL decoded according the specified parameter + * + * @param url + * the URL + * @param decode + * indicates whether to decode the keys and values of the multivalued map should be decoded + * @return + * a multivalued map containing the query parameters for the provided URL + */ public static Map> getQueryParameters(String url, boolean decode) { return parseQuery(getQuery(url), decode); } + /** + * Get a {@link URL} instance that represents the specified parameter + * + * @param url + * the url + * @return + * a {@link URL} instance that represents the specified parameter + * @see + * URL + */ public static URL getURL(String url) { return URL_CACHE.computeIfAbsent(url, k -> computeURL(url)); } + /** + * Parse the provided path part into a List of path segments + * + *

The path segments are URL decoded + * + * @param path + * the path part + * @return + * a list of path segments + */ public static List parsePath(String path) { return parsePath(path, true); } + /** + * Parse the provided path part into a list of path segments + * + *

The path segments are decoded according to the specified parameter + * + * @param path + * the path part + * @param decode + * indicates whether the path segments should be URL decoded + * @return + * a list of path segments + */ public static List parsePath(String path, boolean decode) { return Arrays.stream(path.split("/")) .skip(1) @@ -80,10 +194,34 @@ public static List parsePath(String path, boolean decode) { .collect(Collectors.toList()); } + /** + * Parse the provided query part into a multivalued map of query parameters + * + *

The keys and values of the multivalued map are URL decoded + * + * @param query + * the query part + * @return + * a multivalued map containing the query parameters for the provided URL + * + */ public static Map> parseQuery(String query) { return parseQuery(query, true); } + /** + * Parse the provided query part into a multivalued map of query parameters + * + *

The keys and values of the multivalued map are URL decoded according to the specified parameter + * + * @param query + * the query part + * @param decode + * indicates whether to decode the keys and values of the multivalued map should be decoded + * @return + * a multivalued map containing the query parameters for the provided URL + * + */ public static Map> parseQuery(String query, boolean decode) { if (query == null || query.isEmpty()) { return Collections.emptyMap(); diff --git a/fhir-path/src/main/java/com/ibm/fhir/path/function/FHIRPathAbstractTermFunction.java b/fhir-path/src/main/java/com/ibm/fhir/path/function/FHIRPathAbstractTermFunction.java index 1d307d0c833..cc8bfba6d19 100644 --- a/fhir-path/src/main/java/com/ibm/fhir/path/function/FHIRPathAbstractTermFunction.java +++ b/fhir-path/src/main/java/com/ibm/fhir/path/function/FHIRPathAbstractTermFunction.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2021 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */